Translation: Crazy geek

Original text: blog.logrocket.com/websockets-…

The Web has come a long way to support full-duplex (or two-way) communication between clients and servers. This is the main purpose of the WebSocket protocol: to provide persistent real-time communication between a client and a server over a single TCP socket connection.

The WebSocket protocol has only two agendas: 1) to open the handshake, and 2) to help the data transfer. Once the server and client shake hands, they can freely send data to each other with little overhead.

WebSocket communication takes place over a single TCP socket using the WS (port 80) or WSS (port 443) protocols. According to Can I Use, at the time of writing almost all browsers except Opera Mini support WebSockets.

The status quo

Historically, creating Web applications that require real-time data communication, such as games or chat applications, has required the abuse of the HTTP protocol to establish two-way data transfers. While there are many ways to implement real-time functionality, none is as efficient as WebSockets. HTTP polling, HTTP streaming, Comet, SSE – they all have their drawbacks.

HTTP polling

The first attempt to resolve the problem is to poll the server periodically. The HTTP long polling life cycle is as follows:

  1. The client issues a request and waits for a response.
  2. The server delays the response until it changes, updates, or times out. The request remains “pending” until the server has something to return to the client.
  3. When there are some changes or updates on the server side, it sends the response back to the client.
  4. The client sends a new long polling request to listen for the next set of changes.

There are many bugs in long polling — header overhead, latency, timeouts, caching, and so on.

HTTP streaming

This mechanism reduces the pain of network latency because the initial request remains open indefinitely. Even after the server pushes data, the request never stops. The first three life cycle methods in HTTP flows are the same as in HTTP polling.

However, when the response is sent back to the client, the request never terminates, and the server keeps the connection open and sends new updates when changes occur.

Server sends events (SSE)

With SSE, the server pushes data to the client. Chat or game applications cannot rely entirely on SSE. The perfect use case for SSE is a Facebook-like news Feed: whenever new posts are posted, the server pushes them to the timeline. SSE is sent over traditional HTTP and has a limit on the number of open connections.

Not only are these methods inefficient, but the code to maintain them is boring to developers.

WebSocket

WebSockets are designed to replace existing two-way communication technology. When it comes to full-duplex real-time communication, the existing methods described above are neither reliable nor efficient.

WebSockets are similar to SSE, but good at getting messages from clients back to servers. Because the data is provided over a single TCP socket connection, connection limitations are no longer an issue.


Actual combat tutorial

As mentioned in the introduction, the WebSocket protocol has only two agendas. Let’s see how WebSockets implement these agendas. To do this, I’ll analyze a Node.js server and connect it to a client built using react.js.

Agenda 1: WebSocket establishes a handshake between the server and client

Create a handshake at the server level

We can use a single port to provide HTTP services and WebSocket services respectively. The following code shows the creation of a simple HTTP server. Once created, we bind the WebSocket server to the HTTP port:

const webSocketsServerPort = 8000;
const webSocketServer = require('websocket').server;
const http = require('http');
// Spinning the http server and the websocket server.
const server = http.createServer();
server.listen(webSocketsServerPort);
const wsServer = new webSocketServer({
  httpServer: server
});
Copy the code

After creating the WebSocket server, we need to accept a handshake when we receive a request from the client. I keep all connected clients as objects in the code and use a unique user ID when receiving requests from the browser.

// I'm maintaining all active connections in this object
const clients = {};

// This code generates unique userid for everyuser.
const getUniqueID = (a)= > {
  const s4 = (a)= > Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
  return s4() + s4() + The '-' + s4();
};

wsServer.on('request'.function(request) {
  var userID = getUniqueID();
  console.log((new Date()) + ' Recieved a new connection from origin ' + request.origin + '. ');
  // You can rewrite this part of the code to accept only the requests from allowed origin
  const connection = request.accept(null, request.origin);
  clients[userID] = connection;
  console.log('connected: ' + userID + ' in ' + Object.getOwnPropertyNames(clients))
});
Copy the code

So what happens when you accept a connection?

When sending a regular HTTP request to establish a connection, in the request header, the client sends * sec-websocket-key *. The server encodes and hashes this value and adds a predefined GUID. It responds to the value generated in * sec-websocket-accept * in the handshake sent by the server.

Once the request is accepted in the server (after necessary validation), the handshake is complete with a status code of 101. If you see anything in your browser other than status code 101, it means that the WebSocket upgrade failed and will follow normal HTTP semantics.

The * sec-websocket-accept * header field indicates whether the server is willing to Accept the connection. In addition, if the response is missing the *Upgrade* header field, or if *Upgrade* is not equal to webSocket, the WebSocket connection fails.

A successful server handshake looks like this:

HTTP GET WS :// 127.0.0.1:8000/101 Switching Protocols Connection: Upgrade sec-websocket-accept: Nn/XHq0wK1oO5RTtriEWwR4F7Zw= Upgrade: websocketCopy the code

Create a handshake at the client level

On the client side, I use the same WebSocket package as in the server to establish a connection to the server (the WebSocket API in the Web IDL is being standardized by the W3C). Once the server accepts the request, we will see WebSocket Client Connected on the browser console.

This is the initial scaffolding for creating a connection to the server:

import React, { Component } from 'react';
import { w3cwebsocket as W3CWebSocket } from "websocket";

const client = new W3CWebSocket('ws: / / 127.0.0.1:8000');

class App extends Component {
  componentWillMount() {
    client.onopen = (a)= > {
      console.log('WebSocket Client Connected');
    };
    client.onmessage = (message) = > {
      console.log(message);
    };
  }
  
  render() {
    return (
      <div>
        Practical Intro To WebSockets.
      </div>); }}export default App;
Copy the code

The client sends a subheader to establish a handshake:

HTTP GET WS :// 127.0.0.1:8000/101 Switching Protocols Upgrade: websocket Connection: Upgrade sec-websocket-key: vISxbQhM64Vzcr/CD7WHnw== Origin: http://localhost:3000 Sec-WebSocket-Version: 13Copy the code

Now that the client and server are connected by shaking hands with each other, the WebSocket connection can transfer messages as they are received, thus fulfilling the second agenda of the WebSocket protocol.

Agenda 2: Real-time information transmission

I’ll write a basic real-time document editor that users can link together and edit documents. I followed two events:

  1. ** User activity: ** Every time a user joins or leaves, I broadcast the message to all connected clients.
  2. ** Content changes: ** Every time the content in the editor is modified, it is broadcast to all other connected clients.

This protocol allows us to send and receive messages using binary data or UTF-8 (note: transferring and converting UTF-8 is less expensive).

Understanding and implementing WebSockets is simple once we have a good understanding of socket events onOpen, ONClose, and onMessage. The terms are the same on both the client and server sides.

Send and receive messages on the client

On the client side, when a new user joins or content changes, we send a message to the server with client.send to provide the new information to the server.

/* When a user joins, I notify the
server that a new user has joined to edit the document. */
logInUser = (a)= > {
  const username = this.username.value;
  if (username.trim()) {
    const data = {
      username
    };
    this.setState({ ... data }, () => { client.send(JSON.stringify({ ... data,type: "userevent"})); }); }}/* When content changes, we send the
current content of the editor to the server. */
onEditorStateChange = (text) = > {
 client.send(JSON.stringify({
   type: "contentchange".username: this.state.username,
   content: text
 }));
};
Copy the code

The events we track are: user joins and content changes.

Receiving messages from the server is simple:

componentWillMount() {
  client.onopen = (a)= > {
   console.log('WebSocket Client Connected');
  };
  client.onmessage = (message) = > {
    const dataFromServer = JSON.parse(message.data);
    const stateToChange = {};
    if (dataFromServer.type === "userevent") {
      stateToChange.currentUsers = Object.values(dataFromServer.data.users);
    } else if (dataFromServer.type === "contentchange") {
      stateToChange.text = dataFromServer.data.editorContent || contentDefaultMessage;
    }
    stateToChange.userActivity = dataFromServer.data.userActivity;
    this.setState({ ... stateToChange }); }; }Copy the code

Sends and listens for messages on the server side

In the server, we simply capture the incoming message and broadcast it to all clients connected to the WebSocket. This is one of the differences between the infamous socket. IO and WebSocket: when we use WebSockets, we need to manually send messages to all clients. Socket.IO is a mature library, so it takes care of itself.

const sendMessage = (json) = > {
  // We are sending the current data to all connected clients
  Object.keys(clients).map((client) = > {
    clients[client].sendUTF(json);
  });
}

connection.on('message'.function(message) {
    if (message.type === 'utf8') {
      const dataFromClient = JSON.parse(message.utf8Data);
      const json = { type: dataFromClient.type };
      if (dataFromClient.type === typesDef.USER_EVENT) {
        users[userID] = dataFromClient;
        userActivity.push(`${dataFromClient.username} joined to edit the document`);
        json.data = { users, userActivity };
      } else if (dataFromClient.type === typesDef.CONTENT_CHANGE) {
        editorContent = dataFromClient.content;
        json.data = { editorContent, userActivity };
      }
      sendMessage(JSON.stringify(json)); }});Copy the code

Broadcasts the message to all connected clients.

What happens when the browser closes?

In this case, WebSocket calls the close event, which allows us to write the logic to terminate the current user’s connection. In my code, when the user leaves the document, it broadcasts a message to the rest of the users:

connection.on('close'.function(connection) {
    console.log((new Date()) + " Peer " + userID + " disconnected.");
    const json = { type: typesDef.USER_EVENT };
    userActivity.push(`${users[userID].username} left the document`);
    json.data = { users, userActivity };
    delete clients[userID];
    delete users[userID];
    sendMessage(JSON.stringify(json));
  });
Copy the code

The source code for the application is in a Repo on GitHub.

conclusion

WebSockets are one of the most interesting and convenient ways to implement real-time functionality in your application. It gives us the flexibility to take full advantage of full-duplex communication. I highly recommend trying WebSockets before trying socket. IO and other available libraries.

Happy coding! 😊

Welcome to pay attention to the front-end public number: front-end pioneer, access to front-end engineering practical toolkit.