websocket介绍及简易实现

815 阅读5分钟

概述

历史上,创建需要双向的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.

  1. 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".
  2. An |Upgrade| header field with value "websocket" as per RFC 2616 [RFC2616].
  3. A |Connection| header field with value "Upgrade".
  4. 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> 我们需要对数据进行解析

(mask作用 www.bslxx.com/a/mianshiti…)

解析数据

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;
}

综合实践,简易聊天室

参考资料