let there be websockets

This commit is contained in:
raz-varren
2016-08-09 00:48:14 +00:00
commit 1877a4f174
32 changed files with 3312 additions and 0 deletions

21
LICENSE Normal file
View 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
View 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
View 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
View 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
View 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
View 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())
}

View 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
}

View 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,
}

View 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
View 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
View 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
}
}

View 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);

View File

@@ -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-----

View File

@@ -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-----

View 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)
}
}

View 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&

View 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&

View File

@@ -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;
}

View File

@@ -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>

View 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);

View 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);

View 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)
}

View 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;
}

View 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>

View 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);

View 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
View 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
View 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
View 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
View 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
View 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
View 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
}