preface

WebSocket is a transport protocol for Web applications that provides a two-way, sequential flow of incoming data.

It is an Html5 protocol, WebSocket connection is persistent, it maintains a duplex connection between the client and the server, server updates can be timely pushed to the client, without the client to poll at a certain time interval.

  1. Based on TCP protocol, the implementation of the server side is relatively easy.
  2. It has good compatibility with HTTP protocol. The default ports are also 80 and 443, and the handshake phase uses HTTP protocol, so it is not easy to mask the handshake and can pass various HTTP proxy servers.
  3. The data format is relatively light, with low performance overhead and high communication efficiency.
  4. You can send text or binary data.
  5. There are no same-origin restrictions, and clients can communicate with any server.
  6. The protocol identifier is WS (or WSS if encrypted), and the server URL is the URL.

A simple example of the Api

WebSocket is an Api provided by Html5, so it is very simple to use

The front end

new WebSocket('ws://localhost:3000/ws/chat'); this.ws.onmessage = this.onMessage; this.ws.onerror = this.onError; this.ws.onclose = this.onClose; This. Ws. Onopen () = = > this. Send ({type: "text", Meg: 'Hello Hello ~'}); this.ws.close()Copy the code

So the front end looks something like this

The back-end

index

// koa import Koa from 'koa'; import logger from 'koa-logger'; import bodyParser from 'koa-bodyparser'; import cors from 'koa2-cors'; import ws from 'ws'; import webSocket from './webSocket'; import corsConfig from './corsConfig'; import routes from './router'; const WebSocketServer = ws.Server; const app = new Koa(); const isProduction = process.env.NODE_ENV ! == 'development'; / / log! isProduction ? app.use(logger()) : ''; Use (CORS (corsConfig)); // koa-bodyparser app.use(bodyParser()); // add router middleware: app.use(routes()); // Listen on port 3000: const server = app.listen(3000); Console. log(' KOA service started successfully! '); app.wss = webSocket(server, WebSocketServer); Console. log(' APP HTTP service started successfully! port: 3000... ');Copy the code

webSocket

// webSocket.js import url from 'url'; var messageIndex = 0; Function broadcastUsers(WSS, user) {// broadcastUsers(WSS, user) {... Function cookiesUser(req) {function cookiesUser(req) {... Function createMessage(type, user, data) {... Function onConnect() {function onConnect() {... Function onMessage(message) {function onMessage(message) {... WebSocket function onClose() {... } function onError(err) {console.log('[webSocket] error: '+ err);} function onError(err) {console.log('[webSocket] error:' + err); }; Function createWebSocketServer(server, WebSocketServer) {// createWebSocketServer: const wss = new WebSocketServer({ server: server, }); // Chat function: After receiving the message, Broadcast = function broadcast(data) {wss.clients.forEach(function each(client) {WSS. console.log(client.readyState); if (client.readyState === 1) { client.send(data); }}); }; wss.on('connection', function(client, req) { let location = url.parse(req.url, true); client.on('message', onMessage); client.on('close', onClose); client.on('error', onError); If (location. pathName! == '/ws/chat') { client.close(4000, 'Invalid URL'); } client.user = cookiesUser(req); client.wss = wss; // Custom callback onconnect.apply (client) after successful connection; }); Console. log('webSocket server started successfully! '); return wss; } export default createWebSocketServer;Copy the code

The back-end implementation uses a KOA as the server example, but the focus of this article is not on the back-end implementation of webSocket.

If you are interested in this section, you can check out my simple implementation: Github

WebSocket encapsulation

For WebSocket development, according to their own needs for processing encapsulation, the following to do a simple encapsulation of this, because I reference the project using TypeScript development, so the following will be TS written

interfaces/ws.ts

export interface WSResult {
	code: number;
	message: string; errorCode? :number | string; httpMsg? :string; hide? :boolean;
	[x: string] :any;
}

export interface WebSocketMegFunc {
	(res: WSResult): void;
}

export interface WebSocketFunc {
	(ws: WebSocketExp): void;
}

export interface WebSocketExp extends WebSocket {

}
Copy the code

webSocket.ts

/** * Callback function after receiving server data */
function onMessage(
	this: WebSocketExp,
	ev: MessageEvent,
	onMessageCallback: WebSocketMegFunc
) :void {
	// We can do some general things here, and then pass it to onMessageCallback
	if (onMessageCallback) {
		try {
			const res = JSON.parse(ev.data).data;
			onMessageCallback(res);
		} catch (error) {
			handleThrottleMsg(`[WS JSON ERROR](The ${this.url}) `);
			onMessageCallback(null); }}}/** * an error callback */
function onError(this: WebSocketExp, ev: Event) :void {
	console.error(`[WS ERROR](The ${this.url}) abnormal `);
	this.close();
}

/** * The callback function */ after the connection is closed
function onClose(this: WebSocketExp, ev: CloseEvent) :void {
	console.warn(`[WS CLOSED](The ${this.url}) ${ev.code}: ${ev.reason}`);
}

/** * Callback function */ after successful connection
function onOpen(this: WebSocketExp, ev: Event) :void {
	console.log(`ws: The ${this.url} connection succeeded ~`);
}

export default function wsConstructor(url: string, onMessageCallback: WebSocketMegFunc) :WebSocket {
	const ws: WebSocketExp = new WebSocket(url);
	ws.onmessage = function (this: WebSocket, ev: MessageEvent) :void {
		onMessage.call(this, ev, onMessageCallback);
	};
	ws.onerror = onError;
	ws.onclose = onClose;
	ws.onopen = onOpen;
	return ws;
}
Copy the code

The above websocket.js file is a simple reassembly of websocket, which can be used as follows:

api/ws.ts

import wsConstructor from '~/lib/webSocket'; import { WebSocketMegFunc } from '~/interfaces/ws'; import { getWsUrl } from '~/data/ws'; Const ws = {getCountry: (onMessageCallback: WebSocketMegFunc): {getCountry: (onMessageCallback: WebSocketMegFunc); WebSocket => wsConstructor(getWsUrl('/ws/get_country/'), onMessageCallback), }; export default ws;Copy the code

views/index.tsx

import { ws } from '~/api'; import { WSResult } from '~/interfaces/ws'; Const onCMapWsMessage = (res: WSResult): void => { }; ws.getCountry(onCMapWsMessage);Copy the code

heartbeat

Surely see here, the general front end may realize the business requirements, but often many requirements can be dug down, or can do better, the following is a heartbeat mechanism for WebSocket, if you can know this optimization, in fact, I think better than the general front end.

What is the heartbeat mechanism? Why?

In fact, the heartbeat mechanism can be roughly understood as long as you look at the word, which is similar to a polling mechanism and an operation to ask each other about the situation when necessary.

Websocket is a long connection between the front end and the back end. Both the front end and back end may fail to connect and do not respond to each other due to some circumstances. Therefore, in order to ensure the sustainability and stability of the connection, the Websocket heartbeat mechanism emerged.

When using native Websocket, if the device network is disconnected, no Websocket event is immediately triggered, and the front-end cannot know whether the current connection is disconnected. If the websocket. send method is called, the browser will notice that the connection is broken and will fire onclose immediately or after a certain amount of time (depending on the browser or browser version).

The back-end Websocket service may be abnormal, causing the connection to be disconnected. In this case, the front end does not receive a disconnect notification. Therefore, the front end periodically sends a heartbeat ping message. If no message is received for a certain amount of time, it indicates that the connection is abnormal and the front end will perform reconnection.

To solve the preceding two problems, the former end acts as the active party and periodically sends ping messages to check the connection between the network and the front and back ends. Once an exception is found, the front end continues to perform the reconnect logic until the reconnect succeeds.

Next, add a heartbeat mechanism to the code snippet that was reassembled above

The heartbeat implementation

For the WebSocket construction factory encapsulated above, each call to wsConstructor returns the current WS object for saving to the currently invoked page, and the code for the heartbeat mechanism must also be placed in the encapsulated websocket.ts. Serve all calls to wsConstructor, which raises a problem:

When a page has multiple WS long connections, or there are multiple WS long connections in the background and different pages, when a long connection is broken, or all the long connections are broken, if the heartbeat reconnection is made for the corresponding WS long connections, And the user who restored the last WS long connection will not pick up another WS object to respond to?

As some of you might have thought, that would require a unique flag telling the consumer that I’m responding to an aWs long connection, he’s responding to a bWs long connection, and his cWs long connection is broken, reconnected and regenerated as a cWs object and told to use it in place of an old disconnected long connection. Yes, there is a need for a unique identifier, just like our ID card. Then there is another problem. The user takes the long-link ID card on his hand to find the corresponding WS object.

It’s like when people take their ID card and say, who am I? Who am I? Who are you? The first is, of course, the ID card in your hand, and the second is the data of your ID number in the national computer database. Here, I use Vue3 to store proxy data and source data in a responsive encapsulation method, that is, a large Map set is used for storage. The user can manipulate the WS object in the collection by ID and update the Map database as necessary.

Yes, there is another problem, that is, when a WS long connection does not respond and triggers the heartbeat to confirm that the connection has been disconnected and needs to be reconnected, how does the current code determine which address I should reconnect to? , some people may say that the ws connection is broken and needs to be reconnected, then it (WS) of course knows that the next to initiate reconnection to the server Api, is not its own previously connected XXX/XXX? Yes, there is such a principle, but the writer does not tell the program where to get relevant information, it does not know. Don’t use human granted to act as the brain of the code, write the code you need to use the human mind + computer is neck and neck to better writing logic of thinking, in order to solve this problem, each WebSockek structure factory to create a ws object, some of the information will be passed to the current users to record and preserve, To do this, add additional attributes to WS.

interfaces/ws.ts

export interface WSResult { code: number; message: string; errorCode? : number | string; httpMsg? : string; hide? : boolean; [x: string]: any; } export interface WebSocketMegFunc { (res: WSResult): void; } export interface WebSocketFunc { (ws: WebSocketExp): void; } export interface WebSocketExp extends WebSocket {+ onMessageCallback? : WebSocketMegFunc; // Save the user's callback method
+ wsKey? : string; // Record the current user Id based on the ws
+ timeout? : any; // Records the current WS heartbeat timer
+ serverTimeout? : any; // Records the wait timer for the back-end response sent by the current WS heartbeat
+ debounceTimeout? : any; // Record the timer emitted by the reconnection mechanism for a throttling operation
}
Copy the code

webSocket.ts

+ import { WebSocketMegFunc, WebSocketExp } from '~/interfaces/ws';

+ const timeout = 1000 * 60 * 4; // 4 minutes, the back-end push interval is two minutes
+ const reconnectTimeout = 1000 * 60 * 2; // The retry interval is two minutes
+ export const wsMap = new Map(); // a collection of ws objects/** * initialize Props */+ function initProps(ws: WebSocketExp, onMessageCallback: WebSocketMegFunc, wsKey: string): void {
+ ws.onMessageCallback = onMessageCallback; // Record the user's callback function
+ ws.wsKey = wsKey; // The new WS integrates the Id of the old WS
+ ws.timeout = null; // Declare heartbeat timer
+ ws.serverTimeout = null; // Declare the heartbeat feedback timer
+ // Save the reconnection status of the previous WS, in conjunction with reconnect's throttling function, save not to send meaningless heartbeat frequently
+ ws.debounceTimeout = wsMap.get(wsKey) ? wsMap.get(wsKey).debounceTimeout : null;
+ wsMap.set(wsKey, ws); // Save or update ws data in database map
+}/** * Clear timer */+ function clearTimeout(ws: WebSocketExp): void {
+ ws.timeout && clearTimeout(ws.timeout);
+ ws.serverTimeout && clearTimeout(ws.serverTimeout);
+ ws.debounceTimeout && clearTimeout(ws.debounceTimeout);
+}/** * Reset heartbeat check */+ function reset(ws: WebSocketExp): void {
+ clearTimeout(ws);
+ start(ws);
+}/** * Start heartbeat check */+ function start(ws: WebSocketExp): void {
+ ws.timeout = setTimeout(function () {
+ ws.send(JSON.stringify({ type: 1000, data: 'HeartBeat' })); // By convention with the backend, the HeartBeat sending identifier is HeartBeat
+ ws.serverTimeout = setTimeout(function () {
+ // If a heartbeat check is not returned after a timeout, it is disabled
+ ws.close();
+ }, timeout);
+ }, timeout);
+}/** * Reconnection operation */+ function reconnect(ws: WebSocketExp): void {
+ / / throttling
+ clearTimeout(ws); // If reconnection is initiated, the heartbeat timer is disabled
+	 const callNow = !ws.debounceTimeout;
+ ws.debounceTimeout = setTimeout(() => {
+ ws.debounceTimeout = null;
+ reconnect(ws); // The reconnection can be initiated only after reconnectTimeout
+ }, reconnectTimeout);
+ if (callNow) {
+ console.warn(`[WS RECONNECT](${ws.url})`);
+ wsConstructor(ws.url, ws.onMessageCallback, ws.wsKey);
+}
+}*/ function onMessage(this: WebSocketExp, ev: MessageEvent, onMessageCallback: WebSocketMegFunc): void {+ // Reset heartbeat
+	reset(this);Parse (ev.data).data; if (onMessageCallback) {try {const res = json.parse (ev.data).data;+ if (res.data.text === 'alive') return; // With the convention of the back end, the heartbeat feedback flag is: alive, the link is normal, do nothingonMessageCallback(res); } catch (error) { console.error(`[WS JSON ERROR](${this.url})`); onMessageCallback(null); */ function onError(this: WebSocketExp, ev: Event): Void {console.error(' [WS error](${this.url}) exception '); this.close(); */ function onClose(this: WebSocketExp, ev: CloseEvent): void { console.warn(`[WS CLOSED](${this.url}) ${ev.code}: ${ev.reason}`);+ // If the front end is manually closed, there is no need to reconnect
+ if (ev.code ! = = 1000) {
+ reconnect(this);
+	} else {
+ // The web automatically closes and no longer reconnects
+ // Clear all timers
+ clearTimeout(this);
+ // Deleted the WS instance stored for wsMap
+ wsMap.delete(this.wsKey);
+}*/ function onOpen(this: WebSocketExp, ev: Event): void {console.log(' ws: ${this.url} connection succeeded ~`);+ // Start heartbeat
+	start(this);} export default function wsConstructor( url: string, onMessageCallback: WebSocketMegFunc, wsKey: Const ws: WebSocket {const ws: WebSocketExp = new WebSocket(url); ws.onmessage = function (this: WebSocket, ev: MessageEvent): void { onMessage.call(this, ev, onMessageCallback); }; ws.onerror = onError; ws.onclose = onClose; ws.onopen = onOpen;+ // Initialize Props to record the information provided by the user to build ws and save it in the WS object itself
+	initProps(ws, onMessageCallback, wsKey);
	return ws;
}
Copy the code

Changes in use:

api/ws.ts

import wsConstructor from '~/lib/webSocket'; import { WebSocketMegFunc } from '~/interfaces/ws'; import { getWsUrl } from '~/data/ws'; Const ws = {getCountry: (onMessageCallback: WebSocketMegFunc,+ wsKey: string
	): WebSocket =>
		wsConstructor(
		    getWsUrl('/ws/get_country/'), 
		    onMessageCallback,
+ wsKey: string),}; export default ws;Copy the code

views/index.tsx

import { ws } from '~/api';
import { WSResult } from '~/interfaces/ws';

+ // ws
+ const cMapWs = 'cMapWs';
+ const cDetailsWs = 'cDetailsWs';

+ useEffect(() => {
+ return (): void => {
+ // Leave the page to close the respective WS long connection
+ wsMap.get(cMapWs) && wsMap.get(cMapWs).close();
+ wsMap.get(cDetailsWs) && wsMap.get(cDetailsWs).close();
+};
+} []);Const onCMapWsMessage = (res: WSResult): void => { }; Const onCDetailsWsMessage = (res: WSResult): void => {// Various processes... }; ws.getCountry( onCMapWsMessage,+ cMapWs
);

+ // For example, there are multiple
ws.getCountryDetails(
    onCDetailsWsMessage, 
+ cDetailsWs
);
Copy the code

conclusion

To complete the WebSocket packaging and heartbeat mechanism, I believe that as long as we can understand the meaning and logic of the above code, the developers combined with their own business needs, can be packaged in line with their own WebSocket.

For the use of WebSocket, in fact, most developers, at the beginning may be too much trouble or a variety of external reasons, directly using the polling mechanism to deal with, of course, this way is also possible, all the methods are based on the actual scene, but conditions allow the use of WebSocket is not more elegant?

On the other hand, if you use WebSocket, you can probably do it by checking the official MDN documentation, but you should know that when you go out for an interview, will the interviewer ask you how to use WebSocket? More often, interviewers will ask, “WebSocket for various reasons and the back end of the lost connection does not respond, how to handle?” What if the candidate could know the heartbeat and come up with the solution?

In fact, all functions can be deeply dug and optimized into a feature with highlights, for add, delete, change and check is no exception.

Another amway wave another article: Large file upload solutions