Skip to content

Commit 7467216

Browse files
docs(examples): 4th and final part of the "private messaging" example
See also: https://socket.io/get-started/private-messaging-part-4/
1 parent 7247b40 commit 7467216

File tree

6 files changed

+156
-17
lines changed

6 files changed

+156
-17
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const cluster = require("cluster");
2+
const http = require("http");
3+
const { setupMaster } = require("@socket.io/sticky");
4+
5+
const WORKERS_COUNT = 4;
6+
7+
if (cluster.isMaster) {
8+
console.log(`Master ${process.pid} is running`);
9+
10+
for (let i = 0; i < WORKERS_COUNT; i++) {
11+
cluster.fork();
12+
}
13+
14+
cluster.on("exit", (worker) => {
15+
console.log(`Worker ${worker.process.pid} died`);
16+
cluster.fork();
17+
});
18+
19+
const httpServer = http.createServer();
20+
setupMaster(httpServer, {
21+
loadBalancingMethod: "least-connection", // either "random", "round-robin" or "least-connection"
22+
});
23+
const PORT = process.env.PORT || 3000;
24+
25+
httpServer.listen(PORT, () =>
26+
console.log(`server listening at http://localhost:${PORT}`)
27+
);
28+
} else {
29+
console.log(`Worker ${process.pid} started`);
30+
require("./index");
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
version: "3"
2+
3+
services:
4+
redis:
5+
image: redis:5
6+
ports:
7+
- "6379:6379"

examples/private-messaging/server/index.js

+22-14
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,30 @@
11
const httpServer = require("http").createServer();
2+
const Redis = require("ioredis");
3+
const redisClient = new Redis();
24
const io = require("socket.io")(httpServer, {
35
cors: {
46
origin: "http://localhost:8080",
57
},
8+
adapter: require("socket.io-redis")({
9+
pubClient: redisClient,
10+
subClient: redisClient.duplicate(),
11+
}),
612
});
713

14+
const { setupWorker } = require("@socket.io/sticky");
815
const crypto = require("crypto");
916
const randomId = () => crypto.randomBytes(8).toString("hex");
1017

11-
const { InMemorySessionStore } = require("./sessionStore");
12-
const sessionStore = new InMemorySessionStore();
18+
const { RedisSessionStore } = require("./sessionStore");
19+
const sessionStore = new RedisSessionStore(redisClient);
1320

14-
const { InMemoryMessageStore } = require("./messageStore");
15-
const messageStore = new InMemoryMessageStore();
21+
const { RedisMessageStore } = require("./messageStore");
22+
const messageStore = new RedisMessageStore(redisClient);
1623

17-
io.use((socket, next) => {
24+
io.use(async (socket, next) => {
1825
const sessionID = socket.handshake.auth.sessionID;
1926
if (sessionID) {
20-
const session = sessionStore.findSession(sessionID);
27+
const session = await sessionStore.findSession(sessionID);
2128
if (session) {
2229
socket.sessionID = sessionID;
2330
socket.userID = session.userID;
@@ -35,7 +42,7 @@ io.use((socket, next) => {
3542
next();
3643
});
3744

38-
io.on("connection", (socket) => {
45+
io.on("connection", async (socket) => {
3946
// persist session
4047
sessionStore.saveSession(socket.sessionID, {
4148
userID: socket.userID,
@@ -54,8 +61,12 @@ io.on("connection", (socket) => {
5461

5562
// fetch existing users
5663
const users = [];
64+
const [messages, sessions] = await Promise.all([
65+
messageStore.findMessagesForUser(socket.userID),
66+
sessionStore.findAllSessions(),
67+
]);
5768
const messagesPerUser = new Map();
58-
messageStore.findMessagesForUser(socket.userID).forEach((message) => {
69+
messages.forEach((message) => {
5970
const { from, to } = message;
6071
const otherUser = socket.userID === from ? to : from;
6172
if (messagesPerUser.has(otherUser)) {
@@ -64,7 +75,8 @@ io.on("connection", (socket) => {
6475
messagesPerUser.set(otherUser, [message]);
6576
}
6677
});
67-
sessionStore.findAllSessions().forEach((session) => {
78+
79+
sessions.forEach((session) => {
6880
users.push({
6981
userID: session.userID,
7082
username: session.username,
@@ -110,8 +122,4 @@ io.on("connection", (socket) => {
110122
});
111123
});
112124

113-
const PORT = process.env.PORT || 3000;
114-
115-
httpServer.listen(PORT, () =>
116-
console.log(`server listening at http://localhost:${PORT}`)
117-
);
125+
setupWorker(io);

examples/private-messaging/server/messageStore.js

+29
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,35 @@ class InMemoryMessageStore extends MessageStore {
2020
}
2121
}
2222

23+
const CONVERSATION_TTL = 24 * 60 * 60;
24+
25+
class RedisMessageStore extends MessageStore {
26+
constructor(redisClient) {
27+
super();
28+
this.redisClient = redisClient;
29+
}
30+
31+
saveMessage(message) {
32+
const value = JSON.stringify(message);
33+
this.redisClient
34+
.multi()
35+
.rpush(`messages:${message.from}`, value)
36+
.rpush(`messages:${message.to}`, value)
37+
.expire(`messages:${message.from}`, CONVERSATION_TTL)
38+
.expire(`messages:${message.to}`, CONVERSATION_TTL)
39+
.exec();
40+
}
41+
42+
findMessagesForUser(userID) {
43+
return this.redisClient
44+
.lrange(`messages:${userID}`, 0, -1)
45+
.then((results) => {
46+
return results.map((result) => JSON.parse(result));
47+
});
48+
}
49+
}
50+
2351
module.exports = {
2452
InMemoryMessageStore,
53+
RedisMessageStore,
2554
};

examples/private-messaging/server/package.json

+5-2
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44
"description": "",
55
"main": "index.js",
66
"scripts": {
7-
"start": "node index.js"
7+
"start": "node cluster.js"
88
},
99
"author": "Damien Arrachequesne <[email protected]>",
1010
"license": "MIT",
1111
"dependencies": {
12-
"socket.io": "^3.1.1"
12+
"@socket.io/sticky": "^1.0.0",
13+
"ioredis": "^4.22.0",
14+
"socket.io": "^3.1.1",
15+
"socket.io-redis": "^6.0.1"
1316
}
1417
}

examples/private-messaging/server/sessionStore.js

+62-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,67 @@ class InMemorySessionStore extends SessionStore {
2323
}
2424
}
2525

26+
const SESSION_TTL = 24 * 60 * 60;
27+
const mapSession = ([userID, username, connected]) =>
28+
userID ? { userID, username, connected: connected === "true" } : undefined;
29+
30+
class RedisSessionStore extends SessionStore {
31+
constructor(redisClient) {
32+
super();
33+
this.redisClient = redisClient;
34+
}
35+
36+
findSession(id) {
37+
return this.redisClient
38+
.hmget(`session:${id}`, "userID", "username", "connected")
39+
.then(mapSession);
40+
}
41+
42+
saveSession(id, { userID, username, connected }) {
43+
this.redisClient
44+
.multi()
45+
.hset(
46+
`session:${id}`,
47+
"userID",
48+
userID,
49+
"username",
50+
username,
51+
"connected",
52+
connected
53+
)
54+
.expire(`session:${id}`, SESSION_TTL)
55+
.exec();
56+
}
57+
58+
async findAllSessions() {
59+
const keys = new Set();
60+
let nextIndex = 0;
61+
do {
62+
const [nextIndexAsStr, results] = await this.redisClient.scan(
63+
nextIndex,
64+
"MATCH",
65+
"session:*",
66+
"COUNT",
67+
"100"
68+
);
69+
nextIndex = parseInt(nextIndexAsStr, 10);
70+
results.forEach((s) => keys.add(s));
71+
} while (nextIndex !== 0);
72+
const commands = [];
73+
keys.forEach((key) => {
74+
commands.push(["hmget", key, "userID", "username", "connected"]);
75+
});
76+
return this.redisClient
77+
.multi(commands)
78+
.exec()
79+
.then((results) => {
80+
return results
81+
.map(([err, session]) => (err ? undefined : mapSession(session)))
82+
.filter((v) => !!v);
83+
});
84+
}
85+
}
2686
module.exports = {
27-
InMemorySessionStore
87+
InMemorySessionStore,
88+
RedisSessionStore,
2889
};

0 commit comments

Comments
 (0)