「这是我参与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
模块的使用有所不同。
用法介绍
Html5 WebSocket
和ws
模块的用法有所不同,对于获取到的数据也可能不太一样。
二者对于状态的划分与使用是相同的,通过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)