An egg – socket. IO function

Egg-socket. IO is a wrapper around socket. IO with specifications for router, Controller, Namespace, and Middleware.

The Router and controller are mainly used to distribute and process requests from socket. IO clients. However, we introduced socket. IO to obtain the capability of active push from the server, so these two parts have been skipped.

The rest of namespace and middleware are associated with pushing to specific clients, and in egg-socket. IO they are configured through a configuration file

// config.default.js
config.io = {
  namespace: {
    '/': {
      connectionMiddleware: [ 'auth'].// Connection processing
      packetMiddleware: [], / / packet processing}},generateId: req= > { // Custom socket.id generating function
    const data = qs.parse(req.url.split('? ') [1]);
    return data.userId; // custom id must be unique}};Copy the code

A brief introduction to the functions:

Namespace (namespace)

IO is the namespace of socket. IO, which is used to divide client socket connections into a set. The same set of connections are placed under a namespace.

Through the Namespace server, you can broadcast messages to all clients in the namespace or individually send messages to clients with a specified socketId.

const namespace = app.io.of('/'); // get the namespace "/"
//namespace.sockets: all clients connected to the namespace in the form {id1:socket1, ID2 :socket2}
// The socket.id is used to send messages to specified clients in the namespace
namespace.sockets[socketId].emit('message', msg); 
Copy the code

Middleware

ConnectionMiddleware: connection processing middleware

Handles each socket connection establishment/disconnection

// {app_root}/app/io/middleware/connection.js
module.exports = app= > {
  return async (ctx, next) => {
    console.log('connection! ');
    await next();
    // execute when disconnect.
    console.log('disconnection! ');
  };
};
Copy the code

PacketMiddleware: packet processing middleware

Each message is processed

// {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();
  };
};
Copy the code

Several ways to send messages to specified clients

A common scenario for sending a message to a specified client is when the behavior of one user triggers notification to another user.

For example, if user A likes user B’s activity, we need to push the liked message to user B given his userId.

Namespace.sockets [socketId]. Emit (‘message’, MSG); Push to the specified client.

So now the problem is how to obtain the socketId from the userId, and complete the userId ->socketId -> client chain.

1) Broadcasting (not desirable)

By obtaining a namespace, you can broadcast to all clients connected to the namespace.

const namespace = app.io.of('/');
namespace.emit('event', data);
Copy the code

If the recipient information is carried in the message content, the client can determine whether the message is sent to itself and process it or ignore it accordingly, which can achieve the purpose of message push in a disguised way.

The disadvantages of this approach are obvious:

  • Sending too many invalid messages while broadcasting
  • Other clients will receive messages even if they do not process them, which is not secure

2. generateId()

Egg-socket. IO also provides the generateId configuration item to generate a custom ID, which replaces the default random ID of socket. IO.

  generateId: req= > {
    // Generate an ID based on req
    return id;
  },
Copy the code

If you could return the userId directly from generateId(), you wouldn’t have to worry about the mapping from userId to socketId.

How do I get the user ID from the generateId() function?

The parameter req is HTTP.IncomingMessage, the original REQ object of the Node HTTP module, and does not directly access the session attribute. This starts with the other information carried in the REQ.

  1. cookie

    • Req is attached with the cookie when the request is initiated. If you get the cookie, you willegg-sessionTo retrieve session information, go through the plugin process again
  2. The query parameter in the URL

    • Generally, the client initiates the socket. IO connection after the HTTP login. The client can append user information to the connection URL when it initiates a connection, which is performed by the servergenerateId(req)From the query parameter

Both approaches have their own obvious drawbacks:

Getting a session using cookies looks good, but retracing the egg-session process may not be easy.

Adding a bare userId to a query is a security risk for forgery, and a signature is required to prevent forgery and tampering, adding an additional step.

The value is as follows: User information + signature is returned when the login is successful, which is carried when the socket. IO is connected.

In addition, generateId has the disadvantage that the function is written in a configuration file, which is always not enough to write such complex processing. Uh, think of a word (elegant)

3. Save the mapping between the user ID and socket.id

The final approach is simple, intuitive, transparent to the client, and requires no extra work, just connect.

The specific method is as follows: when the client is connected, the mapping between the current user ID and socket.id is saved in the connection middleware in Redis (if it is a single process, the global Map can be directly used). Sockets [socketId]. Emit (‘message’, MSG) if a notification is sent to a specified client, run namespace.sockets[socketId]. Send.

Complete sample

After introducing several implementation methods, here I use the third way 3. Save user ID and socket.id mapping relationship to write a minimal runnable example, complete code at github.com/hhgfy/demos…

The execution sequence for this example is shown

  1. First make an HTTP request/loginSet the user login information and write it to the session
  // {app_root}/app/controller/home.js
  async login() {
    const { ctx } = this;
    const { id, name } = ctx.query;
    ctx.session.user = { id, name };
    ctx.body = {
      session: ctx.session,
    };
  }
Copy the code
  1. The client gets the returned cookie and carries the connection to execute socket. IO (in practice, the browser does not actively carry cookies).
// {app_root}/test/app/io/io.test.js
const res1 = await app.httpRequest().get('/login? id=100&name=aaa').expect(200);
const cookie1 = res1.headers['set-cookie'].join('; ');// Get the cookie of the session already set
const client1 = client({ extraHeaders: { cookie: cookie1, port: basePort } });
Copy the code
  1. The connection processing middleware saves the mapping between userId and socketId. When this is done, you can send a message to the user.
// {app_root}/app/io/middleware/auth.js
module.exports = app= > {
  return async (ctx, next) => {
    // connect
    if(! ctx.session.user)return;
    const key = `${ctx.enums.prefix.socketId}${ctx.session.user.id}`;
    const MAX_TTL = 24 * 60 * 60;// Maximum expiration time
    await app.redis.set(key, ctx.socket.id, 'EX', MAX_TTL);
    await next();
    await app.redis.del(key); // disconnect
  };
};
Copy the code
  1. Another client triggers a message notification, which is sent to the specified connected client
// {app_root}/test/app/io/io.test.js
await app.httpRequest().get('/push? targetUserId=100&msg=msg1').expect(200);
Copy the code
  1. Look for socketId by user ID and send a message
  // {app_root}/app/controller/home.js
  async push() {
    const { ctx, app } = this;
    const targetUserId = ctx.query.targetUserId;
    const msg = ctx.query.msg;

    const key = `${ctx.enums.prefix.socketId}${targetUserId}`;
    const socketId = await app.redis.get(key); / / get socketId
    ctx.logger.info(key, socketId);
    if (socketId) {
      const namespace = app.io.of('/');
      namespace.sockets[socketId].emit('message', msg);
    }
    ctx.body = ctx.errCodes.success;
  }
Copy the code

Run NPM run test