websocket
# websocket 原理
Websocket 是应用层第七层上的一个应用层协议,它必须依赖 HTTP 协议进行一次握手 ,握手成功后,数据就直接从 TCP 通道传输,与 HTTP 无关了。
即:websocket 分为握手和数据传输阶段,即进行了 HTTP 握手 + 双工的 TCP 连接。
参考:https://www.jianshu.com/p/3444ea70b6cb (opens new window) https://juejin.cn/post/6844904002228846599#heading-3 (opens new window)
# 握手阶段
客户端发送消息:客户端首先会向服务端发送一个 HTTP 请求,包含一个 Upgrade 请求头来告知服务端客户端想要建立一个 WebSocket 连接。
服务端返回消息:响应行(General)中可以看到状态码 status code 是 101 Switching Protocols, 表示该连接已经从 HTTP 协议转换为 WebSocket 通信协议。 转换成功之后,该连接并没有中断,而是建立了一个全双工通信,后续发送和接收消息都会走这个连接通道。
注意,请求头中有个 Sec-WebSocket-Key 字段,和相应头中的 Sec-WebSocket-Accept 是配套对应的,它的作用是提供了基本的防护,比如恶意的连接或者无效的连接。Sec-WebSocket-Key 是客户端随机生成的一个 base64 编码,服务器会使用这个编码,并根据一个固定的算法:
其中 GUID 字符串是 RFC6455 官方定义的一个固定字符串,不得修改。
客户端拿到服务端响应的 Sec-WebSocket-Accept 后,会拿自己之前生成的 Sec-WebSocket-Key 用相同算法算一次,如果匹配,则握手成功。然后判断 HTTP Response 状态码是否为 101(切换协议),如果是,则建立连接,大功告成。
握手阶段详解:若要实现 WebSocket 协议,首先需要浏览器主动发起一个 HTTP 请求。
这个请求头包含“Upgrade”字段,内容为“websocket”(注:upgrade 字段用于改变 HTTP 协议版本或换用其他协议,这里显然是换用了 websocket 协议),还有一个最重要的字段“Sec-WebSocket-Key”,这是一个随机的经过 base64 编码的字符串,像密钥一样用于服务器和客户端的握手过程。一旦服务器君接收到来自客户端的 upgrade 请求,便会将请求头中的“Sec-WebSocket-Key”字段提取出来,追加一个固定的“魔串”:258EAFA5-E914-47DA-95CA-C5AB0DC85B11,并进行 SHA-1 加密,然后再次经过 base64 编码生成一个新的 key,作为响应头中的“Sec-WebSocket-Accept”字段的内容返回给浏览器。一旦浏览器接收到来自服务器的响应,便会解析响应中的“Sec-WebSocket-Accept”字段,与自己加密编码后的串进行匹配,一旦匹配成功,便有建立连接的可能了(因为还依赖许多其他因素)。
这是一个基本的 Client 请求头:(我只写了关键的几个字段)
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: ************==
Sec-WebSocket-Version: **
2
3
4
Server 正确接收后,会返回一个响应头:(同样只有关键的)
Upgrade:websocket
Connnection: Upgrade
Sec-WebSocket-Accept: ******************
2
3
4
这表示双方握手成功了,之后就是全双工的通信。
# 传输阶段
Websocket 的数据传输是 frame 形式传输的,比如会将一条消息分为几个 frame,按照先后顺序传输出去。这样做会有几个好处:
大数据的传输可以分片传输,不用考虑到数据大小导致的长度标志位不足够的情况。
和 http 的 chunk 一样,可以边生成数据边传递消息,即提高传输效率。
# HTML5 WebSocket
心跳检测和断线重连
# WebSocket 简介
- 概念
WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
特点
(1)建立在 TCP 协议之上,服务器端的实现比较容易。
(2)与 HTTP 协议有着良好的兼容性。默认端口也是 80 和 443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
(3)数据格式比较轻量,性能开销小,通信高效。
(4)可以发送文本,也可以发送二进制数据。
(5)没有同源限制,客户端可以与任意服务器通信。
(6)协议标识符是 ws(如果加密,则为 wss),服务器网址就是 URL。
- 语法:
var ws = new WebSocket(url, [protocol] );
url, 指定连接的 URL、 protocol 是可选的,指定了可接受的子协议。
# WebSocket 属性
- readyState
readyState 属性返回实例对象的当前状态,共有四种。
- CONNECTING:值为 0,表示正在连接。
- OPEN:值为 1,表示连接成功,可以通信了。
- CLOSING:值为 2,表示连接正在关闭。
- CLOSED:值为 3,表示连接已经关闭,或者打开连接失败。
switch (ws.readyState) {
case WebSocket.CONNECTING:
// do something
break;
case WebSocket.OPEN:
// do something
break;
case WebSocket.CLOSING:
// do something
break;
case WebSocket.CLOSED:
// do something
break;
default:
// this never happens
break;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- bufferedAmount
实例对象的 bufferedAmount 属性,表示还有多少字节的二进制数据没有发送出去。它可以用来判断发送是否结束。
var data = new ArrayBuffer(10000000);
socket.send(data);
if (socket.bufferedAmount === 0) {
// 发送完毕
} else {
// 发送还没结束
}
2
3
4
5
6
7
8
# WebSocket 事件
- open 连接建立时触发
范例
ws.onopen = function() {
ws.send("Hello Server!");
};
2
3
如果要指定多个回调函数,可以使用 addEventListener 方法。
ws.addEventListener("open", function(event) {
ws.send("Hello Server!");
});
2
3
- message 客户端接收服务端数据时触发
ws.onmessage = function(event) {
var data = event.data;
// 处理数据
};
ws.addEventListener("message", function(event) {
var data = event.data;
// 处理数据
});
2
3
4
5
6
7
8
9
注意,服务器数据可能是文本,也可能是二进制数据(blob 对象或 Arraybuffer 对象)。
ws.onmessage = function(event) {
if (typeof event.data === String) {
console.log("Received data string");
}
if (event.data instanceof ArrayBuffer) {
var buffer = event.data;
console.log("Received arraybuffer");
}
};
2
3
4
5
6
7
8
9
10
除了动态判断收到的数据类型,也可以使用 binaryType 属性,显式指定收到的二进制数据类型。
// 收到的是 blob 数据
ws.binaryType = "blob";
ws.onmessage = function(e) {
console.log(e.data.size);
};
// 收到的是 ArrayBuffer 数据
ws.binaryType = "arraybuffer";
ws.onmessage = function(e) {
console.log(e.data.byteLength);
};
2
3
4
5
6
7
8
9
10
11
- error 通信发生错误时触发
实例对象的 onerror 属性,用于指定报错时的回调函数。
socket.onerror = function(event) {
// handle error event
};
socket.addEventListener("error", function(event) {
// handle error event
});
2
3
4
5
6
7
- close 连接关闭时触发
ws.onclose = function(event) {
var code = event.code;
var reason = event.reason;
var wasClean = event.wasClean;
// handle close event
};
ws.addEventListener("close", function(event) {
var code = event.code;
var reason = event.reason;
var wasClean = event.wasClean;
// handle close event
});
2
3
4
5
6
7
8
9
10
11
12
13
# WebSocket 方法
- send() 使用连接发送数据
发送文本的例子。
ws.send('your message');
发送 Blob 对象的例子。
var file = document.querySelector('input[type="file"]').files[0];
ws.send(file);
2
发送 ArrayBuffer 对象的例子。
// Sending canvas ImageData as ArrayBuffer
var img = canvas_context.getImageData(0, 0, 400, 320);
var binary = new Uint8Array(img.data.length);
for (var i = 0; i < img.data.length; i++) {
binary[i] = img.data[i];
}
ws.send(binary.buffer);
2
3
4
5
6
7
- close() 关闭连接
# WebSocket 心跳检测和断线重连
断线重连引入心跳的原因
WebSocket 最大的优势就是能够保持前后端消息的长连接,但是在某些情况下,长连接失效并不会得到及时的反馈,前端并不知道连接已断开。例如用户网络断开,并不会触发 websocket 的任何事件函数,这个时候如果发送消息,消息便无法发送出去,浏览器会立刻或者一定短时间后(不同浏览器或者浏览器版本可能表现不同)触发 onclose 函数。
为了避免这种情况,保证连接的稳定性,前端需要进行一定的优化处理,一般采用的方案就是心跳重连。前后端约定,前端按一定间隔发送一个心跳包,后端接收到心跳包后返回一个响应包,告知前端连接正常。如果一定时间内未接收到消息,则认为连接断开,前端进行重连。
为什么叫心跳包呢
它就像心跳一样每隔固定的时间发一次,来告诉服务器,我还活着。
心跳机制是?
心跳机制是每隔一段时间会向服务器发送一个数据包,告诉服务器自己还活着,同时客户端会确认服务器端是否还活着,如果还活着的话,就会回传一个数据包给客户端来确定服务器端也还活着,否则的话,有可能是网络断开连接了。需要重连~
websocket 连接断开有以下两证情况:
- 前端断开
在使用 websocket 过程中,可能会出现网络断开的情况,比如信号不好,或者网络临时关闭,这时候 websocket 的连接已经断开,而不同浏览器有不同的机制,触发 onclose 的时机也不同,并不会理想执行 websocket 的 onclose 方法,我们无法知道是否断开连接,也就无法进行重连操作。
- 后端断开
如果后端因为一些情况需要断开 ws,在可控情况下,会下发一个断连的消息通知,之后才会断开,我们便会重连。 如果因为一些异常断开了连接,我们是不会感应到的,所以如果我们发送了心跳一定时间之后,后端既没有返回心跳响应消息,前端又没有收到任何其他消息的话,我们就能断定后端主动断开了。
因此需要一种机制来检测客户端和服务端是否处于正常连接的状态。通过在指定时间间隔发送心跳包来保证连接正常,如果连接出现问题,就需要手动触发 onclose 事件,这时候便可进行重连操作。因此 websocket 心跳重连就应运而生。
心跳机制参考 1 https://www.cnblogs.com/1wen/p/5808276.html (opens new window)
心跳机制参考 2 https://www.cnblogs.com/buxiugangzi/p/11379883.html
# WebSocket 与 Socket 的关系
Socket 其实并不是一个协议,而是为了方便使用 TCP 或 UDP 而抽象出来的一层,是位于应用层和传输控制层之间的一组接口。
Socket 是应用层与 TCP/IP 协议族通信的中间软件抽象层,它是一组接口。在设计模式中,
Socket 其实就是一个门面模式,它把复杂的 TCP/IP 协议族隐藏在 Socket 接口后面,对用户来说,一组简单的接口就是全部,让 Socket 去组织数据,以符合指定的协议。
当两台主机通信时,必须通过 Socket 连接,Socket 则利用 TCP/IP 协议建立 TCP 连接。TCP 连接则更依靠于底层的 IP 协议,IP 协议的连接则依赖于链路层等更低层次。
WebSocket 与 Socket 区别:
Socket 是传输控制层协议,WebSocket 是应用层协议。
# websocket 和 http 的区别和联系
相同点:
都是建立在 TCP 之上,通过 TCP 协议来传输数据。
都是可靠性传输协议。
都是应用层协议。
不同点:
HTTP 主要用来一问一答的方式交换信息;WebSocket 让通信双方都可以主动去交换信息(双向通信协议)。
HTTP2 虽然支持服务器推送资源到客户端,但那不是应用程序可以感知的,主要是让浏览器(用户代理)提前缓存静态资源,所以我们不能指望 HTTP2 可以像 WebSocket 建立双向实时通信。
握手次数不一样,在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就可以建立持久性的连接,并进行双向数据传输。http 需要三次握手
为了让服务器可以推送消息给客户端,使用了一个“偏方”。它就是 SSE(Server Sent Event)。
SSE 简单说就是,借助 http 协议支持分块传输这一特性。在响应报文里,设置 Content-Type: text/event-stream,如此一来响应报文实体就可以多次从服务器返回给客户端,只需要约定好边界就好,比如 SSE 的边界就是以空行作为消息分隔符。
# 应用场景
即时聊天通信
游戏
实时统计(图表)
系统即时提醒
实时地图位置