websocket协议的服务端Nodejs实现
websocket协议的服务端Nodejs实现
虽然已经有很完善的websocket框架,但是从最底层的socket层面实现一下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, _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
}
WebSocket协议的服务端Node.js实现
尽管有许多成熟的WebSocket框架,但自己从底层的Socket层面实现一个WebSocket服务端仍然非常有趣。在这个过程中,我们需要处理以下几个关键问题:
- 握手协议:客户端发送握手请求时,服务器需要验证并返回握手响应。
- 数据帧头处理:解析WebSocket数据帧的头部信息,包括数据长度、掩码等。
- 数据实际长度和长数据:处理不同长度的数据,包括短数据和长数据的传输。
- 编码问题:确保数据在传输过程中的正确编码。
下面是基于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
};
解释
-
握手协议:
- 服务器接收到客户端的握手请求后,会解析请求中的
Sec-WebSocket-Key
,并生成对应的SHA1哈希值。 - 然后构造握手响应,并发送给客户端。
- 服务器接收到客户端的握手请求后,会解析请求中的
-
数据帧头处理:
- 数据帧头包含控制位(如FIN、RSV1-3)和操作码(如文本、二进制等)。
- 还包括数据长度字段,用于标识实际数据的长度。
- 如果数据被掩码,则需要解密数据。
-
数据实际长度和长数据:
- 处理不同长度的数据,包括短数据(小于等于125字节)、中等长度数据(126字节)和长数据(大于65535字节)。
-
编码问题:
- 确保数据在传输过程中正确编码,例如将字符串转换为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 服务器,并处理了连接和数据接收。当接收到握手请求时,我们解析头部并生成相应的握手响应。对于后续的数据帧,我们解析它们并打印出来。
注意,这只是一个基础实现,没有处理所有可能的情况(例如错误处理、不同类型的帧等)。