socket.io多实例集群化实现

Socket.io作为服务器推送的首要选择,可以很方便的在客户端和Web服务器之间实现双向通信。很多人在选择之初都会有个疑问:socket.io是有状态长链接服务,它能支撑多少个用户同时在线?

虽然目前没有标准统一的答案,但是,在开发实践中已经证明,在单机情况下10万用户是没问题的。

如果您觉得10万还是不够,那么可以通过多实例的方式来支持更多的用户也是可行的。实现方式几乎是零成本升级。

您只需要将默认的适配器更换为redis或其他适配器就可以支持多实例:

一个基于epxress和socket.io的多实例服务配置:

#!/usr/bin/env node

const app = require('../app');
const debug = require('debug')('ws_server:www');
const { createServer } = require('http');
const { Server } = require('socket.io');
const { createAdapter } = require("@socket.io/redis-adapter");
const { Redis } = require("ioredis");

const port = normalizePort(process.env.PORT || '3000');
app.set('port', port);

const httpServer = createServer(app);

/**
 * Normalize a port into a number, string, or false.
 */
function normalizePort(val) {
	var port = parseInt(val, 10);
	if (isNaN(port)) {
		// named pipe
		return val;
	}
	if (port >= 0) {
		// port number
		return port;
	}
	return false;
}

/**
 * Event listener for HTTP server "error" event.
 */
function onError(error) {
	if (error.syscall !== 'listen') {
		throw error;
	}

	var bind = typeof port === 'string'
		? 'Pipe ' + port
		: 'Port ' + port;

	// handle specific listen errors with friendly messages
	switch (error.code) {
		case 'EACCES':
			console.error(bind + ' requires elevated privileges');
			process.exit(1);
			break;
		case 'EADDRINUSE':
			console.error(bind + ' is already in use');
			process.exit(1);
			break;
		default:
			throw error;
	}
}

/**
 * Event listener for HTTP server "listening" event.
 */
function onListening() {
	var addr = httpServer.address();
	var bind = typeof addr === 'string'
		? 'pipe ' + addr
		: 'port ' + addr.port;
	console.log('Listening on ' + bind);
}
// 重点部分
const pubClient = new Redis({
	host: process.env.REDIS_HOST,
	port: process.env.REDIS_PORT,
	password: process.env.REDIS_PASSWORD,
});
const subClient = pubClient.duplicate();

const io = new Server(httpServer, {
	adapter: createAdapter(pubClient, subClient),
	cors: {
		origin: ["https://admin.socket.io", "http://127.0.0.1:8080"],
		// allowedHeaders: ["Authorization", "Cookie"],
		credentials: true
	}
});

app.set("IO", io);

httpServer.listen(port);
httpServer.on('error', onError);
httpServer.on('listening', onListening);

经过上面的改动后,所有的socket.io服务器实例可以共享一个redis实例来存储连接的状态信息。

假设部署了两个socektio服务器serverA和serverB,它们既充当了Web服务器,也充当了socket.io的通讯服务。 当客户端clientA连接到服务器serverA时,serverB接收到了restfult请求,需要向clientA推送消息,在不改动原代码的情况下依旧可以顺利完成。