> For the complete documentation index, see [llms.txt](https://1425816423.gitbook.io/my-knowledge-base/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://1425816423.gitbook.io/my-knowledge-base/jing-yan-ji-lu/jing-yan-zong-jie/she-ji-yi-ge-ji-shi-liao-tian-gong-neng.md).

# 设计一个即时聊天功能

### 目录

* [x] websocket基本概念
* [x] 使用nodejs设计一个简单的ws服务器
* [x] stomp基本概念和使用
* [x] 常用的websocket API
* [x] 踩过的坑

## websocket

要实现一个即时聊天概念，首先需要实现一个全双工的通道，让通信双方能够及时的收到对方所发送的消息。 在过去，这通常是通过http轮询实现的，即设置一个定时器，每隔一段时间就发送一个请求获取最新数据，在这一发一收的过程中就完成了双向通讯。但是这种方法最大的弊端在于及时性，消息可能会无法及时的发送给对方，当然这可以通过缩短定时器间隔时间，但是这样做又会导致性能问题。

为了解决这个问题，HTML5 定义了 WebSocket 协议，能更好的节省服务器资源和带宽，并且能够更实时地进行通讯。**Websocket是一种全双工协议**，使用 ws 或 wss 的统一资源标志符（URI），其中 wss 表示使用了 TLS 的 Websocket。websocket和http一样位于应用层，并且websocket的建立依赖于http，具体流程是：

1. 客户端发送Get请求，并在请求头中设置`Upgrade: websocket`;`Connection: Upgrade`;来告诉服务器将转换成websocket协议来通讯。
2. 服务器返回转状态码101的响应，并转换成websocket协议
3. 双方使用ws协议来通信

### **实现一个websocket server**

在nodejs中实现websocket，最简单的方法是使用相关的websocket库，常用的库有socket.io和ws，socket.io相对来说比较重量级一点，ws相对轻量一些，当然最重要的是socket.io实现的websocket，需要客户端也使用对应的socket-client包，这就显得太笨重了，而ws则可以在客户端中使用原生Websocket API。

首先我们安装ws

```jsx
npm i ws
```

#### 简单示例

```jsx
import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', function connection(ws) {
  ws.on('message', function message(data) {
    console.log('received: %s', data);
  });

  ws.send('something');
});
```

#### 用户权限验证

ws连接的建立依靠http，因此我们可以在http upgrade阶段进行身份鉴定，如果用户权限不够则不转换成ws协议，返回401。

这里要注意，ws的noServer要设置为true，并且**host、port和noServer三个参数只能设置一个**。**“noServer”模式的作用是将 WebSocket 服务器与 HTTP/S 服务器完全分离**。例如，这使得在多个 WebSocket 服务器之间共享单个 HTTP/S 服务器成为可能。

```jsx
import { createServer } from "http";
import { WebSocketServer } from "ws";

const server = createServer();
const wss = new WebSocketServer({ noServer: true });

wss.on("connection", function connection(ws, request, client) {
  ws.send("Hello Client");
  ws.on("message", function message(data) {
    console.log(`Received message ${data} from user ${client}`);
  });
});

server.on("upgrade", function upgrade(request, socket, head) {
  /**
   * 在此进行身份验证
   * 如果验证不通过则不建立ws连接
   */
  const user = request.user; // 获取用户信息
  if (!user) {
    socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
    socket.destroy();
    return;
  }

  // 验证通过，建立ws连接
  wss.handleUpgrade(request, socket, head, function (ws) {
    wss.emit("connection", ws, request, user);
  });
});

server.listen(8080);
```

#### wss

wss协议相当于https协议，其实就是使用了 TLS 的 Websocket。使用wss也比较简单，只需要在ws基础上提供证书密钥即可。

```jsx
const server = createServer({
  cert: readFileSync('/path/to/cert.pem'),
  key: readFileSync('/path/to/key.pem')
});
```

#### 广播

ws可以将消息广播到所有已经建立ws连接的客户端。

```jsx
import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', function connection(ws) {
  ws.on('message', function message(data) {
	// 将收到的消息广播
    wss.clients.forEach((client)=>{
			// 避免将消息发送给原发送方，当然你也可以选择发送给他
			if (cliend !== ws) {
        client.send(data);
      }
    })
  });
});
```

#### 异常断线处理——心跳机制

在网络中，由于各种因素的影响，ws连接可能会断开，并且这种断开可能是客户端和服务器无感的，这就导致了在双方不知情的情况下仍然进行通信，这不仅浪费资源，还导致通讯双发无法接收对方的消息。

解决异常断线的方式是使用心跳机制，所谓的心跳机制是每隔一段时间就发送一个空消息(ping)来确认ws连接是否正常，对方收到ping后返回一个空数据的响应(pong)，如果能正常收到pong，就说明ws连接正常，否则就证明ws连接异常，需要重新连接。

具体的做法是首先在每条ws上加上isAlive标记，该标记的作用是表明该条ws连接是否存活。然后每次发送ping前将isAlive设置为false，收到pong时将isAlive重新设置为true，假如第二次发送ping时isAlive是false，说明上个周期没有正常接收到pong，说明该条ws连接已经失效，则进行断线处理。

```jsx
import { WebSocketServer } from "ws";

const wss = new WebSocketServer({ port: 8080 });

wss.on("connection", function connection(ws) {
  ws.on("message", function message(data) {
    console.log(data.toString());
  });

  ws.isAlive = true; // 是否存活

  ws.send("Hello Client");

  ws.on("pong", () => {
    // 收到pong，就表示是存活的
    console.log("收到pong");
    ws.isAlive = true;
  });
});

// 心跳机制
const interval = setInterval(() => {
  wss.clients.forEach((client) => {
    if (client.isAlive === false) {
      // 上个周期没有收到pong，说明连接已经失效
      client.terminate();
    }
    // 先假设所有的client都是未存活，如果收到pong，就表示是存活的
    client.isAlive = false;
    client.ping(() => {
      console.log("发送ping");
    });
  });
}, 6000);

wss.on("close", () => {
  clearInterval(interval);
});
```

## stomp

Stomp全称Simple Text Oriented Messaging Protocol，即简单文本定向消息协议。它提供一种特定的格式，以便Stomp客户端和Stomp服务器进行通讯。stomp支持文本，同时也支持二进制。

#### stomp帧

stomp发送的消息称为帧，帧由三部分组成： command、headers和body。这和http协议有些类似。

* **command** ：字符串类型 针的名称。例如“CONNECT”、“SEND”、“SUBSCRIBE”等。
* **headers** ： JavaScript对象，有content-length、content-type等字段。
* **body**： 可以是二进制，也可以是文本。

command和headers总是会被定义，但是headers和body可以为空。body和headers之间通过一个空行来分隔。

#### stomp客户端

对于stomp客户端来说，它会扮演两种角色的其中一种：

1. 生产者，发送消息到指定地址。
2. 消费者，通过发送subscribe帧进行消息订阅，当消费者发送消息到该订阅地址后，订阅该地址的其他消费者接收消息。

客户端在这两个角色中互相转换，**这种订阅机制使得Websocket可以很方便地实现点对点通信和广播通讯。**

#### **实现点对点通讯和广播通讯的流程**

1. 客户端建立websocket连接
2. 客户端获取消息订阅地址。
3. 客户端作为消费者，通过stomp进行订阅(subsribe)消息地址，并定义接收消息的回调函数
4. 客户端作为生产者，发送消息到指定地址，在消息body中指定消息接收者的id，如果没有指定接收者id，则该地址的所有订阅者都会收到该消息。

```jsx
// 订阅消息和接收消息的回调函数
ws.subscribe("/user/queue/msg", function acceptMessage(msg){
		/* */
});

// 发送消息
ws.send(
	"/user/queue/msg",  // 目标地址
	{},  // headers
	JSON.stringify({  // body
		userBid: this.params.bid,
		reciver: this.othersBid,
		content: JSON.stringify(msg),
	})
);
```

#### 调试

可以在debug中查看stomp发送或者接收的是什么

```jsx
ws.debug = function(str) {
	console.log(str)
}
```

## 常用的Websocket API

### 事件

* error
* message
* close
* open

### 方法

* close
* send

### 属性

`[WebSocket.readyState](https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket/readyState)` ：当前的链接状态。0：connecting、1：open、2：closing、3：closed

`[WebSocket.url](https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket/url)` ：WebSocket 的绝对路径

`[WebSocket.binaryType](https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket/binaryType)：`使用二进制的数据类型连接。

`[WebSocket.bufferedAmount](https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket/bufferedAmount)` ：只读未发送至服务器的字节数。

`[WebSocket.extensions](https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket/extensions)` 服务器选择的扩展。

## 踩过的坑

### 异常断线处理

所谓的异常断线，是指由于各种因素影响，导致ws连接异常断开，并且这种异常断开客户端和服务器都无法感知到。

这个问题是在微信小程序中出现，当微信小程序进入后台5s后，微信会将该小程序挂起，这时就会断开ws连接，最重要的是这种断开操作服务器和前端都无法感知，因此导致了消息发不出去的问题。

处理异常断线的方案是**心跳机制。**

心跳机制前面有讲过，所谓的心跳条件机制就是每隔一段时间就送一个空消息给对方，如果收到确认消息就表示ws连接有效，否则就证明ws连接失效，此时需要重新建立连接。

### 如何确保消息成功发送给了对方

stomp客户端接收到对方消息时，会自动进行应答（ack），这有些类似TCP协议，通过这种应答的方式后端能够判断消息是否正确地传递给了接收方，然后再将这一结果返回给发送方。

具体是思路是后端一旦在一定时间内没有接收到ack确认，就发送一个error事件通知发送方。

## 参考

ws文档： [ws/ws.md at master · websockets/ws (github.com)](https://github.com/websockets/ws/blob/master/doc/ws.md#websocketterminate)

websocket api： <https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket>

websocket介绍： [一篇吃透WebSocket：概念、原理、易错常识、动手实践](http://www.52im.net/thread-3713-1-1.html)

stomp介绍： [STOMP Over WebSocket (jmesnil.net)](http://jmesnil.net/stomp-websocket/doc/)

stomp官网：[stomp.github.io](https://stomp.github.io)

stompjs: [StompJs Family](https://stomp-js.github.io)
