WebSocket的实践心得

「这是我参与2022首次更文挑战的第1天,活动详情查看: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模块中的对象。

let wss = new WebSocket.Server({ port:8080 });
复制代码

监听连接

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

wss.on('connection', async (ws, req) => {
    const { host } = req.headers;
})
复制代码

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

心跳

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

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

ws监听事件

需要说明,ws.onmessage等只能够绑定一个回调函数,而ws.on("message",()=>{})能够绑定多个回调函数,不会进行覆盖。

接收消息,有以下两种方法,这里的回调参数message与前面不同,属于String类型。

ws.onmessage = (message) => {}
ws.on("message", (message) => {})
复制代码

监听关闭:

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)

Acho que você gosta

Origin juejin.im/post/7063018773590048804
Recomendado
Clasificación