简介
WebSocket是一种在单个TCP连接上进行全双工通信的协议。其使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。说明:Websocket是一个持久化的协议,HTTP是非持久的协议。
背景
很多网站为了实现推送技术,所用的技术都是轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。而比较新的技术去做轮询的效果是Comet。这种技术虽然可以双向通信,但依然需要反复发出请求。而且在Comet中,普遍采用的长链接,也会消耗服务器资源。
还有一种方式是使用流技术:就是在客户端的页面使用一个隐藏的窗口向服务端发出一个长连接的请求。服务器端接到这个请求后作出回应并不断更新连接状态以保证客户端和服务 器端的连接不过期。通过这种机制可以将服务器端的信息源源不断地推向客户端。这种机制在用户体验上有一点问题,需要针对不同的浏览器设计不同的方案来改进 用户体验,同时这种机制在并发比较大的情况下,对服务器端的资源是一个极大的考验。
在这种情况下,HTML5定义了WebSocket协议,是HTTP协议上的一种补充,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。
WebSocket优点
-
较少的控制开销。在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。在不包含扩展的情况下,对于服务器到客户端的内容,此头部大小只有2至10字节(和数据包长度有关);对于客户端到服务器的内容,此头部还需要加上额外的4字节的掩码。相对于HTTP请求每次都要携带完整的头部,此项开销显著减少了。
-
更强的实时性。由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少;即使是和Comet等类似的长轮询比较,其也能在短时间内更多次地传递数据。
-
保持连接状态。与HTTP不同的是,Websocket需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。
-
更好的二进制支持。Websocket定义了二进制帧,相对HTTP,可以更轻松地处理二进制内容。
-
可以支持扩展。Websocket定义了扩展,用户可以扩展协议、实现部分自定义的子协议。如部分浏览器支持压缩等。
-
更好的压缩效果。相对于HTTP压缩,Websocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率。
特点
WebSocket 是独立的、创建在 TCP 上的协议。
Websocket 默认端口是80和443,通过HTTP/1.1 协议的101状态码进行握手。因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
没有同源限制,客户端可以和任意服务器通信。协议标识符是ws(若加密则是wss)。
为了创建Websocket连接,需要通过浏览器发出请求,之后服务器进行回应,这个过程通常称为“握手”(handshaking)。
实践
下面以NodeJS为例,演示WebSocket的使用手法:
/// server
let grabers = new Map();
let WebSocket = require('ws');
const ws_s = new WebSocket.Server({ port:11011 });
ws_s.on('connection', function (ws, req) {
ws.upgradeReq = req;
let graber = decodeURIComponent(ws.upgradeReq.url).substring(1); //兼容url中含有中文字符的情况
grabers.set(graber, { ws: ws, id: graber }); //记录所有握手连接
console.logColor(logging.Green
, `ws_s:: server (${req.connection.remoteAddress}:${req.connection.remotePort})(${graber}) connection ok.`
);
ws.on('message', function (message) {
try {
message = JSON.parse(message);
} catch (err) {
console.error(`Cannot parse message: ${err}`);
ws.close(1008, 'Cannot parse');
return;
}
switch(message.type)
{
case 'notice':
console.log(`ws_s:: notice from ${message.src}`);
break;
case 'ping':
console.log(`ws_s:: ping from ${message.src}`);
break;
case 'grab':
console.log(`ws_s:: ${message.src} apply grab authority of ${message.dst}`);
if (grabers.has(message.dst)) {
grabers.get(message.dst).ws.send(JSON.stringify(
createWSSMessage(100, content, message)
));
}
default:
console.error(`ws_s:: server unsupported message type: ${message.type}`);
//ws.close(1008, 'Unsupported message type');
}
});
ws.on('close', function (code, reason) {
console.logColor(logging.Yellow, `ws_s:: server connection closed: ${code} - ${reason}`);
});
ws.on('error', function (error) {
console.error(`ws_s:: server connection error: ${error}`);
ws.close(1006 /* abnormal closure */, error);
});
});
/// client
var wsc;
function wsc_open(name) {
wsc = new WebSocket('ws://192.168.1.1:11011/' + encodeURIComponent(name), "hb");
//open event
wsc.onopen = function() {
console.log("open websocket ok...");
};
//close event
wsc.onclose = function() {
console.log("close websocket ok...");
};
// 响应message事件:
wsc.onmessage = function(msg) {
// console.log(msg);
console.log(msg.data);
let res = JSON.parse(msg.data);
switch(res.code)
{
case 100:
//...
break;
case 200:
//...
break;
default:
;
}
};
}
function wsc_send(content) {
wsc.send(JSON.stringify(content));
}
function wsc_close() {
wsc.close();
}
参考资料
https://www.ruanyifeng.com/blog/2017/05/websocket.html
https://blog.csdn.net/weberhuangxingbo/article/details/105115942