Socket并非是一个协议,而是为了方便使用TCP而抽象出来的一层,是位于应用层和传输控制层之间的一组接口。换句话来说,Socket是应用层与TCP/IP协议簇通信的中间软件抽象层,是一组接口,提供了一套调用TCP/IP协议的API。在设计模式上,Socket是一个门面模式,将复杂的TCP/IP协议簇隐藏在Socket接口后面。对用户来说,Socket就是一组接口,使用Socket去组织数据以符合指定的协议。
当不同的主机进行通信时,必须通过Socket,Socket会利用TCP/IP协议建立TCP连接,而TCP连接则需要依靠下层链路层等更低层的支持。
数据推送
传统的Web应用采用的浏览器/服务器(B/S)的HTTP请求响应模式,即浏览器会主动的向服务器发送请求以获得服务器数据。由于HTTP协议是基于请求和响应的、单向的、无状态的应用层协议,因此HTTP协议并不允许服务器主动向客户端推送数据。考虑到安全性,如果允许服务器向客户端主动推送数据,那么客户端将会很容易地受到攻击,特别是广告商会将广告信息强行推送给客户端,因此HTTP的单向特性是必要的。
Web应用的数据交互过程通常是由客户端通过浏览器发出一个请求,服务器接收审核完请求后进行处理并返回结果给客户端,客户端浏览器将信息呈现出来,这种机制对于数据变化不是特别频繁地应用尚能保证,但对实时要求较高的应用来说,比如在线游戏、在线证券、在线播报、订阅推送等,当浏览器准备呈现数据时在服务器可能已经过时了。所有保持客户端和服务器的数据同步是实时Web应用的关键点。在WebSocket规范出现之前,实现的方式主要是采用折中的方案,典型的是轮询(Polling)和Comet技术,Comet实际上是轮询的改进,又可细分为两种实现方式,一种是长轮询机制,一种是流技术。
轮询(Polling)
早期Web为了实现数据推送,会采用AJAX轮询,轮询(Polling)在特定的时间间隔内(频率),比如每隔1秒,由浏览器向服务器发送HTTP请求,服务器接收后返回最新的数据,因此也叫做定期轮询。
轮询这种传统的方式带来的缺陷很明显,定时轮询在实时性上轮询间隔内,会存在数据延迟。
当客户端以固定频率向服务器发起请求的时候,服务器的数据可能并没有更新,会带来很多无用的网络传输,是一种非常低效的方案。
浏览器需要不断地向服务器发出请求,然后HTTP请求可能会包含较长的Header头信息,但真正有效的数据可能只是很小的一部分,显然这样会浪费大量的带宽资源。
长轮询(Long Polling)
长轮询是对定时轮询的改进,目的是为了降低无效的网络传输以节省带宽。当服务器没有数据更新时,连接会保持一段时间周期直到数据/状态改变或者时间过期。
长轮询的基本原理是在HTTP层保持连接,当服务器接收到客户端的请求后如果没有数据更新则会将连接保持一段时间。保持直到有数据更新或连接超时,以减少无效的服务器和客户端之间的数据交互。通过保持连接,长轮询可以减少请求与响应的数量以节省带宽。
长轮询的缺点是节省的带宽的效果有限,当HTTP的数据包的头信息数据量很大,当超过400字节(Byte)时,真正有效的数据却很少,基本在10字节(Byte)左右,这样的数据包在网络中周期性的传输依然会浪费带宽。
其次,当在服务器数据频繁更新时,服务器必须等待下一个请求到来才能发送更新的数据,期间的延迟最高会达到1.5倍的往返时间(RTT)。另外,在网络拥堵的情况时,服务器等待的时间会更久,因此需要重新建立连接。
造成长轮询效率底下的本质原因是由于连接的保持和数据格式,为了保持连接,就需要重新建立HTTP连接,在HTTP1.1版本中这一点只能得到缓解,但无法从原理上彻底解决。另外,由于仍然采用的是应用层的数据格式,因此HTTP的Header头信息数据占用量依然会很大。
长轮询虽然可以用来减少无效的客户端和服务器的交互,但当服务器数据变更频繁时与定时轮询相比也就没有本质上的性能提升。
流(Stream)
流技术是在客户端页面中使用一个隐蔽的窗口向服务器发出一个长连接的请求,服务器接收到做出回应并不断更新连接状态以保证客户端与服务器之间的连接不过期。
流技术虽然可以将服务器数据源源不断推向客户端,但在用户体验上以及浏览器兼容性上存在问题,需要针对不同浏览器设计不同的方案来改进用户体验。同时在并发较大情况下,对服务器资源也是一个极大的考验。
服务器推送事件(SSE, Server-sent Event)
SSE是为了解决浏览器只能单向传输数据到服务器的问题,HTML5提供了一种新的技术叫做服务器推送事件。SSE技术提供的是从服务器单向推送数据给浏览器的功能,配合浏览器主动请求可以实现客户端和服务器的双向通信。
SSE可以实现服务器到客户端的单向数据通信,相较于轮询具有较好的实时性。通过SSE客户端可以自动获取数据更新而不用重复发送HTTP请求,HTTP连接一旦建立,服务器会通过SSE的格式产生并推送事件,事件会自动地被推送到客户端。
SSE的缺点在于大并发情况下服务器可能会宕机。另外,SSE只支持服务器到客户端的单向事件推送,而且所有版本的IE都不支持事件流。如果需要强行支持IE和部分移动浏览器,可以尝试EventSource Polyfill,虽然EventSourcePolyfill本质上依然是轮询。
WebSocket规范
WebSocket是一种网络通信协议,RFC6455中定义了WebSocket的通信标准。WebSocket是HTML5提供的一种在单个TCP连接上进行全双工通讯的协议。
作为下一代的Web标准,HTML5中的WebSocket又被称为“Web的TCP”。WebSocket的出现使得浏览器提供对Socket的支持成为可能,从而在浏览器和服务器之间提供了一个基于TCP连接的双向通道。HTML5定义的WebSocket协议能更好地节省服务器资源和带宽,并能实时地进行双向通讯。浏览器通过JavaScript向服务器发出建立WebSocket连接的请求,当连接建立后,客户端和服务器可以通过TCP连接直接交换数据。
简单来说,WebSocket是HTML5提供的一种在单个TCP连接上进行全双工通讯的协议。WebSocket使客户端和服务器之间的数据交互变得更加简单,允许服务器主动向客户端推送数据。使用WebSocket浏览器和服务器只需要完成一次握手就直接可以创建持久化的连接,并进行双向数据传输。
WebSocket协议是借用HTTP协议的101状态码来达到协议转换(Switch Protocol),切换为WebSocket协议,其本身是基于TCP协议的。
WebSocket协议本质上是一个基于TCP的协议,为了建立一个WebSocket连接,客户端需要首先向服务器发起一个HTTP请求,这个请求和通常的HTTP请求有所不同,它包含了一些附加Header头信息,其中附加头信息Upgrade:WebSocket
会表明这是一个申请协议升级的HTTP请求。当服务器解析这些附加的头信息后会产生应答信息返回给客户端,客户端和服务器的WebSocket连接就会建立起来了。双方就可以通过这个连接通道自由地传递数据,连接会持续存在直到客户端或服务器某一方主动关闭连接。
WebSocket协议是一个持久化的协议,相对于HTTP非持久的协议来说,HTTP的生命周期是通过Request请求来界定的,也就是一个请求对应一个响应。在HTTP1.0版本中,一个请求对应一个响应,一次HTTP请求也就结束了。在HTTP1.1版本中做了改进,可以使用keep-alive
保持连接,但仍旧是一个请求对应一个响应。响应是非常被动的,并不能主动发起。
WebSocket与HTTP一样都是基于TCP的,因此它们都是可靠的协议。浏览器调用WebSocket API中的send
函数在浏览器中的实现,最终都是通过TCP的系统接口进行传输的。
WebSocket和HTTP都是应用层协议,WebSocket在建立握手连接时数据是通过HTTP协议传输的。当建立连接后,真正的数据传输阶段则无需HTTP协议的参与。
WebSocket握手
当Web应用程序调用new WebSocket(url)
接口时,浏览器会开始与指定URL地址的Web服务器建立握手过程。
- 浏览器与WebSocket服务器通过TCP三次握手建立连接,如果连接建立失败则后续过程不执行,Web应用程序会收到错误消息通知。
- 当TCP建立连接成功后,浏览器通过HTTP协议传送WebSocket所支持的版本号、协议子版本号、原始地址、主机地址等一系列字段给服务器。
- WebSocket服务器接收到浏览器发送过来的握手请求后,如果数据包格式正确,客户端和服务器的协议版本号匹配等,就会接受本次握手连接,并给出相应的数据回复,同样回复的数据包也是采用HTTP协议进行传输。
- 当浏览器接收到服务器回复的数据包后,如果数据包内容和格式都没有问题,就表示本次连接建立成功。浏览器会触发
onopen
消息,此时可通过send
接口向服务器发送数据。若握手失败,Web应用程序则会收到onerror
消息,并知道连接失败的原因。
典型WebSocket的握手协议(Handshake)包含发起HTTP请求和获取HTTP响应两部分
- 请求:从客户端向服务器发起HTTP握手协议的请求报文
GET /api HTTP/1.1
Host: hostname.com
Connection: Upgrade
Sec-WebSocket-Key2: 1dGhlIHNhbXBsZBub25jZQ==
Upgrade: WebSocket
Sec-WebSocket-Key1: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://hostname.com
Sec-WebSocket-Protocol: user
Sec-WebSocket-Version: 1.3
[8-byte security key]
头信息 | 含义 |
---|---|
Connection:Upgrade | 表示要升级协议 |
Upgrade:WebSocket | 表示要升级到WebSocket协议 |
Sec-WebSocket-Version:1.3 | 表示WebSocket协议版本,若服务器不支持则会返回一个Sec-WebSocket-Version 头信息,其中包含服务器支持的版本号。 |
Sec-WebSocket-Key | 一个经过Base64加密后的值,由浏览器随机生成,与服务器响应头信息中的Sec-WebSocket-Accept 对应,用于服务器校验以提供基本的安全防护恶意请求。 |
Sec-WebSocket-Protocol | 一个用户定义的字符串,用来区分相同URL下不同服务器所需要的协议。 |
- 响应:从服务器返回HTTP响应到客户端的握手协议报文
HTTP/1.1 101 WebSocket Protocol Handshake
Upgrade: WebSocket
Connection: Upgrade
WebSocket-Origin: http://hostname.com
WebSocket-Location: ws://hostname.com/api
Sec-WebSocket-Accept:s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol:user
[16-byte hash response]
头信息 | 含义 |
---|---|
Sec-WebSocket-Accept | 表示根据客户端请求首部Sec-WebSocket-Key计算的值 |
WebSocket的握手协议(Handshake)与普通的HTTP相似,其中Upgrade:WebSocket
这是一个特殊的HTTP请求,请求的目的是将客户端和服务器的通讯协议从HTTP协议升级(Upgrade)到WebSocket协议。客户端到服务器请求的头信息中包含Sec-WebSocket-Key1
和Sec-WebSocket-Key2
以及[8-byte securitykey]
,这是客户端需要向服务器提供的握手信息,服务器会解析这些头信息,并在握手过程中一句这些加密信息生成一个16位的安全密钥[16-byte hash response]
返回给客户端,以表明服务器获取了客户端的请求,并同意创建WebSocket连接。一旦连接建立,客户端和服务器就可以通过这个通道双向传输数据。
Sec-WebSocket-Key1
、Sec-WebSocket-Key2
、[8-byte security key]
三个Header头信息是WebSocket服务器用来生成应答信息的来源。根据draft-hixie-thewebsocketprotocol-76
草案定义,WebSocket服务器基于以下算法生成正确的应答(Response)信息。
- 首先逐个字符读取
Sec-WebSocket-Key1
头信息中的值,并将数值型字符串连接到一起放入临时数字字符串中,同时统计所有空格的数量。 - 将生成的数字字符串转换为一个整形数字,并除以统计出来的空格个数,将得到的浮点数转换为整数。
- 将生成的整数转换为符合网络传输的网络字节数字
- 对
Sec-WebSocket-Key2
头信息同样进行1到3步操作得到网络字节数组 - 将
[8-byte security key]
和生成的两个网络字节数组合并为一个16位的数组 - 将16位的字节数组使用MD5算法加密生成一个哈希值,将哈希值作为安全密钥返回给客户端,以表明服务器获取了客户端的请求,并同意创建WebSocket连接。
数据帧(Frame)
WebSocket客户端与服务器通信的最小单位是帧(frame),一条完整消息是由一个或多个帧组成的。数据交互时,发送方会将消息切割为多个帧发送给接收方,接收方接收到消息帧后会重新组装为完整的消息。
WebSocket客户端与服务器一旦成功建立连接后,后续的操作都是基于数据帧的传递。WebSocket的每条消息都可能会被切分为多个数据帧。当WebSocket接收方接收到一个数据帧时会更根据FIN
(数据帧中的一个标识,用来判断当前帧是否当前消息的最后一帧)的值来判断是否已经接收到消息的最后一个数据帧。当接收到消息的最后一帧时,即可对消息进行处理。
每个从客户端发送到服务器的数据帧遵循的格式如下
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
- MASK位
掩码(MASK)是一串二进制代码,对目标字段进行位与运算,屏蔽当前的输入位。
掩码位表明信息是否已经进行掩码处理,来自客户端的消息必须经过处理,应将其置为1,如果客户端发送未掩码处理的消息,服务器必须断开连接。
当发送一个帧到客户端时,不要处理数据且不设置MASK掩码位。
- opcode字段
opcode字段定义了如何解析有效的数据
字段 | 含义 |
---|---|
0x0 | 继续处理 |
0x1 | 必须是UTF-8编码的文本 |
0x2 | 二进制和其他叫做控制代码的数据 |
0x3-0x7 与 0xB-0xF | 无意义 |
- FIN
FIN
表明是否数据集合的最后一段消息,如果未0表示服务器继续监听消息,以等待消息剩余部分,否则服务器默认为消息已经完全发送。
WebSocket服务器
开发中为了使用WebSocket接口构建Web应用,首先需要构建一个实现了WebSocket规范的服务器,服务器实现并不受平台和编程语言的限制,只需要遵从WebSocket规范即可。
简单来说,WebSocket服务器就是一个遵循特殊协议监听服务器任意端口的TCP应用,WebSocket服务器可以使用任意服务器编程语言来实现,只要语言能够实现基本的伯克利套接字(Berkely Sockets)。
WebSocket服务器通常会是独立的服务器,因此会使用一个反向代理,比如标准的HTTP服务器,来发现WebSocket的握手协议(Handshake),预处理握手协议后再将客户端数据发送给真正的WebSocket服务器。这也就意味着WebSocket服务器不必存在Cookie和签名的处理,它们完全可以放在代理中处理。
基于多线程或多进程的服务器是无法适用于WebSocket的,因为WebSocket旨在打开连接,尽可能快地处理请求,然后关闭连接。实际的WebSocket服务器实现会需要一个异步服务器。
市面上常用的WebSocket服务器及其类库
WebSocketd是一款非常特别的WebSocket服务器,它的最大特点是后台脚本不限语言,标准输入stdin
就是WebSocket的输入,标准输出stdout
就是WebSocket的输出。
Node.js中常用的WebSocket服务器类库
Python中常用的WebSocket服务器类库
WebSocket客户端
WebSocket JavaScript接口(WebSocket客户端API)是实现WebSocket的Web浏览器会通过WebSocket对象公开所有必须的客户端功能。
创建WebSocket对象
创建WebSocket对象,创建对象的同时会发起连接,如果连接失败则报错。
new WebSocket(url, [protocol])
参数 | 含义 |
---|---|
url | 指定连接的WebSocket服务器URL地址,支持IP和域名,URL以ws://作为协议前缀。 |
protocol | 可选 指定可以接收的子协议 |
const url = "ws://192.168.50.25/api/api/shake";
const ws = new WebSocket(url);
- ws:// 表示连接协议
- 192.168.50.25 表示WebSocket服务器所在主机的IP地址,也可以是域名。
- api 表示应用的上下文路径
- shake 表示访问地址
WebSocket对象属性
属性 | 描述 |
---|---|
Socket.readyState | 只读属性,表示连接状态。 |
Socket.bufferedAmount | 只读属性,表示已被send() 放入正在队列中等待传输,但还没发出的UTF-8 文本字节数。 |
Socket.readyState 连接状态
连接状态值 | 含义 |
---|---|
0 | 连接尚未建立 |
1 | 连接已经建立,可以进行通信 |
2 | 连接正在进行关闭 |
3 | 连接已经关闭或连接无法打开 |
WebSocket对象方法
方法 | 作用 |
---|---|
Socket.send() | 使用连接发送数据 |
Socket.close() | 关闭连接 |
WebSocket对象事件
事件 | 事件处理器 | 作用 |
---|---|---|
open | Socket.onOpen | 连接建立时触发 |
close | Socket.onClose | 连接关闭时触发 |
error | Socket.onError | 通信发生错误时触发 |
message | Socket.onMessage | 客户端接收服务器数据时触发 |
心跳检测(Heartbeat)
很多情况下都会触发WebSocket连接的关闭,一般情况下都会是触发了连接的onClose
事件。当断网情况下则是不会触发onClose
事件,此时就无法得知连接是断掉的。可采用心跳检测断线重连的方式,客户端每隔一段时间向服务器发送ping
数据,服务器一旦苏醒会进行pong
响应,此时就可以重新连接。心跳重连并不是轮询,因为轮询会不断建立多个连接,而心跳则依旧是在当前连接中,只是会一直发送探测消息而已。