websocket协议的服务端Nodejs实现

websocket协议的服务端Nodejs实现

虽然已经有很完善的websocket框架,但是从最底层的socket层面实现一下websocket还是很有意思的。 要处理的问题包括:

  1. 握手协议
  2. 数据帧头处理
  3. 数据实际长度和长数据
  4. 编码问题

精华都在下面这张图:

enter image description here

var net = require('net');
var crypto = require('crypto');
var events = require("events");

exports.createServer = function (onConnect) { var server = net.createServer(function (con) { var client = new WsClient(con); onConnect(client); });

this.listen = function(port, onlisten){ server.listen(port, onlisten); }

return this; }

function WsClient(con) { var _t = this; var registered = false; var dataLength = 0; var recevBuf = null; var mask = null; var recevHead = null;

var eve = new events.EventEmitter();

this.on = function (_event, _listenter) { eve.on(_event, _listenter); return this; };

con.on(‘data’, function (data) { if (!registered) { _shakeHand(data, con); registered = true; eve.emit(‘connect’, con) } else { _readData(data); } });

con.on(‘end’, function () { eve.emit(‘close’); });

function _shakeHand(data) {

var data = data.toString().split('\r\n');

var header = {};
for (var i = 0; i < data.length; i++) {
    var index = data[i].indexOf(':');
    if (index > 0) {
        var key = data[i].substr(0, index);
        var value = data[i].substr(index + 1);
        header[key.trim()] = value.trim();
    }
}

var shasum = crypto.createHash('sha1');
var m_Magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
shasum.update(header['Sec-WebSocket-Key'] + m_Magic, 'ascii');

var respond = 'HTTP/1.1 101 Web Socket Protocol Handshake\r\n';
respond += 'Upgrade: ' + header['Upgrade'] + '\r\n';
respond += 'Connection: ' + header['Connection'] + '\r\n';
respond += 'Sec-WebSocket-Accept: ' + shasum.digest('base64') + '\r\n';
respond += 'WebSocket-Origin: ' + header['Origin'] + '\r\n';
respond += 'WebSocket-Location: ' + header['Host'] + '\r\n';
respond += '\r\n';

con.write(respond, 'ascii');

}

function _readFrameHead(data) { if (dataLength == 0) { recevHead = data[0];

    //获取实际数据Payload长度
    var length = data[1] & 0x7F;
    //是否使用掩码
    var hasMarsk = (data[1] & 0x80) == 0x80;

    var marskIndex = 2;
    var dataIndex = 6;
    if (length == 126) {
        marskIndex = 4;
        dataIndex = 8;
        length = data.readUInt16BE(2);
    } else if (length == 127) {
        marskIndex = 10;
        dataIndex = 14;
        length = data.readUInt32BE(6);
    }
    dataLength = length;

    if (hasMarsk) {
        recevBuf = new Buffer(length);
        mask = data.slice(marskIndex, marskIndex + 4);
    } else {
        recevBuf = '';
        mask = null;
    }
    return dataIndex;
} else {
    return 0;
}

}

function _readData(data) {

var dataIndex = _readFrameHead(data);

if (mask) {
    var i = recevBuf.length - dataLength;
    var l = Math.min(recevBuf.length, data.length - dataIndex + i);
    dataLength -= data.length - dataIndex;

    for (; i < l; i++) {
        recevBuf[i] = data[dataIndex++] ^ mask[i % 4];
    }
} else {
    recevBuf += data.toString('utf8', dataIndex);
    dataLength -= data.length - dataIndex;
}

if (dataLength == 0) {
    if (recevBuf.length > 0) {
        var out = recevBuf.toString();
        eve.emit('data', out);
    } else {
        eve.emit('close');
    }
}

}

this.send = function (message) { var length = Buffer.byteLength(message); var dataIndex = length > 65535 ? 10 : length > 125 ? 4 : 2;

var buf = new Buffer(dataIndex + length);
buf[0] = recevHead;
if (length > 65535) {
    buf[1] = 127;
    buf.writeUInt32BE(0, 2);
    buf.writeUInt32BE(length, 6);
} else if (length > 125) {
    buf[1] = 126;
    buf.writeUInt16BE(length, 2);
} else {
    buf.writeUInt8(length, 1);
}

buf.write(message, dataIndex);

con.write(buf, 'utf8', function () {

});

} }

OpCode = { Text: 1, Binary: 2, Close: 8, Ping: 9, Pong: 10 }


4 回复

WebSocket协议的服务端Node.js实现

尽管有许多成熟的WebSocket框架,但自己从底层的Socket层面实现一个WebSocket服务端仍然非常有趣。在这个过程中,我们需要处理以下几个关键问题:

  1. 握手协议:客户端发送握手请求时,服务器需要验证并返回握手响应。
  2. 数据帧头处理:解析WebSocket数据帧的头部信息,包括数据长度、掩码等。
  3. 数据实际长度和长数据:处理不同长度的数据,包括短数据和长数据的传输。
  4. 编码问题:确保数据在传输过程中的正确编码。

下面是基于Node.js的WebSocket服务端实现代码:

var net = require('net');
var crypto = require('crypto');
var events = require('events');

exports.createServer = function(onConnect) {
    var server = net.createServer(function(con) {
        var client = new WsClient(con);
        onConnect(client);
    });

    this.listen = function(port, onlisten) {
        server.listen(port, onlisten);
    };

    return this;
};

function WsClient(con) {
    var _t = this;
    var registered = false;
    var dataLength = 0;
    var recevBuf = null;
    var mask = null;
    var recevHead = null;

    var eve = new events.EventEmitter();

    this.on = function(_event, _listener) {
        eve.on(_event, _listener);
        return this;
    };

    con.on('data', function(data) {
        if (!registered) {
            _shakeHand(data, con);
            registered = true;
            eve.emit('connect', con);
        } else {
            _readData(data);
        }
    });

    con.on('end', function() {
        eve.emit('close');
    });

    function _shakeHand(data) {
        var headers = parseHeaders(data);
        var key = headers['Sec-WebSocket-Key'];
        var shasum = crypto.createHash('sha1');
        var magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
        shasum.update(key + magic, 'ascii');

        var response = 'HTTP/1.1 101 Switching Protocols\r\n';
        response += 'Upgrade: websocket\r\n';
        response += 'Connection: Upgrade\r\n';
        response += 'Sec-WebSocket-Accept: ' + shasum.digest('base64') + '\r\n';
        response += '\r\n';

        con.write(response, 'ascii');
    }

    function parseHeaders(data) {
        var lines = data.toString().split('\r\n');
        var headers = {};
        for (var i = 0; i < lines.length; i++) {
            var line = lines[i];
            if (line.indexOf(':') > 0) {
                var [key, value] = line.split(':').map(s => s.trim());
                headers[key] = value;
            }
        }
        return headers;
    }

    function _readFrameHead(data) {
        if (dataLength === 0) {
            recevHead = data[0];

            var length = data[1] & 0x7F;
            var hasMask = (data[1] & 0x80) === 0x80;

            var maskIndex = 2;
            var dataIndex = 6;
            if (length === 126) {
                maskIndex = 4;
                dataIndex = 8;
                length = data.readUInt16BE(2);
            } else if (length === 127) {
                maskIndex = 10;
                dataIndex = 14;
                length = data.readUInt32BE(6);
            }
            dataLength = length;

            if (hasMask) {
                recevBuf = new Buffer(length);
                mask = data.slice(maskIndex, maskIndex + 4);
            } else {
                recevBuf = '';
                mask = null;
            }
            return dataIndex;
        } else {
            return 0;
        }
    }

    function _readData(data) {
        var dataIndex = _readFrameHead(data);

        if (mask) {
            var i = recevBuf.length - dataLength;
            var l = Math.min(recevBuf.length, data.length - dataIndex + i);
            dataLength -= data.length - dataIndex;

            for (; i < l; i++) {
                recevBuf[i] = data[dataIndex++] ^ mask[i % 4];
            }
        } else {
            recevBuf += data.toString('utf8', dataIndex);
            dataLength -= data.length - dataIndex;
        }

        if (dataLength === 0) {
            if (recevBuf.length > 0) {
                var out = recevBuf.toString();
                eve.emit('data', out);
            } else {
                eve.emit('close');
            }
        }
    }

    this.send = function(message) {
        var length = Buffer.byteLength(message);
        var dataIndex = length > 65535 ? 10 : length > 125 ? 4 : 2;

        var buf = new Buffer(dataIndex + length);
        buf[0] = recevHead;
        if (length > 65535) {
            buf[1] = 127;
            buf.writeUInt32BE(0, 2);
            buf.writeUInt32BE(length, 6);
        } else if (length > 125) {
            buf[1] = 126;
            buf.writeUInt16BE(length, 2);
        } else {
            buf.writeUInt8(length, 1);
        }

        buf.write(message, dataIndex);

        con.write(buf, 'utf8', function() {});
    };
}

const OpCode = {
    Text: 1,
    Binary: 2,
    Close: 8,
    Ping: 9,
    Pong: 10
};

解释

  1. 握手协议

    • 服务器接收到客户端的握手请求后,会解析请求中的Sec-WebSocket-Key,并生成对应的SHA1哈希值。
    • 然后构造握手响应,并发送给客户端。
  2. 数据帧头处理

    • 数据帧头包含控制位(如FIN、RSV1-3)和操作码(如文本、二进制等)。
    • 还包括数据长度字段,用于标识实际数据的长度。
    • 如果数据被掩码,则需要解密数据。
  3. 数据实际长度和长数据

    • 处理不同长度的数据,包括短数据(小于等于125字节)、中等长度数据(126字节)和长数据(大于65535字节)。
  4. 编码问题

    • 确保数据在传输过程中正确编码,例如将字符串转换为Buffer,或进行掩码处理。

以上代码实现了WebSocket服务端的基本功能,可以根据具体需求进一步扩展和完善。


如果是show代码 问题相当严重

你根本没管理data的长度 原始socket发出的data事件 data长度是不确定的

写服务端代码 你无法信任你的客户端发送什么 发送多少 什么时候发送

只是拿协议练下手,实际应用中还有很多安全问题要考虑,工作项目中当然直接用socket.io

要实现一个基本的 WebSocket 服务端,我们需要处理握手协议、数据帧处理以及编码问题。以下是一个简单的 WebSocket 服务器的 Node.js 实现示例:

const net = require('net');
const crypto = require('crypto');

const server = net.createServer(handleConnection);

server.listen(8080, () => {
    console.log('Server is listening on port 8080');
});

function handleConnection(socket) {
    socket.on('data', data => {
        if (!socket.handshaken) {
            handshake(socket, data);
        } else {
            readData(socket, data);
        }
    });

    socket.on('end', () => {
        console.log('Client disconnected');
    });
}

function handshake(socket, data) {
    const headers = parseHeaders(data);
    const key = headers['sec-websocket-key'];
    const acceptKey = crypto.createHash('sha1').update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest('base64');

    const response = [
        'HTTP/1.1 101 Switching Protocols',
        'Upgrade: websocket',
        'Connection: Upgrade',
        `Sec-WebSocket-Accept: ${acceptKey}`,
        '',
    ].join('\r\n');

    socket.write(response);
    socket.handshaken = true;
}

function readData(socket, data) {
    let offset = 0;
    while (offset < data.length) {
        const opcode = data[offset] & 0x0F;
        const fin = data[offset] & 0x80;
        const masked = data[offset + 1] & 0x80;

        const payloadLength = data[offset + 1] & 0x7F;
        let lengthOffset = 2;

        if (payloadLength === 126) {
            lengthOffset = 4;
        } else if (payloadLength === 127) {
            lengthOffset = 10;
        }

        const payloadLen = payloadLength === 126 ? data.readUInt16BE(offset + 2) : data.readUInt32BE(offset + 2);

        let mask = [];
        if (masked) {
            mask = data.slice(offset + lengthOffset, offset + lengthOffset + 4);
            lengthOffset += 4;
        }

        const payload = data.slice(offset + lengthOffset, offset + lengthOffset + payloadLen);

        if (masked) {
            for (let i = 0; i < payloadLen; i++) {
                payload[i] ^= mask[i % 4];
            }
        }

        const message = payload.toString();
        console.log(`Received message: ${message}`);

        offset += lengthOffset + payloadLen;
    }
}

这个示例中,我们首先创建了一个 TCP 服务器,并处理了连接和数据接收。当接收到握手请求时,我们解析头部并生成相应的握手响应。对于后续的数据帧,我们解析它们并打印出来。

注意,这只是一个基础实现,没有处理所有可能的情况(例如错误处理、不同类型的帧等)。

回到顶部