mirror of
https://github.com/raz-varren/sacrificial-socket.git
synced 2025-10-29 18:51:46 +08:00
added Redis multihome backend
This commit is contained in:
@@ -5,7 +5,7 @@ A Go server library and pure JS client library for managing communication betwee
|
|||||||
|
|
||||||
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 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.
|
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, one for the moderately popular key/value storage engine Redis, and one for the not so popular GRPC protocol, for syncronizing instances on multiple machines.
|
||||||
|
|
||||||
In depth examples can be found in the [__examples__ ](https://github.com/raz-varren/sacrificial-socket/tree/master/examples "Examples") directory and full documentation can be found at [__Godoc.org__](https://godoc.org/github.com/raz-varren/sacrificial-socket "Sacrificial-Socket Documentation")
|
In depth examples can be found in the [__examples__ ](https://github.com/raz-varren/sacrificial-socket/tree/master/examples "Examples") directory and full documentation can be found at [__Godoc.org__](https://godoc.org/github.com/raz-varren/sacrificial-socket "Sacrificial-Socket Documentation")
|
||||||
|
|
||||||
|
|||||||
181
backend/ssredis/ssredis.go
Normal file
181
backend/ssredis/ssredis.go
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
/*
|
||||||
|
Package ssredis provides a ss.MultihomeBackend interface that uses Redis for synchronizing broadcasts and roomcasts between multiple Sacrificial Socket instances.
|
||||||
|
*/
|
||||||
|
package ssredis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-redis/redis"
|
||||||
|
ss "github.com/raz-varren/sacrificial-socket"
|
||||||
|
"github.com/raz-varren/sacrificial-socket/log"
|
||||||
|
"github.com/raz-varren/sacrificial-socket/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
//the default redis.PubSub channel that will be subscribed to
|
||||||
|
DefServerGroup = "ss-rmhb-group-default"
|
||||||
|
)
|
||||||
|
|
||||||
|
//RMHB implements the ss.MultihomeBackend interface and uses
|
||||||
|
//Redis to syncronize between multiple machines running ss.SocketServer
|
||||||
|
type RMHB struct {
|
||||||
|
r *redis.Client
|
||||||
|
o *Options
|
||||||
|
rps *redis.PubSub
|
||||||
|
bps *redis.PubSub
|
||||||
|
roomPSName string
|
||||||
|
bcastPSName string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Options struct {
|
||||||
|
|
||||||
|
//ServerName is a unique name for the ss.MultihomeBackend instance.
|
||||||
|
//This name must be unique per backend instance or the backend
|
||||||
|
//will not broadcast and roomcast properly.
|
||||||
|
//
|
||||||
|
//Leave this name blank to auto generate a unique name.
|
||||||
|
ServerName string
|
||||||
|
|
||||||
|
//ServerGroup is the server pool name that this instance's broadcasts
|
||||||
|
//and roomcasts will be published to. This can be used to break up
|
||||||
|
//ss.MultihomeBackend instances into separate domains.
|
||||||
|
//
|
||||||
|
//Leave this empty to use the default group "ss-rmhb-group-default"
|
||||||
|
ServerGroup string
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewBackend creates a new *RMHB specified by redis Options and ssredis Options
|
||||||
|
func NewBackend(rOpts *redis.Options, ssrOpts *Options) (*RMHB, error) {
|
||||||
|
rClient := redis.NewClient(rOpts)
|
||||||
|
_, err := rClient.Ping().Result()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if ssrOpts == nil {
|
||||||
|
ssrOpts = &Options{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ssrOpts.ServerGroup == "" {
|
||||||
|
ssrOpts.ServerGroup = DefServerGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
if ssrOpts.ServerName == "" {
|
||||||
|
ssrOpts.ServerName = tools.UID()
|
||||||
|
}
|
||||||
|
|
||||||
|
roomPSName := ssrOpts.ServerGroup + ":_ss_roomcasts"
|
||||||
|
bcastPSName := ssrOpts.ServerGroup + ":_ss_broadcasts"
|
||||||
|
|
||||||
|
rmhb := &RMHB{
|
||||||
|
r: rClient,
|
||||||
|
rps: rClient.Subscribe(roomPSName),
|
||||||
|
bps: rClient.Subscribe(bcastPSName),
|
||||||
|
roomPSName: roomPSName,
|
||||||
|
bcastPSName: bcastPSName,
|
||||||
|
o: ssrOpts,
|
||||||
|
}
|
||||||
|
|
||||||
|
return rmhb, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//Init is just here to satisfy the ss.MultihomeBackend interface.
|
||||||
|
func (r *RMHB) Init() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//Shutdown closes the subscribed redis channel, then the redis connection.
|
||||||
|
func (r *RMHB) Shutdown() {
|
||||||
|
r.rps.Close()
|
||||||
|
r.bps.Close()
|
||||||
|
r.r.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
//BroadcastToBackend will publish a broadcast message to the redis backend
|
||||||
|
func (r *RMHB) BroadcastToBackend(b *ss.BroadcastMsg) {
|
||||||
|
t := &transmission{
|
||||||
|
ServerName: r.o.ServerName,
|
||||||
|
EventName: b.EventName,
|
||||||
|
Data: b.Data,
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := t.toJSON()
|
||||||
|
if err != nil {
|
||||||
|
log.Err.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.r.Publish(r.bcastPSName, string(data)).Err()
|
||||||
|
if err != nil {
|
||||||
|
log.Err.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//RoomcastToBackend will publish a roomcast message to the redis backend
|
||||||
|
func (r *RMHB) RoomcastToBackend(rm *ss.RoomMsg) {
|
||||||
|
t := &transmission{
|
||||||
|
ServerName: r.o.ServerName,
|
||||||
|
EventName: rm.EventName,
|
||||||
|
RoomName: rm.RoomName,
|
||||||
|
Data: rm.Data,
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := t.toJSON()
|
||||||
|
if err != nil {
|
||||||
|
log.Err.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.r.Publish(r.roomPSName, string(data)).Err()
|
||||||
|
if err != nil {
|
||||||
|
log.Err.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//BroadcastFromBackend will receive broadcast messages from redis and propogate them to the neccessary sockets
|
||||||
|
func (r *RMHB) BroadcastFromBackend(bc chan<- *ss.BroadcastMsg) {
|
||||||
|
bChan := r.bps.Channel()
|
||||||
|
|
||||||
|
for d := range bChan {
|
||||||
|
var t transmission
|
||||||
|
|
||||||
|
err := t.fromJSON([]byte(d.Payload))
|
||||||
|
if err != nil {
|
||||||
|
log.Err.Println(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.ServerName == r.o.ServerName {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
bc <- &ss.BroadcastMsg{
|
||||||
|
EventName: t.EventName,
|
||||||
|
Data: t.Data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//RoomcastFromBackend will receive roomcast messages from redis and propogate them to the neccessary sockets
|
||||||
|
func (r *RMHB) RoomcastFromBackend(rc chan<- *ss.RoomMsg) {
|
||||||
|
rChan := r.rps.Channel()
|
||||||
|
|
||||||
|
for d := range rChan {
|
||||||
|
var t transmission
|
||||||
|
|
||||||
|
err := t.fromJSON([]byte(d.Payload))
|
||||||
|
if err != nil {
|
||||||
|
log.Err.Println(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.ServerName == r.o.ServerName {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
rc <- &ss.RoomMsg{
|
||||||
|
EventName: t.EventName,
|
||||||
|
RoomName: t.RoomName,
|
||||||
|
Data: t.Data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
87
backend/ssredis/transmission.go
Normal file
87
backend/ssredis/transmission.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
package ssredis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"github.com/raz-varren/sacrificial-socket/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ttErr int = iota
|
||||||
|
ttStr
|
||||||
|
ttBin
|
||||||
|
ttJSON
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrBadDataType = errors.New("bad data type")
|
||||||
|
ErrNoEventName = errors.New("no event name")
|
||||||
|
)
|
||||||
|
|
||||||
|
type transmission struct {
|
||||||
|
DataType int `json:"d"`
|
||||||
|
EventName string `json:"e"`
|
||||||
|
RoomName string `json:"r,omitempty"`
|
||||||
|
Payload string `json:"p"`
|
||||||
|
ServerName string `json:"s"`
|
||||||
|
Data interface{} `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *transmission) toJSON() ([]byte, error) {
|
||||||
|
data, dType := getDataType(t.Data)
|
||||||
|
t.Payload = base64.StdEncoding.EncodeToString(data)
|
||||||
|
t.DataType = dType
|
||||||
|
|
||||||
|
return json.Marshal(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *transmission) fromJSON(data []byte) error {
|
||||||
|
err := json.Unmarshal(data, t)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.DataType == ttErr {
|
||||||
|
return ErrBadDataType
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.EventName == "" {
|
||||||
|
return ErrNoEventName
|
||||||
|
}
|
||||||
|
|
||||||
|
d, err := base64.StdEncoding.DecodeString(t.Payload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch t.DataType {
|
||||||
|
case ttStr:
|
||||||
|
t.Data = string(d)
|
||||||
|
case ttBin:
|
||||||
|
t.Data = d
|
||||||
|
case ttJSON:
|
||||||
|
err = json.Unmarshal(d, &t.Data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDataType(in interface{}) ([]byte, int) {
|
||||||
|
switch i := in.(type) {
|
||||||
|
case string:
|
||||||
|
return []byte(i), ttStr
|
||||||
|
case []byte:
|
||||||
|
return i, ttBin
|
||||||
|
default:
|
||||||
|
j, err := json.Marshal(i)
|
||||||
|
if err != nil {
|
||||||
|
log.Err.Println(err)
|
||||||
|
return []byte{}, ttStr
|
||||||
|
}
|
||||||
|
return j, ttJSON
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDpTCCAo2gAwIBAgIJAOjluoEIEPwqMA0GCSqGSIb3DQEBCwUAMGkxCzAJBgNV
|
||||||
|
BAYTAlVTMRMwEQYDVQQIDApXYXNoaW5ndG9uMRUwEwYDVQQHDAxEZWZhdWx0IENp
|
||||||
|
dHkxGjAYBgNVBAoMEVNhZEhhcHB5TWVkaWEgOik6MRIwEAYDVQQDDAlsb2NhbGhv
|
||||||
|
c3QwHhcNMTcwNTEyMjI1NzEwWhcNMTgwNTEyMjI1NzEwWjBpMQswCQYDVQQGEwJV
|
||||||
|
UzETMBEGA1UECAwKV2FzaGluZ3RvbjEVMBMGA1UEBwwMRGVmYXVsdCBDaXR5MRow
|
||||||
|
GAYDVQQKDBFTYWRIYXBweU1lZGlhIDopOjESMBAGA1UEAwwJbG9jYWxob3N0MIIB
|
||||||
|
IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuShR38ri55vRvFg+GFWXhLXa
|
||||||
|
2flmbyJL8dTkMu+3eZNN2tIZG4Pr0vDt4gP6hWX+QT9K2K/jF6xRgSo1pOyHst+W
|
||||||
|
Pc+qZWCPOgBsQRfSPSW/EE2Hf7Bqo63iQ3yjTjT3drqXe7NlgXYKhLLjnmjpF6Hy
|
||||||
|
WN7UtOMuPWuxnRgKgEuc0A6hK65s59svO2ToLoIwBS/Pt3QZfHYnfdIUYESDjCm0
|
||||||
|
e1dkBVAtpIDKNay6EH+68I4Ne7cdOPo6O22/CJAySXhojo69jZwZ+FY0TIRK09Om
|
||||||
|
WCTgZwwbr8Reh1qSWaF4FWEg01Iyy1AZQwlU/qsP/CZ5s2Yj5Fq8fwg98T+OSQID
|
||||||
|
AQABo1AwTjAdBgNVHQ4EFgQUWHnpII7hwpJnMipudTbaBLEkuSgwHwYDVR0jBBgw
|
||||||
|
FoAUWHnpII7hwpJnMipudTbaBLEkuSgwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0B
|
||||||
|
AQsFAAOCAQEACMGNDatTnrKMlpdj9Dmw+iFV3fnUgipgoNta0s27n/TkgT1gk8Yy
|
||||||
|
4T4KsTv5u0bfjCmEHBu26c+c8s3C0S6apCOSLRZZ/lu1sHJL0mW+MqMcuZMFF8q8
|
||||||
|
/YIL0JB5DTCJAcAwZ+Z5T3iMN67ya6Pzb8zv5bSm64GXy6pN6D8DMfVqnnmkIUS3
|
||||||
|
HrEUF4FCKYzVED2SYNVoq1CB4dz/MlCFWo9KyqK9kX6HxPJ387VO77z+EBMFlnPT
|
||||||
|
BVPfWYW3tGfFbX1KlkhI/PwB6La8O243jHRHiEiwCvrxw3nGKypazA1eWQM9sLwt
|
||||||
|
wdke5g8R+o5cvKpQFjRp7E6dHe1aeRSpzQ==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5KFHfyuLnm9G8
|
||||||
|
WD4YVZeEtdrZ+WZvIkvx1OQy77d5k03a0hkbg+vS8O3iA/qFZf5BP0rYr+MXrFGB
|
||||||
|
KjWk7Iey35Y9z6plYI86AGxBF9I9Jb8QTYd/sGqjreJDfKNONPd2upd7s2WBdgqE
|
||||||
|
suOeaOkXofJY3tS04y49a7GdGAqAS5zQDqErrmzn2y87ZOgugjAFL8+3dBl8did9
|
||||||
|
0hRgRIOMKbR7V2QFUC2kgMo1rLoQf7rwjg17tx04+jo7bb8IkDJJeGiOjr2NnBn4
|
||||||
|
VjRMhErT06ZYJOBnDBuvxF6HWpJZoXgVYSDTUjLLUBlDCVT+qw/8JnmzZiPkWrx/
|
||||||
|
CD3xP45JAgMBAAECggEAV+EEIwSHd0fkXtE+/4u4M1uguK3/aSXNB8V0XZya50/7
|
||||||
|
tqzbD80oI2EIdqpOv/utlkg0/O1WCklWEcj31wQQT9yA0Wt7w0v2DqBewPJObYk5
|
||||||
|
ysIRWpBfvLnt1vwUAi1vemGLPkHiHnTo/xxsniXK49WQUY/JQuVEcBBqJ1ZevY7W
|
||||||
|
q0zggrmltfYLLLKkPYZp/8bIuzukdYTZTZw7OioagEBcWiaXaGhPm0KbKYtdzLd8
|
||||||
|
ny7+pZ0wPUBkVc32InPNsGnYeXCFgW4P2rncCv5lnVPF+RvaYwi3uqeFdTM7tYcW
|
||||||
|
bR9BGOZ78hK3CCfKoTpbesWZT9qEWufB6+wvXS8vAQKBgQDmS835/E6Zfa4yOtES
|
||||||
|
Ma8gPVmb7czLrk4jk8EMCDUSZDVYVqXqC68Q7s7zVAV+qZVlWV1JE2R2nYbOMOmm
|
||||||
|
VugC6FhFi4zteXCnxxvG6bTntQ+I0/Cv01ufqWciv7cyvts3Q/SmmVh35qMJeoQp
|
||||||
|
ro4kIFIP6WjIgGg4ymdw9Dlr6QKBgQDN0sfPjrHvUXy4ot69RV5BJuWt2XgIQkYE
|
||||||
|
Cs9fnReok0LHyjxZGo+GLMaI6mfjwg/1wLTrtbqoKtcZ6yxKAX+gI7DmAdrZ/x7V
|
||||||
|
x4FuV4Xcpj+Ml43xsOOlMG9Skk8hR4LjtU3G7Bz2+i8uGJwPIfMwPgYB9iNHrPbx
|
||||||
|
mQG5rWRzYQKBgEP4lbe110EITjS3FWQIVAbw9JTIMAzhymBHyM+TUI64EuKa2Gdm
|
||||||
|
wWn/AgfhgamrxdNe9+CMn7c+sT4EQ8H7nojVKNCF6rdgg3aRlsozylglIYuh+kT6
|
||||||
|
3e0W48Dm0txgZnU+UmQlmG3zHaW7imx+/6b7/xyBKJMdCyXP344AFz6ZAoGBAKw5
|
||||||
|
vxavybZ+0kVRi96GyCruWGxTt7v5cMr7HLFeKyjVKKEzWbIZppVYrDxvIMWVYnN6
|
||||||
|
cCl4ZJtJVbqLbgDzJg8jLmgYjz+w2eV6zpQ9Snbq6exD+POP170nPU+zu+EWDLFr
|
||||||
|
yYw1kLsdeBMzZorHFs58Z9yGUNkuI2jgZnAvZgmhAoGBALX5S19DwVa7rrd3TacD
|
||||||
|
J+XeAd1vdZCHEmNxdmz75lJCB56iB4Wty6MzS42IJBAZqQJa1nhz+bP+81A1pXxq
|
||||||
|
TyY+P8S60/tmVPkny/kalYtYvTjNg72Sxdgn3fYvvvdQZ2NhsTiZ27JsZew6L/Ie
|
||||||
|
hAe+mp7TFKmKxiNgUR3Kmy7c
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
160
examples/not-so-simple-examples/redis-multihome/main.go
Normal file
160
examples/not-so-simple-examples/redis-multihome/main.go
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
/*
|
||||||
|
A complex web app example that implements ssredis for synchronizing multiple Sacrificial Socket instances
|
||||||
|
*/
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"github.com/go-redis/redis"
|
||||||
|
"github.com/raz-varren/sacrificial-socket"
|
||||||
|
"github.com/raz-varren/sacrificial-socket/backend/ssredis"
|
||||||
|
"github.com/raz-varren/sacrificial-socket/log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
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")
|
||||||
|
redisPort = flag.String("redisport", ":6379", "host:port number used to connect to the redis server")
|
||||||
|
key = flag.String("key", "./keys/snakeoil.key", "tls key used for https")
|
||||||
|
cert = flag.String("cert", "./keys/snakeoil.crt", "tls cert used for https")
|
||||||
|
pass = flag.String("p", "", "redis password, if there is one")
|
||||||
|
db = flag.Int("db", 0, "redis db (default 0)")
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
log.SetDefaultLogger(log.NewLogger(os.Stdout, log.LogLevelDbg))
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
b, err := ssredis.NewBackend(&redis.Options{
|
||||||
|
Addr: *redisPort,
|
||||||
|
Password: *pass,
|
||||||
|
DB: *db,
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Err.Fatalln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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")))
|
||||||
|
|
||||||
|
if *cert == "" || *key == "" {
|
||||||
|
err = http.ListenAndServe(*webPort, nil)
|
||||||
|
} else {
|
||||||
|
err = http.ListenAndServeTLS(*webPort, *cert, *key, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Err.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.Err.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
6
examples/not-so-simple-examples/redis-multihome/start-servers.sh
Executable file
6
examples/not-so-simple-examples/redis-multihome/start-servers.sh
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
go run main.go -p $1 -webport :8081&
|
||||||
|
go run main.go -p $1 -webport :8082&
|
||||||
|
go run main.go -p $1 -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,101 @@
|
|||||||
|
<!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>
|
||||||
|
<label>
|
||||||
|
<input id="chk-auto-scroll" type="checkbox" checked />
|
||||||
|
Autoscroll
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<input id="chk-clear-periodically" type="checkbox" />
|
||||||
|
Clear Messages Periodically
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="text/javascript" src="/js/sacrificial-socket.js"></script>
|
||||||
|
<script type="text/javascript" src="/js/app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
(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'),
|
||||||
|
autoScroll = get('#chk-auto-scroll'),
|
||||||
|
clearMessages = get('#chk-clear-periodically'),
|
||||||
|
addMessage = function(msg){
|
||||||
|
var li = document.createElement('li'),
|
||||||
|
dt = new Date(),
|
||||||
|
li = document.createElement('li');
|
||||||
|
|
||||||
|
li.innerText = msg;
|
||||||
|
messages.appendChild(li);
|
||||||
|
if(autoScroll.checked) 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 = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
window.setInterval(function(){
|
||||||
|
if(clearMessages.checked) messages.innerHTML = "";
|
||||||
|
}, 60000);
|
||||||
|
|
||||||
|
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)];
|
||||||
|
|
||||||
|
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,242 @@
|
|||||||
|
(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")
|
||||||
|
* @param {Object} opts - connection options
|
||||||
|
*
|
||||||
|
* Default opts = {
|
||||||
|
* reconnectOpts: {
|
||||||
|
* enabled: true,
|
||||||
|
* replayOnConnect: true,
|
||||||
|
* intervalMS: 5000
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
var SS = function(url, opts){
|
||||||
|
opts = opts || {};
|
||||||
|
|
||||||
|
var self = this,
|
||||||
|
events = {},
|
||||||
|
reconnectOpts = {enabled: true, replayOnConnect: true, intervalMS: 5000},
|
||||||
|
reconnecting = false,
|
||||||
|
connectedOnce = false,
|
||||||
|
headerStartCharCode = 1,
|
||||||
|
headerStartChar = String.fromCharCode(headerStartCharCode),
|
||||||
|
dataStartCharCode = 2,
|
||||||
|
dataStartChar = String.fromCharCode(dataStartCharCode),
|
||||||
|
ws = new WebSocket(url, 'sac-sock');
|
||||||
|
|
||||||
|
//blomp blomp-a noop noop a-noop noop noop
|
||||||
|
self.noop = function(){ };
|
||||||
|
|
||||||
|
//we really only support reconnect options for now
|
||||||
|
if(typeof opts.reconnectOpts == 'object'){
|
||||||
|
for(var i in opts.reconnectOpts){
|
||||||
|
if(!opts.reconnectOpts.hasOwnProperty(i)) continue;
|
||||||
|
reconnectOpts[i] = opts.reconnectOpts[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//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].call(self, (headers.J) ? JSON.parse(data) : data);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* startReconnect is an internal function for reconnecting after an unexpected disconnect
|
||||||
|
*
|
||||||
|
* @function startReconnect
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function startReconnect(){
|
||||||
|
setTimeout(function(){
|
||||||
|
console.log('attempting reconnect');
|
||||||
|
var newWS = new WebSocket(url, 'sac-sock');
|
||||||
|
newWS.onmessage = ws.onmessage;
|
||||||
|
newWS.onclose = ws.onclose;
|
||||||
|
newWS.binaryType = ws.binaryType;
|
||||||
|
|
||||||
|
//we need to run the initially set onConnect function on first successful connect,
|
||||||
|
//even if replayOnConnect is disabled. The server might not be available on first
|
||||||
|
//connection attempt.
|
||||||
|
if(reconnectOpts.replayOnConnect || !connectedOnce){
|
||||||
|
newWS.onopen = ws.onopen;
|
||||||
|
}
|
||||||
|
ws = newWS;
|
||||||
|
if(!reconnectOpts.replayOnConnect && connectedOnce){
|
||||||
|
self.onConnect(self.noop);
|
||||||
|
}
|
||||||
|
}, reconnectOpts.intervalMS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onConnect registers a callback to be run when the websocket connection is open.
|
||||||
|
*
|
||||||
|
* @method onConnect
|
||||||
|
* @param {Function} callback(event) - The callback that will be executed when the websocket connection opens.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
self.onConnect = function(callback){
|
||||||
|
ws.onopen = function(){
|
||||||
|
connectedOnce = true;
|
||||||
|
var args = arguments;
|
||||||
|
callback.apply(self, args);
|
||||||
|
if(reconnecting){
|
||||||
|
reconnecting = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
self.onConnect(self.noop);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onDisconnect registers a callback to be run when the websocket connection is closed.
|
||||||
|
*
|
||||||
|
* @method onDisconnect
|
||||||
|
* @param {Function} callback(event) - The callback that will be executed when the websocket connection is closed.
|
||||||
|
*/
|
||||||
|
self.onDisconnect = function(callback){
|
||||||
|
ws.onclose = function(){
|
||||||
|
var args = arguments;
|
||||||
|
if(!reconnecting && connectedOnce){
|
||||||
|
callback.apply(self, args);
|
||||||
|
}
|
||||||
|
if(reconnectOpts.enabled){
|
||||||
|
reconnecting = true;
|
||||||
|
startReconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
self.onDisconnect(self.noop);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(){
|
||||||
|
reconnectOpts.enabled = false; //don't reconnect if close is called
|
||||||
|
return ws.close();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
window.SS = SS;
|
||||||
|
})(window);
|
||||||
@@ -4,6 +4,9 @@ Package tools is really just used during socket creation to generate random numb
|
|||||||
package tools
|
package tools
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
crand "crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -17,3 +20,9 @@ func RandomInt64(min, max int64) int64 {
|
|||||||
rand.Seed(time.Now().UnixNano())
|
rand.Seed(time.Now().UnixNano())
|
||||||
return rand.Int63n(max-min+1) + min
|
return rand.Int63n(max-min+1) + min
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func UID() string {
|
||||||
|
uid := make([]byte, 16)
|
||||||
|
io.ReadFull(crand.Reader, uid)
|
||||||
|
return fmt.Sprintf("%x", uid)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user