HTML5的websocket 协议详细教程

Websocket是html5提出的一个协议规范,参考rfc6455。

websocket约定了一个通信的规范,通过一个握手的机制,客户端(浏览器)和服务器(webserver)之间能建立一个类似tcp的连接,从而方便c-s之间的通信。在websocket出现之前,web交互一般是基于http协议的短连接或者长连接。

WebSocket是为解决客户端与服务端实时通信而产生的技术。websocket协议本质上是一个基于tcp的协议,是先通过HTTP/HTTPS协议发起一条特殊的http请求进行握手后创建一个用于交换数据的TCP连接,此后服务端与客户端通过此TCP连接进行实时通信。

注意:此时不再需要原HTTP协议的参与了

2. websocket的优点

以前web server实现推送技术或者即时通讯,用的都是轮询(polling),在特点的时间间隔(比如1秒钟)由浏览器自动发出请求,将服务器的消息主动的拉回来,在这种情况下,我们需要不断的向服务器发送请求,然而HTTP request 的header是非常长的,里面包含的数据可能只是一个很小的值,这样会占用很多的带宽和服务器资源。

而最比较新的技术去做轮询的效果是Comet – 用了AJAX。但这种技术虽然可达到全双工通信,但依然需要发出请求(reuqest)。

WebSocket API最伟大之处在于服务器和客户端可以在给定的时间范围内的任意时刻,相互推送信息。 浏览器和服务器只需要要做一个握手的动作,在建立连接之后,服务器可以主动传送数据给客户端,客户端也可以随时向服务器发送数据。 此外,服务器与客户端之间交换的标头信息很小。

WebSocket并不限于以Ajax(或XHR)方式通信,因为Ajax技术需要客户端发起请求,而WebSocket服务器和客户端可以彼此相互推送信息;

因此从服务器角度来说,websocket有以下好处

  1. 节省每次请求的header
    http的header一般有几十字节
  2. Server Push
    服务器可以主动传送数据给客户端

~~~

1. 实时通信 #

1.1 轮询 #

浏览器周期性的发出请求,如果服务器没有新数据需要发送就返回以空响应,这种方法问题很大,很快就被淘汰

  1. 大量无意义的请求造成网络压力
  2. 请求周期的限制不能及时地获得最新数据
var xhr = new XMLHttpRequest();
  setInterval(function(){
      xhr.open('GET','/data',true);
      xhr.onreadystatechange = function(){
          if(xhr.readyState == 4 && xhr.status == 200){
            document.querySelector('#content').innerHTML = xhr.responseText;
          }
      }
      xhr.send();
  },1000);

1.2 长轮询 #

长轮询是在打开一条连接以后保持连接,等待服务器推送来数据再关闭连接
然后浏览器再发出新的请求,这能更好地管理请求数量,也能及时得到更新后的数据

function send() {
        var xhr = new XMLHttpRequest();
        xhr.open('GET', '/data', true);
        xhr.onreadystatechange = function () {
            if (xhr.readyState == 4 && xhr.status == 200) {
                document.querySelector('#content').innerHTML = xhr.responseText;
                send();
            }
        }
        xhr.send();
    }

send();

2. WebSocket #

WebSocket是HTML5开始提供的一种浏览器与服务器间进行全双工通讯的网络技术
使用WebSocket,浏览器和服务器只需要要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道,两者之间就直接可以数据互相传送

  • 节省资源:互相沟通的Header是很小的-大概只有 2 Bytes。
  • 推送信息:不需要客户端请求,服务器可以主动传送数据给客户端

4. 对比 #

5. WebSocket实现 #

5.1 WebSocket服务器 #

var WebSocketServer = require('ws').Server;
var wss = new WebSocketServer({port: 8080});

//监听客户端的请求
wss.on('connection', function (ws) {
    //监听客户端的消息
    ws.on('message', function(message) {
        console.log('received: %s', message);
        //向客户端发消息
        ws.send('server hello');
    });
});

5.2 Node客户端 #

var WebSocket = require('ws');
var ws = new WebSocket('ws://localhost:8080/');

ws.on('open', function open() {
    ws.send('hello world!');
});

ws.on('message', function(data, flags) {
    console.log(data);
    console.log('message ',data);
});

5.3 网页客户端 #

//创建socket对象
    var socket = new WebSocket('ws://localhost:8080/');
    //监听连接事件
    socket.onopen = function(){
        //向服务器发送消息
        socket.send('hello server');
    }
    //监听服务器端消息
    socket.onmessage = function(event){
        //输出服务器返回的消息
        console.log(event.data);
    }

~

websocket逻辑

与http协议不同的请求/响应模式不同,Websocket在建立连接之前有一个Handshake(Opening Handshake)过程,在关闭连接前也有一个Handshake(Closing Handshake)过程,建立连接之后,双方即可双向通信。
在websocket协议发展过程中前前后后就出现了多个版本的握手协议,这里分情况说明一下:

  • 基于flash的握手协议
    使用场景是IE的多数版本,因为IE的多数版本不都不支持WebSocket协议,以及FF、CHROME等浏览器的低版本,还没有原生的支持WebSocket。此处,server唯一要做的,就是准备一个WebSocket-Location域给client,没有加密,可靠性很差。

客户端请求:

GET /ls HTTP/1.1
Upgrade: WebSocket
Connection: Upgrade
Host: www.qixing318.com
Origin: http://www.qixing318.com

服务器返回:

HTTP/1.1 101 Web Socket Protocol Handshake
Upgrade: WebSocket
Connection: Upgrade
WebSocket-Origin: http://www.qixing318.com
WebSocket-Location: ws://www.qixing318.com/ls
  • 基于md5加密方式的握手协议
    客户端请求:

    GET /demo HTTP/1.1
    Host: example.com
    Connection: Upgrade
    Sec-WebSocket-Key2:
    Upgrade: WebSocket
    Sec-WebSocket-Key1:

    Origin: http://www.qixing318.com
    [8-byte security key]

服务端返回:

HTTP/1.1 101 WebSocket Protocol Handshake
Upgrade: WebSocket
Connection: Upgrade
WebSocket-Origin: http://www.qixing318.com
WebSocket-Location: ws://example.com/demo
[16-byte hash response]

其中 Sec-WebSocket-Key1,Sec-WebSocket-Key2 和 [8-byte security key] 这几个头信息是web server用来生成应答信息的来源,依据 draft-hixie-thewebsocketprotocol-76 草案的定义。
web server基于以下的算法来产生正确的应答信息:

1. 逐个字符读取 Sec-WebSocket-Key1 头信息中的值,将数值型字符连接到一起放到一个临时字符串里,同时统计所有空格的数量;
2. 将在第(1)步里生成的数字字符串转换成一个整型数字,然后除以第(1)步里统计出来的空格数量,将得到的浮点数转换成整数型;
3. 将第(2)步里生成的整型值转换为符合网络传输的网络字节数组;
4. 对 Sec-WebSocket-Key2 头信息同样进行第(1)到第(3)步的操作,得到另外一个网络字节数组;
5. 将 [8-byte security key] 和在第(3)、(4)步里生成的网络字节数组合并成一个16字节的数组;
6. 对第(5)步生成的字节数组使用MD5算法生成一个哈希值,这个哈希值就作为安全密钥返回给客户端,以表明服务器端获取了客户端的请求,同意创建websocket连接
  • 基于sha加密方式的握手协议
    也是目前见的最多的一种方式,这里的版本号目前是需要13以上的版本。
    客户端请求:

    GET /ls HTTP/1.1
    Upgrade: websocket
    Connection: Upgrade
    Host: www.qixing318.com
    Sec-WebSocket-Origin: http://www.qixing318.com
    Sec-WebSocket-Key: 2SCVXUeP9cTjV+0mWB8J6A==
    Sec-WebSocket-Version: 13

服务器返回:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: mLDKNeBNWz6T9SxU+o0Fy/HgeSw=

其中 server就是把客户端上报的key拼上一段GUID( “258EAFA5-E914-47DA-95CA-C5AB0DC85B11″),拿这个字符串做SHA-1 hash计算,然后再把得到的结果通过base64加密,最后再返回给客户端。

下面是一段代码

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

~~~

未经允许不得转载:WEB前端开发 » HTML5的websocket 协议详细教程

赞 (0)