1 | # socket.io-redis
|
2 |
|
3 | [![Build Status](https://github.com/socketio/socket.io-redis/workflows/CI/badge.svg?branch=master)](https://github.com/socketio/socket.io-redis/actions)
|
4 | [![NPM version](https://badge.fury.io/js/socket.io-redis.svg)](http://badge.fury.io/js/socket.io-redis)
|
5 |
|
6 | ## Table of contents
|
7 |
|
8 | - [How to use](#how-to-use)
|
9 | - [CommonJS](#commonjs)
|
10 | - [ES6 module](#es6-modules)
|
11 | - [TypeScript](#typescript)
|
12 | - [Compatibility table](#compatibility-table)
|
13 | - [How does it work under the hood?](#how-does-it-work-under-the-hood)
|
14 | - [API](#api)
|
15 | - [adapter(uri[, opts])](#adapteruri-opts)
|
16 | - [adapter(opts)](#adapteropts)
|
17 | - [RedisAdapter](#redisadapter)
|
18 | - [RedisAdapter#sockets(rooms: Set<String>)](#redisadaptersocketsrooms-setstring)
|
19 | - [RedisAdapter#allRooms()](#redisadapterallrooms)
|
20 | - [RedisAdapter#remoteJoin(id:String, room:String)](#redisadapterremotejoinidstring-roomstring)
|
21 | - [RedisAdapter#remoteLeave(id:String, room:String)](#redisadapterremoteleaveidstring-roomstring)
|
22 | - [RedisAdapter#remoteDisconnect(id:String, close:Boolean)](#redisadapterremotedisconnectidstring-closeboolean)
|
23 | - [Client error handling](#client-error-handling)
|
24 | - [Custom client (eg: with authentication)](#custom-client-eg-with-authentication)
|
25 | - [With ioredis client](#with-ioredishttpsgithubcomluinioredis-client)
|
26 | - [Cluster example](#cluster-example)
|
27 | - [Sentinel Example](#sentinel-example)
|
28 | - [Protocol](#protocol)
|
29 | - [License](#license)
|
30 |
|
31 | ## How to use
|
32 |
|
33 | ### CommonJS
|
34 |
|
35 | ```js
|
36 | const io = require('socket.io')(3000);
|
37 | const redisAdapter = require('socket.io-redis');
|
38 | io.adapter(redisAdapter({ host: 'localhost', port: 6379 }));
|
39 | ```
|
40 |
|
41 | ### ES6 modules
|
42 |
|
43 | ```js
|
44 | import { Server } from 'socket.io';
|
45 | import redisAdapter from 'socket.io-redis';
|
46 |
|
47 | const io = new Server(3000);
|
48 | io.adapter(redisAdapter({ host: 'localhost', port: 6379 }));
|
49 | ```
|
50 |
|
51 | ### TypeScript
|
52 |
|
53 | ```ts
|
54 | // npm i -D @types/redis
|
55 | import { Server } from 'socket.io';
|
56 | import { createAdapter } from 'socket.io-redis';
|
57 | import { RedisClient } from 'redis';
|
58 |
|
59 | const io = new Server(8080);
|
60 | const pubClient = new RedisClient({ host: 'localhost', port: 6379 });
|
61 | const subClient = pubClient.duplicate();
|
62 |
|
63 | io.adapter(createAdapter({ pubClient, subClient }));
|
64 | ```
|
65 |
|
66 | By running Socket.IO with the `socket.io-redis` adapter you can run
|
67 | multiple Socket.IO instances in different processes or servers that can
|
68 | all broadcast and emit events to and from each other.
|
69 |
|
70 | So any of the following commands:
|
71 |
|
72 | ```js
|
73 | io.emit('hello', 'to all clients');
|
74 | io.to('room42').emit('hello', "to all clients in 'room42' room");
|
75 |
|
76 | io.on('connection', (socket) => {
|
77 | socket.broadcast.emit('hello', 'to all clients except sender');
|
78 | socket.to('room42').emit('hello', "to all clients in 'room42' room except sender");
|
79 | });
|
80 | ```
|
81 |
|
82 | will properly be broadcast to the clients through the Redis [Pub/Sub mechanism](https://redis.io/topics/pubsub).
|
83 |
|
84 | If you need to emit events to socket.io instances from a non-socket.io
|
85 | process, you should use [socket.io-emitter](https://github.com/socketio/socket.io-emitter).
|
86 |
|
87 | ## Compatibility table
|
88 |
|
89 | | Redis Adapter version | Socket.IO server version |
|
90 | |-----------------------| ------------------------ |
|
91 | | 4.x | 1.x |
|
92 | | 5.x | 2.x |
|
93 | | 6.0.x | 3.x |
|
94 | | 6.1.x and above | 4.x |
|
95 |
|
96 | ## How does it work under the hood?
|
97 |
|
98 | This adapter extends the [in-memory adapter](https://github.com/socketio/socket.io-adapter/) that is included by default with the Socket.IO server.
|
99 |
|
100 | The in-memory adapter stores the relationships between Sockets and Rooms in two Maps.
|
101 |
|
102 | When you run `socket.join("room21")`, here's what happens:
|
103 |
|
104 | ```
|
105 | console.log(adapter.rooms); // Map { "room21" => Set { "mdpk4kxF5CmhwfCdAHD8" } }
|
106 | console.log(adapter.sids); // Map { "mdpk4kxF5CmhwfCdAHD8" => Set { "mdpk4kxF5CmhwfCdAHD8", room21" } }
|
107 | // "mdpk4kxF5CmhwfCdAHD8" being the ID of the given socket
|
108 | ```
|
109 |
|
110 | Those two Maps are then used when broadcasting:
|
111 |
|
112 | - a broadcast to all sockets (`io.emit()`) loops through the `sids` Map, and send the packet to all sockets
|
113 | - a broadcast to a given room (`io.to("room21").emit()`) loops through the Set in the `rooms` Map, and sends the packet to all matching sockets
|
114 |
|
115 | The Redis adapter extends the broadcast function of the in-memory adapter: the packet is also [published](https://redis.io/topics/pubsub) to a Redis channel (see [below](#protocol) for the format of the channel name).
|
116 |
|
117 | Each Socket.IO server receives this packet and broadcasts it to its own list of connected sockets.
|
118 |
|
119 | To check what's happening on your Redis instance:
|
120 |
|
121 | ```
|
122 | $ redis-cli
|
123 | 127.0.0.1:6379> PSUBSCRIBE *
|
124 | Reading messages... (press Ctrl-C to quit)
|
125 | 1) "psubscribe"
|
126 | 2) "*"
|
127 | 3) (integer) 1
|
128 |
|
129 | 1) "pmessage"
|
130 | 2) "*"
|
131 | 3) "socket.io#/#" (a broadcast to all sockets or to a list of rooms)
|
132 | 4) <the packet content>
|
133 |
|
134 | 1) "pmessage"
|
135 | 2) "*"
|
136 | 3) "socket.io#/#room21#" (a broadcast to a single room)
|
137 | 4) <the packet content>
|
138 | ```
|
139 |
|
140 | Note: **no data** is stored in Redis itself
|
141 |
|
142 | There are 3 Redis subscriptions per namespace:
|
143 |
|
144 | - main channel: `<prefix>#<namespace>#*` (glob)
|
145 | - request channel: `<prefix>-request#<namespace>#`
|
146 | - response channel: `<prefix>-response#<namespace>#`
|
147 |
|
148 | The request and response channels are used in the additional methods exposed by the Redis adapter, like [RedisAdapter#allRooms()](#redisadapterallrooms).
|
149 |
|
150 |
|
151 | ## API
|
152 |
|
153 | ### adapter(uri[, opts])
|
154 |
|
155 | `uri` is a string like `localhost:6379` where your redis server
|
156 | is located. For a list of options see below.
|
157 |
|
158 | ### adapter(opts)
|
159 |
|
160 | The following options are allowed:
|
161 |
|
162 | - `key`: the name of the key to pub/sub events on as prefix (`socket.io`)
|
163 | - `host`: host to connect to redis on (`localhost`)
|
164 | - `port`: port to connect to redis on (`6379`)
|
165 | - `pubClient`: optional, the redis client to publish events on
|
166 | - `subClient`: optional, the redis client to subscribe to events on
|
167 | - `requestsTimeout`: optional, after this timeout the adapter will stop waiting from responses to request (`5000ms`)
|
168 |
|
169 | If you decide to supply `pubClient` and `subClient`, make sure you use
|
170 | [node_redis](https://github.com/mranney/node_redis) as a client or one
|
171 | with an equivalent API.
|
172 |
|
173 | ### RedisAdapter
|
174 |
|
175 | The redis adapter instances expose the following properties
|
176 | that a regular `Adapter` does not
|
177 |
|
178 | - `uid`
|
179 | - `prefix`
|
180 | - `pubClient`
|
181 | - `subClient`
|
182 | - `requestsTimeout`
|
183 |
|
184 | ### RedisAdapter#sockets(rooms: Set<String>)
|
185 |
|
186 | Returns the list of socket IDs connected to `rooms` across all nodes. See [Namespace#allSockets()](https://socket.io/docs/v3/server-api/#namespace-allSockets)
|
187 |
|
188 | ```js
|
189 | const sockets = await io.of('/').adapter.sockets();
|
190 | console.log(sockets); // a Set containing all the connected socket ids
|
191 |
|
192 | const sockets = await io.of('/').adapter.sockets(new Set(['room1', 'room2']));
|
193 | console.log(sockets); // a Set containing the socket ids in 'room1' or in 'room2'
|
194 |
|
195 | // this method is also exposed by the Server instance
|
196 | const sockets = await io.in('room3').allSockets();
|
197 | console.log(sockets); // a Set containing the socket ids in 'room3'
|
198 | ```
|
199 |
|
200 | ### RedisAdapter#allRooms()
|
201 |
|
202 | Returns the list of all rooms.
|
203 |
|
204 | ```js
|
205 | const rooms = await io.of('/').adapter.allRooms();
|
206 | console.log(rooms); // a Set containing all rooms (across every node)
|
207 | ```
|
208 |
|
209 | ### RedisAdapter#remoteJoin(id:String, room:String)
|
210 |
|
211 | Makes the socket with the given id join the room.
|
212 |
|
213 | ```js
|
214 | try {
|
215 | await io.of('/').adapter.remoteJoin('<my-id>', 'room1');
|
216 | } catch (e) {
|
217 | // the socket was not found
|
218 | }
|
219 | ```
|
220 |
|
221 | ### RedisAdapter#remoteLeave(id:String, room:String)
|
222 |
|
223 | Makes the socket with the given id leave the room.
|
224 |
|
225 | ```js
|
226 | try {
|
227 | await io.of('/').adapter.remoteLeave('<my-id>', 'room1');
|
228 | } catch (e) {
|
229 | // the socket was not found
|
230 | }
|
231 | ```
|
232 |
|
233 | ### RedisAdapter#remoteDisconnect(id:String, close:Boolean)
|
234 |
|
235 | Makes the socket with the given id to get disconnected. If `close` is set to true, it also closes the underlying socket.
|
236 |
|
237 | ```js
|
238 | try {
|
239 | await io.of('/').adapter.remoteDisconnect('<my-id>', true);
|
240 | } catch (e) {
|
241 | // the socket was not found
|
242 | }
|
243 | ```
|
244 |
|
245 | ## Client error handling
|
246 |
|
247 | Access the `pubClient` and `subClient` properties of the
|
248 | Redis Adapter instance to subscribe to its `error` event:
|
249 |
|
250 | ```js
|
251 | const adapter = require('socket.io-redis')('localhost:6379');
|
252 | adapter.pubClient.on('error', function(){});
|
253 | adapter.subClient.on('error', function(){});
|
254 | ```
|
255 |
|
256 | The errors emitted from `pubClient` and `subClient` will
|
257 | also be forwarded to the adapter instance:
|
258 |
|
259 | ```js
|
260 | const io = require('socket.io')(3000);
|
261 | const redisAdapter = require('socket.io-redis');
|
262 | io.adapter(redisAdapter({ host: 'localhost', port: 6379 }));
|
263 | io.of('/').adapter.on('error', function(){});
|
264 | ```
|
265 |
|
266 | ## Custom client (eg: with authentication)
|
267 |
|
268 | If you need to create a redisAdapter to a redis instance
|
269 | that has a password, use pub/sub options instead of passing
|
270 | a connection string.
|
271 |
|
272 | ```js
|
273 | const redis = require('redis');
|
274 | const redisAdapter = require('socket.io-redis');
|
275 | const pubClient = redis.createClient(port, host, { auth_pass: "pwd" });
|
276 | const subClient = pubClient.duplicate();
|
277 | io.adapter(redisAdapter({ pubClient, subClient }));
|
278 | ```
|
279 |
|
280 | ## With ioredis client
|
281 |
|
282 | ### Cluster example
|
283 |
|
284 | ```js
|
285 | const io = require('socket.io')(3000);
|
286 | const redisAdapter = require('socket.io-redis');
|
287 | const Redis = require('ioredis');
|
288 |
|
289 | const startupNodes = [
|
290 | {
|
291 | port: 6380,
|
292 | host: '127.0.0.1'
|
293 | },
|
294 | {
|
295 | port: 6381,
|
296 | host: '127.0.0.1'
|
297 | }
|
298 | ];
|
299 |
|
300 | io.adapter(redisAdapter({
|
301 | pubClient: new Redis.Cluster(startupNodes),
|
302 | subClient: new Redis.Cluster(startupNodes)
|
303 | }));
|
304 | ```
|
305 |
|
306 | ### Sentinel Example
|
307 |
|
308 | ```js
|
309 | const io = require('socket.io')(3000);
|
310 | const redisAdapter = require('socket.io-redis');
|
311 | const Redis = require('ioredis');
|
312 |
|
313 | const options = {
|
314 | sentinels: [
|
315 | { host: 'somehost1', port: 26379 },
|
316 | { host: 'somehost2', port: 26379 }
|
317 | ],
|
318 | name: 'master01'
|
319 | };
|
320 |
|
321 | io.adapter(redisAdapter({
|
322 | pubClient: new Redis(options),
|
323 | subClient: new Redis(options)
|
324 | }));
|
325 | ```
|
326 |
|
327 | ## Protocol
|
328 |
|
329 | The `socket.io-redis` adapter broadcasts and receives messages on particularly named Redis channels. For global broadcasts the channel name is:
|
330 | ```
|
331 | prefix + '#' + namespace + '#'
|
332 | ```
|
333 |
|
334 | In broadcasting to a single room the channel name is:
|
335 | ```
|
336 | prefix + '#' + namespace + '#' + room + '#'
|
337 | ```
|
338 |
|
339 |
|
340 | - `prefix`: The base channel name. Default value is `socket.io`. Changed by setting `opts.key` in `adapter(opts)` constructor
|
341 | - `namespace`: See https://github.com/socketio/socket.io#namespace.
|
342 | - `room` : Used if targeting a specific room.
|
343 |
|
344 | A number of other libraries adopt this protocol including:
|
345 |
|
346 | - [socket.io-emitter](https://github.com/socketio/socket.io-emitter)
|
347 | - [socket.io-python-emitter](https://github.com/GameXG/socket.io-python-emitter)
|
348 | - [socket.io-emitter-go](https://github.com/stackcats/socket.io-emitter-go)
|
349 |
|
350 | ## License
|
351 |
|
352 | MIT
|