├── .gitignore ├── README.md ├── cmap_string_cmap └── cmap_string_cmap.go ├── cmap_string_socket └── cmap_string_socket.go └── redis.go /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-socket.io-redis 2 | 3 | By running go-socket.io with this adapter, you can run multiple socket.io 4 | instances in different processes or servers that can all broadcast and emit 5 | events to and from each other. 6 | 7 | ## How to use 8 | 9 | Install the package using: 10 | 11 | ```bash 12 | go get "github.com/satyakb/go-socket.io-redis" 13 | ``` 14 | 15 | Usage: 16 | 17 | ```go 18 | import ( 19 | "log" 20 | "net/http" 21 | "github.com/googollee/go-socket.io" 22 | "github.com/satyakb/go-socket.io-redis" 23 | ) 24 | 25 | func main() { 26 | server, err := socketio.NewServer(nil) 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | 31 | opts := make(map[string]string) 32 | server.SetAdaptor(redis.Redis(opts)) 33 | 34 | server.On("connection", func(so socketio.Socket) { 35 | log.Println("on connection") 36 | so.Join("chat") 37 | so.On("chat message", func(msg string) { 38 | log.Println("emit:", so.Emit("chat message", msg)) 39 | so.BroadcastTo("chat", "chat message", msg) 40 | }) 41 | so.On("disconnection", func() { 42 | log.Println("on disconnect") 43 | }) 44 | }) 45 | server.On("error", func(so socketio.Socket, err error) { 46 | log.Println("error:", err) 47 | }) 48 | 49 | http.Handle("/socket.io/", server) 50 | http.Handle("/", http.FileServer(http.Dir("./asset"))) 51 | log.Println("Serving at localhost:5000...") 52 | log.Fatal(http.ListenAndServe(":5000", nil)) 53 | } 54 | ``` 55 | 56 | **Note:** The package is named `redis` for use in your code 57 | 58 | ## API 59 | 60 | ### Redis(opts map[string]string) 61 | 62 | The following options are allowed: 63 | 64 | - `host`: host to connect to redis on (`"localhost"`) 65 | - `port`: port to connect to redis on (`"6379"`) 66 | - `prefix`: the prefix of the key to pub/sub events on (`"socket.io"`) 67 | 68 | ## References 69 | 70 | Code and README based off of: 71 | - https://github.com/Automattic/socket.io-redis 72 | 73 | -------------------------------------------------------------------------------- /cmap_string_cmap/cmap_string_cmap.go: -------------------------------------------------------------------------------- 1 | package cmap_string_cmap 2 | 3 | import ( 4 | "encoding/json" 5 | "hash/fnv" 6 | "sync" 7 | "github.com/satyakb/go-socket.io-redis/cmap_string_socket" 8 | ) 9 | 10 | var SHARD_COUNT = 32 11 | 12 | // TODO: Add Keys function which returns an array of keys for the map. 13 | 14 | // A "thread" safe map of type string:ConcurrentMap. 15 | // To avoid lock bottlenecks this map is dived to several (SHARD_COUNT) map shards. 16 | type ConcurrentMap []*ConcurrentMapShared 17 | type ConcurrentMapShared struct { 18 | items map[string]cmap_string_socket.ConcurrentMap 19 | sync.RWMutex // Read Write mutex, guards access to internal map. 20 | } 21 | 22 | // Creates a new concurrent map. 23 | func New() ConcurrentMap { 24 | m := make(ConcurrentMap, SHARD_COUNT) 25 | for i := 0; i < SHARD_COUNT; i++ { 26 | m[i] = &ConcurrentMapShared{items: make(map[string]cmap_string_socket.ConcurrentMap)} 27 | } 28 | return m 29 | } 30 | 31 | // Returns shard under given key 32 | func (m ConcurrentMap) GetShard(key string) *ConcurrentMapShared { 33 | hasher := fnv.New32() 34 | hasher.Write([]byte(key)) 35 | return m[int(hasher.Sum32())%SHARD_COUNT] 36 | } 37 | 38 | // Sets the given value under the specified key. 39 | func (m *ConcurrentMap) Set(key string, value cmap_string_socket.ConcurrentMap) { 40 | // Get map shard. 41 | shard := m.GetShard(key) 42 | shard.Lock() 43 | defer shard.Unlock() 44 | shard.items[key] = value 45 | } 46 | 47 | // Retrieves an element from map under given key. 48 | func (m ConcurrentMap) Get(key string) (cmap_string_socket.ConcurrentMap, bool) { 49 | // Get shard 50 | shard := m.GetShard(key) 51 | shard.RLock() 52 | defer shard.RUnlock() 53 | 54 | // Get item from shard. 55 | val, ok := shard.items[key] 56 | return val, ok 57 | } 58 | 59 | // Returns the number of elements within the map. 60 | func (m ConcurrentMap) Count() int { 61 | count := 0 62 | for i := 0; i < SHARD_COUNT; i++ { 63 | shard := m[i] 64 | shard.RLock() 65 | count += len(shard.items) 66 | shard.RUnlock() 67 | } 68 | return count 69 | } 70 | 71 | // Looks up an item under specified key 72 | func (m *ConcurrentMap) Has(key string) bool { 73 | // Get shard 74 | shard := m.GetShard(key) 75 | shard.RLock() 76 | defer shard.RUnlock() 77 | 78 | // See if element is within shard. 79 | _, ok := shard.items[key] 80 | return ok 81 | } 82 | 83 | // Removes an element from the map. 84 | func (m *ConcurrentMap) Remove(key string) { 85 | // Try to get shard. 86 | shard := m.GetShard(key) 87 | shard.Lock() 88 | defer shard.Unlock() 89 | delete(shard.items, key) 90 | } 91 | 92 | // Checks if map is empty. 93 | func (m *ConcurrentMap) IsEmpty() bool { 94 | return m.Count() == 0 95 | } 96 | 97 | // Used by the Iter & IterBuffered functions to wrap two variables together over a channel, 98 | type Tuple struct { 99 | Key string 100 | Val cmap_string_socket.ConcurrentMap 101 | } 102 | 103 | // Returns an iterator which could be used in a for range loop. 104 | func (m ConcurrentMap) Iter() <-chan Tuple { 105 | ch := make(chan Tuple) 106 | go func() { 107 | // Foreach shard. 108 | for _, shard := range m { 109 | // Foreach key, value pair. 110 | shard.RLock() 111 | for key, val := range shard.items { 112 | ch <- Tuple{key, val} 113 | } 114 | shard.RUnlock() 115 | } 116 | close(ch) 117 | }() 118 | return ch 119 | } 120 | 121 | // Returns a buffered iterator which could be used in a for range loop. 122 | func (m ConcurrentMap) IterBuffered() <-chan Tuple { 123 | ch := make(chan Tuple, m.Count()) 124 | go func() { 125 | // Foreach shard. 126 | for _, shard := range m { 127 | // Foreach key, value pair. 128 | shard.RLock() 129 | for key, val := range shard.items { 130 | ch <- Tuple{key, val} 131 | } 132 | shard.RUnlock() 133 | } 134 | close(ch) 135 | }() 136 | return ch 137 | } 138 | 139 | //Reviles ConcurrentMap "private" variables to json marshal. 140 | func (m ConcurrentMap) MarshalJSON() ([]byte, error) { 141 | // Create a temporary map, which will hold all item spread across shards. 142 | tmp := make(map[string]cmap_string_socket.ConcurrentMap) 143 | 144 | // Insert items to temporary map. 145 | for item := range m.Iter() { 146 | tmp[item.Key] = item.Val 147 | } 148 | return json.Marshal(tmp) 149 | } 150 | 151 | func (m *ConcurrentMap) UnmarshalJSON(b []byte) (err error) { 152 | // Reverse process of Marshal. 153 | 154 | tmp := make(map[string]cmap_string_socket.ConcurrentMap) 155 | 156 | // Unmarshal into a single map. 157 | if err := json.Unmarshal(b, &tmp); err != nil { 158 | return nil 159 | } 160 | 161 | // foreach key,value pair in temporary map insert into our concurrent map. 162 | for key, val := range tmp { 163 | m.Set(key, val) 164 | } 165 | return nil 166 | } 167 | -------------------------------------------------------------------------------- /cmap_string_socket/cmap_string_socket.go: -------------------------------------------------------------------------------- 1 | package cmap_string_socket 2 | 3 | import ( 4 | "encoding/json" 5 | "hash/fnv" 6 | "sync" 7 | "github.com/googollee/go-socket.io" 8 | ) 9 | 10 | var SHARD_COUNT = 32 11 | 12 | // TODO: Add Keys function which returns an array of keys for the map. 13 | 14 | // A "thread" safe map of type string:socketio.Socket. 15 | // To avoid lock bottlenecks this map is dived to several (SHARD_COUNT) map shards. 16 | type ConcurrentMap []*ConcurrentMapShared 17 | type ConcurrentMapShared struct { 18 | items map[string]socketio.Socket 19 | sync.RWMutex // Read Write mutex, guards access to internal map. 20 | } 21 | 22 | // Creates a new concurrent map. 23 | func New() ConcurrentMap { 24 | m := make(ConcurrentMap, SHARD_COUNT) 25 | for i := 0; i < SHARD_COUNT; i++ { 26 | m[i] = &ConcurrentMapShared{items: make(map[string]socketio.Socket)} 27 | } 28 | return m 29 | } 30 | 31 | // Returns shard under given key 32 | func (m ConcurrentMap) GetShard(key string) *ConcurrentMapShared { 33 | hasher := fnv.New32() 34 | hasher.Write([]byte(key)) 35 | return m[int(hasher.Sum32())%SHARD_COUNT] 36 | } 37 | 38 | // Sets the given value under the specified key. 39 | func (m *ConcurrentMap) Set(key string, value socketio.Socket) { 40 | // Get map shard. 41 | shard := m.GetShard(key) 42 | shard.Lock() 43 | defer shard.Unlock() 44 | shard.items[key] = value 45 | } 46 | 47 | // Retrieves an element from map under given key. 48 | func (m ConcurrentMap) Get(key string) (socketio.Socket, bool) { 49 | // Get shard 50 | shard := m.GetShard(key) 51 | shard.RLock() 52 | defer shard.RUnlock() 53 | 54 | // Get item from shard. 55 | val, ok := shard.items[key] 56 | return val, ok 57 | } 58 | 59 | // Returns the number of elements within the map. 60 | func (m ConcurrentMap) Count() int { 61 | count := 0 62 | for i := 0; i < SHARD_COUNT; i++ { 63 | shard := m[i] 64 | shard.RLock() 65 | count += len(shard.items) 66 | shard.RUnlock() 67 | } 68 | return count 69 | } 70 | 71 | // Looks up an item under specified key 72 | func (m *ConcurrentMap) Has(key string) bool { 73 | // Get shard 74 | shard := m.GetShard(key) 75 | shard.RLock() 76 | defer shard.RUnlock() 77 | 78 | // See if element is within shard. 79 | _, ok := shard.items[key] 80 | return ok 81 | } 82 | 83 | // Removes an element from the map. 84 | func (m *ConcurrentMap) Remove(key string) { 85 | // Try to get shard. 86 | shard := m.GetShard(key) 87 | shard.Lock() 88 | defer shard.Unlock() 89 | delete(shard.items, key) 90 | } 91 | 92 | // Checks if map is empty. 93 | func (m *ConcurrentMap) IsEmpty() bool { 94 | return m.Count() == 0 95 | } 96 | 97 | // Used by the Iter & IterBuffered functions to wrap two variables together over a channel, 98 | type Tuple struct { 99 | Key string 100 | Val socketio.Socket 101 | } 102 | 103 | // Returns an iterator which could be used in a for range loop. 104 | func (m ConcurrentMap) Iter() <-chan Tuple { 105 | ch := make(chan Tuple) 106 | go func() { 107 | // Foreach shard. 108 | for _, shard := range m { 109 | // Foreach key, value pair. 110 | shard.RLock() 111 | for key, val := range shard.items { 112 | ch <- Tuple{key, val} 113 | } 114 | shard.RUnlock() 115 | } 116 | close(ch) 117 | }() 118 | return ch 119 | } 120 | 121 | // Returns a buffered iterator which could be used in a for range loop. 122 | func (m ConcurrentMap) IterBuffered() <-chan Tuple { 123 | ch := make(chan Tuple, m.Count()) 124 | go func() { 125 | // Foreach shard. 126 | for _, shard := range m { 127 | // Foreach key, value pair. 128 | shard.RLock() 129 | for key, val := range shard.items { 130 | ch <- Tuple{key, val} 131 | } 132 | shard.RUnlock() 133 | } 134 | close(ch) 135 | }() 136 | return ch 137 | } 138 | 139 | //Reviles ConcurrentMap "private" variables to json marshal. 140 | func (m ConcurrentMap) MarshalJSON() ([]byte, error) { 141 | // Create a temporary map, which will hold all item spread across shards. 142 | tmp := make(map[string]socketio.Socket) 143 | 144 | // Insert items to temporary map. 145 | for item := range m.Iter() { 146 | tmp[item.Key] = item.Val 147 | } 148 | return json.Marshal(tmp) 149 | } 150 | 151 | func (m *ConcurrentMap) UnmarshalJSON(b []byte) (err error) { 152 | // Reverse process of Marshal. 153 | 154 | tmp := make(map[string]socketio.Socket) 155 | 156 | // Unmarshal into a single map. 157 | if err := json.Unmarshal(b, &tmp); err != nil { 158 | return nil 159 | } 160 | 161 | // foreach key,value pair in temporary map insert into our concurrent map. 162 | for key, val := range tmp { 163 | m.Set(key, val) 164 | } 165 | return nil 166 | } 167 | -------------------------------------------------------------------------------- /redis.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "log" 5 | "strings" 6 | "github.com/googollee/go-socket.io" 7 | "github.com/garyburd/redigo/redis" 8 | "github.com/nu7hatch/gouuid" 9 | "github.com/satyakb/go-socket.io-redis/cmap_string_cmap" 10 | "github.com/satyakb/go-socket.io-redis/cmap_string_socket" 11 | // "github.com/vmihailenco/msgpack" // screwed up types after decoding 12 | "encoding/json" 13 | ) 14 | 15 | type broadcast struct { 16 | host string 17 | port string 18 | pub redis.PubSubConn 19 | sub redis.PubSubConn 20 | prefix string 21 | uid string 22 | key string 23 | remote bool 24 | rooms cmap_string_cmap.ConcurrentMap 25 | } 26 | 27 | // 28 | // opts: { 29 | // "host": "127.0.0.1", 30 | // "port": "6379" 31 | // "prefix": "socket.io" 32 | // } 33 | func Redis(opts map[string]string) socketio.BroadcastAdaptor { 34 | b := broadcast { 35 | rooms: cmap_string_cmap.New(), 36 | } 37 | 38 | var ok bool 39 | b.host, ok = opts["host"] 40 | if !ok { 41 | b.host = "127.0.0.1" 42 | } 43 | b.port, ok = opts["port"] 44 | if !ok { 45 | b.port = "6379" 46 | } 47 | b.prefix, ok = opts["prefix"] 48 | if !ok { 49 | b.prefix = "socket.io" 50 | } 51 | 52 | pub, err := redis.Dial("tcp", b.host + ":" + b.port) 53 | if err != nil { 54 | panic(err) 55 | } 56 | sub, err := redis.Dial("tcp", b.host + ":" + b.port) 57 | if err != nil { 58 | panic(err) 59 | } 60 | 61 | b.pub = redis.PubSubConn{Conn: pub} 62 | b.sub = redis.PubSubConn{Conn: sub} 63 | 64 | uid, err := uuid.NewV4(); 65 | if err != nil { 66 | log.Println("error generating uid:", err) 67 | return nil 68 | } 69 | b.uid = uid.String() 70 | b.key = b.prefix + "#" + b.uid 71 | 72 | b.remote = false 73 | 74 | b.sub.PSubscribe(b.prefix + "#*") 75 | 76 | // This goroutine receives and prints pushed notifications from the server. 77 | // The goroutine exits when there is an error. 78 | go func() { 79 | for { 80 | switch n := b.sub.Receive().(type) { 81 | case redis.Message: 82 | log.Printf("Message: %s %s\n", n.Channel, n.Data) 83 | case redis.PMessage: 84 | b.onmessage(n.Channel, n.Data) 85 | log.Printf("PMessage: %s %s %s\n", n.Pattern, n.Channel, n.Data) 86 | case redis.Subscription: 87 | log.Printf("Subscription: %s %s %d\n", n.Kind, n.Channel, n.Count) 88 | if n.Count == 0 { 89 | return 90 | } 91 | case error: 92 | log.Printf("error: %v\n", n) 93 | return 94 | } 95 | } 96 | }() 97 | 98 | return b 99 | } 100 | 101 | func (b broadcast) onmessage(channel string, data []byte) error { 102 | pieces := strings.Split(channel, "#"); 103 | uid := pieces[len(pieces) - 1] 104 | if b.uid == uid { 105 | log.Println("ignore same uid") 106 | return nil 107 | } 108 | 109 | var out map[string][]interface{} 110 | err := json.Unmarshal(data, &out) 111 | if err != nil { 112 | log.Println("error decoding data") 113 | return nil 114 | } 115 | 116 | args := out["args"] 117 | opts := out["opts"] 118 | ignore, ok := opts[0].(socketio.Socket) 119 | if !ok { 120 | log.Println("ignore is not a socket") 121 | ignore = nil 122 | } 123 | room, ok := opts[1].(string) 124 | if !ok { 125 | log.Println("room is not a string") 126 | room = "" 127 | } 128 | message, ok := opts[2].(string) 129 | if !ok { 130 | log.Println("message is not a string") 131 | message = "" 132 | } 133 | 134 | b.remote = true; 135 | b.Send(ignore, room, message, args...) 136 | return nil 137 | } 138 | 139 | func (b broadcast) Join(room string, socket socketio.Socket) error { 140 | sockets, ok := b.rooms.Get(room) 141 | if !ok { 142 | sockets = cmap_string_socket.New() 143 | } 144 | sockets.Set(socket.Id(), socket) 145 | b.rooms.Set(room, sockets) 146 | return nil 147 | } 148 | 149 | func (b broadcast) Leave(room string, socket socketio.Socket) error { 150 | sockets, ok := b.rooms.Get(room) 151 | if !ok { 152 | return nil 153 | } 154 | sockets.Remove(socket.Id()) 155 | if sockets.IsEmpty() { 156 | b.rooms.Remove(room) 157 | return nil 158 | } 159 | b.rooms.Set(room, sockets) 160 | return nil 161 | } 162 | 163 | // Same as Broadcast 164 | func (b broadcast) Send(ignore socketio.Socket, room, message string, args ...interface{}) error { 165 | sockets, ok := b.rooms.Get(room) 166 | if !ok { 167 | return nil 168 | } 169 | for item := range sockets.Iter() { 170 | id := item.Key 171 | s := item.Val 172 | if ignore != nil && ignore.Id() == id { 173 | continue 174 | } 175 | err := (s.Emit(message, args...)) 176 | if err != nil { 177 | log.Println("error broadcasting:", err) 178 | } 179 | } 180 | 181 | opts := make([]interface{}, 3) 182 | opts[0] = ignore 183 | opts[1] = room 184 | opts[2] = message 185 | in := map[string][]interface{}{ 186 | "args": args, 187 | "opts": opts, 188 | } 189 | 190 | buf, err := json.Marshal(in) 191 | _ = err 192 | 193 | if !b.remote { 194 | b.pub.Conn.Do("PUBLISH", b.key, buf) 195 | } 196 | b.remote = false 197 | return nil 198 | } 199 | --------------------------------------------------------------------------------