Based on the communication WebSocket Explanation of ThinkJS

Foreword

Our project is based on ThinkJS + Vue developed recently implemented a multiport feature real-time synchronization of data, so write the next article describes how to use real-time communication WebSocket achieve many-fold in ThinkJS project. ThinkJS Koa 2 is based on the development of enterprise-class server Node.js framework, the article will start from scratch to achieve a simple chat room, I hope readers can gain something.

WebSocket

HTML5 WebSocket is a protocol proposed. It appears to solve the problem of real-time communications client and server. Before WebSocket appear, if you want to achieve real-time messaging, there are two ways:

  1. The client sends a request to stop the server by polling, if there is a new message to update the client. The disadvantage of this approach is obvious, the client needs to stop sending requests to the server, however, HTTP requests may contain a long head, which truly effective data may only be a small part, obviously it will waste a lot of bandwidth resources
  2. Long HTTP connection, the client through the HTTP request to connect to the server, the underlying TCP connection is not disconnected immediately, or subsequent information may be transmitted via the same connection. In this way there is a problem is that each connection will take up server resources, disconnect after receiving the message, you need to resend the request. So the cycle.

You can see, the nature of the client or two implementations of the process to the server "Pull", and not a server initiative "Push" to the client the way, all the way are dependent client first initiates a request. In order to meet both the real-time communication, WebSocket came into being.

WebSocket protocol

First, WebSocket is based on the HTTP protocol, or HTTP protocol borrowed to complete the handshake part of the connection. Second, is a persistent WebSocket protocol, with respect to such non-persistent HTTP protocol, the request after receiving a HTTP server replies directly disconnect messages need to acquire the next HTTP request, and the WebSocket after a successful connection can stay connected. The following diagram should be able to reflect the relationship between the two:

When initiating WebSocket requests need to tell the server via HTTP request needs to upgrade protocol WebSocket.

The browser sends a request to:

GET / HTTP/1.1
Host: localhost:8080
Origin: [url=http://127.0.0.1:3000]http://127.0.0.1:3000[/url]
Connection: Upgrade
Upgrade: WebSocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw==
复制代码

Server response to the request:

HTTP/1.1 101 Switching Protocols
Connection:Upgrade
Upgrade: WebSocket
Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=
复制代码

The core portion is in the request header and Connection Upgrade, by these two fields will HTTP server upgraded WebSocket protocol. After the server returns the corresponding information successful connection, the client and server can be the normal communication.

With the new standards to promote, WebSocket is relatively mature, and each major browsers support WebSocket situation is better (not compatible with low version of IE, IE 10 or less)

Socket.io

Socket.io is a fully implemented JavaScript, based on Node.js, support WebSocket protocol for real-time communication, cross-platform, open-source framework. It includes client-side JavaScript and server-side Node.js, and has good compatibility, can choose different ways to communicate, such as described above polling and HTTP long connections based on the browser support.

Simple chat room

Support for WebSocket currently ThinkJS Socket.io and its packing some simple, requires only some simple configuration can be
to use the WebSocket.

Server configuration

stickyCluster

ThinkJS default uses a multi-process model, each request will be transported to a different process, based policy enforcement, on its multi-process model can refer to "go into the details ThinkJS multi-process model" . The need to use an HTTP request to complete the handshake before upgrading WebSocket connection, multiple requests need to ensure that the same hit process, in order to ensure the success of the handshake. This time we need to open StickyCluster function, so that the client requests all hit the same process. Modify the configuration file src/config/config.jscan be.

module.exports = {
    stickyCluster: true,
    // ...
}
复制代码

Add WebSocket Configuration

In the src/config/extend.jsintroduction WebSocket:

const websocket = require('think-websocket');
module.exports = [
  // ...
  websocket(think.app),
];
复制代码

In the src/config/adapter.jsconfiguration file WebSocket

const socketio = require('think-websocket-socket.io');
exports.websocket = {
  type: 'socketio',
  common: {
    // common config
  },
  socketio: {
    handle: socketio,
    messages: {
      open: '/websocket/open', //建立连接时处理对应到 websocket Controller 下的 open Action
      close: '/websocket/close', // 关闭连接时处理的 Action
      room: '/websocket/room' // room 事件处理的 Action
    }
  }
}
复制代码

Configuration messagecorresponds to the mapping between events. For example, the above example, the client trigger event room, the server needs to websocket under the controller's roomActionprocessing messages.

Add WebSocket achieve

Create a file processing controller messages. The above configuration is /websocket/xxxso directly in the project root directory src/controllercreate websocket.js file.

module.exports = class extends think.Controller {
// this.socket 为发送消息的客户端对应的 socket 实例, this.io 为Socket.io 的一个实例
  constructor(...arg) {
    super(...arg);
    this.io = this.ctx.req.io;
    this.socket = this.ctx.req.websocket;
  }
  async openAction() {
    this.socket.emit('open', 'websocket success')
  }
  
  closeAction() {
    this.socket.disconnect(true);
  }
};
复制代码

This time the server code is already configured finished.

Client Configuration

The client code is relatively simple to use, only need to introduce socket.io.js can be used directly.

<script src="https://lib.baomitu.com/socket.io/2.0.1/socket.io.js"></script>
复制代码

After the introduction of WebSocket connection is created in the initialization code:

this.socket = io();
this.socket.on('open', data => {
    console.log('open', data)
})
复制代码

Such a simple WebSocket of the demo is complete, open the page of time will automatically create a WebSocket connection, created after the success of the server will trigger open event, the client open the event listener's will receive websocket success returned from the server string.
Then we began to implement a simple chat room.

Achieve simple chat room

Just from the content, we know that each WebSocket connection will create a Socket handle to create, correspond to the code this.socketvariable. So essentially chat room communication between people who can be converted into the corresponding Socket communication with each handle. I just need to find the person corresponding Socket handle, it can send messages to each other to achieve.

Simple to realize we can set a global variable to store some information to connect to the WebSocket server. Set a global variable in src / bootstrap / global.js of:

global.$socketChat = {};
复制代码

Then introduced global.js in src / bootstrap / worker.js in the global variable to take effect.

require('./global');
复制代码

The server controller then increases roomActionand messageAction, messageActionfor receiving the user's chat client information, and transmits the message to all members of the client. roomActionFor receiving the client to enter / leave the chat room. The difference is that these two chat message is the need to synchronize all of the members so the use of this.io.emitchat rooms are synchronized message to all the members of all other than the current client use itthis.socket.broadcast.emit

module.exports = class extends think.Controller {
    constructor(...arg) {
        super(...arg);
        this.io = this.ctx.req.io;
        this.socket = this.ctx.req.websocket;
        global.$socketChat.io = this.io;
    }

    async messageAction() {
        this.io.emit('message', {
            nickname: this.wsData.nickname,
            type: 'message',
            message: this.wsData.message,
            id: this.socket.id
        })
    }
    async roomAction() {
        global.$socketChat[this.socket.id] = {
          nickname: this.wsData.nickname,
          socket: this.socket
        }
        this.socket.broadcast.emit('room', {
            nickname: this.wsData.nickname,
            type: 'in',
            id: this.socket.id
        })
    }
    async closeAction() {
        const closeSocket = global.$socketChat[this.socket.id];
        const nickname = closeSocket && closeSocket.nickname;
        this.socket.disconnect(true);
        this.socket.removeAllListeners();
        this.socket.broadcast.emit('room', {
            nickname,
            type: 'out',
            id: this.socket.id
        })
        delete global.$socketChat[this.socket.id]
    }
}
复制代码

The client service by listening to the end emit the event to handle information

this.socket.on('message', data => {
    // 通过socket的id的对比,判断消息的发送方
    data.isMe = (data.id === this.socket.id);
    this.chatData.push(data);
})
this.socket.on('room', (data) => {
    this.chatData.push(data);
})
复制代码

Message transmitted by the server emit corresponding action

this.socket.emit('room', {
    nickname: this.nickname
})
this.socket.emit('message', {
    message: this.chatMsg,
    nickname: this.nickname
})
复制代码

The message type determines the type of transmitted / received message

<div class="chat-box">
    <div v-for="(item, index) in chatData" :key="index">
    <p v-if="item.type == 'in'" class="enter-tip">{{item.nickname}}进入聊天室</p>
    <p v-if="item.type == 'out'" class="enter-tip">{{item.nickname}}离开聊天室</p>
    <p v-else-if="item.type == 'message'" :class="['message',{'me':item.isMe}]">
        {{item.nickname}}:{{item.message}}
    </p>
    </div>
</div>
复制代码

Thus a simple chat room is complete.

Multi-node communication problems

We have just said that the essence of communication is the process of actually using the Socket handle queries, in essence, we are using the global variable to store all of the handles WebSocket way to solve the problem of WebSocket connection lookup. But when our server expansion, there will be way more servers have a WebSocket connection, this time across the nodes WebSocket connection lookup using global variables is invalid. At this point we need another way to communicate synchronized across servers, generally have the following ways:

message queue

Sending a message does not perform direct emitevent, but to send a message to the message queue, then all nodes consume this news. See recipient WebSocket get the data connection is on the current node, then it is not ignore this data, then the transmission operation is performed.

Node communication

Through the role of "global variables" before Redis act as an external storage services for example, all nodes are registered to create a WebSocket connection to Redis know about it, to tell you that a man named "A" connected guy in "192.168.1.1" it. When B after A sends a message that it would like to find the node connected Redis located A notifies the Node B 192.168.1.1 A To send a message, then the node performs the transmission operation.

Based on the communication node Redis

Redis the pub / sub message is a communication mode: a sender (Pub) sends a message, the subscriber (Sub) receives the message. After a node receives a message WebSocket, published by Redis (Pub), the other node receives the message as a subscriber (Sub) before subsequent handling.

This time we will realize functional nodes communicating over a demo chat room.

First, the increase in websocket controller interface calls file

const ip = require('ip');
const host = ip.address();

module.exports = class extends think.Controller {
  async openAction() {
      // 记录当前 WebSocket 连接到的服务器ip
      await global.rediser.hset('-socket-chat', host, 1);
  }
  
  emit(action, data) {
  	if (action === 'message') {
      this.io.emit(action, data)
    } else {
      this.socket.broadcast.emit(action, data);
    }
    this.crossSync(action, data)
  }

  
  async messageAction() {
    const data = {
      nickname: this.wsData.nickname,
      type: 'message',
      message: this.wsData.message,
      id: this.socket.id
    };
    this.emit('message', data);
  }
  
  async closeAction() {
      const connectSocketCount = Object.keys(this.io.sockets.connected).length;
      this.crossSync(action, data);
      if (connectSocketCount <= 0) {
        await global.rediser.hdel('-socket-chat', host);
      }
  }

  async crossSync(action, params) {
    const ips = await global.rediser.hkeys('-socket-chat').filter(ip => ip !== host);
    ips.forEach(ip => request({
        method: 'POST',
        uri: `http://${ip}/api/websocket/sync`,
        form: {
          action,
          data: JSON.stringify(params)
        },
        json: true
      });
    );
  }
}
复制代码

Then src/controller/api/websocketimplement a communication interface

const Base = require('../base');

module.exports = class extends Base {
  async syncAction() {
    const {action, data} = this.post();
    const blackApi = ['room', 'message', 'close', 'open'];
    if (!blackApi.includes(action)) return this.fail();
    
    // 由于是跨服务器接口,所以直接使用io.emit发送给当前所有客户端
    const io = global.$socketChat.io;
    io && io.emit(action, JSON.parse(data));
  }
};
复制代码

This realization of the inter-service communication function, of course, this is just a simple demo, but the basic principle is the same.

socket.io-redis

The second Redis (sub / pub) way, socket.io provides a library official socket.io-redis to achieve. It is encapsulated in the Redis pub / sub functionality that allows developers to ignore the part Redis related facilitate the developers. Only need to pass the configuration to use Redis.

// Thinkjs socket.io-redis 配置
const redis = require('socket.io-redis');
exports.websocket = {
  ...
  socketio: {
    adapter: redis({ host: 'localhost', port: 6379 }),
    message: {
    	...
    }
  }
}
  
// then controller websocket.js
this.io.emit('hi', 'all sockets');
复制代码

HTTP and WebSocket communication

If you want to socket.io service communication, for example by non-socket.io process: HTTP, you can use the official socket.io-emitter library. Used as follows:

var io = require('socket.io-emitter')({ host: '127.0.0.1', port: 6379 });
setInterval(function(){
  io.emit('time', new Date);
}, 5000);
复制代码

postscript

Code entire chat room has been uploaded to github, you can download directly experience the chat room example .

Reproduced in: https: //juejin.im/post/5d008eeae51d45108126d221

Guess you like

Origin blog.csdn.net/weixin_34128411/article/details/93175919