added Redis multihome backend

This commit is contained in:
skedar46
2017-06-21 18:46:11 -07:00
parent 81baa50a15
commit 202f7457ff
12 changed files with 1142 additions and 1 deletions

View File

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

181
backend/ssredis/ssredis.go Normal file
View 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,
}
}
}

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

View File

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

View File

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

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

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

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,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>
&nbsp;&nbsp;&nbsp;
<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>

View File

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

View File

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

View File

@@ -4,6 +4,9 @@ Package tools is really just used during socket creation to generate random numb
package tools
import (
crand "crypto/rand"
"fmt"
"io"
"math/rand"
"time"
)
@@ -17,3 +20,9 @@ func RandomInt64(min, max int64) int64 {
rand.Seed(time.Now().UnixNano())
return rand.Int63n(max-min+1) + min
}
func UID() string {
uid := make([]byte, 16)
io.ReadFull(crand.Reader, uid)
return fmt.Sprintf("%x", uid)
}