“This is the 11th day of my participation in the First Challenge 2022. For details: First Challenge 2022”


Introduction of WebSocket

WebSocket is a new HTML5 protocol that aims to create an unrestricted two-way communication channel between the browser and the server, for example, so that the server can send a message to the browser at any time.

Why can’t the traditional HTTP protocol do what WebSocket does? This is because HTTP is a request-response protocol. The request must be sent from the browser to the server before the server can respond to the request and then send the data to the browser. In other words, the server cannot send data to the browser without the browser asking for it.

In this way, you can’t do live chat or online multiplayer games in the browser, you have to use plug-ins like Flash.

Others say that HTTP can be implemented using polling or Comet. Polling is when the browser starts a timer through JavaScript and then sends a request to the server at regular intervals asking for new messages. The disadvantages of this mechanism are that first, it is not real-time enough, and second, frequent requests will put great pressure on the server.

Comet is also polling by nature, but in the absence of a message, the server waits for a message and then responds. This mechanism solves the real-time problem for the time being, but it introduces a new problem: a server running in multithreaded mode leaves most threads hanging most of the time, wasting server resources. In addition, any gateway on an HTTP connection can close the connection if there is no data transfer over a long period of time, and gateways are out of our control, requiring Comet connections to send periodic pings to indicate that the connection is “working.”

Both mechanisms address the symptoms rather than the cause, so HTML5 introduced the WebSocket standard, which allows unlimited full-duplex communication between the browser and the server, with either side actively sending messages to the other.

The WebSocket protocol

WebSocket is not a new protocol, but uses HTTP to establish connections. Let’s look at how WebSocket connections are created.

First, the WebSocket connection must be initiated by the browser, because the request protocol is a standard HTTP request in the following format:

GET ws: / / localhost: 3000 / ws/chat HTTP / 1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Origin: http://localhost:3000
Sec-WebSocket-Key: client-random-string
Sec-WebSocket-Version: 13
Copy the code

This request differs from a normal HTTP request in several ways:

  1. The address of the GET request is not similar/path/, but inws://Leading address
  2. Request headerUpgrade: websocketConnection: UpgradeIndicates that the connection will be converted to a WebSocket connection
  3. Sec-WebSocket-KeyIs used to identify the connection, not to encrypt data
  4. Sec-WebSocket-VersionSpecifies the protocol version of the WebSocket.

The server then returns the following response if it accepts the request:

HTTP/1.1 101 Switching Protocols 
Upgrade: websocket 
Connection: Upgrade 
Sec-WebSocket-Accept: server-random-string
Copy the code

This response code 101 indicates that the HTTP protocol for this connection is about to be changed, and the changed protocol is Upgrade: webSocket specified webSocket protocol.

The version number and subprotocol specify the data format that both parties understand, whether compression is supported, and so on. If you just use the WebSocket API, you don’t need to worry about this.

Now that a WebSocket connection has been established, the browser and server can actively send messages to each other at any time. There are two types of messages, text and binary data. Usually, you can send text in JSON format, which is easy to process in the browser.

Why can WebSocket connections achieve full-duplex communication but NOT HTTP connections? In fact, HTTP is based on TCP, which implements full-duplex communication. However, the request-reply mechanism of HTTP limits full-duplex communication. After the WebSocket connection is set up, it simply states: Let’s communicate without using HTTP and send data directly to each other.

The secure WebSocket connection mechanism is similar to HTTPS. First, when a browser creates a WebSocket connection using WSS :// XXX, it creates a secure connection using HTTPS. Then, the HTTPS connection is upgraded to a WebSocket connection, and the underlying communication is still using the secure SSL/TLS protocol.

Obviously, to support WebSocket communication, browsers need to support this protocol in order to issue WS :// XXX requests. At present, the mainstream browsers that support WebSocket are as follows:

  • Chrome
  • Firefox
  • IE >= 10
  • Sarafi >= 6
  • Android > = 4.4
  • iOS >= 8

The server

Since WebSocket is a protocol, how the server is implemented depends on the programming language and framework used. Node.js supports TCP and HTTP. To support WebSocket, additional development is required for the HTTPServer provided by Node.js. There are several stable and reliable WebSocket implementations based on Node.js that can be installed directly using NPM.

Why is the heartbeat detected

Simply to prove that the client and server are still alive. If the webSocket encounters network problems, the server does not trigger the onClose event. In this case, redundant connections are generated and the server continues to send messages to the client, resulting in data loss. Therefore, a mechanism was needed to detect whether the client and server were properly connected, and the heartbeat detection and reconnection mechanism came into being.


Theory exists, practice begins

React + Antd + Typescript

const Notification = () = > {
  return null;
}
export { Notification }
Copy the code

Initialize the

At the beginning, you can determine whether the user is logged in or not. Of course, this step can be omitted. Initialization using the WebSocket constructor, listening for open, message, error, and close events

const [reset, setReset] = useState<boolean>(false);
const socket = useRef<WebSocket>();

const socketInit = useCallback(() = > {
  try {
    if(! userInfo)return;
    const url = `${webSocketUrl}/ * * * * * * * / * * * * `;
    const socketObj = new WebSocket(url, userInfo);
    socketObj.addEventListener("close", socketOnClose);
    socketObj.addEventListener("error", socketOnError);
    socketObj.addEventListener("message", socketOnMessage);
    socketObj.addEventListener("open", socketOnOpen);
    socket.current = socketObj;
  } catch (e) {
    console.log(e);
  }
}, [socketOnMessage, socketOnOpen, userInfo]);

useEffect(() = > {
  socketInit();
}, [socketInit]);

useEffect(() = > {
  if(! reset)return;
  setTimeout(() = > {
    socketInit();
    setReset(false);
  }, 30000);
}, [reset, socketInit]);
Copy the code

A project, normally, has many environments, local, beta, pre-release, online, etc. But if we had to change the code every time we changed the environment, that would be too much trouble. What’s more, sometimes easy to forget, write to write, submitted, a deployment online, cool cool.

Use the process.env environment variable to switch.

enum BuildEnv {
  local = "local",
  dev = "dev",
  pre = "pre",}export const BUILD_ENV = process.env.BUILD_ENV as BuildEnv;

const webSocketUrl = {
  [BuildEnv.local]: "ws://*******",
  [BuildEnv.dev]: "ws://*******",
  [BuildEnv.pre]: "wss://******",
}[BUILD_ENV];
Copy the code

WebSocket To maintain real-time bidirectional communication between the client and server, ensure that the TCP channel between the client and server is not disconnected. However, if a connection is maintained for a long time without data exchange, the connection resources may be wasted.

However, in some scenarios, the client and server need to be connected even though no data has been exchanged for a long time. At this point, a heartbeat can be used to achieve this.

  • Sender -> Receiver: ping
  • Recipient -> Sender: Pong

Ping and pong operations correspond to two control frames of WebSocket with opcode 0x9 and 0xA respectively.

Define variables

let timerPing = 0;
let timerPong = 0;

const PADDING_TIME = 5000;
const CLOSE_TIME = PADDING_TIME * 3;
Copy the code

Listen for open events and send(ping) every once in a while. If you already saved timerPing, clear it first.

socketObj.addEventListener("open", socketOnOpen);
Copy the code
const socketOnOpen = useCallback(() = > {
  // Trigger once
  if(socket? .current? .readyState === WebSocketStatus.OPEN) { socket? .current? .send(SocketMessage.ping); }if (timerPing) window.clearInterval(timerPing);
  timerPing = window.setInterval(() = > {
    if(socket? .current? .readyState === WebSocketStatus.OPEN) { socket? .current? .send(SocketMessage.ping); } }, PADDING_TIME); pongHeart(); }, [pongHeart]);const pongHeart = useCallback(() = > {
  if (timerPong) window.clearTimeout(timerPong);
  timerPong = window.setTimeout(() = >{ socket? .current? .close(); }, CLOSE_TIME); } []);Copy the code

Listen for message events, and if the back end returns Message Pong, call pongHeart(), otherwise it’s a normal message, and do the rest.

const socketOnMessage = useCallback(
  (e: MessageEvent) = > {
    // Heartbeat link
    if (e.data === SocketMessage.pong) {
      pongHeart();
      return;
    }
    const data: Message = JSON.parse(e.data);
    / /...
  },
  [pongHeart],
);
Copy the code

Listen for close and empty timerPing, timerPong

const socketOnClose = () = > {
  console.log("Link closed");
  if (timerPing) {
    window.clearInterval(timerPing);
  }
  if (timerPong) {
    window.clearTimeout(timerPong);
  }
  setReset(true);
};
Copy the code

Listening for Error Events

const socketOnError = (e: Event) = > {
  console.error(e);
};
Copy the code

UI style, directly using Antd Notification Notification reminding box.

import { Button, notification } from 'antd';

const openNotification = () = > {
  notification.open({
    message: 'Notification Title'.description:
      'This is the content of the notification. '.onClick: () = > {
      console.log('Notification Clicked! '); }}); }; ReactDOM.render(<Button type="primary" onClick={openNotification}>
    Open the notification box
  </Button>,
  mountNode,
);
Copy the code

When does this box appear? When the current end receives a message from the back end and the message is not Pong. Display the reminder box and fill it with data.

const socketOnMessage = useCallback(
  (e: MessageEvent) = > {
    // Heartbeat link
    if (e.data === SocketMessage.pong) {
      pongHeart();
      return;
    }
    openNotification(data);
  },
  [handleIgnore, openNotification, pongHeart],
);
Copy the code
const openNotification = useCallback(
  (data: Message) = > {
    notification.open({
      message: "".description: data,
      duration: null.key: `${data.msgId}`}); } []);Copy the code

So far, basically a page reminder function, on the implementation. When the backend actively pushes the message, the front-end receives the corresponding information and displays it.

However, for a prompt box, there might be other options, such as a 10-minute reminder to ⏰, ignore the message, view the details, and so on.

SubscribeRemind.tsx

const SubscribeRemind = (props: SubscribeRemindProps) = >{};export default SubscribeRemind;
Copy the code

The description of the Notification can not only display a text, but also render a React.Node.

const openNotification = useCallback(
  (data: Message) = > {
    notification.open({
      message: "".description: (
        <SubscribeRemind
          openDetail={openDetail}
          handleRemind={handleRemind}
          handleIgnore={handleIgnore}
          content={data}
        />
      ),
      duration: null.key: `${data.msgId}`}); }, [handleIgnore, handleRemind, openDetail], );Copy the code
const SubscribeRemind = (props: SubscribeRemindProps) = > {
  const { content, openDetail, handleRemind, handleIgnore } = props;

  return (
    <div className="subscribe-remind">
      <span
        className="ignore_text"
        style={{ color: "#3b73dd"}}onClick={ignoreClick}
      >ignore</span>
      <p className="subscribe_text">
        {content}
      </p>
      <p className="bottom-content">
        <span className="bottom-text" onClick={remindTenClick)}>Remind me in 10 minutes</span>
        <span className="bottom-text" onClick={openDetailClick}>Check the details</span>
      </p>
    </div>
  );
};

export default SubscribeRemind;
Copy the code

The ignore function of the prompt box

const ignoreClick = useCallback(() = > {
  if(! content)return;
  handleIgnore(content.msgId);
}, [handleIgnore, content]);
Copy the code

The parent component calls the close method of notification, passing in the ID.

const handleIgnore = useCallback((id: number) = >{ notification.close(id); } []);Copy the code

10 minutes after the reminder, click the 10 minutes after the reminder, also need to close the prompt box.

const remindTenClick = useCallback(() = > {
  if(! content)return;
  handleRemind({
    msgId: content.msgId,
  });
  ignoreClick();
}, [handleRemind, ignoreClick, content]);
Copy the code

The parent component

const handleRemind = useCallback((params: RemindTenClickParams) = > {
  const requestBody = {
    id: params.reserveId,
    time: 10}; post("/localhost:5789/v1/***", requestBody); } []);Copy the code

Check the details

const openDetailClick = useCallback(() = > {
  if(! content)return;
  openDetail({
    id: content.id,
  });
  ignoreClick();
}, [ignoreClick, content, openDetail]);
Copy the code

After the parent component gets the ID, the rest of the operation is the implementation of the business.

const openDetail = useCallback((params) = > {
  / /...} []);Copy the code

Reference article:

WebSocket Heartbeat detection and reconnection mechanism