概述
历史上,创建需要双向的Web应用程序客户端和服务器之间的通信(例如,即时消息传递和游戏应用程序),要求滥用HTTP来轮询服务器发送更新。
导致的问题:
- 服务器为每个客户端创建许多不同的基础TCP:
- 向客户端发送信息的连接
- 为每个传入消息添加一个新客户端
- 有线协议的开销很高,每个客户端到服务器都具有HTTP头信息
- 从发送请求到响应回复,客户端需要跟踪答复
解决办法:
将单个TCP连接用于 双向通信,这就是WebSocket协议(为了能够在一个端口同时处理http请求和websocket请求,websocket使用upgrade字段)
特点&优势:
客户端和服务端都可以主动的推送消息,可以是文本也可以是二进制数据。而且没有同源策略的限制,不存在跨域问题。
和http的关系
- WebSocket协议是一个独立的基于TCP的协议。它与HTTP的唯一关系是其握手由HTTP服务器作为升级请求
- 成功握手确立webSocket的连接之后,通信时不再使用HTTP的数据帧,而采用WebSocket独立的数据帧。
浏览器websocket 常用api介绍
developer.mozilla.org/en-US/docs/…
协议实现
参考文献https://tools.ietf.org/html/rfc6455
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
字段含义解析
- Sec-WebSocket-Key:握手必须的键值。
- Sec-webSocket-Protocol:使用的子协议
- Upgrade:websocket (升级为其他协议,用来检测HTTP协议以及其他协议是否可用,告诉服务器需要用wobsocket进行通信)
- (使用Upgrade时候需要额外指定Connection:Upgrade)
- 对于附有首部字段Upgrade的请求,服务器可用101 Switching Protocols状态码作为响应返回
- Sec-WebSocket-Accept:由Sec-WebSocket-Key生成,握手校验值
这些字段由WebSocket客户端检查脚本页面。如果 Sec-WebSocket-Accept 值与预期不符,或响应头字段丢失,或者HTTP状态代码为不是101,将不会建立连接,并且WebSocket帧将不会发送。
握手.响应的正确返回
If the server chooses to accept the incoming connection, it MUST reply with a valid HTTP response indicating the following.
- A Status-Line with a 101 response code as per RFC 2616 [RFC2616]. Such a response could look like "HTTP/1.1 101 Switching Protocols".
- An |Upgrade| header field with value "websocket" as per RFC 2616 [RFC2616].
- A |Connection| header field with value "Upgrade".
- A |Sec-WebSocket-Accept| header field. The value of this header field is constructed by concatenating /key/, defined above in step 4 in Section 4.2.2, with the string "258EAFA5- E914-47DA-95CA-C5AB0DC85B11", taking the SHA-1 hash of this concatenated value to obtain a 20-byte value and base64- encoding (see Section 4 of [RFC4648]) this 20-byte hash.
响应必须包含以下内容
- HTTP/1.1 101 Switching Protocols
- Upgrade: websocket
- Connection: Upgrade
- Sec-WebSocket-Accept:XXXXXX
Sec-WebSocket-Accept的计算规则
取请求字段Sec-WebSocket-Key,连接全局唯一标识符 258EAFA5-E914-47DA-95CA-C5AB0DC85B11,对其进行SHA-1哈希处理,并base64编码,将得到的值写入响应字段Sec-WebSocket-Accept
const crypto = require('crypto');
const key=crypto.createHash('sha1').update('NlkoAtcpQlENt8+gHHx1Aw==258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest('base64');
console.log(key);//JBGobEpXbLx2POeZo0zcho4iZ1c=
服务器必须向客户端证明其已收到客户端的WebSocket握手,以便服务器不接受不是WebSocket连接的连接.
【代码】server-1.js
const net = require('net');
const crypto = require('crypto');
net.createServer((socket)=>{
//完成tcp握手之后开始传输数据
const header = {};
socket.once('data',(data)=>{
let tmpHeader = data.toString().split('\r\n');
tmpHeader.shift();
tmpHeader.forEach(item=>{
if(item){
let index = item.indexOf(':');
const key = item.substr(0,index);
const value = item.substr(index+1);
header[key.trim().toLocaleLowerCase()] = value.trim();
}
})
if(header.upgrade.toLocaleLowerCase()==='websocket'&&header['sec-websocket-version']=='13'){
const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
const socketkey = header['sec-websocket-key']
const hash = crypto.createHash('sha1') // 创建一个签名算法为sha1的哈希对象
hash.update(`${socketkey}${GUID}`) // 将key和GUID连接后,更新到hash
const result = hash.digest('base64') // 生成base64字符串
const responseHeader = `HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-Websocket-Accept: ${result}\r\n\r\n`;
socket.write(responseHeader)
//server-2.js
}
})
}).listen(8083);
【代码】server-1.html
let ws = new WebSocket("ws://localhost:8083");
数据处理
连接成功后,从浏览器开始发送数据,并在服务器打印收到的数据。
- server-1.js
socket.write(responseHeader)
//server-2.js
socket.on('data',(d)=>{
console.log(d);
})
- server-1.html
let ws = new WebSocket("ws://localhost:8083");
ws.onopen = function(){
setTimeout(() => {
ws.send('你好,来自客户端');
}, 2000);
}
服务器收到如下数据
<Buffer 88 82 4d 1a a9 17 4e f3>
我们需要对数据进行解析
解析数据
function decodeWsFrame(data) {
let start = 0;
let frame = {
isFinal: (data[start] & 0x80) === 0x80,//取前1位做&操作
opcode: data[start++] & 0xF,//取后4位做&操作
masked: (data[start] & 0x80) === 0x80,//取前1位做&操作
payloadLen: data[start++] & 0x7F,//取后7位做&操作
maskingKey: '',
payloadData: null
};
// if 0-125, that is the payload length
// If 126, the following 2 bytes interpreted as a 16-bit unsigned integer are the payload length
// If 127, the following 8 bytes interpreted as a 64-bit unsigned integer (the most significant bit MUST be 0) are the payload length.
if (frame.payloadLen === 126) {
//如果等于126 ,接下来的两个字节作为长度
//data[3]左移8位 + data[4];
frame.payloadLen = (data[start++] << 8) + data[start++];
} else if (frame.payloadLen === 127) {
//如果等于127 接下来的8个字节作为长度
frame.payloadLen = 0;
for (let i = 7; i >= 0; --i) {
frame.payloadLen += (data[start++] << (i * 8));
}
}
if (frame.payloadLen) {
if (frame.masked) {
const maskingKey = [
data[start++],
data[start++],
data[start++],
data[start++]
];
frame.maskingKey = maskingKey;
// Octet i of the transformed data ("transformed-octet-i") is the XOR of octet i of the
//original data ("original-octet-i") with octet at indexi modulo 4 of the masking key ("masking-key-octet-j"):
// j = i MOD 4
//transformed-octet-i = original-octet-i XOR masking-key-octet-j
frame.payloadData = data
.slice(start, start + frame.payloadLen)//截取数据的长度 单位为字节
.map((byte, idx) => byte ^ maskingKey[idx % 4]);//解密
} else {
frame.payloadData = data.slice(start, start + frame.payloadLen);//不需要解密
}
}
return frame;
}
数据编码
function encodeWsFrame(data) {//对上面函数的反向编码,不需要mask
const isFinal = data.isFinal !== undefined ? data.isFinal : true,
opcode = data.opcode !== undefined ? data.opcode : 1,
payloadData = data.payloadData ? Buffer.from(data.payloadData) : null,
payloadLen = payloadData ? payloadData.length : 0;
let frame = [];
if (isFinal) frame.push((1 << 7) + opcode);
else frame.push(opcode);
if (payloadLen < 126) {
frame.push(payloadLen);
} else if (payloadLen < 65536) {// 127*2^8 + 127
frame.push(126, payloadLen >> 8, payloadLen & 0xFF);
} else {
frame.push(127);
for (let i = 7; i >= 0; --i) {
frame.push((payloadLen & (0xFF << (i * 8))) >> (i * 8));
}
}
frame = payloadData ? Buffer.concat([Buffer.from(frame), payloadData]) : Buffer.from(frame);
return frame;
}