最近有个项目要做一个Slack Robot, 就是根据用户发到Slack 某个Channel的内容, 给出智能回复.
与Slack的交互方式有2种: HTTP连接的方式和WebSocket的方式. HTTP 连接的方式又有2种实现方式:
- 我们提供一个公开的endpoint, 当Slack的Channel 有消息的时候, Slack 以webhook的方式及时通知我们, 我们的app 给出回复.
- 我们不提供endpoint, 我们连接Slack的endpoint, 以固定时间间隔的方式去poll 消息, 如果有新消息, 我们的app 给出回复.
以上这2种方式都不是很好的方式, 对于第一种, 我们要提供一个公开的endpoint, 在我们的生产环境基本不可能, 对于第二种方式, 不是基于事件的, 不管有没有消息, 都要固定的去poll, 对于一个app 里面有多个实例的情况, 还要控制那个实例去poll.
所以 WebSocket 的方式是最好的, 不过当时就有开发人员提出了意见: 我们的生产环境时不能连外网的, 要连只能通过http 代理, 而我们的生产环境的代理只支持 http 代理, 不支持sock 方式, 所以这种 WebSocket 行不通. 是真的吗?
在我们看看 WebSocket 到底是什么之前, 先做一个 WebSocket 的例子: 使用Node.js 开一个WebSocket 服务, 然后在浏览器开一个 WebSocket 客户端.
WebSocket 服务端
新建一个文件夹
mkdir wsServer
cd wsServer
新建一个 package.json
vim package.json
输入如下代码
{
"name": "wsServer",
"version": "0.0.1",
"type": "module",
"dependencies": {
"ws":"8.8.1"
}
}
新建一个服务端代码文件
vim server.js
输入如下代码:
var WebSocketServer = require('ws').Server
var fs = require('fs')
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(data + " -> server ack");
});
fs.watch('/tmp/', (eventType, fileName) => {
console.log('Get event on file ' + fileName + ', type: ' + eventType);
ws.send('file -> ' + fileName + ' -> ' + eventType);
})
ws.send('welcome to WebSocket world!');
});
上面这段代码做3件事情:
- 启动 WebSocket 服务器在8080 端口上, 当有人来连接的时候, 发送欢迎消息;
- 当收到客户端消息的时候, 打印收到的消息, 并且发送给客户端 ack 消息;
- 监听本地 /tmp 文件夹的文件变动事件, 打印日志, 并推送给客户端;
启动服务端代码:
node server.js
WebSocket 客户端
在 Chrome 浏览器打开任意页面的控制台, 输入如下JavaScript 代码
const ws = new WebSocket('ws://10.249.64.103:8080');
// Listen for messages
ws.addEventListener('message', (event) => {
console.log('Message from server: ', event.data);
});
document.addEventListener('click', (event) => {
console.log("just clicked");
ws.send('click on (' + event.x + ', ' + event.y + ')');
});
上面的代码做下面的事情:
- 连接服务端 WebSocket;
- 然后当收到服务器端消息的时候, 打印收到的消息;
- 当页面上收到点击事件的时候, 推送给服务端点击事件的坐标;
运行效果
客户端效果:
服务器端效果:
使用代理
因为客户端使用的是浏览器, 可以设置在chrome 设置对于这个IP 启用代理, 使用http 代理, 依然能联通server 端, 正常运转.
使用curl
使用curl 只能被动的接受服务器传来的消息, 不能发送任何消息. (这个 Sec-WebSocket-Key 是从浏览器刚才发送的历史中复制过来的)
curl \
--include \
--no-buffer \
--header "Connection: Upgrade" \
--header "Upgrade: websocket" \
--header "Host: 10.249.64.103:8080" \
--header "Origin: http://10.249.64.103:8080" \
--header "Sec-WebSocket-Key: 1TFTcjPQ7iG2XvsZ83WgZg==" \
--header "Sec-WebSocket-Version: 13" \
http://10.249.64.103:8080
效果(curl 对回应消息只能拼接, 不换行):
JavaScript WebSocket API 文档
官方文档在这里: https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket
这个 WebSocket 类很简单:
- 构造函数给出服务器的地址, 包括ws协议, host 加 port;
- 有几个字段: 比如 url, readyState, protocol, bufferedAcmount, binaryType等, 有些是只读的;
- 客户端只有发送 send() 和 close() 方法;
event handler:
- message: 当收到消息时;
- open: 当连接建立时;
- error: 当发送错误时;
- close: 当关闭连接时;
Node.js 的 WebSocket 实现库 ws 的文档
官方文档: https://www.npmjs.com/package/ws#sending-and-receiving-text-data
它不仅仅包含一个server端的API实现, 还包含一个做为 Node.js 客户端端代码实现
WebSocket 协议
官方文档: https://www.rfc-editor.org/rfc/rfc6455
当我们看过上面的 WebSocket 的例子之后, 再来看这个RFC 文档, 就不是那么难了.
为什么需要 WebSocket 协议
在 web 通信等某些场景下, 如果服务端发生了某些事件, 需要实时推送给客户端. 在传统的基于web的技术下, 需要客户端不断的去poll消息, 不管服务端到底有没有事件更新, 每次poll 都需要客户端发送一个http request, 并且如果发送的频率过低, 可能不能及时收到服务端事件更新, 如果频率过高, 又会对网络和服务端造成一些压力.
所以, 如果建立一个连接的情况下, 服务端事件变更主动推送客户端, 客户端只要等待就好了, 就完美解决了这个问题, 于是就有了 WebSocket 协议.
WebSocket 基本介绍
- 它可以使客户端和服务端在一个连接里面双向不间断通信;
- 通信过程分2阶段, 先是使用http协议握手连接, 然后以数据帧的方式双向发送数据;
- 广泛使用在游戏, 股票等需要实时消息通讯等软件中;
- WebSocket 可以使用现有web的 proxy和认证等成熟的机制;
- 通常开在80或443, 可以通过防火墙;
通常的 连接握手协议:
The handshake from the client looks as follows:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
The handshake from the server looks as follows:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat