Socket.IO
12003Socket.IO is a real-time application framework based on Node.js, widely used in scenarios such as instant messaging, notifications, message pushing, and real-time analytics.
The emergence of WebSocket stems from the growing demand for real-time communication in web development. Compared to traditional HTTP polling methods, it significantly saves network bandwidth and reduces server performance consumption. socket.io supports both WebSocket and polling data transmission methods to be compatible with browsers that do not support WebSocket.
The framework provides the egg-socket.io plugin, which adds the following development specifications:
- namespace: Defines the namespace through configuration.
- middleware: Preprocesses each socket connection establishment/disconnection and each message/data transmission.
- controller: Responds to
socket.ioevent events. - router: Unifies the handling configuration of
socket.ioevents and framework routing.
Installing egg-socket.io
Installation
$ npm i egg-socket.io --saveEnable the plugin:
// {app_root}/config/plugin.js
exports.io = {
enable: true,
package: 'egg-socket.io',
};Configuration
// {app_root}/config/config.${env}.js
exports.io = {
init: {}, // Passed to engine.io
namespace: {
'/': {
connectionMiddleware: [],
packetMiddleware: [],
},
'/example': {
connectionMiddleware: [],
packetMiddleware: [],
},
},
};The namespaces are
/and/example, notexample.
uws
Egg Socket internally defaults to using the ws engine. uws has been deprecated for certain reasons.
If you insist on using uws, please configure as follows:
// {app_root}/config/config.${env}.js
exports.io = {
init: { wsEngine: 'uws' }, // Default is ws
};redis
egg-socket.io has built-in support for socket.io-redis. In cluster mode, using Redis can easily achieve sharing of clients/rooms and other information.
// {app_root}/config/config.${env}.js
exports.io = {
redis: {
host: { redis server host },
port: { redis server port },
auth_pass: { redis server password },
db: 0,
},
};After enabling
redis, the program will attempt to connect to the Redis server at startup. Theredishere is only used to store connection instance information; see #server.adapter for details.
Note:
If egg-redis is used in the project, please configure them separately; they cannot be shared.
Deployment
Since the framework is started in Cluster mode, and the socket.io protocol implementation requires sticky feature support, it can only work properly in multi-process mode.
Due to the design of socket.io, multi-process servers must operate in sticky mode. Therefore, the sticky parameter needs to be passed to startCluster.
Modify the npm scripts in package.json:
{
"scripts": {
"dev": "egg-bin dev --sticky",
"start": "egg-scripts start --sticky"
}
}Nginx Configuration
location / {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:7001;
# http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_bind
# proxy_bind $remote_addr transparent;
}Using egg-socket.io
The project directory structure for enabling egg-socket.io is as follows:
chat
├── app
│ ├── extend
│ │ └── helper.js
│ ├── io
│ │ ├── controller
│ │ │ └── default.js
│ │ └── middleware
│ │ ├── connection.js
│ │ └── packet.js
│ └── router.js
├── config
└── package.jsonNote: The corresponding files are all located in the
app/iodirectory.
Middleware
There are two scenarios for middleware:
- Connection
- Packet
Their configurations are located under each namespace and take effect according to the above two scenarios.
Note:
If the framework middleware is enabled, the following directories will be found in the project:
app/middleware: Framework middleware.app/io/middleware: Plugin middleware.
The differences are:
- Framework middleware is designed based on the HTTP model and handles HTTP requests.
- Plugin middleware is designed based on the socket model and handles
socket.iorequests.
Although the framework tries to unify their styles through plugins, it must be noted that their usage scenarios are different. For details, please refer to issue #1416.
Connection
This takes effect when each client connects or disconnects. Therefore, authorization and authentication are usually performed at this step, and clients that fail authentication are handled.
// {app_root}/app/io/middleware/connection.js
module.exports = app => {
return async (ctx, next) => {
ctx.socket.emit('res', 'connected!');
await next();
// execute when disconnect.
console.log('disconnection!');
};
};Example of kicking out a user:
const tick = (id, msg) => {
logger.debug('#tick', id, msg);
socket.emit(id, msg);
app.io.of('/').adapter.remoteDisconnect(id, true, err => {
logger.error(err);
});
};Example of simple handling for the current connection:
// {app_root}/app/io/middleware/connection.js
module.exports = app => {
return async (ctx, next) => {
if (true) {
ctx.socket.disconnect();
return;
}
await next();
console.log('disconnection!');
};
};Packet
This middleware is executed for each data packet (message). In production environments, it is typically used for preprocessing messages or decrypting encrypted messages.
// {app_root}/app/io/middleware/packet.js
module.exports = app => {
return async (ctx, next) => {
ctx.socket.emit('res', 'packet received!');
console.log('packet:', ctx.packet);
await next();
};
};Controller
The Controller handles events sent by the client; since it inherits from egg.Controller, it has the following member objects:
ctxappserviceconfiglogger
For details, refer to the Controller documentation.
// {app_root}/app/io/controller/default.js
'use strict';
const Controller = require('egg').Controller;
class DefaultController extends Controller {
async ping() {
const { ctx } = this;
const message = ctx.args[0];
await ctx.socket.emit('res', `Hi! I've got your message: ${message}`);
}
}
module.exports = DefaultController;Router
The router is responsible for dispatching different events of socket connections to the corresponding controllers, and the framework has unified its usage.
// {app_root}/app/router.js
module.exports = app => {
const { router, controller, io } = app;
// default
router.get('/', controller.home.index);
// socket.io
io.of('/').route('server', io.controller.home.server);
};Note:
nsp has the following system events:
disconnecting: Disconnecting.disconnect: Disconnected.error: An error occurred.
Namespace/Room
Namespace (nsp)
namespace usually means assigned to different access points or paths. If the client does not specify nsp, it is by default assigned to the "/" default namespace.
In socket.io, we use of to delineate namespaces; since nsp is usually predefined and relatively fixed, the framework encapsulates it and uses configuration to delineate different namespaces.
// socket.io
const nsp = io.of('/my-namespace');
nsp.on('connection', socket => {
console.log('someone connected');
});
nsp.emit('hi', 'everyone!');
// egg
exports.io = {
namespace: {
'/': {
connectionMiddleware: [],
packetMiddleware: [],
},
},
};Room
room exists within nsp, and you can join or leave using the join/leave methods; the framework uses the same methods.
const room = 'default_room';
module.exports = app => {
return async (ctx, next) => {
ctx.socket.join(room);
ctx.app.io
.of('/')
.to(room)
.emit('online', { msg: 'welcome', id: ctx.socket.id });
await next();
console.log('disconnection!');
};
};Note: Each socket connection will have a randomly generated and unpredictable unique id Socket#id, and will automatically join the room named after this id.
Example
Here we use egg-socket.io to create a small example that supports P2P chat.
Client
UI-related content is not repeated; it can be called through window.socket.
// Browser
const log = console.log;
window.onload = function () {
// Initialization
const socket = io('/', {
// In actual use, parameters can be passed here
query: {
room: 'demo',
userId: `client_${Math.random()}`, // Passed room and userId parameters
},
transports: ['websocket'],
});
socket.on('connect', () => {
const id = socket.id;
log('#connect,', id, socket);
// Listen to its own id for P2P communication
socket.on(id, (msg) => {
log('#receive,', msg);
});
});
// Receive online user information
socket.on('online', (msg) => {
log('#online,', msg);
});
// System events
socket.on('disconnect', (msg) => {
log('#disconnect', msg);
});
socket.on('disconnecting', () => {
log('#disconnecting');
});
socket.on('error', () => {
log('#error');
});
window.socket = socket;
};WeChat Mini Program
The API provided by WeChat Mini Program is WebSocket, and since socket.io is a layer of encapsulation over WebSocket, we cannot directly use the Mini Program's API to connect. Libraries like weapp.socket.io can be used for adaptation.
Example code is as follows:
// Mini program example code
const io = require('./your_path/weapp.socket.io.js'); // Please replace with the actual path
const socket = io('http://localhost:8000');
socket.on('connect', function () {
console.log('connected');
});
socket.on('news', (d) => {
console.log('received news:', d);
});
socket.emit('news', {
title: 'this is a news',
});Server
The following is part of the code for the demo, explaining the function of each method.
config
// {app_root}/config/config.${env}.js
exports.io = {
namespace: {
'/': {
connectionMiddleware: ['auth'],
packetMiddleware: [] // Message handling is not implemented for now
}
},
// In cluster mode, data sharing is implemented through redis
redis: {
host: '127.0.0.1',
port: 6379
}
};
// Optional
exports.redis = {
client: {
port: 6379,
host: '127.0.0.1',
password: '',
db: 0
}
};helper
The framework extension is used to encapsulate data formats.
// {app_root}/app/extend/helper.js
module.exports = {
parseMsg(action, payload = {}, metadata = {}) {
const meta = Object.assign({}, { timestamp: Date.now() }, metadata);
return {
meta,
data: {
action,
payload
}
};
}
};Format:
{
data: {
action: 'exchange', // 'deny' || 'exchange' || 'broadcast'
payload: {}
},
meta: {
timestamp: 1512116201597,
client: 'nNx88r1c5WuHf9XuAAAB',
target: 'nNx88r1c5WuHf9XuAAAB'
}
}Middleware
egg-socket.io middleware is responsible for handling socket connections.
// {app_root}/app/io/middleware/auth.js
const PREFIX = 'room';
module.exports = () => {
return async (ctx, next) => {
const { app, socket, logger, helper } = ctx;
const id = socket.id;
const nsp = app.io.of('/');
const query = socket.handshake.query;
// User information
const { room, userId } = query;
const rooms = [room];
logger.debug('#user_info', id, room, userId);
const tick = (id, msg) => {
logger.debug('#tick', id, msg);
// Send message before kicking out the user
socket.emit(id, helper.parseMsg('deny', msg));
// Call adapter method to kick out the user, the client will trigger disconnect event
nsp.adapter.remoteDisconnect(id, true, (err) => {
logger.error(err);
});
};
// Check if the room exists, if not, kick out the user
// Note: app.redis here is unrelated to the plugin and can be replaced with other storage
const hasRoom = await app.redis.get(`${PREFIX}:${room}`);
logger.debug('#has_exist', hasRoom);
// If the room does not exist
if (!hasRoom) {
tick(id, {
type: 'deleted',
message: 'deleted, room has been deleted.'
});
return;
}
// User joins the room
logger.debug('#join', room);
socket.join(room);
// Get online list
nsp.adapter.clients(rooms, (err, clients) => {
logger.debug('#online_join', clients);
// Update online user list
nsp.to(room).emit('online', {
clients,
action: 'join',
target: 'participator',
message: `User(${id}) joined.`
});
});
await next();
// User leaves the room
logger.debug('#leave', room);
// Get online list
nsp.adapter.clients(rooms, (err, clients) => {
logger.debug('#online_leave', clients);
// Update online user list
nsp.to(room).emit('online', {
clients,
action: 'leave',
target: 'participator',
message: `User(${id}) leaved.`
});
});
};
};Controller
P2P communication is achieved through the exchange method for data exchange.
// {app_root}/app/io/controller/nsp.js
const Controller = require('egg').Controller;
class NspController extends Controller {
async exchange() {
const { ctx, app } = this;
const nsp = app.io.of('/');
const message = ctx.args[0] || {};
const socket = ctx.socket;
const client = socket.id;
try {
const { target, payload } = message;
if (!target) return;
const msg = ctx.helper.parseMsg('exchange', payload, { client, target });
nsp.emit(target, msg);
} catch (error) {
app.logger.error(error);
}
}
}
module.exports = NspController;Router
// {app_root}/app/router.js
module.exports = (app) => {
const { router, controller, io } = app;
router.get('/', controller.home.index);
// socket.io
io.of('/').route('exchange', io.controller.nsp.exchange);
};Open two tab pages and bring up the console:
socket.emit('exchange', {
target: 'Dkn3UXSu8_jHvKBmAAHW',
payload: {
msg: 'test',
},
});
Reference Links
- socket.io
- egg-socket.io
- egg-socket.io Example (egg-socket.io example)
- egg-socket.io Demo (egg-socket.io demo)
- nginx proxy bind (nginx proxy_bind)