Websocket 是HTML5中的一种新的Web通信技术,它实现了浏览器与服务器之间的双向通信(full-duplex).
背景
在Websocket之前,实现双向通信的技术有轮询, Comet
技术 | |
---|---|
轮询 | 客户端定时向服务器发送Ajax请求,服务器接到请求后马上返回响应信息并关闭连接 |
优点 | 后端容易实现 |
缺点 | 大部分是无用的请求,浪费服务器资源和带宽 |
长轮询 | 客户端向服务器发送Ajax请求,服务器接到请求后hold住连接,直到有新消息才返回响应信息并关闭连接,客户端处理完响应信息后再向服务器发送新的请求 |
优点 | 在无消息的情况下不会频繁的请求,耗费资源小 |
缺点 | 服务器保持连接会消耗资源,返回数据顺序无保证 |
iframe | 在页面里嵌入一个隐蔵iframe,将这个隐蔵iframe的src属性设为对一个长连接的请求或是采用xhr请求,服务器端就能源源不断地往客户端输入数据 |
优点 | 消息即时到达,不发无用请求;管理起来也相对方便 |
缺点 | 服务器维护一个长连接会增加开销 |
什么是Websocket
Websocket协议是基于TCP的一种新的通信协议,可以在浏览器和服务器之间建立“套接字(Socket)”连接,简单地说:客户端和服务器之间存在持久的连接,而且双方都可以随时开始发送数据。
Websocket协议有两部分,握手和数据传输
Websocket握手
一个典型的Websocket握手请求如下:
GET / HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: example.com
Origin: http://example.com
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
Sec-WebSocket-Version: 13
服务器回应
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: fFBooB7FAkLlXgRSz0BT3v4hq5s=
Sec-WebSocket-Location: ws://example.com/
- Connection必须设置Upgrade,表示客户端希望连接升级。
- Upgrade字段必须设置Websocket,表示希望升级到Websocket协议。
- Sec-WebSocket-Key是随机的字符串,服务器端会用这些数据来构造出一个SHA-1的信息摘要。把“Sec-WebSocket-Key”加上一个特殊字符串“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后计算SHA-1摘要,之后进行BASE-64编码,将结果做为“Sec-WebSocket-Accept”头的值,返回给客户端。如此操作,可以尽量避免普通HTTP请求被误认为Websocket协议。
- Sec-WebSocket-Version 表示支持的Websocket版本。RFC6455要求使用的版本是13,之前草案的版本均应当弃用。
- Origin字段是可选的,通常用来表示在浏览器中发起此Websocket连接所在的页面,类似于Referer。但是,与Referer不同的是,Origin只包含了协议和主机名称。
- 其他一些定义在HTTP协议中的字段,如Cookie等,也可以在Websocket中使用。
在Node中可以使用http
模块实现一个简单的服务器来完成Websocket的握手。
服务器要监听upgrade的请求。
server.on('upgrade', (req, socket, head) => {
});
完成Sec-WebSocket-Key -> Sec-WebSocket-Accept
const guid = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
const key = crypto
.createHash('sha1')
.update(`${req.headers['sec-websocket-key']}${guid}`)
.digest('base64');
返回必要的头信息
socket.write(
'HTTP/1.1 101 Switching Protocols\r\n' +
'Upgrade: webSocket\r\n' +
'Connection: upgrade\r\n' +
`Sec-WebSocket-Accept: ${key}\r\n` +
'\r\n'
);
Websocket数据传输(frame)
在Websocket协议中,客户端和服务器端都可以互相发送数据。
发送和接受的数据如下面
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
- FIN: 1bit
是否为最后的frame标记 - FSV: 3bits
保留 - opcode: 4bits
payload数据说明 - MASK: 1bit
是否有mask标记 - Payload len: 7bits
如果payload长度126,延长16bits,如果payload长度是127,延长到64bits - Masking-key: 4 bits
- Payload Data
opcode的类型
|Opcode | Meaning | Reference |
-+--------+-------------------------------------+-----------|
| 0 | Continuation Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|
| 1 | Text Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|
| 2 | Binary Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|
| 8 | Connection Close Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|
| 9 | Ping Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|
| 10 | Pong Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|
在Node中用net
这个模块读取socket中frame的数据
socket.on('data', (buf: Buffer) => {
const fro = buf[0]; // 读取第一个字节
const fin = (fro & 0x80) === 0x80;
const opcode = fro & 0x0f;
console.log("fin: ", fin);
console.log("opcode: ", opcode);
const mp = buf[1]; // 读取第二个字节
const mask = (mp & 0x80) === 0x80;
const payloadLen = mp & 0x7f;
console.log("mask: ", mask);
console.log("payloadLen: ", payloadLen);
// 这里做了简化处理,实际过程中需要判断mask和payloadLen
const maskKey = buf.slice(2, 6);
const payload = buf.slice(6, 6+payloadLen);
const data = payload.map((p, i) => {
return p ^ maskKey[i%4] // 利用掩码解析数据
});
console.log(data.toString('utf8'));
});
发送数据也是同样的道理,先组装一个frame,然后写入到socket中去
const text = Buffer.from("Hello there");
const finfo = Buffer.allocUnsafe(2);
finfo[0] = 0b10000001;
finfo[1] = text.length;
const ret = Buffer.concat([finfo, text]);
socket.write(ret);
完整的代码
浏览器端代码
<!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>Document</title>
</head>
<body>
<script>
var ws = new WebSocket("ws://localhost:2345");
ws.addEventListener('open', function(e) {
ws.send("can you hear me?");
})
ws.addEventListener('message', function(e) {
console.log(e.data);
});
</script>
</body>
</html>
服务器端代码(用ts实现的)
import * as http from 'http';
import * as net from 'net';
import * as crypto from 'crypto';
const server = http.createServer();
server.on('upgrade', (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => {
const key = req.headers['sec-websocket-key'];
let accept;
if (key && typeof key !== 'undefined') {
accept = webSocketAccept(key as string);
}
socket.write(
'HTTP/1.1 101 Switching Protocols\r\n' +
'Upgrade: webSocket\r\n' +
'Connection: upgrade\r\n' +
`Sec-WebSocket-Accept: ${accept}\r\n` +
'\r\n'
);
socket.on('data', (buf: Buffer) => {
const fro = buf[0];
const fin = (fro & 0x80) === 0x80;
const opcode = fro & 0x0f;
console.log("fin: ", fin);
console.log("opcode: ", opcode);
const mp = buf[1];
const mask = (mp & 0x80) === 0x80;
const payloadLen = mp & 0x7f;
console.log("mask: ", mask);
console.log("payloadLen: ", payloadLen);
const maskKey = buf.slice(2, 6);
const payload = buf.slice(6, 6+payloadLen);
const data = payload.map((p, i) => {
return p ^ maskKey[i%4]
});
console.log(data.toString('utf8'));
const text = Buffer.from("Hello there");
const finfo = Buffer.allocUnsafe(2);
finfo[0] = 0b10000001;
finfo[1] = text.length;
const ret = Buffer.concat([finfo, text]);
let i = 3;
do {
socket.write(ret);
i = i -1;
} while (i < 3);
});
});
function webSocketAccept(key: string): string {
const hash = crypto.createHash('sha1');
hash.update(`${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`)
return hash.digest('base64');
}
server.listen(2345, () => {
console.log("server started at 2345");
});