WebSocket的实践心得

1,155 阅读6分钟

「这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战

由于已经接触过不少B/S结构项目,于是希望以C/S结构来构建一个项目,而socket通信就多用于此。本文章中会主要介绍客户端与服务端建立WebSocket连接的方式,消息的封装,消息的加密,及一些常见问题。

概念介绍

WebSocket

传统的Http请求是无状态的,每次请求只能由一方发起,另一方接收消息后响应即结束。对于需要频繁交互的,如聊天室,对于需要实时更新的项目,以往的实现采用轮询的方式执行,每隔一段极短时间向服务器发起请求,从而更新新的内容,每次建立握手连接与关闭是非常耗费时间与资源的。

WebSocket协议是在Http 1.1协议基础上产生,解决了Http的上述问题,能够进行全双工的TCP通信。只需要建立一次握手连接,之后便可以维持一个通信信道,借助监听消息事件来发送消息。

需要注意的是,WebSocket的消息传输以Frame帧的形式分段发送,获取的结果一般为字符串,因此若想传输对象,数组等结构,需要进行序列化与反序列化。

目标

个人实践将构建一个客户端与服务端的机器人通信项目。

服务端解析定制的DSL脚本,进行分词/解析AST;根据客户端的不同响应来进行不同的通信。主要借助后端ws模块来通信。

领域特定语言(Domain Specific Language,DSL)可以提供一种相对简单的文法,用于特定领域的业务流程定制。

客户端即为人交互,用户与机器人进行交流。主要借助Html5原生的Websocket来进行通信,与ws模块的使用有所不同。

socket.png

用法介绍

Html5 WebSocketws模块的用法有所不同,前者仅可在网页中使用,后者仅可在服务端使用;且对于获取到的数据也可能有所差异。

二者对于状态的划分与使用是相同的,通过ws.readyState获取状态码:

ws.CONNECTING === 0;
ws.OPEN === 1;
ws.CLOSING === 2;
ws.CLOSED === 3;

Html5 WebSocket

建立连接

字符串为ws协议的地址:ws://...

let ws = new WebSocket('ws://localhost:8080');

接收消息

ws.onmessage = function(event){
    let msg = event.data;
    // ...
}

这里需要强调对于数据的获取,需要间接通过event对象,且event.data可能有两种数据格式,一种为String,另一种为Blob。对于String的处理不必赘述,需要额外说明对于Blob的处理:借助FileReader来回调出其信息。

const reader = new FileReader();

// reader.readAsArrayBuffer(event.data);
reader.readAsText(event.data, 'utf8');
reader.onload = () => {
  const message = reader.result;
  this.receiveMsg(message);
};

发送消息

// typeof data === 'string'
ws.send(data);

关闭

ws.onclose = funtion(){
    // ...
}

ws模块

建立服务

这里的WebSocket属于ws模块中的对象。

// 创建websocket服务端
let wss = new WebSocket.Server({ port:8080 });

// 创建一个新的连接
let ws = new WebSocket("ws://localhost:8080");

监听连接

每次有新的客户端建立连接后,服务端便可收到消息。

wss.on('connection', async (ws, req) => {
    const { host } = req.headers;
    
    // close event
    ws.on("close", (code, reason) => {
        console.log("ID:%d Server closed: %d %s", ClientId, code, reason);
    });
    
    // message event
    ws.on("message", (message) => {
        stdout.info("ID:%d Server receive: %s",message.toString());
    }
})

回调函数中的ws便是对于客户端通信来说是一对一的,req附带响应TCP连接的信息,如上面是host是连接方的主机名。

心跳

对于每个ws连接,可以通过以下方式来进行心跳测试,每隔waitTime的时间内,监测是否收到回复。长时间未收到回复即会断开连接,以节省资源。

setTimeout(() => {
  if (!reply) {
    ws.close();
  }
}, waitTime);

关于心跳的应用,在我的项目中有另外一处应用,即限时断线,作为机器人来说,是长时间保持在空闲状态的,否则会消耗资源,但是对于消息的监听是异步的,限时在某种意义上代表需要计算时间,需要阻塞,因此这里推荐一种时间片的做法:

时间片设置为500ms,计算循环轮数,每轮检测是否收到消息即可。这里的阻塞是必须的,否则在异步的情况下无法计算时间。

let INTERVAL = 500;
let loops = Math.ceil((seconds * 1000) / INTERVAL);

while (loops > 0) {
  if (reply) {
    console.log("Receive");
    return;
  }
  await sleep(INTERVAL);
  loops--;
}

if (!reply) {
  console.log("Closed.");
  return;
}

ws监听事件

需要说明,ws.onmessage等只能够绑定一个回调函数,而ws.on("message",()=>{})能够绑定多个回调函数,不会进行覆盖。这点与网页中点击事件的绑定类似,即onclickaddListener('click',()=>{})

接收消息,有以下两种方法,这里的回调参数message与前面的又不太一致,存在StringWebSocket.RawData类型,因此可统一使用toString来转换。

ws.onmessage = (message) => {}
ws.on("message", (message) => {
    console.log("Receive %s", message.toString());
})

监听关闭:

ws.onclose = () => {}
ws.on("close",() => {})

通信封装

用到WebSocket的过程中势必会碰到一个问题,那是我们的需要不仅仅是收发消息,还需要对消息划分类型,传输类似于Http消息的数据,因此这里就可以用到序列化的知识。

封装

在我的项目中,构造了如下结构:

{
    type: "init",  // "message" or "close"
    data: {
        name: "xxx",
        avatar: "xxx",
        account: 100
    }
}

type可以定义不同的消息类型,data中即为返回的数据,通过JSON.stringify()来序列化,接收消息后通过JSON.parse()来反序列化。

加密

在一些消息传递过程中,同时需要做到对信息的保密,如初始化时传递密码等,这时就需要用到RSA非对称加密(通过openssl生成公钥和私钥,详见相关中的另一篇文章),需要用到jsencrypt模块。

// const { JSEncrypt } = require('nodejs-encrypt');
const { JSEncrypt } = require('jsencrypt');

// 公钥加密
publicEncrypt(str) {
    const encrypt = new JSEncrypt();
    encrypt.setPublicKey(pubKey);
    return encrypt.encryptLong(str);
},

// 私钥解密
privateDecrypt(str) {
    const encrypt = new JSEncrypt();
    encrypt.setPrivateKey(privKey);
    return encrypt.decryptLong(str);
},

在使用上述模块加密过程中可能会碰到加密失败问题,这是由于字符串过长,可以借助encryptlong模块来进行。另外,个人的另外一种思路是采用md5加密缩短字符串后再进行RSA加密。

最后

这篇文章的内容以WebSocket为主,讲述了它的一些基本使用和做项目过程中所碰到的一些问题。

这里推荐一下个人的项目,分为Electron客户端和Nodejs服务端,包含了DSL解析模块(Tokenize,AST),日志模块,测试模块等:MrPluto0/dslBot (github.com)

相关

MrPluto0/dslBot解释器(人机交互)

实践:使用jsencrypt配合axios实现非对称加密传输数据 - 掘金 (juejin.cn)

阮一峰的个人日志:WebSocket

ws模块英文文档(github.com)