tags:webSocket 和 Socket
引言: 好多朋友想知道 WebSocket 和 Socket 的联系和区别, 下面就是你们想要的
先来一张之前收集的图, 我看到这张图真的是笑了, 当时还给我朋友门转发了一下, 不知道你笑了没有.
看完上面的图, 应该猜到了, 他们之间也确实没有什么实质性的联系, 当然除了名字有点相同, 文章后面有名称的由来可以参考阅读
Socket
英文 socket 的意思是插座, 网络中的 Socket 是一个抽象的接口, 可以理解为网络中连接的两端. 通常被叫做套接字接口, 其意义在对传输层进行封装屏蔽了传输层的复杂性. 它并不是一个协议, 是为了大家更方便的使用传输层协议产生的一个抽象层. 大部分的主流编程语言都提供 socket 函数. 我们拿 php 来举例说明
<?php
1. socket_create() - 创建一个套接字
2. socket_accept() - Accepts a connection on a socket (接收)
3. socket_bind() - 给套接字绑定 IP 和端口号
4. socket_connect() - 和一个套接字建立连接
- 5. socket_listen() - Listens for a connection on a socket (监听)
- 6. socket_last_error() - Returns the last error on the socket
- 7. socket_strerror() - Return a string describing a socket error
- ?>
我们可以用这些函数来建立连接实现通信功能. 关于 Socket 我们就说这些
WebSocket
说道 WebSocket 了解过一些的人可能会觉得有些高大上的感觉, 它的诞生还有些故事可以讲, 大概是在为 w3c 放弃了 html 后, 还有那么一群人不服气 (不想放弃), 想要继续推动 html 发展, 同时他们也发展了一些其他的网络标准, 并且被官方接收. 而 WebSocket 就是其中一种, 是为了创建一种双向通信(全双工) 的协议, 来作为 HTTP 协议的一个替代者, 以解决基于 http 上的长轮询等技术解决不了 (或者解决的不那么优美) 的问题. 而且这厮一开始并不叫 WebSocket, 好像是叫 webConnect 之类的, 最后是一位工程师提议说要么咱们叫 WebSocket 吧, 然后....., 好了故事就这样, 他既然是 HTTP 的替代者, 我们首先看一下它和 HTTP(或者 HTTP 的长连接)的联系和区别.
WebSocket 和 HTTP 1.1 的联系
首先两者都是应用层协议, 而且 WebSocket 在建立连接时, 需要借用 http 的 101 switch protocol 来达到协议转换, 为了建立一个 WebSocket 连接, 客户端浏览器首先要向服务器发起一个 HTTP 请求, 这个请求和通常的 HTTP 请求不同, 包含了一些附加头信息, 其中附加头信息 "Upgrade: WebSocket" 和 "Connection: Upgrade" 表明这是一个申请协议升级的 HTTP 请求, 服务器端解析这些附加的头信息然后产生应答信息返回给客户端, 客户端和服务器端的 WebSocket 连接就建立起来了, 在建立连接后, 就和 HTTP 没有关系了, 双方就可以通过这个连接通道自由的传递信息.
当然, 也有可能服务器不支持 WebSocket, 那就老老实实的用 http 吧, 目前大部分浏览器和服务器都已支持 WebSocket.
贴一段简单 WebSocket 客户端的 js 代码
- <script type="text/javascript">
- // 语法 var Socket = new WebSocket(url, [protocol] );
- var ws = new WebSocket("ws://localhost:6688/send");
- // 连接建立时触发
- ws.onopen = function(evt) {
- console.log("Connection open ...");
- ws.send("Hello WebSockets!");
- };
- // 接收消息时触发
- ws.onmessage = function(evt) {
- console.log("Received Message:" + evt.data);
- ws.close();
- };
- // 关闭连接触发
- ws.onclose = function(evt) {
- console.log("Connection closed.");
- };
- // 通信发生错误时触发
- ws.onerror = function(evt) {
- console.log("Connection Error.");
- };
- // 检查浏览器是否支持 WebSocket
- if(typeof WebSocket != 'undefined'){
- alert("您的浏览器支持 WebSocket!");
- }else{
- // 浏览器不支持 WebSocket
- alert("您的浏览器不支持 WebSocket!");
- }
- </script>
WebSocket 和 HTTP 1.1 区别
我们来看一下他的格式:
- // 一个 WebSocket 连接始于握手(handshake)
- GET /chat HTTP/1.1
- Host: server.example.com
- Upgrade: websocket
- Connection: Upgrade
- Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
- Origin: http://example.com
- Sec-WebSocket-Protocol: chat, superchat
- Sec-WebSocket-Version: 13
对上面状态进行解释:
前两行跟 HTTP 的 Request 的起始行一样, 而真正在 WS 的握手过程中起到作用的是下面几个 header 域.
Upgrade:websocket
upgrade 是 HTTP1.1 中用于定义转换协议的 header 域. 它表示要升级 (转换) 到某个协议(如果服务器支持的话).
Connection:Upgrade 表示要进行升级协议
Sec-WebSocket-Key: 用来发送给服务器过滤非预期的请求(比如手动填写 header 中的一些信息, 但本身不想升级到 WebSocket. 这时候, 由于 Sec-WebSocket-Key 和一些相关项被禁止手动设置, 所以可以过滤掉出现非预期的情况).
Origin: 作安全使用, 防止跨站攻击, 浏览器一般会使用这个来标识原始域.
Sec-WebSocket-Protocol: 客户端支持的子协议的列表.
Sec-WebSocket-Version: 客户端支持的 WS 协议的版本.
- // 服务端应答 handshake 101 表示切换
- HTTP/1.1 101 Switching Protocols
- Upgrade: websocket
- Connection: Upgrade
- Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
- Sec-WebSocket-Protocol: chat
上面是报文区别, 还有一些其他的特性
WebSocket 的连接必须是一个直接连接(这个我还需要仔细研究研究, 还不是很透彻, 如果有懂的朋友可以帮我理解理解将不胜感谢. 回头我仔细研究一下通信的方式和数据帧格式).
WebSocket 连接建立之后, 通信双方都可以在任何时刻向另一方发送数据(即全双工, 这是最主要的).
WebSocket 连接建立之后, 数据的传输使用帧来传递, 不同于 Request.
由于展开来讲的话篇幅太长, 大家也可以自行深入了解.
WebSocket 的全双工和 HTTP
在 HTTP 中, 一个 Request 对应着一个 Response, 早期的 HTTP1.0 每次的 HTTP 连接都需要打开一个 TCP 连接, 在一个 Request 后, 服务器产生一个应答 Request, 这次 HTTP 连接就结束了, 同时关闭了 TCP 连接, 重复的建立 TCP 连接是一种资源浪费, 主动关闭 TCP 连接后还会出现 time_wait 状态, 继续占用资源 一段时间(可以看上一篇文章 TCP 连接和 time_wait,close_waite)
这种情况在 HTTP1.1 中进行了一定的改进, 使得有一个 keep-alive, 也就是说, 在一个 HTTP 连接中, 可以发送多个 Request, 接收多个 Response, 可以减少建立和拆除 TCP 连接的次数, 因此同时减少了 time_wait 状态的连接, 但是, 如果设置了 keep-alive 的超时时间比如 nginx 中是 keepalive_timeout, 一段时间没有通信超时后服务器主动关闭连接也是可能造成服务器出现 time_wait 状态的, 如果不设置超时时间也会造成一定的资源浪费(占用连接却不发送数据), 所以怎么设置这个超时时间也很重要.
本质上 HTTP1.1 中虽然可以保持持久的连接, 但是它依然不是全双工的, 因为服务端是不可以主动给客户端发送消息的, ajax 轮询的方式虽然可以达到 WebSocket 全双工的类似效果, 但是会造成大量的资源浪费.
数据帧格式
- 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 ... |
- +---------------------------------------------------------------+
从左到右, 单位是比特. 比如 FIN,RSV1,RSV2,RSV3 各占据 1 比特, opcode 占据 4 比特.
内容包括了标识, 操作代码, 掩码, 数据, 数据长度等.(下一小节会展开)
各个标志位
参考上面的数据帧格式来说一下各个字段的含义, 重要字段加粗
FIN:(finish)1 个比特.
如果是 1, 表示这是消息的最后一个分片(fragment), 如果是 0, 表示不是最后一个分片.
RSV1, RSV2, RSV3: 各占 1 个比特.
若不采用 WebSocket 扩展这里必须为 0. 当客户端, 服务端协商采用 WebSocket 扩展时, 这三个标志位可以非 0, 且值的含义由扩展进行定义. 如果出现非零的值, 且并没有采用 WebSocket 扩展, 连接出错.
Opcode: 4 个比特.
操作代码, 定义了对 "负载数据" 的解释, Opcode 的值决定了应该如何解析后续的数据载荷(data payload). 如果收到一个未知的操作码, 接收端点应该断开连接(fail the connection). 可选的操作代码如下:
%x0: 表示一个延续帧. 当 Opcode 为 0 时, 表示本次数据传输采用了数据分片, 当前收到的数据帧为其中一个数据分片.
%x1: 表示这是一个文本帧(frame)
%x2: 表示这是一个二进制帧(frame)
%x3-7: 保留的操作代码, 用于后续定义的非控制帧,(一般协议中都会预留出一些码用于扩展).
%x8: 表示连接断开 / 关闭.
%x9: 表示这是一个 ping 操作.
%xA: 表示这是一个 pong 操作.
%xB-F: 保留的操作代码, 用于后续定义的控制帧(一般协议中都会预留出一些码用于扩展).
Mask: 1 个比特.
表示是否要对数据载荷进行掩码操作. 从客户端向服务端发送数据时, 需要对数据进行掩码操作, Mask 需要为 1,masking-key(掩码键)字段存在值; 从服务端向客户端发送数据时, 不需要对数据进行掩码操作, Mask 需要为 0. 如果服务端接收到的数据没有进行过掩码操作, 服务端需要断开连接.
Payload length: 数据载荷的长度, 单位是字节. 为 7 位, 或 7+16 位, 或 1+64 位.
假设 Payload length 的值为 x, 如果
x 为 0~126: 数据的长度为 x 字节.
x 为 126: 后续 2 个字节代表一个 16 位的无符号整数, 该无符号整数的值为数据的长度.
x 为 127: 后续 8 个字节代表一个 64 位的无符号整数(最高位为 0), 该无符号整数的值为数据的长度.
上面这种定义负载长度方式是一种网络协议中常用的方法, 可以实现灵活扩展的数据长度定义.
Masking-key:0 或 4 字节(32 位)
所有从客户端传送到服务端的数据帧, 数据载荷都进行了掩码操作, Mask 为 1, 且携带了 4 字节的 Masking-key. 如果 Mask 为 0, 则没有 Masking-key.
Payload data:(载荷数据 x+y 字节)
载荷数据: 包括了扩展数据, 应用数据. 其中, 扩展数据 x 字节, 应用数据 y 字节. 载荷数据的长度, 不包括 mask key 的长度.
Extension data(扩展数据 x 字节):
如果没有协商使用扩展的话, 扩展数据数据为 0 字节. 所有的扩展都必须声明扩展数据的长度, 或者可以如何计算出扩展数据的长度. 此外, 扩展如何使用必须在握手阶段就协商好. 如果扩展数据存在, 那么载荷数据长度必须将扩展数据的长度包含在内.
Application data(应用数据 y 字节):
任意的应用数据在扩展数据之后(如果存在扩展数据), 占据了数据帧剩余的位置. 载荷数据长度 减去 扩展数据长度, 就得到应用数据的长度.
Ping Pang 心跳
对于长时间没有数据交互的连接, 会浪费连接资源. 但不排除有些场景, 客户端, 服务端虽然长时间没有数据往来, 但仍需要保持连接. 这个时候, 可以采用心跳来实现.(不知道心跳为什么是 ping,pang)
ping
Ping 帧操作码(opcode)0x9. 可以包含 " 应用数据
当收到一个 Ping 帧时, 接收方必须在响应中发送一个 Pong 帧, 除非它早已接收到一个关闭帧. 它应该尽可能快地以 Pong 帧响应.(也用来验证远程端点是否可响应)
Pong
Pong 帧操作码(opcode)0x9.
一个 Pong 帧必须携和被响应的 Ping 帧中相同的数据
如果再一个 ping 到达服务端, 服务端尚未响应前, 由到达同源的 ping 帧, 则可以只响应最新的 ping 帧,
未收到 ping 也可以发送一个 Pong 帧. 这个充当单向的心跳(heartbeat), 另一方不需要响应.
来源: https://www.cnblogs.com/vinter/p/9071650.html