mirror of
https://github.com/raz-varren/sacrificial-socket.git
synced 2025-10-05 16:16:58 +08:00
let there be websockets
This commit is contained in:
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) [2016] [raz-varren]
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
54
README.md
Normal file
54
README.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
Sacrificial-Socket
|
||||||
|
==================
|
||||||
|
|
||||||
|
A Go server library and pure JS client library for managing communication between websockets, that has an API similar to Socket.IO, but feels less... well, *Javascripty*. Socket.IO is great, but nowadays all modern browsers support websockets natively, so in most cases there is no need to have websocket simulation fallbacks like XHR long polling or Flash. Removing these allows Sacrificial-Socket to be lightweight and very performant.
|
||||||
|
|
||||||
|
Sacrificial-Socket supports rooms, roomcasts, broadcasts, and event emitting just like Socket.IO, but with one key difference. The data passed into event functions is not an interface{} that is implied to be a string or map[string]interface{}, but is always passed in as a []byte making it easier to unmarshal into your own JSON data structs, convert to a string, or keep as binary data without the need to check the data's type before processing it. It also means there aren't any unnecessary conversions to the data between the client and the server.
|
||||||
|
|
||||||
|
Sacrificial-Socket also has a MultihomeBackend interface for syncronizing broadcasts and roomcasts across multiple instances of Sacrificial-Socket running on multiple machines. Out of the box Sacrificial-Socket provides a MultihomeBackend interface for the popular noSQL database MongoDB, and one for the not so popular GRPC protocol, for syncronizing instances on multiple machines.
|
||||||
|
|
||||||
|
In depth examples can be found in the __examples__ directory and full documentation can be found at [Godoc.org](https://godoc.org/github.com/raz-varren/sacrificial-socket "Sacrificial-Socket Documentation")
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
#### Client Javascript:
|
||||||
|
```javascript
|
||||||
|
(function(SS){ 'use strict';
|
||||||
|
var ss = new SS('ws://localhost:8080/socket');
|
||||||
|
ss.onConnect(function(){
|
||||||
|
ss.emit('echo', 'hello echo!');
|
||||||
|
});
|
||||||
|
|
||||||
|
ss.on('echo', function(data){
|
||||||
|
alert('got echo:', data);
|
||||||
|
ss.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
ss.onDisconnect(function(){
|
||||||
|
console.log('socket connection closed');
|
||||||
|
});
|
||||||
|
})(window.SS);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Server Go:
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import(
|
||||||
|
"net/http"
|
||||||
|
ss "github.com/raz-varren/sacrificial-socket"
|
||||||
|
)
|
||||||
|
|
||||||
|
func doEcho(s *ss.Socket, data []byte) {
|
||||||
|
s.Emit("echo", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
s := ss.NewServer()
|
||||||
|
s.On("echo", doEcho)
|
||||||
|
|
||||||
|
http.Handle("/socket", s.WebHandler())
|
||||||
|
http.ListenAndServe(":8080", nil);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
12
backend/ssgrpc/client.go
Normal file
12
backend/ssgrpc/client.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package ssgrpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"github.com/raz-varren/sacrificial-socket/backend/ssgrpc/transport"
|
||||||
|
//"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type propagateClient struct {
|
||||||
|
conn *grpc.ClientConn
|
||||||
|
client transport.PropagateClient
|
||||||
|
}
|
56
backend/ssgrpc/cred.go
Normal file
56
backend/ssgrpc/cred.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package ssgrpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
"github.com/raz-varren/sacrificial-socket/backend/ssgrpc/token"
|
||||||
|
"github.com/raz-varren/sacrificial-socket/log"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type perRPCCreds struct {
|
||||||
|
tokenStr string
|
||||||
|
tokenExpire int64
|
||||||
|
sharedKey []byte
|
||||||
|
l *sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *perRPCCreds) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
|
||||||
|
var tok string
|
||||||
|
var exp int64
|
||||||
|
var sharedKey []byte
|
||||||
|
|
||||||
|
c.l.RLock()
|
||||||
|
exp = c.tokenExpire
|
||||||
|
tok = c.tokenStr
|
||||||
|
sharedKey = c.sharedKey
|
||||||
|
c.l.RUnlock()
|
||||||
|
|
||||||
|
meta := make(map[string]string)
|
||||||
|
|
||||||
|
if exp-300 < time.Now().Unix() {
|
||||||
|
u, t, err := token.GenUserToken("ssgrpcClient", time.Hour, sharedKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Err.Println("gen token error:", err)
|
||||||
|
return meta, err
|
||||||
|
}
|
||||||
|
|
||||||
|
exp = u.EXP
|
||||||
|
tok = t
|
||||||
|
|
||||||
|
c.l.Lock()
|
||||||
|
c.tokenExpire = exp
|
||||||
|
c.tokenStr = tok
|
||||||
|
c.l.Unlock()
|
||||||
|
|
||||||
|
log.Info.Println("token refreshed:", tok)
|
||||||
|
}
|
||||||
|
|
||||||
|
meta["authorization"] = "Bearer " + tok
|
||||||
|
|
||||||
|
return meta, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *perRPCCreds) RequireTransportSecurity() bool {
|
||||||
|
return true
|
||||||
|
}
|
147
backend/ssgrpc/server.go
Normal file
147
backend/ssgrpc/server.go
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
package ssgrpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
ss "github.com/raz-varren/sacrificial-socket"
|
||||||
|
"github.com/raz-varren/sacrificial-socket/backend/ssgrpc/token"
|
||||||
|
"github.com/raz-varren/sacrificial-socket/backend/ssgrpc/transport"
|
||||||
|
"github.com/raz-varren/sacrificial-socket/log"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrBadRPCCredentials = errors.New("the client provided invalid RPC credentials")
|
||||||
|
|
||||||
|
ErrNilBroadcastChannel = errors.New("broadcast channel is not open yet")
|
||||||
|
ErrNilRoomcastChannel = errors.New("roomcast channel is not open yet")
|
||||||
|
|
||||||
|
ErrBadDataType = errors.New("bad data type used")
|
||||||
|
ErrBadContext = errors.New("bad context used in transport")
|
||||||
|
)
|
||||||
|
|
||||||
|
type propagateServer struct {
|
||||||
|
sharedKey []byte
|
||||||
|
bChan chan<- *ss.BroadcastMsg
|
||||||
|
rChan chan<- *ss.RoomMsg
|
||||||
|
insecure bool
|
||||||
|
l *sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *propagateServer) checkCreds(ctx context.Context) error {
|
||||||
|
if p.insecure {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
meta, ok := metadata.FromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
return ErrBadContext
|
||||||
|
}
|
||||||
|
|
||||||
|
if auth, exists := meta["authorization"]; exists && len(auth) == 1 {
|
||||||
|
t := strings.Split(auth[0], " ")
|
||||||
|
if len(t) != 2 || t[0] != "Bearer" {
|
||||||
|
return token.ErrBadBearerValue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := token.ValidateUserToken(t[1], p.sharedKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return ErrBadRPCCredentials
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *propagateServer) DoBroadcast(ctx context.Context, b *transport.Broadcast) (*transport.Result, error) {
|
||||||
|
tr := &transport.Result{Timestamp: b.Timestamp, Success: false}
|
||||||
|
|
||||||
|
err := p.checkCreds(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Err.Println(err)
|
||||||
|
return tr, err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.l.RLock()
|
||||||
|
bChan := p.bChan
|
||||||
|
p.l.RUnlock()
|
||||||
|
|
||||||
|
//channel is not open yet
|
||||||
|
if bChan == nil {
|
||||||
|
return tr, ErrNilBroadcastChannel
|
||||||
|
}
|
||||||
|
|
||||||
|
bCast := &ss.BroadcastMsg{EventName: b.Event, Data: b.Data}
|
||||||
|
|
||||||
|
switch b.DataType {
|
||||||
|
case transport.DataType_JSON:
|
||||||
|
d := make(map[string]interface{})
|
||||||
|
err := json.Unmarshal(b.Data, &d)
|
||||||
|
if err != nil {
|
||||||
|
return tr, err
|
||||||
|
}
|
||||||
|
bCast.Data = d
|
||||||
|
bChan <- bCast
|
||||||
|
|
||||||
|
case transport.DataType_STR:
|
||||||
|
bCast.Data = string(b.Data)
|
||||||
|
bChan <- bCast
|
||||||
|
|
||||||
|
case transport.DataType_BIN:
|
||||||
|
bChan <- bCast
|
||||||
|
|
||||||
|
default:
|
||||||
|
return tr, ErrBadDataType
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.Success = true
|
||||||
|
return tr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *propagateServer) DoRoomcast(ctx context.Context, r *transport.Roomcast) (*transport.Result, error) {
|
||||||
|
tr := &transport.Result{Timestamp: r.Timestamp, Success: false}
|
||||||
|
|
||||||
|
err := p.checkCreds(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Err.Println(err)
|
||||||
|
return tr, err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.l.RLock()
|
||||||
|
rChan := p.rChan
|
||||||
|
p.l.RUnlock()
|
||||||
|
if rChan == nil {
|
||||||
|
return tr, ErrNilRoomcastChannel
|
||||||
|
}
|
||||||
|
|
||||||
|
rCast := &ss.RoomMsg{RoomName: r.Room, EventName: r.Event, Data: r.Data}
|
||||||
|
|
||||||
|
switch r.DataType {
|
||||||
|
case transport.DataType_JSON:
|
||||||
|
d := make(map[string]interface{})
|
||||||
|
err := json.Unmarshal(r.Data, &d)
|
||||||
|
if err != nil {
|
||||||
|
return tr, err
|
||||||
|
}
|
||||||
|
rCast.Data = d
|
||||||
|
rChan <- rCast
|
||||||
|
|
||||||
|
case transport.DataType_STR:
|
||||||
|
rCast.Data = string(r.Data)
|
||||||
|
rChan <- rCast
|
||||||
|
|
||||||
|
case transport.DataType_BIN:
|
||||||
|
rChan <- rCast
|
||||||
|
|
||||||
|
default:
|
||||||
|
return tr, ErrBadDataType
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.Success = true
|
||||||
|
return tr, nil
|
||||||
|
}
|
233
backend/ssgrpc/ssgrpc.go
Normal file
233
backend/ssgrpc/ssgrpc.go
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
package ssgrpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials"
|
||||||
|
"net"
|
||||||
|
ss "github.com/raz-varren/sacrificial-socket"
|
||||||
|
"github.com/raz-varren/sacrificial-socket/backend/ssgrpc/transport"
|
||||||
|
"github.com/raz-varren/sacrificial-socket/log"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
//GRPCMHB... yep that's what I'm calling it. All you need to know is that GRPCMHB
|
||||||
|
//satifies the ss.MultihomeBackend interface
|
||||||
|
type GRPCMHB struct {
|
||||||
|
peerList []string
|
||||||
|
peers map[string]*propagateClient
|
||||||
|
keyFile, certFile string
|
||||||
|
sharedKey []byte
|
||||||
|
gServer *grpc.Server
|
||||||
|
pServer *propagateServer
|
||||||
|
serverHostPort string
|
||||||
|
insecure bool
|
||||||
|
|
||||||
|
l *sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewBackend returns a GRPCMHB that will use TLS and HMAC-SHA256 signed JWTs to connect and authenticate
|
||||||
|
//to the peers in peerList
|
||||||
|
func NewBackend(tlsKeyFile, tlsCertFile, grpcHostPort string, sharedKey []byte, peerList []string) *GRPCMHB {
|
||||||
|
return &GRPCMHB{
|
||||||
|
peerList: peerList,
|
||||||
|
peers: make(map[string]*propagateClient),
|
||||||
|
l: &sync.RWMutex{},
|
||||||
|
keyFile: tlsKeyFile,
|
||||||
|
certFile: tlsCertFile,
|
||||||
|
sharedKey: sharedKey,
|
||||||
|
serverHostPort: grpcHostPort,
|
||||||
|
insecure: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewInsecureBackend returns a GRPCMHB that will use no encryption or authentication to connect to the
|
||||||
|
//peers in peerList
|
||||||
|
//
|
||||||
|
//It is highly discouraged to use this for production systems, as all data will be sent in clear
|
||||||
|
//text and no authentication will be done on peer connections
|
||||||
|
func NewInsecureBackend(grpcHostPort string, peerList []string) *GRPCMHB {
|
||||||
|
return &GRPCMHB{
|
||||||
|
peerList: peerList,
|
||||||
|
peers: make(map[string]*propagateClient),
|
||||||
|
l: &sync.RWMutex{},
|
||||||
|
serverHostPort: grpcHostPort,
|
||||||
|
insecure: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GRPCMHB) constructClient(peer string) {
|
||||||
|
var host, cn string
|
||||||
|
|
||||||
|
g.l.RLock()
|
||||||
|
certFile := g.certFile
|
||||||
|
sharedKey := g.sharedKey
|
||||||
|
insecure := g.insecure
|
||||||
|
g.l.RUnlock()
|
||||||
|
|
||||||
|
hcn := strings.Split(peer, "@")
|
||||||
|
if len(hcn) == 2 {
|
||||||
|
cn = hcn[0]
|
||||||
|
host = hcn[1]
|
||||||
|
} else {
|
||||||
|
hp := strings.Split(hcn[0], ":")
|
||||||
|
cn = hp[0]
|
||||||
|
host = hcn[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
dialOpts := []grpc.DialOption{grpc.WithBlock()}
|
||||||
|
|
||||||
|
if insecure {
|
||||||
|
dialOpts = append(dialOpts, grpc.WithInsecure())
|
||||||
|
} else {
|
||||||
|
tlsCred, err := credentials.NewClientTLSFromFile(certFile, cn)
|
||||||
|
if err != nil {
|
||||||
|
log.Err.Fatalln(err)
|
||||||
|
}
|
||||||
|
rpcCred := &perRPCCreds{l: &sync.RWMutex{}, sharedKey: sharedKey}
|
||||||
|
|
||||||
|
dialOpts = append(dialOpts, grpc.WithTransportCredentials(tlsCred))
|
||||||
|
dialOpts = append(dialOpts, grpc.WithPerRPCCredentials(rpcCred))
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := grpc.Dial(host, dialOpts...)
|
||||||
|
if err != nil {
|
||||||
|
log.Err.Fatalln(err)
|
||||||
|
}
|
||||||
|
g.l.Lock()
|
||||||
|
g.peers[host] = &propagateClient{conn: conn, client: transport.NewPropagateClient(conn)}
|
||||||
|
g.l.Unlock()
|
||||||
|
log.Info.Println("grpc connected to:", host)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Init sets up the grpc server and creates connections to all grpc peers
|
||||||
|
func (g *GRPCMHB) Init() {
|
||||||
|
g.l.Lock()
|
||||||
|
defer g.l.Unlock()
|
||||||
|
lis, err := net.Listen("tcp", g.serverHostPort)
|
||||||
|
if err != nil {
|
||||||
|
log.Err.Fatalln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var opts []grpc.ServerOption
|
||||||
|
if !g.insecure {
|
||||||
|
srvCreds, err := credentials.NewServerTLSFromFile(g.certFile, g.keyFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Err.Fatalln(err)
|
||||||
|
}
|
||||||
|
opts = append(opts, grpc.Creds(srvCreds))
|
||||||
|
}
|
||||||
|
|
||||||
|
serv := grpc.NewServer(opts...)
|
||||||
|
|
||||||
|
g.gServer = serv
|
||||||
|
g.pServer = &propagateServer{sharedKey: g.sharedKey, l: &sync.RWMutex{}, insecure: g.insecure}
|
||||||
|
|
||||||
|
transport.RegisterPropagateServer(g.gServer, g.pServer)
|
||||||
|
|
||||||
|
go g.gServer.Serve(lis)
|
||||||
|
|
||||||
|
for _, host := range g.peerList {
|
||||||
|
go g.constructClient(host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Shutdown stops the grpc service and closes any peer connections
|
||||||
|
func (g *GRPCMHB) Shutdown() {
|
||||||
|
g.l.Lock()
|
||||||
|
defer g.l.Unlock()
|
||||||
|
g.gServer.Stop()
|
||||||
|
for _, peer := range g.peers {
|
||||||
|
peer.conn.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//BroadcastToBackend propagates the broadcast to all active peer connections
|
||||||
|
func (g *GRPCMHB) BroadcastToBackend(b *ss.BroadcastMsg) {
|
||||||
|
data, dataType := getDataType(b.Data)
|
||||||
|
bCast := &transport.Broadcast{
|
||||||
|
Timestamp: timestamp(),
|
||||||
|
Event: b.EventName,
|
||||||
|
Data: data,
|
||||||
|
DataType: dataType,
|
||||||
|
}
|
||||||
|
|
||||||
|
g.l.RLock()
|
||||||
|
defer g.l.RUnlock()
|
||||||
|
|
||||||
|
for _, peer := range g.peers {
|
||||||
|
_, err := peer.client.DoBroadcast(context.Background(), bCast)
|
||||||
|
if err != nil {
|
||||||
|
log.Err.Println(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
//log.Info.Printf("round trip time: %.3f\n", roundTrip(res.Timestamp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//RoomcastToBackend propagates the roomcast to all active peer connections
|
||||||
|
func (g *GRPCMHB) RoomcastToBackend(r *ss.RoomMsg) {
|
||||||
|
data, dataType := getDataType(r.Data)
|
||||||
|
rCast := &transport.Roomcast{
|
||||||
|
Timestamp: timestamp(),
|
||||||
|
Room: r.RoomName,
|
||||||
|
Event: r.EventName,
|
||||||
|
Data: data,
|
||||||
|
DataType: dataType,
|
||||||
|
}
|
||||||
|
|
||||||
|
g.l.RLock()
|
||||||
|
defer g.l.RUnlock()
|
||||||
|
|
||||||
|
for _, peer := range g.peers {
|
||||||
|
_, err := peer.client.DoRoomcast(context.Background(), rCast)
|
||||||
|
if err != nil {
|
||||||
|
log.Err.Println(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
//log.Info.Printf("round trip time: %.3f\n", roundTrip(res.Timestamp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//BroadcastFromBackend listens on the local grpc service for calls from remote peers and
|
||||||
|
//propagates broadcasts to locally connected websockets
|
||||||
|
func (g *GRPCMHB) BroadcastFromBackend(b chan<- *ss.BroadcastMsg) {
|
||||||
|
g.pServer.l.Lock()
|
||||||
|
defer g.pServer.l.Unlock()
|
||||||
|
g.pServer.bChan = b
|
||||||
|
}
|
||||||
|
|
||||||
|
//RoomcastFromBackend listens on the local grpc service for calls from remote peers and
|
||||||
|
//propagates roomcasts to locally connected websockets
|
||||||
|
func (g *GRPCMHB) RoomcastFromBackend(r chan<- *ss.RoomMsg) {
|
||||||
|
g.pServer.l.Lock()
|
||||||
|
defer g.pServer.l.Unlock()
|
||||||
|
g.pServer.rChan = r
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDataType(in interface{}) ([]byte, transport.DataType) {
|
||||||
|
switch i := in.(type) {
|
||||||
|
case string:
|
||||||
|
return []byte(i), transport.DataType_STR
|
||||||
|
case []byte:
|
||||||
|
return i, transport.DataType_BIN
|
||||||
|
default:
|
||||||
|
j, err := json.Marshal(i)
|
||||||
|
if err != nil {
|
||||||
|
log.Err.Println(err)
|
||||||
|
return []byte{}, transport.DataType_STR
|
||||||
|
}
|
||||||
|
return j, transport.DataType_JSON
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func roundTrip(timestamp uint64) float64 {
|
||||||
|
return (float64(time.Now().UnixNano()) - float64(timestamp)) / 1000000000
|
||||||
|
}
|
||||||
|
|
||||||
|
func timestamp() uint64 {
|
||||||
|
return uint64(time.Now().UnixNano())
|
||||||
|
}
|
65
backend/ssgrpc/token/token.go
Normal file
65
backend/ssgrpc/token/token.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package token
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
//"fmt"
|
||||||
|
"github.com/dvsekhvalnov/jose2go"
|
||||||
|
//"net/http"
|
||||||
|
//"strings"
|
||||||
|
//"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrTokenExpired = errors.New("token expired")
|
||||||
|
ErrBadPayload = errors.New("payload is missing critical values")
|
||||||
|
|
||||||
|
ErrNoToken = errors.New("user did not provide a token")
|
||||||
|
ErrBadBearerValue = errors.New("user provided an invalid Bearer value")
|
||||||
|
)
|
||||||
|
|
||||||
|
//UserToken represents an authenticated user
|
||||||
|
type UserToken struct {
|
||||||
|
//authenticated user name
|
||||||
|
IAM string `json:"iam"`
|
||||||
|
|
||||||
|
//unix epoc expire time
|
||||||
|
EXP int64 `json:"exp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//GenUserToken will generate an expiring HMAC-SHA256 signed token representing an authenticated user
|
||||||
|
func GenUserToken(user string, expires time.Duration, signingKey []byte) (UserToken, string, error) {
|
||||||
|
var u UserToken
|
||||||
|
u.IAM = user
|
||||||
|
u.EXP = time.Now().Add(expires).Unix()
|
||||||
|
data, _ := json.Marshal(u)
|
||||||
|
tok, err := jose.Sign(string(data), jose.HS256, signingKey)
|
||||||
|
return u, tok, err
|
||||||
|
}
|
||||||
|
|
||||||
|
//ValidateUserToken will validate the token and attempt to unmarshal it's payload into a UserToken.
|
||||||
|
//
|
||||||
|
//error is nil if validation succeeded.
|
||||||
|
func ValidateUserToken(token string, signingKey []byte) (UserToken, error) {
|
||||||
|
var u UserToken
|
||||||
|
payload, _, err := jose.Decode(token, signingKey)
|
||||||
|
if err != nil {
|
||||||
|
return u, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal([]byte(payload), &u)
|
||||||
|
if err != nil {
|
||||||
|
return u, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.IAM == "" || u.EXP == 0 {
|
||||||
|
return u, ErrBadPayload
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.EXP < time.Now().Unix() {
|
||||||
|
return u, ErrTokenExpired
|
||||||
|
}
|
||||||
|
|
||||||
|
return u, nil
|
||||||
|
}
|
236
backend/ssgrpc/transport/transport.pb.go
Normal file
236
backend/ssgrpc/transport/transport.pb.go
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
// Code generated by protoc-gen-go.
|
||||||
|
// source: transport.proto
|
||||||
|
// DO NOT EDIT!
|
||||||
|
|
||||||
|
/*
|
||||||
|
Package transport is a generated protocol buffer package.
|
||||||
|
|
||||||
|
It is generated from these files:
|
||||||
|
transport.proto
|
||||||
|
|
||||||
|
It has these top-level messages:
|
||||||
|
Broadcast
|
||||||
|
Roomcast
|
||||||
|
Result
|
||||||
|
*/
|
||||||
|
package transport
|
||||||
|
|
||||||
|
import proto "github.com/golang/protobuf/proto"
|
||||||
|
import fmt "fmt"
|
||||||
|
import math "math"
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "golang.org/x/net/context"
|
||||||
|
grpc "google.golang.org/grpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Reference imports to suppress errors if they are not otherwise used.
|
||||||
|
var _ = proto.Marshal
|
||||||
|
var _ = fmt.Errorf
|
||||||
|
var _ = math.Inf
|
||||||
|
|
||||||
|
// This is a compile-time assertion to ensure that this generated file
|
||||||
|
// is compatible with the proto package it is being compiled against.
|
||||||
|
// A compilation error at this line likely means your copy of the
|
||||||
|
// proto package needs to be updated.
|
||||||
|
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
|
||||||
|
|
||||||
|
type DataType int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
DataType_STR DataType = 0
|
||||||
|
DataType_BIN DataType = 1
|
||||||
|
DataType_JSON DataType = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
var DataType_name = map[int32]string{
|
||||||
|
0: "STR",
|
||||||
|
1: "BIN",
|
||||||
|
2: "JSON",
|
||||||
|
}
|
||||||
|
var DataType_value = map[string]int32{
|
||||||
|
"STR": 0,
|
||||||
|
"BIN": 1,
|
||||||
|
"JSON": 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x DataType) String() string {
|
||||||
|
return proto.EnumName(DataType_name, int32(x))
|
||||||
|
}
|
||||||
|
func (DataType) EnumDescriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
|
||||||
|
|
||||||
|
type Broadcast struct {
|
||||||
|
// unix nano timestamp
|
||||||
|
Timestamp uint64 `protobuf:"fixed64,1,opt,name=timestamp" json:"timestamp,omitempty"`
|
||||||
|
Event string `protobuf:"bytes,2,opt,name=event" json:"event,omitempty"`
|
||||||
|
Data []byte `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"`
|
||||||
|
DataType DataType `protobuf:"varint,4,opt,name=dataType,enum=transport.DataType" json:"dataType,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Broadcast) Reset() { *m = Broadcast{} }
|
||||||
|
func (m *Broadcast) String() string { return proto.CompactTextString(m) }
|
||||||
|
func (*Broadcast) ProtoMessage() {}
|
||||||
|
func (*Broadcast) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
|
||||||
|
|
||||||
|
type Roomcast struct {
|
||||||
|
// unix nano timestamp
|
||||||
|
Timestamp uint64 `protobuf:"fixed64,1,opt,name=timestamp" json:"timestamp,omitempty"`
|
||||||
|
Room string `protobuf:"bytes,2,opt,name=room" json:"room,omitempty"`
|
||||||
|
Event string `protobuf:"bytes,3,opt,name=event" json:"event,omitempty"`
|
||||||
|
Data []byte `protobuf:"bytes,4,opt,name=data,proto3" json:"data,omitempty"`
|
||||||
|
DataType DataType `protobuf:"varint,5,opt,name=dataType,enum=transport.DataType" json:"dataType,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Roomcast) Reset() { *m = Roomcast{} }
|
||||||
|
func (m *Roomcast) String() string { return proto.CompactTextString(m) }
|
||||||
|
func (*Roomcast) ProtoMessage() {}
|
||||||
|
func (*Roomcast) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} }
|
||||||
|
|
||||||
|
type Result struct {
|
||||||
|
Success bool `protobuf:"varint,1,opt,name=success" json:"success,omitempty"`
|
||||||
|
//
|
||||||
|
// should be the original unix nano timestamp sent by the client
|
||||||
|
// useful for calculating round trip time
|
||||||
|
Timestamp uint64 `protobuf:"fixed64,2,opt,name=timestamp" json:"timestamp,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Result) Reset() { *m = Result{} }
|
||||||
|
func (m *Result) String() string { return proto.CompactTextString(m) }
|
||||||
|
func (*Result) ProtoMessage() {}
|
||||||
|
func (*Result) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} }
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
proto.RegisterType((*Broadcast)(nil), "transport.Broadcast")
|
||||||
|
proto.RegisterType((*Roomcast)(nil), "transport.Roomcast")
|
||||||
|
proto.RegisterType((*Result)(nil), "transport.Result")
|
||||||
|
proto.RegisterEnum("transport.DataType", DataType_name, DataType_value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reference imports to suppress errors if they are not otherwise used.
|
||||||
|
var _ context.Context
|
||||||
|
var _ grpc.ClientConn
|
||||||
|
|
||||||
|
// This is a compile-time assertion to ensure that this generated file
|
||||||
|
// is compatible with the grpc package it is being compiled against.
|
||||||
|
const _ = grpc.SupportPackageIsVersion3
|
||||||
|
|
||||||
|
// Client API for Propagate service
|
||||||
|
|
||||||
|
type PropagateClient interface {
|
||||||
|
DoBroadcast(ctx context.Context, in *Broadcast, opts ...grpc.CallOption) (*Result, error)
|
||||||
|
DoRoomcast(ctx context.Context, in *Roomcast, opts ...grpc.CallOption) (*Result, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type propagateClient struct {
|
||||||
|
cc *grpc.ClientConn
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPropagateClient(cc *grpc.ClientConn) PropagateClient {
|
||||||
|
return &propagateClient{cc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *propagateClient) DoBroadcast(ctx context.Context, in *Broadcast, opts ...grpc.CallOption) (*Result, error) {
|
||||||
|
out := new(Result)
|
||||||
|
err := grpc.Invoke(ctx, "/transport.Propagate/DoBroadcast", in, out, c.cc, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *propagateClient) DoRoomcast(ctx context.Context, in *Roomcast, opts ...grpc.CallOption) (*Result, error) {
|
||||||
|
out := new(Result)
|
||||||
|
err := grpc.Invoke(ctx, "/transport.Propagate/DoRoomcast", in, out, c.cc, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server API for Propagate service
|
||||||
|
|
||||||
|
type PropagateServer interface {
|
||||||
|
DoBroadcast(context.Context, *Broadcast) (*Result, error)
|
||||||
|
DoRoomcast(context.Context, *Roomcast) (*Result, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterPropagateServer(s *grpc.Server, srv PropagateServer) {
|
||||||
|
s.RegisterService(&_Propagate_serviceDesc, srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Propagate_DoBroadcast_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(Broadcast)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(PropagateServer).DoBroadcast(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/transport.Propagate/DoBroadcast",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(PropagateServer).DoBroadcast(ctx, req.(*Broadcast))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Propagate_DoRoomcast_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(Roomcast)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(PropagateServer).DoRoomcast(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/transport.Propagate/DoRoomcast",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(PropagateServer).DoRoomcast(ctx, req.(*Roomcast))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _Propagate_serviceDesc = grpc.ServiceDesc{
|
||||||
|
ServiceName: "transport.Propagate",
|
||||||
|
HandlerType: (*PropagateServer)(nil),
|
||||||
|
Methods: []grpc.MethodDesc{
|
||||||
|
{
|
||||||
|
MethodName: "DoBroadcast",
|
||||||
|
Handler: _Propagate_DoBroadcast_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "DoRoomcast",
|
||||||
|
Handler: _Propagate_DoRoomcast_Handler,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Streams: []grpc.StreamDesc{},
|
||||||
|
Metadata: fileDescriptor0,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() { proto.RegisterFile("transport.proto", fileDescriptor0) }
|
||||||
|
|
||||||
|
var fileDescriptor0 = []byte{
|
||||||
|
// 283 bytes of a gzipped FileDescriptorProto
|
||||||
|
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x8c, 0x92, 0xcd, 0x4a, 0x03, 0x31,
|
||||||
|
0x10, 0x80, 0xdd, 0x9f, 0x6e, 0x77, 0x47, 0xd1, 0x75, 0xec, 0x21, 0x88, 0x07, 0xe9, 0x41, 0xc4,
|
||||||
|
0x43, 0x85, 0x2a, 0x9e, 0xa5, 0xec, 0x45, 0x0f, 0x55, 0xd2, 0xbe, 0x40, 0xdc, 0x06, 0x11, 0xdc,
|
||||||
|
0x26, 0x24, 0x53, 0xa1, 0x77, 0xdf, 0xc1, 0xd7, 0x35, 0x5d, 0xf7, 0x4f, 0xa9, 0xd0, 0x53, 0x66,
|
||||||
|
0xbe, 0x99, 0x24, 0x5f, 0x7e, 0xe0, 0x88, 0x8c, 0x58, 0x5a, 0xad, 0x0c, 0x8d, 0xb4, 0x51, 0xa4,
|
||||||
|
0x30, 0x69, 0xc0, 0xf0, 0xd3, 0x83, 0x64, 0x62, 0x94, 0x58, 0xe4, 0xc2, 0x12, 0x9e, 0x41, 0x42,
|
||||||
|
0x6f, 0x85, 0xb4, 0x24, 0x0a, 0xcd, 0xbc, 0x73, 0xef, 0x32, 0xe2, 0x2d, 0xc0, 0x01, 0xf4, 0xe4,
|
||||||
|
0x87, 0x5c, 0x12, 0xf3, 0x5d, 0x25, 0xe1, 0x3f, 0x09, 0x22, 0x84, 0x0b, 0x41, 0x82, 0x05, 0x0e,
|
||||||
|
0x1e, 0xf0, 0x32, 0xc6, 0x6b, 0x88, 0x37, 0xe3, 0x7c, 0xad, 0x25, 0x0b, 0x1d, 0x3f, 0x1c, 0x9f,
|
||||||
|
0x8c, 0x5a, 0x89, 0xac, 0x2a, 0xf1, 0xa6, 0x69, 0xf8, 0xe5, 0x41, 0xcc, 0x95, 0x2a, 0x76, 0xb0,
|
||||||
|
0x70, 0xfb, 0x19, 0xd7, 0x59, 0x49, 0x94, 0x71, 0x6b, 0x16, 0x6c, 0x33, 0x0b, 0xff, 0x31, 0xeb,
|
||||||
|
0xed, 0x62, 0x76, 0x0f, 0x11, 0x97, 0x76, 0xf5, 0x4e, 0xc8, 0xa0, 0x6f, 0x57, 0x79, 0x2e, 0xad,
|
||||||
|
0x2d, 0xa5, 0x62, 0x5e, 0xa7, 0xbf, 0x85, 0xfd, 0x3f, 0xc2, 0x57, 0x17, 0x10, 0xd7, 0xeb, 0x62,
|
||||||
|
0x1f, 0x82, 0xd9, 0x9c, 0xa7, 0x7b, 0x9b, 0x60, 0xf2, 0x30, 0x4d, 0x3d, 0x8c, 0x21, 0x7c, 0x9c,
|
||||||
|
0x3d, 0x4d, 0x53, 0x7f, 0xbc, 0x86, 0xe4, 0xd9, 0x28, 0x2d, 0x5e, 0x05, 0x49, 0xbc, 0x83, 0xfd,
|
||||||
|
0x4c, 0xb5, 0x0f, 0x33, 0xe8, 0x48, 0x36, 0xf4, 0xf4, 0xb8, 0x43, 0x2b, 0xc9, 0x5b, 0x80, 0x4c,
|
||||||
|
0x35, 0x37, 0xd9, 0x3d, 0x5b, 0x0d, 0xb7, 0xcc, 0x7a, 0x89, 0xca, 0x7f, 0x71, 0xf3, 0x1d, 0x00,
|
||||||
|
0x00, 0xff, 0xff, 0xdb, 0x8f, 0xc3, 0x46, 0x2a, 0x02, 0x00, 0x00,
|
||||||
|
}
|
39
backend/ssgrpc/transport/transport.proto
Normal file
39
backend/ssgrpc/transport/transport.proto
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package transport;
|
||||||
|
|
||||||
|
service Propagate {
|
||||||
|
rpc DoBroadcast(Broadcast) returns (Result);
|
||||||
|
rpc DoRoomcast(Roomcast) returns (Result);
|
||||||
|
}
|
||||||
|
|
||||||
|
message Broadcast {
|
||||||
|
//unix nano timestamp
|
||||||
|
fixed64 timestamp = 1;
|
||||||
|
string event = 2;
|
||||||
|
bytes data = 3;
|
||||||
|
DataType dataType = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Roomcast {
|
||||||
|
//unix nano timestamp
|
||||||
|
fixed64 timestamp = 1;
|
||||||
|
string room = 2;
|
||||||
|
string event = 3;
|
||||||
|
bytes data = 4;
|
||||||
|
DataType dataType = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DataType {
|
||||||
|
STR = 0;
|
||||||
|
BIN = 1;
|
||||||
|
JSON = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Result {
|
||||||
|
bool success = 1;
|
||||||
|
//
|
||||||
|
//should be the original unix nano timestamp sent by the client
|
||||||
|
//useful for calculating round trip time
|
||||||
|
fixed64 timestamp = 2;
|
||||||
|
}
|
60
backend/ssmongo/bson.go
Normal file
60
backend/ssmongo/bson.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package ssmongo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gopkg.in/mgo.v2/bson"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type backendServer struct {
|
||||||
|
ID bson.ObjectId `bson:"_id,omitempty"`
|
||||||
|
ServerName string `bson:"ServerName"`
|
||||||
|
ServerGroup string `bson:"ServerGroup"`
|
||||||
|
Expire time.Time `bson:"Expire"`
|
||||||
|
l *sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
type broadcast struct {
|
||||||
|
ID bson.ObjectId `bson:"_id,omitempty"`
|
||||||
|
ServerName string `bson:"ServerName"`
|
||||||
|
ServerGroup string `bson:"ServerGroup"`
|
||||||
|
Expire time.Time `bson:"Expire"`
|
||||||
|
EventName string `bson:"EventName"`
|
||||||
|
Data interface{} `bson:"Data"`
|
||||||
|
JSON bool `bson:"JSON"`
|
||||||
|
Read bool `bson:"Read"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type roomcast struct {
|
||||||
|
ID bson.ObjectId `bson:"_id,omitempty"`
|
||||||
|
ServerName string `bson:"ServerName"`
|
||||||
|
ServerGroup string `bson:"ServerGroup"`
|
||||||
|
Expire time.Time `bson:"Expire"`
|
||||||
|
RoomName string `bson:"RoomName"`
|
||||||
|
EventName string `bson:"EventName"`
|
||||||
|
Data interface{} `bson:"Data"`
|
||||||
|
JSON bool `bson:"JSON"`
|
||||||
|
Read bool `bson:"Read"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *backendServer) setNextExpire() {
|
||||||
|
s.l.Lock()
|
||||||
|
defer s.l.Unlock()
|
||||||
|
s.Expire = time.Now().Add(time.Minute * 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *broadcast) setNextExpire() {
|
||||||
|
b.Expire = time.Now().Add(time.Minute * 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *broadcast) expireNow() {
|
||||||
|
b.Expire = time.Now().Add(time.Minute * 5 * -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roomcast) setNextExpire() {
|
||||||
|
r.Expire = time.Now().Add(time.Minute * 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roomcast) expireNow() {
|
||||||
|
r.Expire = time.Now().Add(time.Minute * 5 * -1)
|
||||||
|
}
|
343
backend/ssmongo/ssmongo.go
Normal file
343
backend/ssmongo/ssmongo.go
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
package ssmongo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"gopkg.in/mgo.v2"
|
||||||
|
"gopkg.in/mgo.v2/bson"
|
||||||
|
"io"
|
||||||
|
ss "github.com/raz-varren/sacrificial-socket"
|
||||||
|
"github.com/raz-varren/sacrificial-socket/log"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
//MMHB implements ss.MultihomeBackend and uses MongoDB to syncronize between
|
||||||
|
//multiple machines running ss.SocketServer
|
||||||
|
type MMHB struct {
|
||||||
|
session *mgo.Session
|
||||||
|
serverC *mgo.Collection
|
||||||
|
roomcastC *mgo.Collection
|
||||||
|
broadcastC *mgo.Collection
|
||||||
|
server backendServer
|
||||||
|
pollFrequency time.Duration
|
||||||
|
l *sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewMongoDBBackend returns a new instance of MMHB which satisfies the ss.MultihomeBackend interface.
|
||||||
|
//A new database "SSMultihome" will be created at the specified mongoURL, and under it 3 collections "activeServers",
|
||||||
|
//"ss.roomcasts", and "ss.broadcasts" will be created if they don't already exist.
|
||||||
|
//
|
||||||
|
//serverName must be unique per running ss.SocketServer instance, otherwise broadcasts, and roomcasts
|
||||||
|
//will not propogate correctly to the other running instances
|
||||||
|
//
|
||||||
|
//serverGroup is used to break up broadcast and roomcast domains between multiple ss.SocketServer instances.
|
||||||
|
//Most of the time you will want this to be the same for all of your running ss.SocketServer instances
|
||||||
|
//
|
||||||
|
//pollFrequency is used to determine how frequently MongoDB is queried for broadcasts or roomcasts
|
||||||
|
func NewBackend(mongoURL, serverName, serverGroup string, pollFrequency time.Duration) *MMHB {
|
||||||
|
m, err := mgo.Dial(mongoURL)
|
||||||
|
log.CheckFatal(err)
|
||||||
|
db := m.DB("SSMultihome")
|
||||||
|
|
||||||
|
s := backendServer{
|
||||||
|
ServerName: serverName,
|
||||||
|
ServerGroup: serverGroup,
|
||||||
|
l: &sync.RWMutex{},
|
||||||
|
}
|
||||||
|
|
||||||
|
return &MMHB{
|
||||||
|
session: m,
|
||||||
|
serverC: db.C("ss.activeServers"),
|
||||||
|
roomcastC: db.C("ss.roomcasts"),
|
||||||
|
broadcastC: db.C("ss.broadcasts"),
|
||||||
|
server: s,
|
||||||
|
pollFrequency: pollFrequency,
|
||||||
|
l: &sync.RWMutex{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mmhb *MMHB) getActiveServers() []backendServer {
|
||||||
|
server := mmhb.getServer()
|
||||||
|
|
||||||
|
var servers []backendServer
|
||||||
|
|
||||||
|
err := mmhb.serverC.Find(bson.M{
|
||||||
|
"ServerGroup": server.ServerGroup,
|
||||||
|
"ServerName": bson.M{"$ne": server.ServerName},
|
||||||
|
}).All(&servers)
|
||||||
|
if err != nil {
|
||||||
|
log.Err.Println(err)
|
||||||
|
}
|
||||||
|
return servers
|
||||||
|
}
|
||||||
|
|
||||||
|
//Init will create the "SSMultihome" database along with the "ss.activeServers", "ss.broadcasts", and "ss.roomcasts"
|
||||||
|
//collections, as well as any neccessary indexes
|
||||||
|
func (mmhb *MMHB) Init() {
|
||||||
|
cols := []*mgo.Collection{mmhb.serverC, mmhb.broadcastC, mmhb.roomcastC}
|
||||||
|
indexes := []mgo.Index{
|
||||||
|
mgo.Index{
|
||||||
|
Key: []string{"Expire"},
|
||||||
|
ExpireAfter: time.Second * 1,
|
||||||
|
},
|
||||||
|
mgo.Index{
|
||||||
|
Key: []string{"ServerGroup"},
|
||||||
|
},
|
||||||
|
mgo.Index{
|
||||||
|
Key: []string{"ServerName"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, col := range cols {
|
||||||
|
for _, i := range indexes {
|
||||||
|
err := col.EnsureIndex(i)
|
||||||
|
if err != nil {
|
||||||
|
log.Err.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mmhb.beat()
|
||||||
|
go mmhb.heartbeat()
|
||||||
|
}
|
||||||
|
|
||||||
|
//Shutdown will remove this server from the activeServers collection
|
||||||
|
func (mmhb *MMHB) Shutdown() {
|
||||||
|
defer mmhb.session.Close()
|
||||||
|
server := mmhb.getServer()
|
||||||
|
err := mmhb.serverC.Remove(bson.M{"ServerGroup": server.ServerGroup, "ServerName": server.ServerName})
|
||||||
|
if err != nil {
|
||||||
|
log.Err.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//BroadcastToBackend will insert one broadcast document into the ss.broadcasts collection for each
|
||||||
|
//server in the activeServers collection excluding itself, each time BroadcastToBackend is called.
|
||||||
|
//
|
||||||
|
//See documentation on the ss.MultihomeBackend interface for more information
|
||||||
|
func (mmhb *MMHB) BroadcastToBackend(b *ss.BroadcastMsg) {
|
||||||
|
servers := mmhb.getActiveServers()
|
||||||
|
|
||||||
|
if len(servers) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bulk := mmhb.broadcastC.Bulk()
|
||||||
|
d, isJ := isJSON(b.Data)
|
||||||
|
for _, s := range servers {
|
||||||
|
bcast := broadcast{
|
||||||
|
ServerName: s.ServerName,
|
||||||
|
ServerGroup: s.ServerGroup,
|
||||||
|
EventName: b.EventName,
|
||||||
|
Data: d,
|
||||||
|
JSON: isJ,
|
||||||
|
Read: false,
|
||||||
|
}
|
||||||
|
bcast.setNextExpire()
|
||||||
|
bulk.Insert(bcast)
|
||||||
|
}
|
||||||
|
_, err := bulk.Run()
|
||||||
|
if err != nil {
|
||||||
|
log.Err.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//RoomcastToBackend will insert one roomcast document into the roomcasts collection for each
|
||||||
|
//server in the activeServers collection excluding itself, each time RoomcastToBackend is called.
|
||||||
|
//
|
||||||
|
//See documentation on the ss.MultihomeBackend interface for more information
|
||||||
|
func (mmhb *MMHB) RoomcastToBackend(r *ss.RoomMsg) {
|
||||||
|
servers := mmhb.getActiveServers()
|
||||||
|
|
||||||
|
if len(servers) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bulk := mmhb.roomcastC.Bulk()
|
||||||
|
d, isJ := isJSON(r.Data)
|
||||||
|
for _, s := range servers {
|
||||||
|
rcast := roomcast{
|
||||||
|
ServerName: s.ServerName,
|
||||||
|
ServerGroup: s.ServerGroup,
|
||||||
|
RoomName: r.RoomName,
|
||||||
|
EventName: r.EventName,
|
||||||
|
Data: d,
|
||||||
|
JSON: isJ,
|
||||||
|
Read: false,
|
||||||
|
}
|
||||||
|
rcast.setNextExpire()
|
||||||
|
bulk.Insert(rcast)
|
||||||
|
}
|
||||||
|
_, err := bulk.Run()
|
||||||
|
if err != nil {
|
||||||
|
log.Err.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//BroadcastFromBackend polls the ss.broadcasts collection, based on the pollFrequency provided to NewBackend, for new messages designated
|
||||||
|
//to this serverName and inserts a ss.BroadcastMsg into b to be dispatched by the server
|
||||||
|
//
|
||||||
|
//See documentation on the ss.MultihomeBackend interface for more information
|
||||||
|
func (mmhb *MMHB) BroadcastFromBackend(b chan<- *ss.BroadcastMsg) {
|
||||||
|
server := mmhb.getServer()
|
||||||
|
for {
|
||||||
|
time.Sleep(mmhb.pollFrequency)
|
||||||
|
|
||||||
|
q := mmhb.broadcastC.Find(bson.M{
|
||||||
|
"ServerName": server.ServerName,
|
||||||
|
"ServerGroup": server.ServerGroup,
|
||||||
|
"Read": false,
|
||||||
|
}).Sort("Expire")
|
||||||
|
|
||||||
|
count, err := q.Count()
|
||||||
|
if err == io.EOF {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Err.Println(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if count == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
bulk := mmhb.broadcastC.Bulk()
|
||||||
|
iter := q.Iter()
|
||||||
|
var bcast broadcast
|
||||||
|
i := 0
|
||||||
|
for iter.Next(&bcast) {
|
||||||
|
var d interface{}
|
||||||
|
d = bcast.Data
|
||||||
|
if bcast.JSON {
|
||||||
|
d = make(map[string]interface{})
|
||||||
|
err = json.Unmarshal(bcast.Data.([]byte), &d)
|
||||||
|
if err != nil {
|
||||||
|
log.Err.Println(err)
|
||||||
|
d = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b <- &ss.BroadcastMsg{bcast.EventName, d}
|
||||||
|
bcast.expireNow()
|
||||||
|
bcast.Read = true
|
||||||
|
bulk.Update(bson.M{"_id": bcast.ID}, bson.M{"$set": bcast})
|
||||||
|
i++
|
||||||
|
if i >= 900 {
|
||||||
|
_, err = bulk.Run()
|
||||||
|
if err != nil {
|
||||||
|
log.Err.Println(err)
|
||||||
|
}
|
||||||
|
bulk = mmhb.broadcastC.Bulk()
|
||||||
|
i = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err = bulk.Run()
|
||||||
|
if err != nil {
|
||||||
|
log.Err.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//RoomcastFromBackend polls the roomcasts collection, based on the pollFrequency provided to NewBackend, for new messages designated
|
||||||
|
//to this serverName and inserts a ss.RoomMsg into r to be dispatched by the ss.SocketServer
|
||||||
|
//
|
||||||
|
//See documentation on the ss.MultihomeBackend interface for more information
|
||||||
|
func (mmhb *MMHB) RoomcastFromBackend(r chan<- *ss.RoomMsg) {
|
||||||
|
server := mmhb.getServer()
|
||||||
|
for {
|
||||||
|
time.Sleep(mmhb.pollFrequency)
|
||||||
|
q := mmhb.roomcastC.Find(bson.M{
|
||||||
|
"ServerName": server.ServerName,
|
||||||
|
"ServerGroup": server.ServerGroup,
|
||||||
|
"Read": false,
|
||||||
|
}).Sort("Expire")
|
||||||
|
|
||||||
|
count, err := q.Count()
|
||||||
|
|
||||||
|
if err == io.EOF {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Err.Println(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if count == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
bulk := mmhb.roomcastC.Bulk()
|
||||||
|
iter := q.Iter()
|
||||||
|
var rcast roomcast
|
||||||
|
i := 0
|
||||||
|
for iter.Next(&rcast) {
|
||||||
|
var d interface{}
|
||||||
|
d = rcast.Data
|
||||||
|
if rcast.JSON {
|
||||||
|
d = make(map[string]interface{})
|
||||||
|
err = json.Unmarshal(rcast.Data.([]byte), &d)
|
||||||
|
if err != nil {
|
||||||
|
log.Err.Println(err)
|
||||||
|
d = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r <- &ss.RoomMsg{rcast.RoomName, rcast.EventName, d}
|
||||||
|
rcast.expireNow()
|
||||||
|
rcast.Read = true
|
||||||
|
bulk.Update(bson.M{"_id": rcast.ID}, bson.M{"$set": rcast})
|
||||||
|
i++
|
||||||
|
if i >= 900 {
|
||||||
|
_, err = bulk.Run()
|
||||||
|
if err != nil {
|
||||||
|
log.Err.Println(err)
|
||||||
|
}
|
||||||
|
bulk = mmhb.roomcastC.Bulk()
|
||||||
|
i = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err = bulk.Run()
|
||||||
|
if err != nil {
|
||||||
|
log.Err.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//beat updates the Expire key for this server in the activeServers collection
|
||||||
|
func (mmhb *MMHB) beat() {
|
||||||
|
server := mmhb.getServer()
|
||||||
|
server.setNextExpire()
|
||||||
|
_, err := mmhb.serverC.Upsert(bson.M{
|
||||||
|
"ServerName": server.ServerName,
|
||||||
|
"ServerGroup": server.ServerGroup,
|
||||||
|
}, bson.M{
|
||||||
|
"$set": server,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Err.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//heartbeat calls beat every minute
|
||||||
|
func (mmhb *MMHB) heartbeat() {
|
||||||
|
for {
|
||||||
|
time.Sleep(time.Minute * 1)
|
||||||
|
mmhb.beat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mmhb *MMHB) getServer() backendServer {
|
||||||
|
mmhb.l.RLock()
|
||||||
|
s := mmhb.server
|
||||||
|
mmhb.l.RUnlock()
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func isJSON(in interface{}) (interface{}, bool) {
|
||||||
|
switch i := in.(type) {
|
||||||
|
case string, []byte:
|
||||||
|
return i, false
|
||||||
|
default:
|
||||||
|
j, err := json.Marshal(i)
|
||||||
|
if err != nil {
|
||||||
|
log.Err.Println(err)
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return j, true
|
||||||
|
}
|
||||||
|
}
|
170
client/sacrificial-socket.js
Normal file
170
client/sacrificial-socket.js
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
(function(window){ 'use strict';
|
||||||
|
/**
|
||||||
|
* SS is the constructor for the sacrificial-socket client
|
||||||
|
*
|
||||||
|
* @class SS
|
||||||
|
* @constructor
|
||||||
|
* @param {String} url - The url to the sacrificial-socket server endpoint. The url must conform to the websocket URI Scheme ("ws" or "wss")
|
||||||
|
*/
|
||||||
|
var SS = function(url){
|
||||||
|
var self = this,
|
||||||
|
ws = new WebSocket(url, 'sac-sock'),
|
||||||
|
events = {},
|
||||||
|
headerStartCharCode = 1,
|
||||||
|
headerStartChar = String.fromCharCode(headerStartCharCode),
|
||||||
|
dataStartCharCode = 2,
|
||||||
|
dataStartChar = String.fromCharCode(dataStartCharCode);
|
||||||
|
|
||||||
|
//sorry, only supporting arraybuffer at this time
|
||||||
|
//maybe if there is demand for it, I'll add Blob support
|
||||||
|
ws.binaryType = 'arraybuffer';
|
||||||
|
|
||||||
|
//Parses all incoming messages and dispatches their payload to the appropriate eventName if one has been registered. Messages received for unregistered events will be ignored.
|
||||||
|
ws.onmessage = function(e){
|
||||||
|
var msg = e.data,
|
||||||
|
headers = {},
|
||||||
|
eventName = '',
|
||||||
|
data = '',
|
||||||
|
chr = null,
|
||||||
|
i, msgLen;
|
||||||
|
|
||||||
|
if(typeof msg === 'string'){
|
||||||
|
var dataStarted = false,
|
||||||
|
headerStarted = false;
|
||||||
|
|
||||||
|
for(i = 0, msgLen = msg.length; i < msgLen; i++){
|
||||||
|
chr = msg[i];
|
||||||
|
if(!dataStarted && !headerStarted && chr !== dataStartChar && chr !== headerStartChar){
|
||||||
|
eventName += chr;
|
||||||
|
}else if(!headerStarted && chr === headerStartChar){
|
||||||
|
headerStarted = true;
|
||||||
|
}else if(headerStarted && !dataStarted && chr !== dataStartChar){
|
||||||
|
headers[chr] = true;
|
||||||
|
}else if(!dataStarted && chr === dataStartChar){
|
||||||
|
dataStarted = true;
|
||||||
|
}else{
|
||||||
|
data += chr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}else if(msg && msg instanceof ArrayBuffer && msg.byteLength !== undefined){
|
||||||
|
var dv = new DataView(msg),
|
||||||
|
headersStarted = false;
|
||||||
|
|
||||||
|
for(i = 0, msgLen = dv.byteLength; i < msgLen; i++){
|
||||||
|
chr = dv.getUint8(i);
|
||||||
|
|
||||||
|
if(chr !== dataStartCharCode && chr !== headerStartCharCode && !headersStarted){
|
||||||
|
eventName += String.fromCharCode(chr);
|
||||||
|
}else if(chr === headerStartCharCode && !headersStarted){
|
||||||
|
headersStarted = true;
|
||||||
|
}else if(headersStarted && chr !== dataStartCharCode){
|
||||||
|
headers[String.fromCharCode(chr)] = true;
|
||||||
|
}else if(chr === dataStartCharCode){
|
||||||
|
data = dv.buffer.slice(i+1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(eventName.length === 0) return; //no event to dispatch
|
||||||
|
if(typeof events[eventName] === 'undefined') return;
|
||||||
|
events[eventName]((headers.J) ? JSON.parse(data) : data);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onConnect registers a callback to be run when the websocket connection is open.
|
||||||
|
*
|
||||||
|
* @method onConnect
|
||||||
|
* @param {Function} callback(SS) - The callback that will be executed when the websocket connection opens.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
self.onConnect = function(callback){
|
||||||
|
ws.onopen = function(){ callback(self); };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onDisconnect registers a callback to be run when the websocket connection is closed.
|
||||||
|
*
|
||||||
|
* @method onDisconnect
|
||||||
|
* @param {Function} callback(SS) - The callback that will be executed when the websocket connection is closed.
|
||||||
|
*/
|
||||||
|
self.onDisconnect = function(callback){
|
||||||
|
ws.onclose = function(){ callback(self); };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* on registers an event to be called when the client receives an emit from the server for
|
||||||
|
* the given eventName.
|
||||||
|
*
|
||||||
|
* @method on
|
||||||
|
* @param {String} eventName - The name of the event being registerd
|
||||||
|
* @param {Function} callback(payload) - The callback that will be ran whenever the client receives an emit from the server for the given eventName. The payload passed into callback may be of type String, Object, or ArrayBuffer
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
self.on = function(eventName, callback){
|
||||||
|
events[eventName] = callback;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* off unregisters an emit event
|
||||||
|
*
|
||||||
|
* @method off
|
||||||
|
* @param {String} eventName - The name of event being unregistered
|
||||||
|
*/
|
||||||
|
self.off = function(eventName){
|
||||||
|
if(events[eventName]){
|
||||||
|
delete events[eventName];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* emit dispatches an event to the server
|
||||||
|
*
|
||||||
|
* @method emit
|
||||||
|
* @param {String} eventName - The event to dispatch
|
||||||
|
* @param {String|Object|ArrayBuffer} data - The data to be sent to the server. If data is a string then it will be sent as a normal string to the server. If data is an object it will be converted to JSON before being sent to the server. If data is an ArrayBuffer then it will be sent to the server as a uint8 binary payload.
|
||||||
|
*/
|
||||||
|
self.emit = function(eventName, data){
|
||||||
|
var rs = ws.readyState;
|
||||||
|
if(rs === 0){
|
||||||
|
console.warn("websocket is not open yet");
|
||||||
|
return;
|
||||||
|
}else if(rs === 2 || rs === 3){
|
||||||
|
console.error("websocket is closed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var msg = '';
|
||||||
|
if(data instanceof ArrayBuffer){
|
||||||
|
var ab = new ArrayBuffer(data.byteLength+eventName.length+1),
|
||||||
|
newBuf = new DataView(ab),
|
||||||
|
oldBuf = new DataView(data),
|
||||||
|
i = 0;
|
||||||
|
for(var evtLen = eventName.length; i < evtLen; i++){
|
||||||
|
newBuf.setUint8(i, eventName.charCodeAt(i));
|
||||||
|
}
|
||||||
|
newBuf.setUint8(i, dataStartCharCode);
|
||||||
|
i++;
|
||||||
|
for(var x = 0, xLen = oldBuf.byteLength; x < xLen; x++, i++){
|
||||||
|
newBuf.setUint8(i, oldBuf.getUint8(x));
|
||||||
|
}
|
||||||
|
msg = ab;
|
||||||
|
}else if(typeof data === 'object'){
|
||||||
|
msg = eventName+dataStartChar+JSON.stringify(data);
|
||||||
|
}else{
|
||||||
|
msg = eventName+dataStartChar+data;
|
||||||
|
}
|
||||||
|
ws.send(msg);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* close will close the websocket connection, calling the "onDisconnect" event if one has been registered.
|
||||||
|
*
|
||||||
|
* @method close
|
||||||
|
*/
|
||||||
|
self.close = function(){
|
||||||
|
return ws.close();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
window.SS = SS;
|
||||||
|
})(window);
|
@@ -0,0 +1,22 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDpTCCAo2gAwIBAgIJAI2B3qJSjKaiMA0GCSqGSIb3DQEBCwUAMGkxCzAJBgNV
|
||||||
|
BAYTAlVTMRMwEQYDVQQIDApXYXNoaW5ndG9uMRUwEwYDVQQHDAxEZWZhdWx0IENp
|
||||||
|
dHkxGjAYBgNVBAoMEVNhZEhhcHB5TWVkaWEgOik6MRIwEAYDVQQDDAlsb2NhbGhv
|
||||||
|
c3QwHhcNMTYwODA4MDQzMjAzWhcNMTYwOTA3MDQzMjAzWjBpMQswCQYDVQQGEwJV
|
||||||
|
UzETMBEGA1UECAwKV2FzaGluZ3RvbjEVMBMGA1UEBwwMRGVmYXVsdCBDaXR5MRow
|
||||||
|
GAYDVQQKDBFTYWRIYXBweU1lZGlhIDopOjESMBAGA1UEAwwJbG9jYWxob3N0MIIB
|
||||||
|
IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA75wdXP7CuJm9UEezJ0QL1UVx
|
||||||
|
LicbvwsnG4+OBlnUUOGQw8blvxRuMyLATX44gy3rYLj9CTgDSjMK2yrjJ9SAgK/J
|
||||||
|
nWilCA+mrrepkKfUJMvtIDiFhINsaf7zNPJH1Nu3D84CcSXGpleYIsjasZ5neBT9
|
||||||
|
V2O+7DIEadi28zuAYvaJ7LdvQOYJBwXx1PAFPQiO7XkYqyhDCdvzjBtRBui55sGg
|
||||||
|
51eFUOtIKvX0R9RvSOdjkdHXI3EvIm7WL6LtwosWO6HA67liLpTbTAl50L0JO94J
|
||||||
|
i5aYob/q9kuZhRErpV+K/cDajtP/yPwYPbilz9BtRmj+R9aX06313y+DEYGaxQID
|
||||||
|
AQABo1AwTjAdBgNVHQ4EFgQUbLEsoxZ1QKYezZmGnJldJZU2+TswHwYDVR0jBBgw
|
||||||
|
FoAUbLEsoxZ1QKYezZmGnJldJZU2+TswDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0B
|
||||||
|
AQsFAAOCAQEALe261wGoaLeyBkdXMGkT/mZde7LO5tGAMlff+HfnekZVzxrYkSVG
|
||||||
|
IjcbUMok78bi9hPp42JVqSPqjtI4pV6gL1xoyf7heRAaSt/2xXfZ/VohQrrfUOOp
|
||||||
|
3sCWnuepYMW376COC+2uYw0OQzcBpU5fbXLnXBHK3RGO6HcbjL5vLWkZiFcVKIi/
|
||||||
|
qLwn9Ksrz/QAM6gmo+B3X8iAoib2iD8+ZAGd5HblSpLhOTcOgB4MmnHz1t5teWkJ
|
||||||
|
gWd3G1yOzl55eLg082O4OZnhTv1memhejhpTjOt2T422xgvHqCNNgKGcF2WUKn0D
|
||||||
|
M/zvFv3peoEE+5F9GttExMXMrQ3UTUYstQ==
|
||||||
|
-----END CERTIFICATE-----
|
@@ -0,0 +1,28 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDvnB1c/sK4mb1Q
|
||||||
|
R7MnRAvVRXEuJxu/Cycbj44GWdRQ4ZDDxuW/FG4zIsBNfjiDLetguP0JOANKMwrb
|
||||||
|
KuMn1ICAr8mdaKUID6aut6mQp9Qky+0gOIWEg2xp/vM08kfU27cPzgJxJcamV5gi
|
||||||
|
yNqxnmd4FP1XY77sMgRp2LbzO4Bi9onst29A5gkHBfHU8AU9CI7teRirKEMJ2/OM
|
||||||
|
G1EG6LnmwaDnV4VQ60gq9fRH1G9I52OR0dcjcS8ibtYvou3CixY7ocDruWIulNtM
|
||||||
|
CXnQvQk73gmLlpihv+r2S5mFESulX4r9wNqO0//I/Bg9uKXP0G1GaP5H1pfTrfXf
|
||||||
|
L4MRgZrFAgMBAAECggEALzUi09+dnaEsm1SFB4dwjWRRGDa8ULdxzQxLfxTTr9vB
|
||||||
|
GdFmEm+EBq8iFCJ6H948MaJAuEBA5a2IxgV9G6vSZYg8wLaNTTlILcQhtE5cjaNf
|
||||||
|
5Kk+JQ1/nqKMaDIT1Ow09Pgxovk+WieH4dKQw83htNbt0vnj88Um+XOpF+LxOI3Y
|
||||||
|
pJUo2Wa47RqRo/gyfs+79R3+AbKYXeChxUG4XBQ+CFfcno9ezwBkPwDdAtWfWW1g
|
||||||
|
79ub5IRShq+JkBTWg67IY0E/diQ51ED3aAIo88rEd8k7AnYJcsFwg9kulrYXU87d
|
||||||
|
P+BTIidhe6i8sx6f1DGmq0C2kWlsRqsbBeYWAdsYAQKBgQD7Mpj9kcDaznHPA8wi
|
||||||
|
E6cEADaJorqG9qKhJtlcfJKuyAjjC3cXZJJtWwqYLqgGdzE4cKuviHXtjsNAhkcw
|
||||||
|
t4eqwxl7uLm9/kKTwSVX7vR1oQBGERi7fBYMZ6gXS6Tv5AlLL0QGIB/DANPy4D+m
|
||||||
|
5ms/m52Yk1DrBoREMCVSxm8XoQKBgQD0MM3rht3CSfTqEBKhCj+0EdxZvtIYYMP0
|
||||||
|
spdV+Mwq6P5uviMZQbI/YmAtZJ7iVb3kwq9HpH5yh0HxqgZGO3GTFV4GjzzdwWVm
|
||||||
|
IqbjlocQimqORS1et7AmJSDDkxq+48qmS4WnBS1YUfJejDKDdKCZ5uu30vnTLwnw
|
||||||
|
KDKOqytgpQKBgQDAVuTIO8NEhxCjp0+1xAB9UsBvsNdMIisyY7HJXGVgSTBs0MmW
|
||||||
|
ct6ftzcsgYUMtlVM1xDOrhlGFasxi9+U0OKjgRHRJweqD7bgteKnKwOp0eaIv+yF
|
||||||
|
GiUzyGbvt28KdDwdgop+ejh3svmXHdf/Lq1uHfSU8C/kVbAPGiJp+CD2IQKBgQDh
|
||||||
|
nspUiQsSvhSdw3YC954+eZY4Ebi7L4U+7Vgo5jV2nEqh9eomJu5T5EhrCKJJC+Hl
|
||||||
|
oQAk2NbAaTrD2E3tzHS26rIyq1wYpN1UjkXW2Lk4zjt/8mjmMCCATiPEsIGwyHXw
|
||||||
|
Sq1V0dHA3g5rz2vIzBSrvpIjCbsspjSvgeScr4jnxQKBgQCHuqyVev8kbbUkRaIJ
|
||||||
|
nNiSWETX8Cn//1Dm5iXM38LstErAnHGwLc1lWO/vIFxyhwgguJBnHsuwNA8u4nv7
|
||||||
|
y9HQ3jwp7Cmt1DAwb7daaDv6rp3A3LZdVgfc23M3LXcPYUvQxBw4fPU11WrzBsdc
|
||||||
|
Ve98JThtbfJ68LWIUAEY8p27Yg==
|
||||||
|
-----END PRIVATE KEY-----
|
165
examples/not-so-simple-examples/grpc-multihome/main.go
Normal file
165
examples/not-so-simple-examples/grpc-multihome/main.go
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"github.com/raz-varren/sacrificial-socket"
|
||||||
|
"github.com/raz-varren/sacrificial-socket/backend/ssgrpc"
|
||||||
|
"strings"
|
||||||
|
//"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type roomcast struct {
|
||||||
|
Room string `json:"room"`
|
||||||
|
Data string `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type message struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
webPort = flag.String("webport", ":8081", "host:port number used for webpage and socket connections")
|
||||||
|
key = flag.String("key", "./keys/snakeoil.key", "tls key used for grpc peer connections")
|
||||||
|
cert = flag.String("cert", "./keys/snakeoil.crt", "tls cert used for grpc peer connections")
|
||||||
|
peerList = flag.String("peers", "", "comma separated list of peerCN@host:port peers for grpc connections. if peerCN is not provided host will be used as the peer Common Name")
|
||||||
|
sharedKey = flag.String("sharedkey", "insecuresharedkeystring", "the shared key used to sign HMAC-SHA256 JWTs for authenticating grpc peers. must be the same on all peers")
|
||||||
|
grpcHostPort = flag.String("grpchostport", ":30001", "listen host:port for grpc peer connections")
|
||||||
|
insecure = flag.Bool("insecure", false, "if the insecure flag is set, no tls or shared key authentication will be used in the grpc peer connections. insecure should not be used on production instances")
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
s := ss.NewServer()
|
||||||
|
|
||||||
|
s.On("echo", Echo)
|
||||||
|
s.On("echobin", EchoBin)
|
||||||
|
s.On("echojson", EchoJSON)
|
||||||
|
s.On("join", Join)
|
||||||
|
s.On("leave", Leave)
|
||||||
|
s.On("roomcast", Roomcast)
|
||||||
|
s.On("roomcastbin", RoomcastBin)
|
||||||
|
s.On("roomcastjson", RoomcastJSON)
|
||||||
|
s.On("broadcast", Broadcast)
|
||||||
|
s.On("broadcastbin", BroadcastBin)
|
||||||
|
s.On("broadcastjson", BroadcastJSON)
|
||||||
|
|
||||||
|
if *peerList == "" {
|
||||||
|
log.Println("must provide peers to connect to")
|
||||||
|
flag.PrintDefaults()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
peers := strings.Split(*peerList, ",")
|
||||||
|
|
||||||
|
var b ss.MultihomeBackend
|
||||||
|
|
||||||
|
if *insecure {
|
||||||
|
b = ssgrpc.NewInsecureBackend(*grpcHostPort, peers)
|
||||||
|
} else {
|
||||||
|
b = ssgrpc.NewBackend(*key, *cert, *grpcHostPort, []byte(*sharedKey), peers)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.SetMultihomeBackend(b)
|
||||||
|
|
||||||
|
c := make(chan bool)
|
||||||
|
s.EnableSignalShutdown(c)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-c
|
||||||
|
os.Exit(0)
|
||||||
|
}()
|
||||||
|
|
||||||
|
http.Handle("/socket", s.WebHandler())
|
||||||
|
http.Handle("/", http.FileServer(http.Dir("webroot")))
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if *insecure {
|
||||||
|
err = http.ListenAndServe(*webPort, nil)
|
||||||
|
} else {
|
||||||
|
err = http.ListenAndServeTLS(*webPort, *cert, *key, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Echo(s *ss.Socket, data []byte) {
|
||||||
|
s.Emit("echo", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func EchoBin(s *ss.Socket, data []byte) {
|
||||||
|
s.Emit("echobin", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func EchoJSON(s *ss.Socket, data []byte) {
|
||||||
|
var m message
|
||||||
|
err := json.Unmarshal(data, &m)
|
||||||
|
check(err)
|
||||||
|
|
||||||
|
s.Emit("echojson", m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Join(s *ss.Socket, data []byte) {
|
||||||
|
d := string(data)
|
||||||
|
s.Join(d)
|
||||||
|
s.Emit("echo", "joined room:" + d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Leave(s *ss.Socket, data []byte) {
|
||||||
|
d := string(data)
|
||||||
|
s.Leave(d)
|
||||||
|
s.Emit("echo", "left room:" + d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Roomcast(s *ss.Socket, data []byte) {
|
||||||
|
var r roomcast
|
||||||
|
err := json.Unmarshal(data, &r)
|
||||||
|
check(err)
|
||||||
|
|
||||||
|
s.Roomcast(r.Room, "roomcast", r.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RoomcastBin(s *ss.Socket, data []byte) {
|
||||||
|
var r roomcast
|
||||||
|
err := json.Unmarshal(data, &r)
|
||||||
|
check(err)
|
||||||
|
|
||||||
|
s.Roomcast(r.Room, "roomcastbin", []byte(r.Data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func RoomcastJSON(s *ss.Socket, data []byte) {
|
||||||
|
var r roomcast
|
||||||
|
err := json.Unmarshal(data, &r)
|
||||||
|
check(err)
|
||||||
|
|
||||||
|
s.Roomcast(r.Room, "roomcastjson", r)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func Broadcast(s *ss.Socket, data []byte) {
|
||||||
|
s.Broadcast("broadcast", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func BroadcastBin(s *ss.Socket, data []byte){
|
||||||
|
s.Broadcast("broadcastbin", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BroadcastJSON(s *ss.Socket, data []byte) {
|
||||||
|
var m message
|
||||||
|
err := json.Unmarshal(data, &m)
|
||||||
|
check(err)
|
||||||
|
|
||||||
|
s.Broadcast("broadcastjson", m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func check(err error) {
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
}
|
6
examples/not-so-simple-examples/grpc-multihome/start-servers-insecure.sh
Executable file
6
examples/not-so-simple-examples/grpc-multihome/start-servers-insecure.sh
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
go run main.go -insecure -grpchostport ":30001" -peers localhost@localhost:30002,localhost@localhost:30003 -webport :8081&
|
||||||
|
go run main.go -insecure -grpchostport ":30002" -peers localhost@localhost:30001,localhost@localhost:30003 -webport :8082&
|
||||||
|
go run main.go -insecure -grpchostport ":30003" -peers localhost@localhost:30001,localhost@localhost:30002 -webport :8083&
|
||||||
|
|
6
examples/not-so-simple-examples/grpc-multihome/start-servers.sh
Executable file
6
examples/not-so-simple-examples/grpc-multihome/start-servers.sh
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
go run main.go -grpchostport ":30001" -peers localhost@localhost:30002,localhost@localhost:30003 -webport :8081&
|
||||||
|
go run main.go -grpchostport ":30002" -peers localhost@localhost:30001,localhost@localhost:30003 -webport :8082&
|
||||||
|
go run main.go -grpchostport ":30003" -peers localhost@localhost:30001,localhost@localhost:30002 -webport :8083&
|
||||||
|
|
@@ -0,0 +1,36 @@
|
|||||||
|
#main{
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#controls{
|
||||||
|
width: 400px
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-block{
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#btn-clear{
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctrl-btn{
|
||||||
|
/*width: 140px;*/
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-100{
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#messages-container{
|
||||||
|
width: 500px;
|
||||||
|
margin-left: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#messages{
|
||||||
|
height: 500px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
@@ -0,0 +1,90 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Sacrificial-Socket Multihome Backend Example</title>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
|
||||||
|
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
>
|
||||||
|
<link rel="stylesheet" type="text/css" href="/css/app.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="main">
|
||||||
|
<div id="controls">
|
||||||
|
<div class="control-block">
|
||||||
|
<label for="in-echo">Echo:</label>
|
||||||
|
<input type="text" id="in-echo" class="form-control" />
|
||||||
|
<div class="btn-group btn-group-justified">
|
||||||
|
<a href="javascript:void(0);" id="btn-echo" class="btn btn-default ctrl-btn">Echo</a>
|
||||||
|
<a href="javascript:void(0);" id="btn-echo-bin" class="btn btn-default ctrl-btn">Echo Binary</a>
|
||||||
|
<a href="javascript:void(0);" id="btn-echo-json" class="btn btn-default ctrl-btn">Echo JSON</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-block">
|
||||||
|
<label for="in-broadcast">Broadcast:</label>
|
||||||
|
<input type="text" id="in-broadcast" class="form-control" />
|
||||||
|
<div class="btn-group btn-group-justified">
|
||||||
|
<a href="javascript:void(0);" id="btn-broadcast" class="btn btn-default ctrl-btn">Broadcast</a>
|
||||||
|
<a href="javascript:void(0);" id="btn-broadcast-bin" class="btn btn-default ctrl-btn">Broadcast Binary</a>
|
||||||
|
<a href="javascript:void(0);" id="btn-broadcast-json" class="btn btn-default ctrl-btn">Broadcast JSON</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-block">
|
||||||
|
<label for="in-join">Join Room:</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" id="in-join" class="form-control" />
|
||||||
|
<div class="input-group-btn">
|
||||||
|
<button id="btn-join" class="btn btn-default ctrl-btn">Join Room</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-block">
|
||||||
|
<label for="in-leave">Leave Room:</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" id="in-leave" class="form-control" />
|
||||||
|
<div class="input-group-btn">
|
||||||
|
<button id="btn-leave" class="btn btn-default ctrl-btn">Leave Room</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-block well well-sm">
|
||||||
|
<label for="in-roomcast-room">Room:</label>
|
||||||
|
<input type="text" id="in-roomcast-room" class="form-control" />
|
||||||
|
<label for="in-roomcast-data">Roomcast:</label>
|
||||||
|
<input type="text" id="in-roomcast-data" class="form-control" />
|
||||||
|
<div class="btn-group btn-group-justified">
|
||||||
|
<a href="javascript:void(0);" id="btn-roomcast" class="btn btn-default ctrl-btn">Roomcast</a>
|
||||||
|
<a href="javascript:void(0);" id="btn-roomcast-bin" class="btn btn-default ctrl-btn">Roomcast Binary</a>
|
||||||
|
<a href="javascript:void(0);" id="btn-roomcast-json" class="btn btn-default ctrl-btn">Roomcast JSON</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-block">
|
||||||
|
<button id="btn-close" class="btn btn-warning btn-100">Close Connection</button>
|
||||||
|
</div>
|
||||||
|
<div class="control-block">
|
||||||
|
<button id="btn-wierd" class="btn btn-danger btn-100">Get Weird!</button>
|
||||||
|
</div>
|
||||||
|
<div class="control-block">
|
||||||
|
<button id="btn-normal" class="btn btn-success btn-100">Get Normal</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="messages-container">
|
||||||
|
<button id="btn-clear" class="btn btn-default btn-100">Clear Messages</button>
|
||||||
|
<div class="well well-sm">
|
||||||
|
<ul id="messages"></ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="text/javascript" src="/js/sacrificial-socket.js"></script>
|
||||||
|
<script type="text/javascript" src="/js/app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
262
examples/not-so-simple-examples/grpc-multihome/webroot/js/app.js
Normal file
262
examples/not-so-simple-examples/grpc-multihome/webroot/js/app.js
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
(function(window){ 'use strict';
|
||||||
|
var ws = new window.SS((window.location.protocol === 'https:' ? 'wss':'ws')+'://'+window.location.host+'/socket'),
|
||||||
|
get = function(selector){
|
||||||
|
return document.querySelector(selector);
|
||||||
|
},
|
||||||
|
rand = function(min, max){
|
||||||
|
return Math.floor(Math.random() * (max - min + 1) + min);
|
||||||
|
},
|
||||||
|
abToStr = function(ab){
|
||||||
|
return String.fromCharCode.apply(null, new Uint8Array(ab));
|
||||||
|
},
|
||||||
|
strToAB = function(str) {
|
||||||
|
var buf = new ArrayBuffer(str.length);
|
||||||
|
var dv = new DataView(buf);
|
||||||
|
for (var i=0, strLen=str.length; i<strLen; i++) {
|
||||||
|
dv.setUint8(i, str.charCodeAt(i));
|
||||||
|
}
|
||||||
|
return buf;
|
||||||
|
},
|
||||||
|
wierdness = null,
|
||||||
|
inEcho = get('#in-echo'),
|
||||||
|
inJoin = get('#in-join'),
|
||||||
|
inLeave = get('#in-leave'),
|
||||||
|
inBroadcast = get('#in-broadcast'),
|
||||||
|
inRoomcastRoom = get('#in-roomcast-room'),
|
||||||
|
inRoomcastData = get('#in-roomcast-data'),
|
||||||
|
btnEcho = get('#btn-echo'),
|
||||||
|
btnEchoBin = get('#btn-echo-bin'),
|
||||||
|
btnEchoJSON = get('#btn-echo-json'),
|
||||||
|
btnJoin = get('#btn-join'),
|
||||||
|
btnLeave = get('#btn-leave'),
|
||||||
|
btnBroadcast = get('#btn-broadcast'),
|
||||||
|
btnBroadcastBin = get('#btn-broadcast-bin'),
|
||||||
|
btnBroadcastJSON = get('#btn-broadcast-json'),
|
||||||
|
btnRoomcast = get('#btn-roomcast'),
|
||||||
|
btnRoomcastBin = get('#btn-roomcast-bin'),
|
||||||
|
btnRoomcastJSON = get('#btn-roomcast-json'),
|
||||||
|
btnClose = get('#btn-close'),
|
||||||
|
btnClear = get('#btn-clear'),
|
||||||
|
btnGetWierd = get('#btn-wierd'),
|
||||||
|
btnGetNormal = get('#btn-normal'),
|
||||||
|
messages = get('#messages'),
|
||||||
|
addMessage = function(msg){
|
||||||
|
var li = document.createElement('li'),
|
||||||
|
dt = new Date(),
|
||||||
|
li = document.createElement('li');
|
||||||
|
|
||||||
|
li.innerText = msg;
|
||||||
|
messages.appendChild(li);
|
||||||
|
messages.scrollTop = messages.scrollHeight;
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onConnect(function(){
|
||||||
|
addMessage('ready');
|
||||||
|
ws.emit('echo', 'test ping');
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.onDisconnect(function(){
|
||||||
|
addMessage('disconnected');
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('echo', function(data){
|
||||||
|
addMessage(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('echobin', function(data){
|
||||||
|
addMessage('got binary: '+data.byteLength+' bytes - '+abToStr(data));
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('echojson', function(data){
|
||||||
|
addMessage('got JSON: '+JSON.stringify(data));
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('roomcast', function(data){
|
||||||
|
addMessage('got roomcast: '+data);
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('roomcastbin', function(data){
|
||||||
|
addMessage('got binary roomcast: '+data.byteLength+' bytes - '+abToStr(data));
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('roomcastjson', function(data){
|
||||||
|
addMessage('got JSON roomcast: '+JSON.stringify(data));
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('broadcast', function(data){
|
||||||
|
addMessage('got broadcast: '+data);
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('broadcastbin', function(data){
|
||||||
|
addMessage('got binary broadcast: '+data.byteLength+' bytes - '+abToStr(data));
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('broadcastjson', function(data){
|
||||||
|
addMessage('got JSON broadcast: '+JSON.stringify(data));
|
||||||
|
});
|
||||||
|
|
||||||
|
btnGetWierd.addEventListener('click', function(){
|
||||||
|
getWierd();
|
||||||
|
});
|
||||||
|
|
||||||
|
btnGetNormal.addEventListener('click', function(){
|
||||||
|
if(wierdness){
|
||||||
|
window.clearInterval(wierdness);
|
||||||
|
wierdness = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
btnEcho.addEventListener('click', function(){
|
||||||
|
if(inEcho.value.length === 0) return;
|
||||||
|
ws.emit('echo', inEcho.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
btnEchoBin.addEventListener('click', function(){
|
||||||
|
if(inEcho.value.length === 0) return;
|
||||||
|
ws.emit('echobin', strToAB(inEcho.value));
|
||||||
|
});
|
||||||
|
|
||||||
|
btnEchoJSON.addEventListener('click', function(){
|
||||||
|
if(inEcho.value.length === 0) return;
|
||||||
|
ws.emit('echojson', {message: inEcho.value});
|
||||||
|
});
|
||||||
|
|
||||||
|
btnJoin.addEventListener('click', function(){
|
||||||
|
if(inJoin.value.length === 0) return;
|
||||||
|
ws.emit('join', inJoin.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
btnLeave.addEventListener('click', function(){
|
||||||
|
if(inLeave.value.length === 0) return;
|
||||||
|
ws.emit('leave', inLeave.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
btnBroadcast.addEventListener('click', function(){
|
||||||
|
if(inBroadcast.value.length === 0) return;
|
||||||
|
ws.emit('broadcast', inBroadcast.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
btnBroadcastBin.addEventListener('click', function(){
|
||||||
|
if(inBroadcast.value.length === 0) return;
|
||||||
|
ws.emit('broadcastbin', strToAB(inBroadcast.value));
|
||||||
|
});
|
||||||
|
|
||||||
|
btnBroadcastJSON.addEventListener('click', function(){
|
||||||
|
if(inBroadcast.value.length === 0) return;
|
||||||
|
ws.emit('broadcastjson', {message: inBroadcast.value});
|
||||||
|
});
|
||||||
|
|
||||||
|
btnRoomcast.addEventListener('click', function(){
|
||||||
|
if(inRoomcastRoom.value.length === 0 || inRoomcastData.value.length === 0) return;
|
||||||
|
ws.emit('roomcast', JSON.stringify({room: inRoomcastRoom.value, data: inRoomcastData.value}));
|
||||||
|
});
|
||||||
|
|
||||||
|
btnRoomcastBin.addEventListener('click', function(){
|
||||||
|
if(inRoomcastRoom.value.length === 0 || inRoomcastData.value.length === 0) return;
|
||||||
|
ws.emit('roomcastbin', strToAB(JSON.stringify({room: inRoomcastRoom.value, data: inRoomcastData.value})));
|
||||||
|
});
|
||||||
|
|
||||||
|
btnRoomcastJSON.addEventListener('click', function(){
|
||||||
|
if(inRoomcastRoom.value.length === 0 || inRoomcastData.value.length === 0) return;
|
||||||
|
ws.emit('roomcastjson', {room: inRoomcastRoom.value, data: inRoomcastData.value});
|
||||||
|
});
|
||||||
|
|
||||||
|
btnClose.addEventListener('click', function(){
|
||||||
|
ws.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
btnClear.addEventListener('click', function(){
|
||||||
|
messages.innerHTML = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
function getWierd(){
|
||||||
|
var rooms = [
|
||||||
|
'unknownplace',
|
||||||
|
'trl',
|
||||||
|
'purgatory',
|
||||||
|
'southdakota',
|
||||||
|
'animalhouse',
|
||||||
|
'orangecounty',
|
||||||
|
'andyrichtersbasement',
|
||||||
|
'baldpersonemporium'
|
||||||
|
],
|
||||||
|
phrases = [
|
||||||
|
'welcome to hell',
|
||||||
|
'alls wells thats ends wells',
|
||||||
|
'we\'ve been waiting for you',
|
||||||
|
'did you ever stop to think how that makes your insurance adjuster feel?',
|
||||||
|
'with these weaponized puppies I will finally rule the world',
|
||||||
|
'my only friend is this series of ones and zeros',
|
||||||
|
'how much wood could a woodchuck chuck if a woodchuck was really drunk',
|
||||||
|
'if it\'s not syphillis then why does it itch so much',
|
||||||
|
],
|
||||||
|
actions = [
|
||||||
|
'echo',
|
||||||
|
'echobin',
|
||||||
|
'echojson',
|
||||||
|
'join',
|
||||||
|
'leave',
|
||||||
|
'broadcast',
|
||||||
|
'broadcastbin',
|
||||||
|
'broadcastjson',
|
||||||
|
'roomcast',
|
||||||
|
'roomcastbin',
|
||||||
|
'roomcastjson'
|
||||||
|
],
|
||||||
|
i = 1;
|
||||||
|
|
||||||
|
wierdness = setInterval(function(){
|
||||||
|
//if(ws.readyState !== 1) return;
|
||||||
|
var action = actions[rand(0, actions.length-1)],
|
||||||
|
phrase = phrases[rand(0, phrases.length-1)],
|
||||||
|
room = rooms[rand(0, rooms.length-1)];
|
||||||
|
phrase = (Date.now()/1000)+' - '+phrase;
|
||||||
|
|
||||||
|
if(action == 'echo'){
|
||||||
|
ws.emit('echo', phrase);
|
||||||
|
}else if(action == 'echobin'){
|
||||||
|
ws.emit('echobin', strToAB(phrase));
|
||||||
|
}else if(action == 'echojson'){
|
||||||
|
ws.emit('echojson', {message: phrase});
|
||||||
|
}else if(action == 'join'){
|
||||||
|
ws.emit('join', room);
|
||||||
|
}else if(action == 'leave'){
|
||||||
|
ws.emit('leave', room);
|
||||||
|
}else if(action == 'broadcast'){
|
||||||
|
ws.emit('broadcast', phrase);
|
||||||
|
}else if(action == 'broadcastbin'){
|
||||||
|
ws.emit('broadcastbin', strToAB(phrase));
|
||||||
|
}else if(action == 'broadcastjson'){
|
||||||
|
ws.emit('broadcastjson', {message: phrase});
|
||||||
|
}else if(action == 'roomcast'){
|
||||||
|
ws.emit('roomcast', JSON.stringify({room: room, data: phrase}));
|
||||||
|
}else if(action == 'roomcastbin'){
|
||||||
|
ws.emit('roomcastbin', strToAB(JSON.stringify({room: room, data: phrase})));
|
||||||
|
}else if(action == 'roomcastjson'){
|
||||||
|
ws.emit('roomcastjson', {room: room, data: phrase})
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
})(window);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@@ -0,0 +1,170 @@
|
|||||||
|
(function(window){ 'use strict';
|
||||||
|
/**
|
||||||
|
* SS is the constructor for the sacrificial-socket client
|
||||||
|
*
|
||||||
|
* @class SS
|
||||||
|
* @constructor
|
||||||
|
* @param {String} url - The url to the sacrificial-socket server endpoint. The url must conform to the websocket URI Scheme ("ws" or "wss")
|
||||||
|
*/
|
||||||
|
var SS = function(url){
|
||||||
|
var self = this,
|
||||||
|
ws = new WebSocket(url, 'sac-sock'),
|
||||||
|
events = {},
|
||||||
|
headerStartCharCode = 1,
|
||||||
|
headerStartChar = String.fromCharCode(headerStartCharCode),
|
||||||
|
dataStartCharCode = 2,
|
||||||
|
dataStartChar = String.fromCharCode(dataStartCharCode);
|
||||||
|
|
||||||
|
//sorry, only supporting arraybuffer at this time
|
||||||
|
//maybe if there is demand for it, I'll add Blob support
|
||||||
|
ws.binaryType = 'arraybuffer';
|
||||||
|
|
||||||
|
//Parses all incoming messages and dispatches their payload to the appropriate eventName if one has been registered. Messages received for unregistered events will be ignored.
|
||||||
|
ws.onmessage = function(e){
|
||||||
|
var msg = e.data,
|
||||||
|
headers = {},
|
||||||
|
eventName = '',
|
||||||
|
data = '',
|
||||||
|
chr = null,
|
||||||
|
i, msgLen;
|
||||||
|
|
||||||
|
if(typeof msg === 'string'){
|
||||||
|
var dataStarted = false,
|
||||||
|
headerStarted = false;
|
||||||
|
|
||||||
|
for(i = 0, msgLen = msg.length; i < msgLen; i++){
|
||||||
|
chr = msg[i];
|
||||||
|
if(!dataStarted && !headerStarted && chr !== dataStartChar && chr !== headerStartChar){
|
||||||
|
eventName += chr;
|
||||||
|
}else if(!headerStarted && chr === headerStartChar){
|
||||||
|
headerStarted = true;
|
||||||
|
}else if(headerStarted && !dataStarted && chr !== dataStartChar){
|
||||||
|
headers[chr] = true;
|
||||||
|
}else if(!dataStarted && chr === dataStartChar){
|
||||||
|
dataStarted = true;
|
||||||
|
}else{
|
||||||
|
data += chr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}else if(msg && msg instanceof ArrayBuffer && msg.byteLength !== undefined){
|
||||||
|
var dv = new DataView(msg),
|
||||||
|
headersStarted = false;
|
||||||
|
|
||||||
|
for(i = 0, msgLen = dv.byteLength; i < msgLen; i++){
|
||||||
|
chr = dv.getUint8(i);
|
||||||
|
|
||||||
|
if(chr !== dataStartCharCode && chr !== headerStartCharCode && !headersStarted){
|
||||||
|
eventName += String.fromCharCode(chr);
|
||||||
|
}else if(chr === headerStartCharCode && !headersStarted){
|
||||||
|
headersStarted = true;
|
||||||
|
}else if(headersStarted && chr !== dataStartCharCode){
|
||||||
|
headers[String.fromCharCode(chr)] = true;
|
||||||
|
}else if(chr === dataStartCharCode){
|
||||||
|
data = dv.buffer.slice(i+1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(eventName.length === 0) return; //no event to dispatch
|
||||||
|
if(typeof events[eventName] === 'undefined') return;
|
||||||
|
events[eventName]((headers.J) ? JSON.parse(data) : data);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onConnect registers a callback to be run when the websocket connection is open.
|
||||||
|
*
|
||||||
|
* @method onConnect
|
||||||
|
* @param {Function} callback(SS) - The callback that will be executed when the websocket connection opens.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
self.onConnect = function(callback){
|
||||||
|
ws.onopen = function(){ callback(self); };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onDisconnect registers a callback to be run when the websocket connection is closed.
|
||||||
|
*
|
||||||
|
* @method onDisconnect
|
||||||
|
* @param {Function} callback(SS) - The callback that will be executed when the websocket connection is closed.
|
||||||
|
*/
|
||||||
|
self.onDisconnect = function(callback){
|
||||||
|
ws.onclose = function(){ callback(self); };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* on registers an event to be called when the client receives an emit from the server for
|
||||||
|
* the given eventName.
|
||||||
|
*
|
||||||
|
* @method on
|
||||||
|
* @param {String} eventName - The name of the event being registerd
|
||||||
|
* @param {Function} callback(payload) - The callback that will be ran whenever the client receives an emit from the server for the given eventName. The payload passed into callback may be of type String, Object, or ArrayBuffer
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
self.on = function(eventName, callback){
|
||||||
|
events[eventName] = callback;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* off unregisters an emit event
|
||||||
|
*
|
||||||
|
* @method off
|
||||||
|
* @param {String} eventName - The name of event being unregistered
|
||||||
|
*/
|
||||||
|
self.off = function(eventName){
|
||||||
|
if(events[eventName]){
|
||||||
|
delete events[eventName];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* emit dispatches an event to the server
|
||||||
|
*
|
||||||
|
* @method emit
|
||||||
|
* @param {String} eventName - The event to dispatch
|
||||||
|
* @param {String|Object|ArrayBuffer} data - The data to be sent to the server. If data is a string then it will be sent as a normal string to the server. If data is an object it will be converted to JSON before being sent to the server. If data is an ArrayBuffer then it will be sent to the server as a uint8 binary payload.
|
||||||
|
*/
|
||||||
|
self.emit = function(eventName, data){
|
||||||
|
var rs = ws.readyState;
|
||||||
|
if(rs === 0){
|
||||||
|
console.warn("websocket is not open yet");
|
||||||
|
return;
|
||||||
|
}else if(rs === 2 || rs === 3){
|
||||||
|
console.error("websocket is closed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var msg = '';
|
||||||
|
if(data instanceof ArrayBuffer){
|
||||||
|
var ab = new ArrayBuffer(data.byteLength+eventName.length+1),
|
||||||
|
newBuf = new DataView(ab),
|
||||||
|
oldBuf = new DataView(data),
|
||||||
|
i = 0;
|
||||||
|
for(var evtLen = eventName.length; i < evtLen; i++){
|
||||||
|
newBuf.setUint8(i, eventName.charCodeAt(i));
|
||||||
|
}
|
||||||
|
newBuf.setUint8(i, dataStartCharCode);
|
||||||
|
i++;
|
||||||
|
for(var x = 0, xLen = oldBuf.byteLength; x < xLen; x++, i++){
|
||||||
|
newBuf.setUint8(i, oldBuf.getUint8(x));
|
||||||
|
}
|
||||||
|
msg = ab;
|
||||||
|
}else if(typeof data === 'object'){
|
||||||
|
msg = eventName+dataStartChar+JSON.stringify(data);
|
||||||
|
}else{
|
||||||
|
msg = eventName+dataStartChar+data;
|
||||||
|
}
|
||||||
|
ws.send(msg);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* close will close the websocket connection, calling the "onDisconnect" event if one has been registered.
|
||||||
|
*
|
||||||
|
* @method close
|
||||||
|
*/
|
||||||
|
self.close = function(){
|
||||||
|
return ws.close();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
window.SS = SS;
|
||||||
|
})(window);
|
41
examples/simple-examples/chat/main.go
Normal file
41
examples/simple-examples/chat/main.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
ss "github.com/raz-varren/sacrificial-socket"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
s := ss.NewServer()
|
||||||
|
|
||||||
|
s.On("join", join)
|
||||||
|
s.On("message", message)
|
||||||
|
|
||||||
|
http.Handle("/socket", s.WebHandler())
|
||||||
|
http.Handle("/", http.FileServer(http.Dir("webroot")))
|
||||||
|
|
||||||
|
log.Fatalln(http.ListenAndServe(":80", nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func join(s *ss.Socket, data []byte) {
|
||||||
|
//just one room at a time for the simple example
|
||||||
|
currentRooms := s.GetRooms()
|
||||||
|
for _, room := range currentRooms {
|
||||||
|
s.Leave(room)
|
||||||
|
}
|
||||||
|
s.Join(string(data))
|
||||||
|
s.Emit("joinedRoom", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
type msg struct {
|
||||||
|
Room string
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func message(s *ss.Socket, data []byte) {
|
||||||
|
var m msg
|
||||||
|
json.Unmarshal(data, &m)
|
||||||
|
s.Roomcast(m.Room, "message", m.Message)
|
||||||
|
}
|
27
examples/simple-examples/chat/webroot/css/app.css
Normal file
27
examples/simple-examples/chat/webroot/css/app.css
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
|
||||||
|
#container{
|
||||||
|
width: 400px;
|
||||||
|
margin: 30px auto 0px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#messages{
|
||||||
|
height: 200px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-item{
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#message-send-btn,
|
||||||
|
#clear-messages-btn{
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#message-list{
|
||||||
|
padding-left: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item{
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
52
examples/simple-examples/chat/webroot/index.html
Normal file
52
examples/simple-examples/chat/webroot/index.html
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Sacrificial-Socket Chat Client</title>
|
||||||
|
<link type="text/css" rel="stylesheet" href="/css/app.css">
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
|
||||||
|
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="container">
|
||||||
|
<div id="messages" class="well well-sm">
|
||||||
|
<ul id="message-list"></ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="controls">
|
||||||
|
<div class="control-item">
|
||||||
|
<div id="clear-messages-btn" class="btn btn-default">Clear Messages</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-item">
|
||||||
|
<label for="name-input">Name:</label>
|
||||||
|
<input id="name-input" class="form-control" type="text" value="stranger" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-item">
|
||||||
|
<label for="join-room-input">Join Chat Room:</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input id="join-room-input" class="form-control" type="text" value="global" />
|
||||||
|
<div class="input-group-btn">
|
||||||
|
<div id="join-room-btn" class="btn btn-info">Join</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-item">
|
||||||
|
<textarea id="message-body-input" class="form-control" placeholder="Type message here"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-item">
|
||||||
|
<div id="message-send-btn" class="btn btn-success">Send Message</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="text/javascript" src="/js/sacrificial-socket.js"></script>
|
||||||
|
<script type="text/javascript" src="/js/app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
69
examples/simple-examples/chat/webroot/js/app.js
Normal file
69
examples/simple-examples/chat/webroot/js/app.js
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
(function(SS){ 'use strict';
|
||||||
|
var msgCnt = document.querySelector('#messages'),
|
||||||
|
msgList = document.querySelector('#message-list'),
|
||||||
|
msgClear = document.querySelector('#clear-messages-btn'),
|
||||||
|
nameInput = document.querySelector('#name-input'),
|
||||||
|
joinInput = document.querySelector('#join-room-input'),
|
||||||
|
joinBtn = document.querySelector('#join-room-btn'),
|
||||||
|
msgBody = document.querySelector('#message-body-input'),
|
||||||
|
send = document.querySelector('#message-send-btn'),
|
||||||
|
currentRoom = null,
|
||||||
|
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
|
||||||
|
|
||||||
|
getTime = function(dt) {
|
||||||
|
var hours = dt.getHours(),
|
||||||
|
minutes = dt.getMinutes(),
|
||||||
|
ampm = hours >= 12 ? 'pm' : 'am';
|
||||||
|
hours %= 12;
|
||||||
|
hours = hours ? hours : 12; // the hour '0' should be '12'
|
||||||
|
minutes = minutes < 10 ? '0'+minutes : minutes;
|
||||||
|
var strTime = hours + ':' + minutes + ' ' + ampm;
|
||||||
|
return strTime;
|
||||||
|
},
|
||||||
|
addMsg = function(msg){
|
||||||
|
var li = document.createElement('li'),
|
||||||
|
dt = new Date(),
|
||||||
|
dtString = months[dt.getMonth()]+' '+dt.getDate()+', '+dt.getFullYear();
|
||||||
|
li.innerText = dtString+' '+getTime(dt)+' - '+msg;
|
||||||
|
msgList.appendChild(li);
|
||||||
|
msgCnt.scrollTop = msgCnt.scrollHeight;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
var ss = new SS('ws://'+window.location.host+'/socket');
|
||||||
|
|
||||||
|
ss.onConnect(function(){
|
||||||
|
ss.emit('join', joinInput.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
ss.onDisconnect(function(){
|
||||||
|
alert('chat disconnected');
|
||||||
|
});
|
||||||
|
|
||||||
|
ss.on('joinedRoom', function(room){
|
||||||
|
currentRoom = room;
|
||||||
|
addMsg('joined room: '+room);
|
||||||
|
});
|
||||||
|
|
||||||
|
ss.on('message', function(msg){
|
||||||
|
addMsg(msg);
|
||||||
|
});
|
||||||
|
|
||||||
|
send.addEventListener('click', function(){
|
||||||
|
var msg = msgBody.value;
|
||||||
|
if(msg.length === 0) return;
|
||||||
|
|
||||||
|
ss.emit('message', {Room: currentRoom, Message: nameInput.value+' says: '+msg});
|
||||||
|
});
|
||||||
|
|
||||||
|
joinBtn.addEventListener('click', function(){
|
||||||
|
var room = joinInput.value;
|
||||||
|
if(room.length === 0) return;
|
||||||
|
ss.emit('join', joinInput.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
msgClear.addEventListener('click', function(){
|
||||||
|
msgList.innerHTML = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
})(window.SS);
|
170
examples/simple-examples/chat/webroot/js/sacrificial-socket.js
Normal file
170
examples/simple-examples/chat/webroot/js/sacrificial-socket.js
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
(function(window){ 'use strict';
|
||||||
|
/**
|
||||||
|
* SS is the constructor for the sacrificial-socket client
|
||||||
|
*
|
||||||
|
* @class SS
|
||||||
|
* @constructor
|
||||||
|
* @param {String} url - The url to the sacrificial-socket server endpoint. The url must conform to the websocket URI Scheme ("ws" or "wss")
|
||||||
|
*/
|
||||||
|
var SS = function(url){
|
||||||
|
var self = this,
|
||||||
|
ws = new WebSocket(url, 'sac-sock'),
|
||||||
|
events = {},
|
||||||
|
headerStartCharCode = 1,
|
||||||
|
headerStartChar = String.fromCharCode(headerStartCharCode),
|
||||||
|
dataStartCharCode = 2,
|
||||||
|
dataStartChar = String.fromCharCode(dataStartCharCode);
|
||||||
|
|
||||||
|
//sorry, only supporting arraybuffer at this time
|
||||||
|
//maybe if there is demand for it, I'll add Blob support
|
||||||
|
ws.binaryType = 'arraybuffer';
|
||||||
|
|
||||||
|
//Parses all incoming messages and dispatches their payload to the appropriate eventName if one has been registered. Messages received for unregistered events will be ignored.
|
||||||
|
ws.onmessage = function(e){
|
||||||
|
var msg = e.data,
|
||||||
|
headers = {},
|
||||||
|
eventName = '',
|
||||||
|
data = '',
|
||||||
|
chr = null,
|
||||||
|
i, msgLen;
|
||||||
|
|
||||||
|
if(typeof msg === 'string'){
|
||||||
|
var dataStarted = false,
|
||||||
|
headerStarted = false;
|
||||||
|
|
||||||
|
for(i = 0, msgLen = msg.length; i < msgLen; i++){
|
||||||
|
chr = msg[i];
|
||||||
|
if(!dataStarted && !headerStarted && chr !== dataStartChar && chr !== headerStartChar){
|
||||||
|
eventName += chr;
|
||||||
|
}else if(!headerStarted && chr === headerStartChar){
|
||||||
|
headerStarted = true;
|
||||||
|
}else if(headerStarted && !dataStarted && chr !== dataStartChar){
|
||||||
|
headers[chr] = true;
|
||||||
|
}else if(!dataStarted && chr === dataStartChar){
|
||||||
|
dataStarted = true;
|
||||||
|
}else{
|
||||||
|
data += chr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}else if(msg && msg instanceof ArrayBuffer && msg.byteLength !== undefined){
|
||||||
|
var dv = new DataView(msg),
|
||||||
|
headersStarted = false;
|
||||||
|
|
||||||
|
for(i = 0, msgLen = dv.byteLength; i < msgLen; i++){
|
||||||
|
chr = dv.getUint8(i);
|
||||||
|
|
||||||
|
if(chr !== dataStartCharCode && chr !== headerStartCharCode && !headersStarted){
|
||||||
|
eventName += String.fromCharCode(chr);
|
||||||
|
}else if(chr === headerStartCharCode && !headersStarted){
|
||||||
|
headersStarted = true;
|
||||||
|
}else if(headersStarted && chr !== dataStartCharCode){
|
||||||
|
headers[String.fromCharCode(chr)] = true;
|
||||||
|
}else if(chr === dataStartCharCode){
|
||||||
|
data = dv.buffer.slice(i+1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(eventName.length === 0) return; //no event to dispatch
|
||||||
|
if(typeof events[eventName] === 'undefined') return;
|
||||||
|
events[eventName]((headers.J) ? JSON.parse(data) : data);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onConnect registers a callback to be run when the websocket connection is open.
|
||||||
|
*
|
||||||
|
* @method onConnect
|
||||||
|
* @param {Function} callback(SS) - The callback that will be executed when the websocket connection opens.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
self.onConnect = function(callback){
|
||||||
|
ws.onopen = function(){ callback(self); };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onDisconnect registers a callback to be run when the websocket connection is closed.
|
||||||
|
*
|
||||||
|
* @method onDisconnect
|
||||||
|
* @param {Function} callback(SS) - The callback that will be executed when the websocket connection is closed.
|
||||||
|
*/
|
||||||
|
self.onDisconnect = function(callback){
|
||||||
|
ws.onclose = function(){ callback(self); };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* on registers an event to be called when the client receives an emit from the server for
|
||||||
|
* the given eventName.
|
||||||
|
*
|
||||||
|
* @method on
|
||||||
|
* @param {String} eventName - The name of the event being registerd
|
||||||
|
* @param {Function} callback(payload) - The callback that will be ran whenever the client receives an emit from the server for the given eventName. The payload passed into callback may be of type String, Object, or ArrayBuffer
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
self.on = function(eventName, callback){
|
||||||
|
events[eventName] = callback;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* off unregisters an emit event
|
||||||
|
*
|
||||||
|
* @method off
|
||||||
|
* @param {String} eventName - The name of event being unregistered
|
||||||
|
*/
|
||||||
|
self.off = function(eventName){
|
||||||
|
if(events[eventName]){
|
||||||
|
delete events[eventName];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* emit dispatches an event to the server
|
||||||
|
*
|
||||||
|
* @method emit
|
||||||
|
* @param {String} eventName - The event to dispatch
|
||||||
|
* @param {String|Object|ArrayBuffer} data - The data to be sent to the server. If data is a string then it will be sent as a normal string to the server. If data is an object it will be converted to JSON before being sent to the server. If data is an ArrayBuffer then it will be sent to the server as a uint8 binary payload.
|
||||||
|
*/
|
||||||
|
self.emit = function(eventName, data){
|
||||||
|
var rs = ws.readyState;
|
||||||
|
if(rs === 0){
|
||||||
|
console.warn("websocket is not open yet");
|
||||||
|
return;
|
||||||
|
}else if(rs === 2 || rs === 3){
|
||||||
|
console.error("websocket is closed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var msg = '';
|
||||||
|
if(data instanceof ArrayBuffer){
|
||||||
|
var ab = new ArrayBuffer(data.byteLength+eventName.length+1),
|
||||||
|
newBuf = new DataView(ab),
|
||||||
|
oldBuf = new DataView(data),
|
||||||
|
i = 0;
|
||||||
|
for(var evtLen = eventName.length; i < evtLen; i++){
|
||||||
|
newBuf.setUint8(i, eventName.charCodeAt(i));
|
||||||
|
}
|
||||||
|
newBuf.setUint8(i, dataStartCharCode);
|
||||||
|
i++;
|
||||||
|
for(var x = 0, xLen = oldBuf.byteLength; x < xLen; x++, i++){
|
||||||
|
newBuf.setUint8(i, oldBuf.getUint8(x));
|
||||||
|
}
|
||||||
|
msg = ab;
|
||||||
|
}else if(typeof data === 'object'){
|
||||||
|
msg = eventName+dataStartChar+JSON.stringify(data);
|
||||||
|
}else{
|
||||||
|
msg = eventName+dataStartChar+data;
|
||||||
|
}
|
||||||
|
ws.send(msg);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* close will close the websocket connection, calling the "onDisconnect" event if one has been registered.
|
||||||
|
*
|
||||||
|
* @method close
|
||||||
|
*/
|
||||||
|
self.close = function(){
|
||||||
|
return ws.close();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
window.SS = SS;
|
||||||
|
})(window);
|
209
hub.go
Normal file
209
hub.go
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
package ss
|
||||||
|
|
||||||
|
type socketHub struct {
|
||||||
|
sockets map[string]*Socket
|
||||||
|
rooms map[string]*room
|
||||||
|
|
||||||
|
shutdownCh chan bool
|
||||||
|
socketList chan []*Socket
|
||||||
|
addCh chan *Socket
|
||||||
|
delCh chan *Socket
|
||||||
|
joinRoomCh chan *joinRequest
|
||||||
|
leaveRoomCh chan *leaveRequest
|
||||||
|
roomMsgCh chan *RoomMsg
|
||||||
|
broomcastCh chan *RoomMsg //for passing data from the backend
|
||||||
|
broadcastCh chan *BroadcastMsg
|
||||||
|
bbroadcastCh chan *BroadcastMsg
|
||||||
|
multihomeEnabled bool
|
||||||
|
multihomeBackend MultihomeBackend
|
||||||
|
}
|
||||||
|
|
||||||
|
type room struct {
|
||||||
|
name string
|
||||||
|
sockets map[string]*Socket
|
||||||
|
}
|
||||||
|
|
||||||
|
type joinRequest struct {
|
||||||
|
roomName string
|
||||||
|
socket *Socket
|
||||||
|
}
|
||||||
|
|
||||||
|
type leaveRequest struct {
|
||||||
|
roomName string
|
||||||
|
socket *Socket
|
||||||
|
}
|
||||||
|
|
||||||
|
//RoomMsg represents an event to be dispatched to a room of sockets
|
||||||
|
type RoomMsg struct {
|
||||||
|
RoomName string
|
||||||
|
EventName string
|
||||||
|
Data interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
//BroadcastMsg represents an event to be dispatched to all Sockets on the SocketServer
|
||||||
|
type BroadcastMsg struct {
|
||||||
|
EventName string
|
||||||
|
Data interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *socketHub) addSocket(s *Socket) {
|
||||||
|
h.addCh <- s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *socketHub) removeSocket(s *Socket) {
|
||||||
|
h.delCh <- s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *socketHub) joinRoom(j *joinRequest) {
|
||||||
|
h.joinRoomCh <- j
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *socketHub) leaveRoom(l *leaveRequest) {
|
||||||
|
h.leaveRoomCh <- l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *socketHub) roomcast(msg *RoomMsg) {
|
||||||
|
h.roomMsgCh <- msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *socketHub) broadcast(b *BroadcastMsg) {
|
||||||
|
h.broadcastCh <- b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *socketHub) setMultihomeBackend(b MultihomeBackend) {
|
||||||
|
if h.multihomeEnabled {
|
||||||
|
return //can't have two backends... yet
|
||||||
|
}
|
||||||
|
|
||||||
|
h.multihomeBackend = b
|
||||||
|
h.multihomeEnabled = true
|
||||||
|
|
||||||
|
h.multihomeBackend.Init()
|
||||||
|
|
||||||
|
go h.multihomeBackend.BroadcastFromBackend(h.bbroadcastCh)
|
||||||
|
go h.multihomeBackend.RoomcastFromBackend(h.broomcastCh)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *socketHub) listen() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case c := <-h.addCh:
|
||||||
|
h.sockets[c.ID()] = c
|
||||||
|
case c := <-h.delCh:
|
||||||
|
delete(h.sockets, c.ID())
|
||||||
|
case c := <-h.joinRoomCh:
|
||||||
|
if _, exists := h.rooms[c.roomName]; !exists { //make the room if it doesn't exist
|
||||||
|
h.rooms[c.roomName] = &room{c.roomName, make(map[string]*Socket)}
|
||||||
|
}
|
||||||
|
h.rooms[c.roomName].sockets[c.socket.ID()] = c.socket
|
||||||
|
case c := <-h.leaveRoomCh:
|
||||||
|
if room, exists := h.rooms[c.roomName]; exists {
|
||||||
|
delete(room.sockets, c.socket.ID())
|
||||||
|
if len(room.sockets) == 0 { //room is empty, delete it
|
||||||
|
delete(h.rooms, c.roomName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case c := <-h.roomMsgCh:
|
||||||
|
if room, exists := h.rooms[c.RoomName]; exists {
|
||||||
|
for _, s := range room.sockets {
|
||||||
|
s.Emit(c.EventName, c.Data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if h.multihomeEnabled { //the room may exist on the other end
|
||||||
|
go h.multihomeBackend.RoomcastToBackend(c)
|
||||||
|
}
|
||||||
|
case c := <-h.broomcastCh:
|
||||||
|
if room, exists := h.rooms[c.RoomName]; exists {
|
||||||
|
for _, s := range room.sockets {
|
||||||
|
s.Emit(c.EventName, c.Data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case c := <-h.broadcastCh:
|
||||||
|
for _, s := range h.sockets {
|
||||||
|
s.Emit(c.EventName, c.Data)
|
||||||
|
}
|
||||||
|
if h.multihomeEnabled {
|
||||||
|
go h.multihomeBackend.BroadcastToBackend(c)
|
||||||
|
}
|
||||||
|
case c := <-h.bbroadcastCh:
|
||||||
|
for _, s := range h.sockets {
|
||||||
|
s.Emit(c.EventName, c.Data)
|
||||||
|
}
|
||||||
|
case _ = <-h.shutdownCh:
|
||||||
|
var socketList []*Socket
|
||||||
|
for _, s := range h.sockets {
|
||||||
|
socketList = append(socketList, s)
|
||||||
|
}
|
||||||
|
h.socketList <- socketList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHub() *socketHub {
|
||||||
|
h := &socketHub{
|
||||||
|
shutdownCh: make(chan bool),
|
||||||
|
socketList: make(chan []*Socket),
|
||||||
|
sockets: make(map[string]*Socket),
|
||||||
|
rooms: make(map[string]*room),
|
||||||
|
addCh: make(chan *Socket),
|
||||||
|
delCh: make(chan *Socket),
|
||||||
|
joinRoomCh: make(chan *joinRequest),
|
||||||
|
leaveRoomCh: make(chan *leaveRequest),
|
||||||
|
roomMsgCh: make(chan *RoomMsg),
|
||||||
|
broomcastCh: make(chan *RoomMsg),
|
||||||
|
broadcastCh: make(chan *BroadcastMsg),
|
||||||
|
bbroadcastCh: make(chan *BroadcastMsg),
|
||||||
|
multihomeEnabled: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
go h.listen()
|
||||||
|
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
//MultihomeBackend is an interface for implementing a mechanism
|
||||||
|
//to syncronize Broadcasts and Roomcasts to multiple SocketServers
|
||||||
|
//running separate machines.
|
||||||
|
//
|
||||||
|
//Sacrificial-Socket provides a MultihomeBackend for use with MongoDB
|
||||||
|
//in sacrificial-socket/backend
|
||||||
|
type MultihomeBackend interface {
|
||||||
|
//Init is called as soon as the MultihomeBackend is
|
||||||
|
//registered using SocketServer.SetMultihomeBackend
|
||||||
|
Init()
|
||||||
|
|
||||||
|
//Shutdown is called immediately after all sockets have
|
||||||
|
//been closed
|
||||||
|
Shutdown()
|
||||||
|
|
||||||
|
//BroadcastToBackend is called everytime a BroadcastMsg is
|
||||||
|
//sent by a Socket
|
||||||
|
//
|
||||||
|
//BroadcastToBackend must be safe for concurrent use by multiple
|
||||||
|
//go routines
|
||||||
|
BroadcastToBackend(*BroadcastMsg)
|
||||||
|
|
||||||
|
//RoomcastToBackend is called everytime a RoomMsg is sent
|
||||||
|
//by a socket, even if none of this server's sockets are
|
||||||
|
//members of that room
|
||||||
|
//
|
||||||
|
//RoomcastToBackend must be safe for concurrent use by multiple
|
||||||
|
//go routines
|
||||||
|
RoomcastToBackend(*RoomMsg)
|
||||||
|
|
||||||
|
//BroadcastFromBackend is called once and only once as a go routine as
|
||||||
|
//soon as the MultihomeBackend is registered using
|
||||||
|
//SocketServer.SetMultihomeBackend
|
||||||
|
//
|
||||||
|
//b consumes a BroadcastMsg and dispatches
|
||||||
|
//it to all sockets on this server
|
||||||
|
BroadcastFromBackend(b chan<- *BroadcastMsg)
|
||||||
|
|
||||||
|
//RoomcastFromBackend is called once and only once as a go routine as
|
||||||
|
//soon as the MultihomeBackend is registered using
|
||||||
|
//SocketServer.SetMultihomeBackend
|
||||||
|
//
|
||||||
|
//r consumes a RoomMsg and dispatches it to all sockets
|
||||||
|
//that are members the specified room
|
||||||
|
RoomcastFromBackend(r chan<- *RoomMsg)
|
||||||
|
}
|
16
log/log.go
Normal file
16
log/log.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
l "log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Err = l.New(os.Stderr, "ERROR: ", l.Ldate|l.Ltime|l.Lshortfile)
|
||||||
|
var Info = l.New(os.Stdout, "INFO: ", l.Ldate|l.Ltime)
|
||||||
|
|
||||||
|
func CheckFatal(err error) {
|
||||||
|
if err != nil {
|
||||||
|
Err.Output(2, err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
196
server.go
Normal file
196
server.go
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
/*
|
||||||
|
Package ss (Sacrificial-Socket) is a Go server library and pure JS client library for managing communication between websockets, that has an API similar to Socket.IO, but feels less... well, Javascripty. Socket.IO is great, but nowadays all modern browsers support websockets natively, so in most cases there is no need to have websocket simulation fallbacks like XHR long polling or Flash. Removing these allows Sacrificial-Socket to be lightweight and very performant.
|
||||||
|
|
||||||
|
Sacrificial-Socket supports rooms, roomcasts, broadcasts, and event emitting just like Socket.IO, but with one key difference. The data passed into event functions is not an interface{} that is implied to be a string or map[string]interface{}, but is always passed in as a []byte making it easier to unmarshal into your own JSON data structs, convert to a string, or keep as binary data without the need to check the data's type before processing it. It also means there aren't any unnecessary conversions to the data between the client and the server.
|
||||||
|
|
||||||
|
Sacrificial-Socket also has a MultihomeBackend interface for syncronizing broadcasts and roomcasts across multiple instances of Sacrificial-Socket running on multiple machines. Out of the box Sacrificial-Socket provides a MultihomeBackend interface for the popular noSQL database MongoDB, and one for the not so popular GRPC protocol, for syncronizing instances on multiple machines.
|
||||||
|
*/
|
||||||
|
package ss
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"golang.org/x/net/websocket"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"github.com/raz-varren/sacrificial-socket/log"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
const ( // ASCII chars
|
||||||
|
startOfHeaderByte uint8 = 1 //SOH
|
||||||
|
startOfDataByte = 2 //STX
|
||||||
|
|
||||||
|
SupportedSubProtocol string = "sac-sock"
|
||||||
|
)
|
||||||
|
|
||||||
|
type event struct {
|
||||||
|
eventName string
|
||||||
|
eventHandler func(*Socket, []byte)
|
||||||
|
}
|
||||||
|
|
||||||
|
//SocketServer manages the coordination between
|
||||||
|
//sockets, rooms, events and the socket hub
|
||||||
|
type SocketServer struct {
|
||||||
|
hub *socketHub
|
||||||
|
events map[string]*event
|
||||||
|
onConnectFunc func(*Socket)
|
||||||
|
onDisconnectFunc func(*Socket)
|
||||||
|
l *sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewServer creates a new instance of SocketServer
|
||||||
|
func NewServer() *SocketServer {
|
||||||
|
s := &SocketServer{
|
||||||
|
hub: newHub(),
|
||||||
|
events: make(map[string]*event),
|
||||||
|
l: &sync.RWMutex{},
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
//EnableSignalShutdown listens for linux syscalls SIGHUP, SIGINT, SIGTERM, SIGQUIT, SIGKILL and
|
||||||
|
//calls the SocketServer.Shutdown() to perform a clean shutdown. true will be passed into complete
|
||||||
|
//after the Shutdown proccess is finished
|
||||||
|
func (serv *SocketServer) EnableSignalShutdown(complete chan<- bool) {
|
||||||
|
c := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(c,
|
||||||
|
syscall.SIGHUP,
|
||||||
|
syscall.SIGINT,
|
||||||
|
syscall.SIGTERM,
|
||||||
|
syscall.SIGQUIT,
|
||||||
|
syscall.SIGKILL)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-c
|
||||||
|
complete <- serv.Shutdown()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
//Shutdown closes all active sockets and triggers the Shutdown()
|
||||||
|
//method on any MultihomeBackend that is currently set.
|
||||||
|
func (serv *SocketServer) Shutdown() bool {
|
||||||
|
log.Info.Println("shutting down")
|
||||||
|
//complete := serv.hub.shutdown()
|
||||||
|
|
||||||
|
serv.hub.shutdownCh <- true
|
||||||
|
socketList := <-serv.hub.socketList
|
||||||
|
|
||||||
|
for _, s := range socketList {
|
||||||
|
s.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
if serv.hub.multihomeEnabled {
|
||||||
|
log.Info.Println("shutting down multihome backend")
|
||||||
|
serv.hub.multihomeBackend.Shutdown()
|
||||||
|
log.Info.Println("backend shutdown")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info.Println("shutdown")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
//On registers event functions to be called on individual Socket connections
|
||||||
|
//when the server's socket receives an Emit from the client's socket.
|
||||||
|
//
|
||||||
|
//Any event functions registered with On, must be safe for concurrent use by multiple
|
||||||
|
//go routines
|
||||||
|
func (serv *SocketServer) On(eventName string, handleFunc func(*Socket, []byte)) {
|
||||||
|
serv.events[eventName] = &event{eventName, handleFunc} //you think you can handle the func?
|
||||||
|
}
|
||||||
|
|
||||||
|
//OnConnect registers an event function to be called whenever a new Socket connection
|
||||||
|
//is created
|
||||||
|
func (serv *SocketServer) OnConnect(handleFunc func(*Socket)) {
|
||||||
|
serv.onConnectFunc = handleFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
//OnDisconnect registers an event function to be called as soon as a Socket connection
|
||||||
|
//is closed
|
||||||
|
func (serv *SocketServer) OnDisconnect(handleFunc func(*Socket)) {
|
||||||
|
serv.onDisconnectFunc = handleFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
//WebHandler returns a http.Handler to be passed into http.Handle
|
||||||
|
func (serv *SocketServer) WebHandler() http.Handler {
|
||||||
|
return websocket.Server{
|
||||||
|
Handshake: func(c *websocket.Config, r *http.Request) error {
|
||||||
|
if !protocolSupported(c) {
|
||||||
|
return websocket.ErrBadWebSocketProtocol
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Handler: serv.loop,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//SetMultihomeBackend registers a MultihomeBackend interface and calls it's Init() method
|
||||||
|
func (serv *SocketServer) SetMultihomeBackend(b MultihomeBackend) {
|
||||||
|
serv.hub.setMultihomeBackend(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
//loop handles all the coordination between new sockets
|
||||||
|
//reading frames and dispatching events
|
||||||
|
func (serv *SocketServer) loop(ws *websocket.Conn) {
|
||||||
|
s := newSocket(serv, ws)
|
||||||
|
log.Info.Println(s.ID(), "connected")
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
serv.l.RLock()
|
||||||
|
e := serv.onConnectFunc
|
||||||
|
serv.l.RUnlock()
|
||||||
|
|
||||||
|
if e != nil {
|
||||||
|
e(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
var msg []byte
|
||||||
|
var eventName string
|
||||||
|
|
||||||
|
err := s.receive(&msg)
|
||||||
|
if err == io.EOF {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Err.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
eventNameRead := false
|
||||||
|
|
||||||
|
for _, chr := range msg {
|
||||||
|
if !eventNameRead && chr == startOfDataByte {
|
||||||
|
eventName = buf.String()
|
||||||
|
eventNameRead = true
|
||||||
|
buf.Reset()
|
||||||
|
} else {
|
||||||
|
buf.WriteByte(chr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !eventNameRead {
|
||||||
|
continue //no event to dispatch
|
||||||
|
}
|
||||||
|
|
||||||
|
serv.l.RLock()
|
||||||
|
e, exists := serv.events[eventName]
|
||||||
|
serv.l.RUnlock()
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
go e.eventHandler(s, buf.Bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func protocolSupported(conf *websocket.Config) bool {
|
||||||
|
for _, p := range conf.Protocol {
|
||||||
|
if p == SupportedSubProtocol {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
89
server_test.go
Normal file
89
server_test.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package ss_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"github.com/raz-varren/sacrificial-socket"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleNewServer() {
|
||||||
|
serv := ss.NewServer()
|
||||||
|
serv.On("echo", Echo)
|
||||||
|
serv.On("join", Join)
|
||||||
|
serv.On("leave", Leave)
|
||||||
|
serv.On("roomcast", Roomcast)
|
||||||
|
serv.On("broadcast", Broadcast)
|
||||||
|
|
||||||
|
done := make(chan bool)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
serv.EnableSignalShutdown(done)
|
||||||
|
<-done
|
||||||
|
os.Exit(0)
|
||||||
|
}()
|
||||||
|
|
||||||
|
http.Handle("/socket", s.WebHandler())
|
||||||
|
log.Fatalln(http.ListenAndServe(":8080", nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
type JoinJSON struct {
|
||||||
|
Room string
|
||||||
|
}
|
||||||
|
|
||||||
|
type LeaveJSON struct {
|
||||||
|
Room string
|
||||||
|
}
|
||||||
|
|
||||||
|
type RoomcastJSON struct {
|
||||||
|
Room, Event, Data string
|
||||||
|
}
|
||||||
|
|
||||||
|
type BroadcastJSON struct {
|
||||||
|
Event, Data string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Echo(s *ss.Socket, data []byte) {
|
||||||
|
s.Emit("echo", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Join(s *ss.Socket, data []byte) {
|
||||||
|
var j JoinJSON
|
||||||
|
err := json.Unmarshal(data, &j)
|
||||||
|
check(err)
|
||||||
|
|
||||||
|
s.Join(j.Room)
|
||||||
|
s.Emit("echo", "joined: "+j.Room)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Leave(s *ss.Socket, data []byte) {
|
||||||
|
var l LeaveJSON
|
||||||
|
err := json.Unmarshal(data, &l)
|
||||||
|
check(err)
|
||||||
|
|
||||||
|
s.Leave(l.Room)
|
||||||
|
s.Emit("echo", "left: "+l.Room)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Broadcast(s *ss.Socket, data []byte) {
|
||||||
|
var b BroadcastJSON
|
||||||
|
err := json.Unmarshal(data, &b)
|
||||||
|
check(err)
|
||||||
|
|
||||||
|
s.Broadcast(b.Event, b.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Roomcast(s *ss.Socket, data []byte) {
|
||||||
|
var r RoomcastJSON
|
||||||
|
err := json.Unmarshal(data, &r)
|
||||||
|
check(err)
|
||||||
|
|
||||||
|
s.Roomcast(r.Room, r.Event, r.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func check(err error) {
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
}
|
206
socket.go
Normal file
206
socket.go
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
package ss
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"golang.org/x/net/websocket"
|
||||||
|
"github.com/raz-varren/sacrificial-socket/log"
|
||||||
|
"github.com/raz-varren/sacrificial-socket/tools"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
//Socket represents a websocket connection
|
||||||
|
type Socket struct {
|
||||||
|
l *sync.RWMutex
|
||||||
|
id string
|
||||||
|
ws *websocket.Conn
|
||||||
|
closed bool
|
||||||
|
serv *SocketServer
|
||||||
|
roomsl *sync.RWMutex
|
||||||
|
rooms map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
idLen int = 32
|
||||||
|
|
||||||
|
typeJSON string = "J"
|
||||||
|
typeBin = "B"
|
||||||
|
typeStr = "S"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
idChars = []string{
|
||||||
|
"0", "1", "2", "3", "4",
|
||||||
|
"5", "6", "7", "8", "9",
|
||||||
|
"A", "B", "C", "D", "E",
|
||||||
|
"F", "G", "H", "I", "J",
|
||||||
|
"K", "L", "M", "N", "O",
|
||||||
|
"P", "Q", "R", "S", "T",
|
||||||
|
"U", "V", "W", "X", "Y",
|
||||||
|
"Z", "a", "b", "c", "d",
|
||||||
|
"e", "f", "g", "h", "i",
|
||||||
|
"j", "k", "l", "m", "n",
|
||||||
|
"o", "p", "q", "r", "s",
|
||||||
|
"t", "u", "v", "w", "x",
|
||||||
|
"y", "z", "=", "_", "-",
|
||||||
|
"#", ".",
|
||||||
|
}
|
||||||
|
|
||||||
|
idCharLen int = len(idChars) - 1
|
||||||
|
)
|
||||||
|
|
||||||
|
func newSocket(serv *SocketServer, ws *websocket.Conn) *Socket {
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
for i := 0; i < idLen; i++ {
|
||||||
|
buf.WriteString(idChars[tools.RandomInt(0, idCharLen)])
|
||||||
|
}
|
||||||
|
s := &Socket{
|
||||||
|
l: &sync.RWMutex{},
|
||||||
|
id: buf.String(),
|
||||||
|
ws: ws,
|
||||||
|
closed: false,
|
||||||
|
serv: serv,
|
||||||
|
roomsl: &sync.RWMutex{},
|
||||||
|
rooms: make(map[string]bool),
|
||||||
|
}
|
||||||
|
serv.hub.addSocket(s)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Socket) receive(v interface{}) error {
|
||||||
|
return websocket.Message.Receive(s.ws, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Socket) send(data interface{}) error {
|
||||||
|
return websocket.Message.Send(s.ws, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Socket) InRoom(roomName string) bool {
|
||||||
|
s.roomsl.RLock()
|
||||||
|
defer s.roomsl.RUnlock()
|
||||||
|
inRoom := s.rooms[roomName]
|
||||||
|
return inRoom
|
||||||
|
}
|
||||||
|
|
||||||
|
//GetRooms returns a list of rooms that s is a member of
|
||||||
|
func (s *Socket) GetRooms() []string {
|
||||||
|
s.roomsl.RLock()
|
||||||
|
defer s.roomsl.RUnlock()
|
||||||
|
|
||||||
|
var roomList []string
|
||||||
|
for room, _ := range s.rooms {
|
||||||
|
roomList = append(roomList, room)
|
||||||
|
}
|
||||||
|
return roomList
|
||||||
|
}
|
||||||
|
|
||||||
|
//Join adds s to the specified room. If the room does
|
||||||
|
//not exist, it will be created
|
||||||
|
func (s *Socket) Join(roomName string) {
|
||||||
|
s.roomsl.Lock()
|
||||||
|
defer s.roomsl.Unlock()
|
||||||
|
s.serv.hub.joinRoom(&joinRequest{roomName, s})
|
||||||
|
s.rooms[roomName] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
//Leave removes s from the specified room. If s
|
||||||
|
//is not a member of the room, nothing will happen. If the room is
|
||||||
|
//empty upon removal of s, the room will be closed
|
||||||
|
func (s *Socket) Leave(roomName string) {
|
||||||
|
s.roomsl.Lock()
|
||||||
|
defer s.roomsl.Unlock()
|
||||||
|
s.serv.hub.leaveRoom(&leaveRequest{roomName, s})
|
||||||
|
delete(s.rooms, roomName)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Roomcast dispatches an event to all Sockets in the specified room.
|
||||||
|
func (s *Socket) Roomcast(roomName, eventName string, data interface{}) {
|
||||||
|
s.serv.hub.roomcast(&RoomMsg{roomName, eventName, data})
|
||||||
|
}
|
||||||
|
|
||||||
|
//Broadcast dispatches an event to all Sockets on the SocketServer.
|
||||||
|
func (s *Socket) Broadcast(eventName string, data interface{}) {
|
||||||
|
s.serv.hub.broadcast(&BroadcastMsg{eventName, data})
|
||||||
|
}
|
||||||
|
|
||||||
|
//Emit dispatches an event to s.
|
||||||
|
func (s *Socket) Emit(eventName string, data interface{}) error {
|
||||||
|
return s.send(emitData(eventName, data))
|
||||||
|
}
|
||||||
|
|
||||||
|
//ID returns the unique ID of s
|
||||||
|
func (s *Socket) ID() string {
|
||||||
|
s.l.RLock()
|
||||||
|
defer s.l.RUnlock()
|
||||||
|
id := s.id
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
//emitData combines the eventName and data into a payload that is understood
|
||||||
|
//by the sac-sock protocol. It will return either a string or a []byte
|
||||||
|
func emitData(eventName string, data interface{}) interface{} {
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
buf.WriteString(eventName)
|
||||||
|
buf.WriteByte(startOfHeaderByte)
|
||||||
|
|
||||||
|
switch d := data.(type) {
|
||||||
|
case string:
|
||||||
|
buf.WriteString(typeStr)
|
||||||
|
buf.WriteByte(startOfDataByte)
|
||||||
|
buf.WriteString(d)
|
||||||
|
return buf.String()
|
||||||
|
|
||||||
|
case []byte:
|
||||||
|
buf.WriteString(typeBin)
|
||||||
|
buf.WriteByte(startOfDataByte)
|
||||||
|
buf.Write(d)
|
||||||
|
return buf.Bytes()
|
||||||
|
|
||||||
|
default:
|
||||||
|
buf.WriteString(typeJSON)
|
||||||
|
buf.WriteByte(startOfDataByte)
|
||||||
|
jsonData, err := json.Marshal(d)
|
||||||
|
if err != nil {
|
||||||
|
log.Err.Println(err)
|
||||||
|
} else {
|
||||||
|
buf.Write(jsonData)
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Close closes the Socket connection and removes the Socket
|
||||||
|
//from any rooms that it was a member of
|
||||||
|
func (s *Socket) Close() {
|
||||||
|
s.l.Lock()
|
||||||
|
isAlreadyClosed := s.closed
|
||||||
|
s.closed = true
|
||||||
|
s.l.Unlock()
|
||||||
|
|
||||||
|
if isAlreadyClosed { //can't reclose the socket
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer log.Info.Println(s.ID(), "disconnected")
|
||||||
|
|
||||||
|
err := s.ws.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Err.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rooms := s.GetRooms()
|
||||||
|
|
||||||
|
for _, room := range rooms {
|
||||||
|
s.Leave(room)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.serv.l.RLock()
|
||||||
|
event := s.serv.onDisconnectFunc
|
||||||
|
s.serv.l.RUnlock()
|
||||||
|
|
||||||
|
if event != nil {
|
||||||
|
event(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.serv.hub.removeSocket(s)
|
||||||
|
}
|
16
tools/random.go
Normal file
16
tools/random.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RandomInt(min, max int) int {
|
||||||
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
return rand.Intn(max-min+1) + min
|
||||||
|
}
|
||||||
|
|
||||||
|
func RandomInt64(min, max int64) int64 {
|
||||||
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
return rand.Int63n(max-min+1) + min
|
||||||
|
}
|
Reference in New Issue
Block a user