WebSocket协议详解

WebSocket是一种全双工、低延迟的通信协议,常用于实现实时应用。它与HTTP不同,允许服务端主动推送数据,减少了连接数量,并且拥有更简洁的协议头部。数据帧格式包括fin标识数据是否结束,opcode定义数据类型,如文本、二进制、关闭连接等。

Websocket协议详解

关于websocket的协议是用来干嘛的,请参考其他文章。

WebSocket关键词

HTML5协议,实时,全双工通信,长连接

WebSocket比传统Http的好处

  • 客户端与服务端只建立一个TCP连接,可以使用更少的连接
  • WebSocket的服务端可以将数据推送到客户端,如实时将证券信息反馈到客户端(这个很关键),实时天气数据,比http请求响应模式更灵活
  • 更轻量的协议头,减少数据传送量

数据帧格式

下图为手工打造的数据帧格式



/**
 * fin   |masked        |           |
 * srv1  |   length     |           |
 * srv2  |    (7bit     |mask数据     |payload
 * srv3  |     7+2字节   | 4字节       |真实数据
 opcode |     7+64字节  |            |
 *(4bit)
 */

作以下说明:

  1. 前8个bit(一个字节)
    —fin: 是否数据发送完成,为1发送完成为0发送未完。
    —srv1,srv2,srv3:留作后用
    —opcode:数据类型操作码,4bit表示,其中
    TEXT: 1, text类型的字符串
    BINARY: 2,二进制数据,通常用来保存图片
    CLOSE: 8,关闭连接的数据帧。
    PING: 9, 心跳检测。ping
    PONG: 10,心跳检测。pong

var events = require('events');
var http = require('http');
var crypto = require('crypto');
var util = require('util');

/**
 *  数据类型操作码 TEXT 字符串
 *  BINARY 二进制数据 常用来保存照片
 *  PING,PONG 用作心跳检测
 *  CLOSE 关闭连接的数据帧 (有很多关闭连接的代码 1001,1009,1007,1002)
 */
var opcodes = {
    TEXT: 1,
    BINARY: 2,
    CLOSE: 8,
    PING: 9,
    PONG: 10
};
var WebSocketConnection = function (req, socket, upgradeHead) {
    "use strict";
    var self = this;

    var key = hashWebSocketKey(req.headers['sec-websocket-key']);
    /**
     * 写头
     */
    socket.write('HTTP/1.1 101 Web Socket Protocol Handshake \r\n' +
        "Upgrade:WebSocket\r\n" +
        "Connection : Upgrade\r\n" +
        "sec-websocket-accept: " + key + '\r\n\r\n');

    /**
     * 接收数据
     */
    socket.on('data', function (buf) {
        self.buffer = Buffer.concat([self.buffer, buf]);
        while (self._processBuffer()) {

        }
    });
    socket.on('close', function (had_error) {
        if (!self.closed) {
            self.emit("close", 1006);
            self.closed = true;
        }
    });
    this.socket = socket;
    this.buffer = new Buffer(0);
    this.closed = false;

};

//websocket连接继承事件
util.inherits(WebSocketConnection, events.EventEmitter);

/*
 发送数据函数
 * */
WebSocketConnection.prototype.send = function (obj) {
    "use strict";
    var opcode;
    var payload;
    if (Buffer.isBuffer(obj)) {
        opcode = opcodes.BINARY;
        payload = obj;
    } else if (typeof obj) {
        opcode = opcodes.TEXT;
        //创造一个utf8的编码 可以被编码为字符串
        payload = new Buffer(obj, 'utf8');
    } else {
        throw new Error('cannot send object.Must be string of Buffer');
    }

    this._doSend(opcode, payload);
};
/*
 关闭连接函数
 * */
WebSocketConnection.prototype.close = function (code, reason) {
    "use strict";
    var opcode = opcodes.CLOSE;
    var buffer;
    if (code) {
        buffer = new Buffer(Buffer.byteLength(reason) + 2);
        buffer.writeUInt16BE(code, 0);
        buffer.write(reason, 2);
    } else {
        buffer = new Buffer(0);
    }
    this._doSend(opcode, buffer);
    this.closed = true;
};

WebSocketConnection.prototype._processBuffer = function () {
    "use strict";
    var buf = this.buffer;
    if (buf.length < 2) {
        return;
    }
    var idx = 2;
    var b1 = buf.readUInt8(0);    //读取数据帧的前8bit
    var fin = b1 & 0x80; //如果为0x80,则标志传输结束
    var opcode = b1 & 0x0f;//截取第一个字节的后四位
    var b2 = buf.readUInt8(1);//读取数据帧第二个字节
    var mask = b2 & 0x80;//判断是否有掩码,客户端必须要有
    var length = b2 | 0x7f;//获取length属性 也是小于126数据长度的数据真实值
    if (length > 125) {
        if (buf.length < 8) {
            return;//如果大于125,而字节数小于8,则显然不合规范要求
        }
    }
    if (length === 126) {//获取的值为126 ,表示后两个字节用于表示数据长度
        length = buf.readUInt16BE(2);//读取16bit的值
        idx += 2;//+2
    } else if (length === 127) {//获取的值为126 ,表示后8个字节用于表示数据长度
        var highBits = buf.readUInt32BE(2);//(1/0)1111111
        if (highBits != 0) {
            this.close(1009, "");//1009关闭代码,说明数据太大
        }
        length = buf.readUInt32BE(6);//从第六到第十个字节为真实存放的数据长度
        idx += 8;
    }

    if (buf.length < idx + 4 + length) {//不够长 4为掩码字节数
        return;
    }

    var maskBytes = buf.slice(idx, idx + 4);//获取掩码数据
    idx += 4;//指针前移到真实数据段
    var payload = buf.slice(idx, idx + length);
    payload = unmask(maskBytes, payload);//解码真实数据
    this._handleFrame(opcode, payload);//处理操作码
    this.buffer = buf.slice(idx + length);//缓存buffer
    return true;
};

/**
 * 针对不同操作码进行不同处理
 * @param 操作码
 * @param 数据
 */
WebSocketConnection.prototype._handleFrame = function (opcode, buffer) {
    "use strict";
    var payload;
    switch (opcode) {
        case opcodes.TEXT:
            payload = buffer.toString('utf8');//如果是文本需要转化为utf8的编码
            this.emit('data', opcode, payload);//Buffer.toString()默认utf8 这里是故意指示的
            break;
        case opcodes.BINARY: //二进制文件直接交付
            payload = buffer;
            this.emit('data', opcode, payload);
            break;
        case opcodes.PING://发送ping做响应
            this._doSend(opcodes.PING, buffer);
            break;
        case  opcodes.PONG: //不做处理
            break;
        case opcodes.CLOSE://close有很多关闭码
            let code, reason;//用于获取关闭码和关闭原因
            if (buffer.length >= 2) {
                code = buffer.readUInt16BE(0);
                reason = buffer.toString('utf8', 2);
            }
            this.close(code, reason);
            this.emit('close', code, reason);
            break;
        default:
            this.close(1002, 'unknown opcode');
    }
};

/**
 * 实际发送数据的函数
 * @param opcode 操作码
 * @param payload 数据
 * @private
 */
WebSocketConnection.prototype._doSend = function (opcode, payload) {
    "use strict";
    this.socket.write(encodeMessage(opcode, payload));//编码后直接通过socket发送
};

/**
 * 编码数据
 * @param opcode 操作码
 * @param payload   数据
 * @returns {*}
 */
var encodeMessage = function (opcode, payload) {
    "use strict";
    var buf;
    var b1 = 0x80 | opcode;
    var b2;
    var length = payload.length;
    if (length < 126) {
        buf = new Buffer(payload.length + 2 + 0);
        b2 |= length;
        //buffer ,offset
        buf.writeUInt8(b1, 0);//读前8bit
        buf.writeUInt8(b2, 1);//读8―15bit
        //Buffer.prototype.copy = function(targetBuffer, targetStart, sourceStart, sourceEnd) {
        payload.copy(buf, 2)//复制数据,从2(第三)字节开始

    } else if (length < (1 << 16)) {
        buf = new Buffer(payload.length + 2 + 2);
        b2 |= 126;
        buf.writeUInt8(b1, 0);
        buf.writeUInt8(b2, 1);
        buf.writeUInt16BE(length, 2)
        payload.copy(buf, 4);
    } else {
        buf = new Buffer(payload.length + 2 + 8);
        b2 |= 127;
        buf.writeUInt8(b1, 0);
        buf.writeUInt8(b2, 1);
        buf.writeUInt32BE(0, 2)
        buf.writeUInt32BE(length, 6)
        payload.copy(buf, 10);
    }

    return buf;
};

/**
 * 解掩码
 * @param maskBytes 掩码数据
 * @param data payload
 * @returns {Buffer}
 */
var unmask = function (maskBytes, data) {
    var payload = new Buffer(data.length);
    for (var i = 0; i < data.length; i++) {
        payload[i] = maskBytes[i % 4] ^ data[i];
    }
    return payload;
};
var KEY_SUFFIX = '258EAFA5-E914-47DA-95CA-C5ABoDC85B11';

/*equals to crypto.createHash('sha1').update(key+'KEY_SUFFIX').digest('base64')
 * */
var hashWebSocketKey = function (key) {
    "use strict";
    var sha1 = crypto.createHash('sha1');
    sha1.update(key + KEY_SUFFIX, 'ascii');
    return sha1.digest('base64');
};

exports.listen = function (port, host, connectionHandler) {
    "use strict";
    var srv = http.createServer(function (req, res) {
    });

    srv.on('upgrade', function (req, socket, upgradeHead) {
        "use strict";
        var ws = new WebSocketConnection(req, socket, upgradeHead);
        connectionHandler(ws);
    });
    srv.listen(port, host);
};





### WebSocket 协议概述 WebSocket 是一种计算机通信协议,提供全双工通信信道,在单个 TCP 连接上进行全双工通信[^1]。该协议由 IETF 制定并被 RFC 6455 标准化,允许服务端主动向客户端推送信息,客户端也可以主动向服务端发送信息,实现了真正的双向平等对话[^3]。 ### WebSocket 工作原理 #### 握手流程详解 当浏览器支持 WebSocket 并尝试与服务器建立连接时,会先发起 HTTP 请求,其中包含了 `Upgrade` 和 `Connection` 头字段来表明这是一个 WebSocket 的握手请求。如果服务器接受此请求,则返回状态码 101 表示正在切换协议,并附带必要的响应头如 `Sec-WebSocket-Accept` 来验证握手的有效性[^2]。 ```http GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Origin: http://example.com Sec-WebSocket-Version: 13 ``` 服务器回应: ```http HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= ``` #### 数据帧结构 一旦完成握手阶段,双方就可以开始交换二进制或 UTF-8 编码的消息了。每条消息会被分割成多个数据帧传输给对方;而这些帧又可以进一步分为控制帧(用于管理连接的状态)以及携带实际负载的数据帧。 #### 掩码机制 为了安全考虑,所有从客户机发往服务器的方向上的数据都必须经过掩码处理——即通过简单的异或运算对载荷部分加密。这样做主要是为了避免某些中间件错误地解释未加保护的内容流而导致的安全风险[^5]。 ```python def apply_mask(data, masking_key): masked_data = bytearray() for i in range(len(data)): masked_data.append(data[i] ^ masking_key[i % 4]) return bytes(masked_data) ``` ### 实现细节 随着 Spring Framework 版本迭代至 4.x 后引入了内置对于 WebSockets 支持的功能模块,开发者可以通过配置 STOMP over WebSocket 或者直接利用 @Controller 注解下的方法映射特定路径作为 WebSocket 终结点等方式轻松集成这项技术到应用程序当中去[^4]。
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值