title: Socket.IO

Socket.IO is a real-time application framework based on Node.js, which has a wide range of applications including instant messaging, notification and message push, real-time analysis and other scenarios.

WebSocket originated from the growing demand for real-time communication in web development, compared with http-based polling, which greatly saves network bandwidth and reduces server performance consumption. Socket.IO supports both websockets and polling. The data transmission method is compatible with the browser and does not support the communication requirements under the WebSocket scenario.

The framework provides the egg-socket.io plugin with the following development rules added:

  • namespace: define the namespace by means of configuration
    • middleware: establish / disconnect every socket connection, preprocess every message / data transfer
    • controller: response socket.io event
    • router: unify the processing configuration of socket.io event and frame routing

install egg-socket.io

Installation

  1. $ npm i egg-socket.io --save

Enable the plugin:

  1. // {app_root} /config/plugin.js
  2. exports.io = {
  3. enable: true,
  4. package: 'egg-socket.io'
  5. };

Configuration

  1. // {app_root} / config / config. $ {env} .js
  2. exports.io = {
  3. init: {}, // passed to engine.io
  4. namespace: {
  5. '/': {
  6. connectionMiddleware: [],
  7. packetMiddleware: []
  8. },
  9. '/ example': {
  10. connectionMiddleware: [],
  11. packetMiddleware: []
  12. }
  13. }
  14. };

Namespaces are / and / example, notexample

uws:

If you want to use uws instead of the default ws you can do the following configuration

  1. // {app_root} / config / config. $ {env} .js
  2. exports.io = {
  3. init: { wsEngine: 'uws' } // default: ws
  4. };

redis:

egg-socket.io has built-in redis support via socket.io-redis. In cluster mode, the use of redis can make it relatively simple to achieve information sharing of clients/rooms and so on

  1. // {app_root} / config / config. $ {env} .js
  2. exports.io = {
  3. redis: {
  4. host: {redis server host}
  5. port: {redis server prot},
  6. auth_pass: {redis server password},
  7. db: 0,
  8. },
  9. };

When redis is turned on, the program tries to connect to the redis server at startup
Here redis is only used to store connection instance information, see # server.adapter

note:
If the project also uses the egg-redis, please configure it separately. Do not share it.

Deployment

If the framework is started in cluster mode, the socket.io protocol implementation needs sticky feature support, otherwise it will not work in multi-process mode.

Due to the design of socket.io, a multi-process server must be in the sticky working mode. As a result, you need the need to pass parameter --sticky when starting the cluster.

Modify the npm scripts script inpackage.json:

  1. {
  2. "scripts": {
  3. "dev": "egg-bin dev --sticky",
  4. "start": "egg-scripts start --sticky"
  5. }
  6. }

Nginx configuration

  1. location / {
  2. proxy_set_header Upgrade $ http_upgrade;
  3. proxy_set_header Connection "upgrade";
  4. proxy_set_header X-Forwarded-For $ proxy_add_x_forwarded_for;
  5. proxy_set_header Host $ host;
  6. proxy_pass http://127.0.0.1:7001;
  7. }

Using egg-socket.io

The directory structure of project which has enabled the egg-socket.io is as follows:

  1. chat
  2. ├── app
  3. ├── extend
  4. └── helper.js
  5. ├── io
  6. ├── controller
  7. └── default.js
  8. └── middleware
  9. ├── connection.js
  10. └── packet.js
  11. └── router.js
  12. ├── config
  13. └── package.json

Note: The corresponding files are in the app / io directory

Middleware

Middleware has the following two scenarios:

  • Connection
  • Packet

It is configured in each namespace, respectively, according to the scenarios given above.

note:

If we enable the framework middleware, you will find the following directory in the project:

  • app / middleware: framework middleware
  • app / io / middleware: plugin middleware

the difference:

  • Framework middleware is based on http model design to handle http requests.
  • Plugin middleware based socket model design, processing socket.io request.

Although the framework tries to unify the style through plugins, it is important to note that their usage scenarios are different. For details, please see: # 1416

Connection

Fires when each client connects or quits. Therefore, we usually perform authorization authentication at this step, and deal with the failed clients.

  1. // {app_root} /app/io/middleware/connection.js
  2. module.exports = app => {
  3. return async (ctx, next) => {
  4. ctx.socket.emit('res', 'connected!');
  5. await next(); // execute when disconnect.
  6. console.log('disconnection!');
  7. };
  8. };

Kick out the user example:

  1. const tick = (id, msg) => {
  2. logger.debug('# tick', id, msg);
  3. socket.emit(id, msg);
  4. app.io.of('/').adapter.remoteDisconnect(id, true, err => {
  5. logger.error(err);
  6. });
  7. };

At the same time, the current connection can also be simple to deal with:

  1. // {app_root} /app/io/middleware/connection.js
  2. module.exports = app => {
  3. return async (ctx, next) => {
  4. if (true) {
  5. ctx.socket.disconnect();
  6. return;
  7. }
  8. await next();
  9. console.log('disconnection!');
  10. };
  11. };

Packet

Acts on each data packet (each message). In the production environment, it is usually used to preprocess messages, or it is used to decrypt encrypted messages.

  1. // {app_root} /app/io/middleware/packet.js
  2. module.exports = app => {
  3. return async (ctx, next) => {
  4. ctx.socket.emit('res', 'packet received!');
  5. console.log('packet:', this.packet);
  6. await next();
  7. };
  8. };

Controller

A controller deals with the events sent by the client. Since it inherits the egg.controller, it has the following member objects:

  • ctx
  • app
  • service
  • config
  • logger

For details, refer to the [Controller] (../ basics / controller.md) documentation

  1. // {app_root} /app/io/controller/default.js
  2. 'use strict';
  3. const Controller = require('egg').Controller;
  4. class DefaultController extends Controller {
  5. async ping() {
  6. const { ctx, app } = this;
  7. const message = ctx.args[0];
  8. await ctx.socket.emit('res', `Hi! I've got your message: $ {message}`);
  9. }
  10. }
  11. module.exports = DefaultController;
  12. // or async functions
  13. exports.ping = async function() {
  14. const message = this.args[0];
  15. await this.socket.emit('res', `Hi! I've got your message: $ {message}`);
  16. };

Router

Routing is responsible for passing various events received by the socket to the corresponding controllers.

  1. // {app_root} /app/router.js
  2. module.exports = app => {
  3. const { router, controller, io } = app; // default
  4. router.get('/', controller.home.index); // socket.io
  5. io.of('/').route('server', io.controller.home.server);
  6. };

note:

Nsp has the following system events:

  • disconnecting doing the disconnect
  • disconnect connection has disconnected.
  • error Error occurred

Namespace/Room

Namespace (nsp)

The namespace is usually meant to be assigned to different access points or paths. If the client does not specify a nsp, it is assigned to “/“ by default.

In socket.io we use the of to divide the namespace; given that nsp is usually pre-defined and relatively fixed, the framework encapsulates it and uses configuration to partition different namespaces.

  1. // socket.io
  2. var nsp = io.of('/my-namespace');
  3. nsp.on('connection', function(socket) {
  4. console.log('someone connected');
  5. });
  6. nsp.emit('hi', 'everyone!');
  7. // egg
  8. exports.io = {
  9. namespace: {
  10. '/': {
  11. connectionMiddleware: [],
  12. packetMiddleware: []
  13. }
  14. }
  15. };

Room

Room exists in nsp and is added or left by the join/leave method; the method used in the framework is the same;

  1. Const room = 'default_room';
  2. Module.exports = app => {
  3. return async (ctx, next) => {
  4. ctx.socket.join(room);
  5. ctx.app.io.of('/').to(room).emit('online', { msg: 'welcome', id: ctx.socket.id });
  6. await next();
  7. console.log('disconnection!');
  8. };
  9. };

Note: Each socket connection will have a random and unpredictable unique id Socket#id and will automatically be added to the room named after this id

Examples

Here we use egg-socket.io to do a small example which supports p2p chat

client

The UI-related content is not rewritten. It can be called via window.socket

  1. // browser
  2. const log = console.log;
  3. window.onload = function() {
  4. // init
  5. const socket = io('/', {
  6. // Actual use can pass parameters here
  7. query: {
  8. room: 'demo',
  9. userId: `client_${Math.random()}`,
  10. },
  11. transports: ['websocket']
  12. });
  13. socket.on('connect', () => {
  14. const id = socket.id;
  15. log('#connect,', id, socket); // receive online user information
  16. // listen for its own id to implement p2p communication
  17. socket.on(id, msg => {
  18. log('#receive,', msg);
  19. });
  20. });
  21. socket.on('online', msg => {
  22. log('#online,', msg);
  23. });
  24. // system events
  25. socket.on('disconnect', msg => {
  26. log('#disconnect', msg);
  27. });
  28. socket.on('disconnecting', () => {
  29. log('#disconnecting');
  30. });
  31. socket.on('error', () => {
  32. log('#error');
  33. });
  34. window.socket = socket;
  35. };

WeChat Applets

The API provided by the WeChat applet is WebSocket, and socket.io is the upper encapsulation of Websocket. Therefore, we cannot directly use the API connection of the applet. You can use something like [wxapp-socket-io] (https://github.com/wxsocketio /wxapp-socket-io) to adapt to the library.

The sample code is as follows:

  1. // Small program-side sample code
  2. import io from 'vendor/wxapp-socket-io.js';
  3. const socket = io('ws://127.0.0.1:7001');
  4. socket.on('connect', function() {
  5. socket.emit('chat', 'hello world!');
  6. });
  7. socket.on('res', msg => {
  8. console.log('res from server: %s!', msg);
  9. });

server

The following is part of the demo code and explains the role of each method

config

  1. // {app_root}/config/config.${env}.js
  2. exports.io = {
  3. namespace: {
  4. '/': {
  5. connectionMiddleware: ['auth'],
  6. packetMiddleware: [] // processing for message is not implemented temporarily
  7. }
  8. }, // Data sharing through redis in cluster mode
  9. redis: {
  10. host: '127.0.0.1',
  11. port: 6379
  12. }
  13. };

helper

Framework extensions for encapsulating data formats

  1. // {app_root}/app/extend/helper.js
  2. module.exports = {
  3. parseMsg(action, payload = {}, metadata = {}) {
  4. const meta = Object.assign(
  5. {},
  6. {
  7. timestamp: Date.now()
  8. },
  9. metadata
  10. );
  11. return {
  12. data: {
  13. action,
  14. payload
  15. },
  16. meta
  17. };
  18. }
  19. };

Format:

  1. {
  2. data: {
  3. action: 'exchange', // 'deny' || 'exchange' || 'broadcast'
  4. payload: {},
  5. },
  6. meta:{
  7. timestamp: 1512116201597,
  8. client: '/webrtc#nNx88r1c5WuHf9XuAAAB',
  9. target: '/webrtc#nNx88r1c5WuHf9XuAAAB'
  10. },
  11. }

middleware

egg-socket.io middleware handles socket connection handling

  1. // {app_root}/app/io/middleware/auth.js
  2. const PREFIX = 'room';
  3. module.exports = () => {
  4. return async (ctx, next) => {
  5. const { app, socket, logger, helper } = ctx;
  6. const id = socket.id;
  7. const nsp = app.io.of('/');
  8. const query = socket.handshake.query; // User Info
  9. const { room, userId } = query;
  10. const rooms = [room];
  11. logger.debug('#user_info', id, room, userId);
  12. const tick = (id, msg) => {
  13. logger.debug('#tick', id, msg); // Send message before kicking user
  14. socket.emit(id, helper.parseMsg('deny', msg)); // Call the adapter method to kick out the user and the client triggers the disconnect event
  15. nsp.adapter.remoteDisconnect(id, true, err => {
  16. logger.error(err);
  17. });
  18. }; // Check if the room exists, kick it out if it doesn't exist // Note: here app.redis has nothing to do with the plugin, it can be replaced by other storage
  19. const hasRoom = await app.redis.get(`${PREFIX}:${room}`);
  20. logger.debug('#has_exist', hasRoom);
  21. if (!hasRoom) {
  22. tick(id, {
  23. type: 'deleted',
  24. message: 'deleted, room has been deleted.'
  25. });
  26. return;
  27. } // When the user joins
  28. nsp.adapter.clients(rooms, (err, clients) => {
  29. // Append current socket information to clients
  30. clients[id] = query; // Join room
  31. socket.join(room);
  32. logger.debug('#online_join', _clients); // Update online user list
  33. nsp.to(room).emit('online', {
  34. clients,
  35. action: 'join',
  36. target: 'participator',
  37. message: `User(${id}) joined.`
  38. });
  39. });
  40. await next(); // When the user leaves
  41. nsp.adapter.clients(rooms, (err, clients) => {
  42. logger.debug('#leave', room);
  43. const _clients = {};
  44. clients.forEach(client => {
  45. const _id = client.split('#')[1];
  46. const _client = app.io.sockets.sockets[_id];
  47. const _query = _client.handshake.query;
  48. _clients[client] = _query;
  49. });
  50. logger.debug('#online_leave', _clients); // Update online user list
  51. nsp.to(room).emit('online', {
  52. clients: _clients,
  53. action: 'leave',
  54. target: 'participator',
  55. message: `User(${id}) leaved.`
  56. });
  57. });
  58. };
  59. };

controller

Data exchange of P2P communication is through exchange

  1. // {app_root}/app/io/controller/nsp.js
  2. const Controller = require('egg').Controller;
  3. class NspController extends controller {
  4. async exchange() {
  5. const { ctx, app } = this;
  6. const nsp = app.io.of('/');
  7. const message = ctx.args[0] || {};
  8. const socket = ctx.socket;
  9. const client = socket.id;
  10. try {
  11. const { target, payload } = message;
  12. if (!target) return;
  13. const msg = ctx.helper.parseMsg('exchange', payload, { client, target });
  14. nsp.emit(target, msg);
  15. } catch (error) {
  16. app.logger.error(error);
  17. }
  18. }
  19. }
  20. module.exports = NspController;

router

  1. // {app_root}/app/router.js
  2. module.exports = app => {
  3. const { router, controller, io } = app;
  4. router.get('/', controller.home.index); // socket.io
  5. io.of('/').route('exchange', io.controller.nsp.exchange);
  6. };

Open two tab pages and call up the console:

  1. socket.emit('exchange', {
  2. target: '/webrtc#Dkn3UXSu8_jHvKBmAAHW',
  3. payload: {
  4. msg: 'test'
  5. }
  6. });

Socket.IO - 图1