Http协议的痛点
1. http的伸缩性
在旧的架构中,http-1.0对从服务器请求单个文档已经足够,但是随之Web的发展,要求页面开始包含更多的交互性且缩短浏览器请求和服务器响应时间。
http-1.0中,每个服务器请求需要一个单独的连接,这种方法没有太好的伸缩性(scalability)。
http的下一个修订版本http-1.1增加了可重用连接。http-1.1通过减少客户端到服务器的连接数量,降低了请求的延迟。
2. http是无状态的
无状态协议具有一些优势,每个请求都是唯一和独立的,服务器不需要保存有关会话的信息,从而不需要存储数据,大大节省空间。
但是,这也意味着在每次HTTP请求和响应中都会发送关于请求的冗余信息用于告诉服务器它的“身份信息”,如下图一中的巨大cookie,而图二为websocket链接则不需要。
3. http是不“可寻址”的
客户端可以通过URL寻找到服务器资源,但是服务端应用程序却没有类似url的标识主动找到客户端并发送资源,这就让web通信变得不对称。
颠倒通知流程:解决这一局限的方法之一是由客户端发出http请求,使用http请求颠倒通知流程,这一个过程用一个伞形术语‘Comet’表示。(Comet本质就是我们常见的轮询、长轮询、http流化)
轮询( polling ):一种定时的同步调用,客户端向服务器发送请求查看是否有可用的新信息。请求以固定的时间间隔发出,不管是否有信息,客户端都会得到响应:如果有可用信息,服务器发送这些信息;如果没有可用信息,服务器返回一个拒绝响应,客户端关闭连接。
const xhr = new XMLHttpRequest();
xhr.open('GET', 'http://www.xxxx.com', true);
const timer = setinterval(() => {
xhr.send();
}, 100000)
xhr.onreadystatechange = function (e) {
if (xhr.readyState == 4 && xhr.status == 200) {
if(xhr.responseText) {
res = xhr.responseText;
clearInterval(timer);
}
}
}
弊端:事先要知道准确的时间间隔,但实时数据的不可预测且有很多无谓的请求
长轮询(long polling): 是另一种流行的通信方法,客户端向服务器请求信息,并在设定的时间段内打开一个连接。服务器如果没有任何信息,会保持请求打开,直到有客户端可用的信息,或者直到指定的超时时间用完为止。这时,客户端重新向服务器请求信息。
长轮询也称作反向AJAX。延长HTTP响应的完成,直到服务器有需要发送给客户端的内容,这种技术常常称作“挂起GET”或“搁置POST”。重要的是要知道,当服务端数据更新很快,长轮询相对于传统轮询并没有明显的性能优势,因为客户端必须频繁地重连到服务器以读取新信息,造成网络的表现和轮询无差。长轮询的另一个问题是缺乏标准实现。
弊端:与轮询类似,无谓请求多,且缺乏标准实现。
HTTP流化:在流化技术中,客户端发送一个请求,服务器发送并维护一个持续更新和保持打开(可以是无限或者规定的时间段)的开放响应。每当服务器有需要交付给客户端的信息时,它就更新响应。
流化是能够适应不可预测的信息交付的极佳方案,但是服务器从不发出完成HTTP响应的请求,从而使连接一直保持打开。在这种情况下,代理和防火墙可能缓存响应,导致信息交付的延迟增加。因此,许多流化的尝试对于存在防火墙和代理的网络是不友好的。
弊端:特殊情况会导致延迟增加
基于Flash:AdobeFlash通过自己的Socket实现完成数据交换,再利用Flash暴露出相应的接口给JavaScript调用,从而达到实时传输目的。此方式比轮询要高效,且因为Flash安装率高,应用场景广泛。然而,移动互联网终端上Flash的支持并不好:IOS系统中无法支持Flash,Android虽然支持Flash但实际的使用效果差强人意,且对移动设备的硬件配置要求较高。2012年Adobe官方宣布不再支持Android4.1+系统,宣告了Flash在移动终端上的死亡。
当然以上都是针对http-1.0 、 http-1.1的解决方案, http-2.0 已经通过新建“流”的方式实现了服务端推送(一个tcp链接,建立多个stream以区分不同的请求)。
WebSocket的诞生
IETF(Internet Engineering Task Force)制定,一种自然的全双工、双向、单套接字链接。
优势
1.减少延迟:与轮询不同,WebSocket只发出一个请求。服务器不需要等待来自客户端的请求。相似地,客户端可以在任何时候向服务器发送消息。相比轮询不管是否有可用消息,每隔一段时间都发送一个请求,单一请求大大减少了延迟。
2.简洁:一次请求多次通信。websocket不像http每次的请求没有‘上下文关系’,为了维护这种‘上下文关系’,http不得不每次请求和响应都携带‘巨大’的头部。
3.Server push: 如下图
通信机制
建立链接
类似TCP需要握手,每个WebSocket连接都始于一个HTTP请求。该请求和其他请求很相似,但是包含一个特殊的首标——Upgrade。Upgrade首标表示客户端将把连接升级到不同的协议。这个不同的协议就是WebSocke。
101 表示连接成功、除了ws协议外,还有wss协议二者之间的关系类似http与https的关系都是在tcp/ip传输层协议上套了一层tls协议。
协议协商-计算响应键值
为了安全的握手websocket服务器必须快速响应出一个键值,如果没有响应出就可能哄骗一些轻信的http服务器意外的升级一个连接。响应过程如下:
服务端的响应函数从客户端发送的sec-WebSocket-Key
首标中取得键值,并在Sec-WebSocket-Accept
首标中返回根据客户端预期计算的键值。
// ws协议规范中包含的固定键值后缀
const KEY_SUFFIX = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
const hashWebSocketKey = function (key) {
// crypto.createHash加密方法返回一个hash对象 sha1加密算法
const sha1 = crypto.createHash("sha1");
// 根据给定的数据更新hash
sha1.update(key + KEY_SUFFIX, 'ascii');
return sha1.digest('base64');
}
其他RFC 6455有关Sec-首标, RFC规范
传输数据
1. 消息格式-协议帧
当ws连接建立后,客户端和服务端就可以在任何时间发送给对方数据了,那么这些数据的格式是什么样的呢?
ws的消息最小单位为帧(frame), 一般用二进制表示 。在协议帧早期草案中‘帧’就是‘消息’二者可以互换,之所以这么说是因为当时的消息只有一帧,实际上消息可以由多帧组成。
协议帧:由控制帧和数据帧两部分组成,如下图
- FIN:1 bit , 表示这是不是消息的最后一帧。第一帧也有可能是最后一帧。
0x0:还有后续帧
0x1:最后一帧
- RSV1、RSV2、RSV3:1 bit, 扩展字段,除非一个扩展经过协商赋予了非零值的某种含义,否则必须为0
- opcode:4 bit, 解释 payload data 的类型,如果收到识别不了的opcode,直接断开。分类值如下:
0x0:连续的帧
0x1:text帧
0x2:binary帧
0x3 - 7:为非控制帧而预留的
0x8:关闭握手帧
0x9:ping帧
0xA:pong帧
0xB - F:为非控制帧而预留的
- MASK:1 bit, 标识 Payload data 是否经过掩码处理,如果是 1,Masking-key域的数据即为掩码密钥,用于解码Payload data。协议规定客户端数据需要进行掩码处理,所以此位为1
- Payload len:7 bit | 7+16 bit | 7+64 bit,表示了 “有效负荷数据 Payload data”,以字节为单位
如果是 0~125,那么就直接表示了 payload 长度
如果是 126,那么 先存储 0x7E(=126)接下来的两个字节表示的 16位无符号整型数的值就是 payload 长度
如果是 127,那么 先存储 0x7E(=126)接下来的八个字节表示的 64位无符号整型数的值就是 payload 长度
- Payload data:(x+y) bytes, 它是 Extension data 和 Application data 数据的总和,一般扩展数据为空。
- Extension data:x bytes, 除非扩展被定义,否则就是0
- Application data:y bytes, 占据 Extension data 后面的所有空间
断开链接
websocket连接可以在任何时候关闭,但是关闭的原因不总是ws.close()。还有可能底层的TCP套接字突然关闭(比如拔网线、杀进程、退出应用程序等,当然这是很极端的情况)。因此让应用程序能够知道有意中断和意外终止之间的差异,就是优雅挥手的关键。
实际上,当WebSocket关闭时,终止连接的端点(客户端/服务端)可以发送一个数字代码,以及一个表示选择关闭套接字原因的字符串。数字代码用一个16位无符号整数表示,原因则是一个UTF-8编码的短字符串。
RFC 6455定义了多种特殊的关闭代码。代码1000~1015规定用于WebSocket连接层。这些代码表示网络中或者协议中的某些故障。下图列出了这一系列代码、描述和每个代码适用的情况。
拓展:与socket关系
Socket 其实并不是一个协议。它工作在 OSI 模型会话层(第5层),是为了方便大家直接使用更底层协议(一般是 TCP 或 UDP )而存在的一个抽象层。Socket则利用TCP/IP协议建立TCP连接。TCP连接则更依靠于底层的IP协议,IP协议的连接则依赖于链路层等更低层次。它大都在java或者C++这类的编程语言中实现,而在浏览器的基于javascript之类的脚本解释型语言中没有实现。而websocket协议大多数浏览器都已经支持,可以让我们在浏览器中像socket通信一样的去使用TCP直接通信。
WebSocket API-W3C
developer.mozilla.org/zh-CN/docs/…
构造函数
Websocket
实例属性
- onclose
- onopen
- onmessage
- onerror
- ....
API
const socket = new WebSoket();
socket.onmessage = function (event) {
console.log("响应信息", event.data);
};
socket.onopen = function (event) {
console.log("连接开启");
};
socket.onclose = function (event) {
console.log("连接关闭啦")
};
socket.onerror = function (event) {
throw new Error("连接出错啦")
};