用 Node 实现简单 WebSocket 协议
从浏览器 WebSocket API 说起
浏览器提供的 WebSocket API 非常简洁。
let ws = new WebSocket('ws://localhost:8124') // 创建连接
ws.onopen = function(e){...} // 连接建立时的回调
ws.onmessage = function(e){...} // 收到消息时的回调
ws.onerror = function(e){...} // 连接出错时的回调
ws.onclose = function(e){...} // 连接终止时的回调
ws.send('Hello Server!') // 发送消息
MDN告诉我们
In order to communicate using the WebSocket protocol, you need to create a WebSocket object; this will automatically attempt to open the connection to the server.
也就是说,在 let ws = new WebSocket('ws://localhost:8124')
创建 WebSocket 对象时,浏览器尝试与服务端建立连接(发送请求)
建立个服务端
const net = require('net')
const server = net.createServer(s => {
s.on('data', d => {
console.log(d.toString())
})
})
server.listen(8124, () => {
console.log('listening on 8124...')
})
一旦收到数据,就会触发 data。
启动服务端,监听8124端口;在浏览器中打开控制台,输入let ws = new WebSocket('ws://localhost:8124')
;得到结果如下
通过这次请求,浏览器告诉服务端,要升级协议为 websocket
服务端收到这个请求后,向浏览器发出响应,这个过程就叫做握手(handshaking)
服务端向浏览器发出同意升级协议的响应后会触发浏览器端 websocket.onopen 的回调
服务端的响应
- Obtain the value of Sec-WebSocket-Key request header without any leading and trailing whitespace
- Link it with "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
- Compute SHA-1 and Base64 code of it
- Write it back as value of Sec-WebSocket-Accept response header as part of a HTTP response.
根据MDN的指示,修改服务端代码
const net = require('net')
const crypto = require('crypto')
const server = net.createServer(s => {
s.on('data', d => {
let req = d.toString()
// Obtain the value of Sec-WebSocket-Key request header without any leading and trailing whitespace
let secWebsocketKey = /Sec-WebSocket-Key:\s(.*)/.exec(req)[1]
// Link it with "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
let key = secWebsocketKey + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
// Compute SHA-1 and Base64 code of it
let secWebSocketAccept = crypto.createHash('sha1').update(key).digest('base64')
// Write it back as value of Sec-WebSocket-Accept response header as part of a HTTP response.
let res = 'HTTP/1.1 101 Switching Protocols\nConnection: Upgrade\nUpgrade: websocket\nSec-WebSocket-Accept: ' + secWebSocketAccept + '\n\n'
s.write(res)
})
})
server.listen(8124, () => {
console.log('listening on 8124...')
})
同时为了方便测试,写个html文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>socketClient</title>
</head>
<body>
socketClient
<script>
const socket = new WebSocket('ws://localhost:8124')
socket.onopen = () => {
console.log('finish handshaking!')
}
</script>
</body>
</html>
启动服务端,用浏览器打开html文件
至此之后,浏览器和服务端之间就可以更加愉快地聊天了,之前的服务端很矜持,浏览器问一句,服务端答一句;而握手之后,服务端也会主动向浏览器发送消息,这样就会触发浏览器端 websocket.onmessage 的回调
服务端主动发送消息
修改服务端代码
...
s.on('data', d => {
let req = d.toString()
console.log(req)
// Obtain the value of Sec-WebSocket-Key request header without any leading and trailing whitespace
let secWebsocketKey = /Sec-WebSocket-Key:\s(.*)/.exec(req)[1]
// Link it with "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
let key = secWebsocketKey + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
// Compute SHA-1 and Base64 code of it
let secWebSocketAccept = crypto.createHash('sha1').update(key).digest('base64')
// Write it back as value of Sec-WebSocket-Accept response header as part of a HTTP response.
let res = 'HTTP/1.1 101 Switching Protocols\nConnection: Upgrade\nUpgrade: websocket\nSec-WebSocket-Accept: ' + secWebSocketAccept + '\n\n'
console.log(res)
s.write(res)
// 服务端主动发消息 Hi, Client! l'm Server.
let dataBuffer = new Buffer(`Hi, Client! l'm Server.`)
let payload_len = dataBuffer.length
let assistData = []
assistData.push(129)
assistData.push(payload_len)
let assistBuffer = new Buffer(assistData)
let message = Buffer.concat([assistBuffer, dataBuffer])
console.log(message)
s.write(message)
})
})
...
代码中
// 服务端主动发消息 Hi, Client! l'm Server.
let dataBuffer = new Buffer(`Hi, Client! l'm Server.`)
let payload_len = dataBuffer.length
let assistData = []
assistData.push(129)
assistData.push(payload_len)
let assistBuffer = new Buffer(assistData)
let message = Buffer.concat([assistBuffer, dataBuffer])
console.log(message)
s.write(message)
实际上是将数据编码成数据帧的过程,其具体细节稍后再说。
修改html
<script>
const socket = new WebSocket('ws://localhost:8124')
socket.onopen = () => {
console.log('finish handshaking!')
}
socket.onmessage = d => {
console.log(d.data)
}
</script>
websocket.send()
浏览器发送消息很简单
修改html
<script>
const socket = new WebSocket('ws://localhost:8124')
socket.onopen = () => {
console.log('finish handshaking!')
}
socket.onmessage = d => {
console.log(d.data)
socket.send(`Hi, Server! l'm Client.`)
}
</script>
修改服务端代码
const net = require('net')
const crypto = require('crypto')
const server = net.createServer(s => {
s.handshaking = false
s.on('data', d => {
if (!s.handshaking) {
let req = d.toString()
console.log(req)
// Obtain the value of Sec-WebSocket-Key request header without any leading and trailing whitespace
let secWebsocketKey = /Sec-WebSocket-Key:\s(.*)/.exec(req)[1]
// Link it with "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
let key = secWebsocketKey + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
// Compute SHA-1 and Base64 code of it
let secWebSocketAccept = crypto.createHash('sha1').update(key).digest('base64')
// Write it back as value of Sec-WebSocket-Accept response header as part of a HTTP response.
let res = 'HTTP/1.1 101 Switching Protocols\nConnection: Upgrade\nUpgrade: websocket\nSec-WebSocket-Accept: ' + secWebSocketAccept + '\n\n'
console.log(res)
s.write(res)
// 服务端主动发消息 Hi, Client! l'm Server.
let dataBuffer = new Buffer(`Hi, Client! l'm Server.`)
let payload_len = dataBuffer.length
let assistData = []
assistData.push(129)
assistData.push(payload_len)
let assistBuffer = new Buffer(assistData)
let message = Buffer.concat([assistBuffer, dataBuffer])
console.log(message)
s.write(message)
s.handshaking = true
} else {
//解析浏览器发送消息
let fin = d[0] >> 7
let opcode = d[0] & parseInt(1111, 2) // 1 表示文本数据帧
let mask = d[1] >> 7 // 标示是否进行掩码处理,客户端发送给服务端时为1,服务端发送给客户端时为0
let payload_len = d[1] & parseInt(1111111, 2) // 这里假设发送的数据长度小于 125
let masking_key = d.slice(2, 6)
let payload_data = new Buffer(payload_len)
for (let i = 0; i < payload_len; i++) {
let j = i % 4
payload_data[i] = d[6 + i] ^ masking_key[j]
}
console.log(payload_data.toString())
}
})
})
server.listen(8124, () => {
console.log('listening on 8124...')
})
编码解码数据帧
解码Hi, Server! l'm Client.
let fin = d[0] >> 7
let opcode = d[0] & parseInt(1111, 2) // 1 表示文本数据帧
let mask = d[1] >> 7 // 标示是否进行掩码处理,客户端发送给服务端时为1,服务端发送给客户端时为0
let payload_len = d[1] & parseInt(1111111, 2) // 这里假设发送的数据长度小于 125
let masking_key = d.slice(2, 6)
let payload_data = new Buffer(payload_len)
for (let i = 0; i < payload_len; i++) {
let j = i % 4
payload_data[i] = d[6 + i] ^ masking_key[j]
}
console.log(payload_data.toString())
编码Hi, Client! l'm Server.
let dataBuffer = new Buffer(`Hi, Client! l'm Server.`)
let payload_len = dataBuffer.length
let assistData = []
assistData.push(129)
assistData.push(payload_len)
let assistBuffer = new Buffer(assistData)
let message = Buffer.concat([assistBuffer, dataBuffer])
console.log(message)
解码编码依据Base Framing Protocol
主要用到 nodeAPI 中 Buffer 的模块以及位运算
其实我主要看的是《深入浅出NodeJS》第七章websocket的那部分
小结
-
握手之后才可以通信
- 从请求头获取websocketKey
- 设置响应头
-
通信
- 编码信息
- 解码信息
- 位运算/Buffer/正则
let socketArr = []
const server = net.createServer(s => {
s.handShaking === false
s.name = `Client_${socketArr.length}`
socketArr.push(s)
s.on('data', d => {})
...
})
将每个socket对象标记并保存,从而实现对象之间的通信
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。