JS WebSocket real – friends chat

Websocket real – friends chat

Do not know websocket with shoes, please learn ruan Yifeng teacher websocket tutorial

websocket

  1. Websocket has a wide range of applications in real projects, such as friend chat, asynchronous requests, react-hot-loader hot updates and so on

  2. In this paper, the front-end uses native WebSocket, back-end uses Express-WS library to achieve chat communication

  3. Back-end mongodb data store using Mongoose operation, do not understand you can first see the documentation oh

  4. The principle of chat is very simple, as shown below:

Simple version

First a simple version, can achieve communication between the user and the server

  1. Front end: WsRequest wrapper classes
class WsRequest {
  ws: WebSocket

  constructor(url: string) {
    this.ws = new WebSocket(url)

    this.initListeners()
  }

  initListeners() {
    this.ws.onopen = _event= > {
      console.log('client connect')}this.ws.onmessage = event= > {
      console.log('Message from the server:${event.data}`)}this.ws.onclose = _event= > {
      console.log('client disconnect')
    }
  }

  send(content: string) {
    this.ws.send(content)
  }

  close() {
    this.ws.close()
  }
}

/ / use
const ws = new WsRequest('your_websocket_url') / / such as: the ws: / / localhost: 4000 / ws
ws.send('hello from user')
Copy the code
  1. Server side: WsRouter encapsulates classes, using singleton mode
import expressWs, { Application, Options } from 'express-ws';
import ws, { Data } from 'ws';
import { Server as hServer } from 'http';
import { Server as hsServer } from 'https';

class WsRouter {
  static instance: WsRouter;

  wsServer: expressWs.Instance;

  clientMap: Map<string, ws>; // Save user ids for all connections

  constructor( private path: string, private app: Application, private server? : hServer | hsServer, private options? : Options ) {this.wsServer = expressWs(this.app, this.server, this.options);

    this.app.ws(this.path, this.wsMiddleWare);

    this.clientMap = new Map(a); }static getInstance(path: string, app: Application, server? : hServer | hsServer,options: Options = {}) {
    if (!this.instance) {
      this.instance = new WsRouter(path, app, server, options);
    }

    return this.instance;
  }

  wsMiddleWare = (wServer: any, _req: any) = > {
    this.clientMap.set(id, wServer);

    this.broadcast('hello from server'); // send data to users

    wServer.on('message'.async (data: Data) => {
      console.log('Information from users:${data.toString()}`);
    });

    wServer.on('close', (closeCode: number) => {
      console.log(`a client has disconnected: ${closeCode}`);
    });
  }

  broadcast(data: Data) { // All broadcast
    this.clientMap.forEach((client: any) = > {
      if(client.readyState === ws.OPEN) { client.send(data); }}); }}export default WsRouter.getInstance;

// Use: bootstrap.ts
const server = new InversifyExpressServer(container);
/ / note: this project the back-end using the framework [Inversify] (https://github.com/inversify)
// Private server? : hServer | hsServer parameter values, analogy, please change
server.setConfig((app: any) = > WsRouter('/ws/:id', app))
server.build().listen(4000);
Copy the code

Upgraded version

To implement friend communication, you of course specify the from and to users in the send method on the front and back ends

Furthermore, in order to record the messages sent in the background, the primary key friendId of the friends table must also be represented as the messages between the two people

  1. Mongoose data storage
// user.ts
const userSchema = new Schema(
  {
    name: { type: String.required: true.unique: true}});export default model('User', userSchema);

// friend.ts Indicates the friend relationship between two users
import { Schema, model, Types } from 'mongoose';

const FriendSchema = new Schema(
  {
    user1: { type: Types.ObjectId, ref: 'User'.required: true }, // user1Id < user2Id
    user2: { type: Types.ObjectId, ref: 'User'.required: true}});export default model('Friend', FriendSchema);

// message.ts
const MessageSchema = new Schema(
  {
    friend: { type: Types.ObjectId, ref: 'Friend'.required: true }, // Associate the Friend table
    from: String.to: String.content: String.type: { type: String.default: 'text'}});export default model('Message', MessageSchema);
Copy the code
  1. Interface specification
type msgType = 'text' | 'emoji' | 'file'

interface IMessage {
  from: string
  to: string
  content: string
  type: msgType
}
Copy the code
  1. Front end: WsRequest wrapper classes
import { IMessage, msgType } from './interface'

export default class WsRequest {
  ws: WebSocket

  constructor(url: string, private userId: string) {
    this.ws = new WebSocket(`${url}/The ${this.userId}`)

    this.initListeners()
  }

  initListeners() {
    this.ws.onopen = _event= > {
      console.log('client connect')}this.ws.onmessage = event= > {
      const msg: IMessage = JSON.parse(event.data)

      console.log(msg.content)
    }

    this.ws.onclose = _event= > {
      console.log('client disconnect')}}// friendId refers to the _id of the Friend Model
  async send(friendId: string, content: string, receiverId: string, type: msgType = 'text') {
    const message: IMessage = { from: this.userId, to: receiverId, content, type }

    await this.ws.send(JSON.stringify({ friend: friendId, ... message })) } close() {this.ws.close()
  }
}

/ / use
const ws = new WsRequest('your_websocket_url'.'your_user_id') // example: ws://localhost:4000/ws
await wsRequest.send('Friend_model_id'.'Hello, Jeffery.'.'jeffery_id')
Copy the code
  1. Server side: WsRouter encapsulates classes, using singleton mode
import expressWs, { Application, Options } from 'express-ws';
import ws, { Data } from 'ws';
import { Server as hServer } from 'http';
import { Server as hsServer } from 'https';
import Message, { IMessage } from 'models/message';
import Friend from 'models/friend';

class WsRouter {
  static instance: WsRouter;

  wsServer: expressWs.Instance;

  clientMap: Map<string, ws>; // Save user ids for all connections

  constructor( private path: string, private app: Application, private server? : hServer | hsServer, private options? : Options ) {this.wsServer = expressWs(this.app, this.server, this.options);

    this.app.ws(this.path, this.wsMiddleWare);

    this.clientMap = new Map(a); }static getInstance(path: string, app: Application, server? : hServer | hsServer,options: Options = {}) {
    if (!this.instance) {
      this.instance = new WsRouter(path, app, server, options);
    }

    return this.instance;
  }

  wsMiddleWare = (wServer: any, req: any) = > {
    const { id } = req.params; // Parse the user ID

    wServer.id = id;
    this.clientMap.set(id, wServer);

    wServer.on('message'.async (data: Data) => {
      const message: IMessage = JSON.parse(data.toString());

      const { _id } = await new Message(message).save(); // Update database

      this.sendMsgToClientById(message);
    });

    wServer.on('close', (closeCode: number) => {
      console.log(`a client has disconnected, closeCode: ${closeCode}`);
    });
  };

  sendMsgToClientById(message: IMessage) {
    const client: any = this.clientMap.get(message.to);

    if(client) { client! .send(JSON.stringify(message));
    }
  }

  broadcast(data: Data) {
    this.clientMap.forEach((client: any) = > {
      if(client.readyState === ws.OPEN) { client.send(data); }}); }}export default WsRouter.getInstance;

// Use: bootstrap.ts
const server = new InversifyExpressServer(container);
/ / note: this project the back-end using the framework [Inversify] (https://github.com/inversify)
// Private server? : hServer | hsServer parameter values, analogy, please change
server.setConfig((app: any) = > WsRouter('/ws/:id', app))
server.build().listen(4000);
Copy the code
  1. The heartbeat detection

Reference:

  • ws faq: how-to-detect-and-close-broken-connections
/ / the server
wsMiddleWare = (wServer: any, req: any) = > {
  const { id } = req.params;

  wServer.id = id;
  wServer.isAlive = true;
  this.clientMap.set(id, wServer);

  wServer.on('message'.async(data: Data) => {... }); wServer.on('pong', () => {
    wServer.isAlive = true;
  });
}

initHeartbeat(during: number = 10000) {
  return setInterval((a)= > {
    this.clientMap.forEach((client: any) = > {
      if(! client.isAlive) {this.clientMap.delete(client.id);

        return client.terminate();
      }

      client.isAlive = false;
      client.ping((a)= > {});
    });
  }, during);
}
Copy the code

The online test

A, preparation,

  1. For the convenience of all testing, access to online service end interface: wss://qaapi.omyleon.com/ws, concrete used to attach a user id, such as: wss://qaapi.omyleon.com/ws/asdf… , see webSocket of the upgraded version

  2. Client: You can use the Google plugin: Simple WebSocket Client; You can also access the online project and use the client provided by the project, for details, see QA-app

  3. Use a pair of friend profiles online

  • Friend: jeffery and testuser => _id: 5d1328295793f14020a979d5

  • jeffery => _id: 5d1327c65793f14020a979ca

  • testuser => _id: 5d1328065793f14020a979cf

2019.11.15 update

The online project data has been reset, please use the new ID to test

Friend: jeffery and testuser => _id: 5d1328295793f14020a979d5

jeffery => _id: 5dce50cbb869af711db528f1

testuser => _id: 5dce5119b869af711db528fc

Second, practical demonstration

  1. Open the WebSocket Client plug-in and enter the test link as shown below:
wss://qaapi.omyleon.com/ws/5d1327c65793f14020a979ca
Copy the code

  1. Click on theOpenBelow,StatusAccording toOpenedIndicates successful connection.

  1. Send messages according toIMessageInterface to send, of course, but also attachedfriendIdOtherwise, it cannot be mapped to the corresponding user
{
  "friend": "5d1328295793f14020a979d5"."from": "5d1327c65793f14020a979ca"."to": "5d1328065793f14020a979cf"."content": "Hello testuser, this is a message sent through the Simple Websocket Client."."type": "text"
}
Copy the code

  1. When the simple WebSocket Client is used to connect to another user, the message is received
wss://qaapi.omyleon.com/ws/5d1328065793f14020a979cf
Copy the code

  1. Similarly, if you change from and to in another client, you can reply to the other client
{
  "friend": "5d1328295793f14020a979d5"."from": "5d1328065793f14020a979cf"."to": "5d1327c65793f14020a979ca"."content": "Yeah, I got Jeffery. This is also sent via the Simple WebSocket Client."."type": "text"
}
Copy the code

  1. In addition, the same principle is used in the online project QA-app, only the content field is displayed after parsing, and nothing else is done in this simple test client

The source code for

  1. Front-end: QA-app app/websocket/index.ts

  2. Backend: Qa-app-server server/wsRouter/index.ts

  3. Online address, go to -> https://qa.omyleon.com