A few days ago, I saw that the “like” area on Twitter was very cute, so I wanted to study how to achieve it. Server Sent Events (WebSocket) Server Sent Events (WebSocket) Server Sent Events (WebSocket)

What is Server-sent Events

Server-sent Events is a Server push technology that enables clients to automatically receive updates from the Server over an HTTP connection. Each notification is sent as a stream of text (the text should be UTF-8), ending with a pair of newlines. Compared to WebSocket:

  1. It is not full-duplex and can only be sent by the server to the browser, because streaming information is essentially a download and cannot be requested again once a connection has been made (otherwise it becomes a new connection).
  2. WebSocket uses WS, while SSE still uses HTTP.

The characteristics of the SSE

Here is a direct copy of ruan Yifeng poly:

  • SSE uses HTTP protocol, which is supported by all existing server software. WebSocket is a standalone protocol.
  • SSE is lightweight and easy to use; The WebSocket protocol is relatively complex.
  • SSE supports disconnection and reconnection by default, and WebSocket needs to be implemented by itself.
  • SSE is generally used to transmit text only. Binary data needs to be encoded and transmitted. WebSocket supports binary data transmission by default.
  • SSE supports custom message types to be sent.

For example

Create an INSTANCE of SSE by creating a new EventSource. The first parameter is the backend interface. The second option argument has only one withCredentials, which, if true, are allowed to send cookies in cross-domain cases.

const evtSource = new EventSource("http://localhost:3002/sse", {
  withCredentials: true});Copy the code

An EventSource can listen for three types of events: onOpen, onMessage, and onError. The first and third events are successful connection establishment and failed connection establishment (CORS, request timeout, etc.). Onmessage is the most important, because it listens to the stream of messages that are pushed.

Because SSE can only receive utF-8 plain text, the most common approach is to pass a JSON string back end. In the following code, every time a new push is received, it prints like_count until like_count > 10, and the client asks the server to stop pushing.

interface Data {
  payload: { like_count: number };
}

evtSource.addEventListener("message".(e: MessageEvent) = > {
  const {
    payload: { like_count },
  }: Data = JSON.parse(e.data);

  console.log(like_count);

  if (like_count > 10) { evtSource.close(); }});Copy the code

So that’s basically what the client is going to do. It’s pretty simple. Now let’s look at the server side. Since NestJS encapsulates SSE support, this framework will be used here.

The event stream is simply a stream of text data, and the text should be encoded in UTF-8 format. Each message is followed by an empty line delimiter. Each message consists of multiple fields, each of which consists of the field name, a colon, and the field value.

Four fields are supported in the specification:

  • Event: this field is the onmessage subset, that is to say. You can use evtSource addEventListener (” customEvt “, () = > {} to fine-grained to monitor delivery of the specified event.

  • Data: The passed entity. If the message contains multiple data fields, the client concatenates them into a string using a newline character as the field value.

  • Id can add an ID to each push, such as tweetId, so that likeCount in the entity can be mapped to tweetId one by one.

  • Retry: Specifies the interval in which the browser retries the connection. It is an integer value specifying the reconnection time in milliseconds, and is ignored if it is not a positive integer

In addition, behavior comment lines that begin with a colon are ignored. Comment lines can be used to prevent connection timeouts, and the server can periodically send a message to comment lines to keep connections going.

: just a comment\n\n

id: 12345\n
event: addLikeCount\n
retry: 10000\n
data: {\n
data: "likeCount": 1,\n
data: }\n\n
Copy the code

Let’s look directly at the back-end code implementation, since NEstJS must use RXJS to implement SSE, so there is a bit of a foundation. Data is spit out every two seconds in the code. Since NestJS will convert data to the structure SSE wants, data can be directly written as an object.

import { Injectable } from "@nestjs/common";
import { interval } from "rxjs";
import { map } from "rxjs/operators";
import { randomSeries } from "yancey-js-util";

@Injectable(a)export class SSEService {
  public sse() {
    let count = 1;
    return interval(2000).pipe(
      map((_) = > ({
        id: randomSeries(6),
        type: "addLikeCount".data: { payload: { tweetId: randomSeries(6), likeCount: count++ } },
        retry: 10000,}))); }}Copy the code

If nothing goes wrong, the front end should see the Stream.

Analyzing the response headers, the two most important flags are that caching is disabled, and content-type: text/event-stream.

Does GraphQL support it?

Emmmm doesn’t seem to support this, but GraphQL already has a very strong Subscriptions system and doesn’t need to support this stuff anymore (after all, GraphQL didn’t support uploads very well last year).

Can I use SSE?

Emmmm, you can use it in all but one browser.

other

It suddenly occurred to me that I configured rateLimit on the back end. Will rateLimit be triggered when the front end gets data so frequently? The answer is no, because SSE is always just an HTTP request, and rateLimit limits only repeated requests.

app.use(
  rateLimit({
    windowMs: 15 * 60 * 1000.max: 100,}));Copy the code

Send me the full code

// The front end

// SSE.tsx
import { FC, useState, useEffect } from "react";

interface CustomEvent extends Event {
  data: string;
}

interface Data {
  payload: {
    likeCount: number;
  };
}

const SSE: FC = () = > {
  const [like, setLike] = useState(0);

  const initialSSE = () = > {
    const evtSource = new EventSource("http://localhost:3002/sse", {
      withCredentials: true}); evtSource.addEventListener("open".() = > {
      console.log("Open");
    });

    // Custom events are used here
    evtSource.addEventListener("addLikeCount", ((e: CustomEvent) = > {
      const {
        payload: { likeCount },
      }: Data = JSON.parse(e.data);

      setLike(likeCount);

      if (likeCount > 10) { evtSource.close(); }})as EventListener);

    evtSource.addEventListener("message".(e: MessageEvent) = > {});

    evtSource.addEventListener("error".(err: Event) = > {
      console.log(err);
    });
  };

  useEffect(() = >{ initialSSE(); } []);return <div>{like}</div>;
};

export default SSE;

// The back-end part

// sse.module.ts
import { Module } from "@nestjs/common";
import { SSEController } from "./sse.controller";
import { SSEService } from "./sse.service";

@Module({
  controllers: [SSEController],
  providers: [SSEService],
})
export class SSEModule {}

// sse.controller.ts
import { Controller, MessageEvent, Sse } from "@nestjs/common";
import { Observable } from "rxjs";
import { SSEService } from "./sse.service";

@Controller(a)export class SSEController {
  constructor(private readonly sseService: SSEService) {
    this.sseService = sseService;
  }

  @Sse("sse")
  public sse(): Observable<MessageEvent> {
    return this.sseService.sse(); }}// sse.service.ts
import { Injectable } from "@nestjs/common";
import { interval } from "rxjs";
import { map } from "rxjs/operators";
import { randomSeries } from "yancey-js-util";

@Injectable(a)export class SSEService {
  public sse() {
    let count = 1;
    return interval(2000).pipe(
      map((_) = > ({
        id: randomSeries(6),
        type: "addLikeCount".data: { payload: { tweetId: randomSeries(6), likeCount: count++ } },
        retry: 10000,}))); }}Copy the code

reference

  • Using server-sent events
  • Server – Sent Events tutorial