websocket协议详解
关于websocket的协议是用来干嘛的,请参考其他文章。
websocket关键词
html5协议,实时,全双工通信,长连接
websocket比传统http的好处
1.客户端与服务端只建立一个tcp连接,可以使用更少的连接
2.websocket的服务端可以将数据推送到客户端,如实时将证券信息反馈到客户端(这个很关键),实时天气数据,比http请求响应模式更灵活
3.更轻量的协议头,减少数据传送量
数据帧格式
下图为手工打造的数据帧格式
/**
* 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);
};