一文详解 WebSocket 网络协议
WebSocket 协议运行在TCP协议之上,与Http协议同属于应用层网络数据传输协议。WebSocket相比于Http协议最大的特点是:允许服务端主动向客户端推送数据(从而解决Http 1.1协议实现中客户端只能通过轮询方式获取服务端推送数据造成的资源消耗和消息延时等问题)。
WebSocket 协议诞生于2008年6月,并在2011年12月成为 RFC6455
https://www.rfc-editor.org/rfc/rfc6455.txt 国际标准,诞生之初被应用于HTML5相关规范中(移动互联网时代,也多应用于服务端向客户端推送消息的场景)。
WebSocket协议定义中客户端和服务端只需要完成一次握手,两者之间就可以建立持久性的连接,并进行双向数据传输。而且WebSocket 握手阶段采用的是HTTP 协议;完成协议握手后,后续的数据通信采用WebSocket数据格式通信。

这篇文章,我们按照如下顺序,学习一下 WebSocket 全向数据传输协议:
- WebSocket 诞生;
- WebSocket 简介;
- WebSocket 使用举例;
- WebSocket 协议握手;
- WebSocket 数据帧格式详解;
- WebSocket 关闭连接;
- WebSocket 心跳消息;
一、诞生
早期的很多网站为具备数据推送能力,所在用的技术基本都是HTTP轮询。
轮询是由由客户端每隔一段时间(如每隔5s)向服务器发出HTTP请求,服务端接收到请求后向客户端返回最新的数据。
客户端的轮询方式一般为短轮询或长轮询。
短轮询:
一般是由客户端每隔一段时间(如每隔5s)向服务器发起一次普通 HTTP 请求。服务端查询当前接口是否有数据更新,若有数据更新则向客户端返回最新数据,若无则提示客户端无数据更新。长轮询:
一般是由客户端向服务端发出一个设置较长网络超时时间的 HTTP 请求,并在Http连接超时前,不主动断开连接;待客户端超时或有数据返回后,再次建立一个同样的Http请求,重复以上过程。

以上两种轮询方式也带来了很明显的缺点:
- 首先,客户端需要不断的向服务器发出请求,在消耗较多客户端资源的情况下,服务端并不一定有新的数据下发;
- 其次,HTTP协议请求与回复消息中,需包含较长的头部信息,其中真正有效的数据有可能只占较小的一部分,带来较多的带宽资源消耗。
- 另外,若服务端在同一时间存在连续频繁的数据变化(例如:聊天室场景中),客户端获知数据更新相对较慢(可能存在时间的滞后性)无法保证客户端的用户体验。
因此,工程师们一直在思考,有没有更好的方法,可以减少资源的消耗,同时提高客户端的用户体验 !,在以上情况下 WebSocket 孕育而生。
二、简介
WebSocket 协议运行在TCP协议之上,与Http协议同属于应用层网络数据传输协议。WebSocket相比于Http协议最大的特点是:允许服务端主动向客户端推送数据。
WebSocket 协议诞生于2008年6月,并在2011年12月成为 RFC6455
https://www.rfc-editor.org/rfc/rfc6455.txt 国际标准,诞生之初被应用于HTML5相关规范中(移动互联网时代,也多应用于服务端向客户端推送消息的场景)。
WebSocket协议定义中客户端和服务端只需要完成一次握手,两者之间就可以建立持久性的连接,并进行双向数据传输。
2.1 协议特点
建立在 TCP 协议之上,属于应用层协议;与 HTTP 协议有着良好的兼容性:
WebSocket 默认端口是80,WebSocket Over SSL的默认端口是443;
WebSocket 握手阶段采用的是 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。数据格式比较轻量,性能开销小:
WebSocket数据帧格式相对简单,用于协议控制的数据包头部相对较小(在不包含扩展的情况下,对于服务端到客户端的内容,此头部大小只有2至10字节)。数据实时性较高:
由于WebSocket是全双工的,所以服务端可以随时主动给客户端下发数据,相对于HTTP请求需要待客户端发起请求服务端才能响应,延迟明显更少,用户体验更高。- 可以发送的载荷数据可以是
文本数据,也可以是二进制数据;
WebSocket协议的数据载荷可以是普通的文本数据,也可以是二进制数据,数据量相对较大时,还可以分片多帧进行数据发送与接收。
2.2 协议标识
WebSocket 的协议标识符是ws(如果Over SSL,则为wss):
ws://example.com:80/some/path
wss://example.com:443/some/path
三、使用举例
对于客户端 WebSocket 的使用方面:OkHttp框架为我们提供了较好的使用封装。
/**
* WebSocketAgent
*
* @author https://blog.csdn.net/xiaxl
*/
public class WebSocketAgent {
//
private static final String TAG = "WebSocketAgent";
// OkHttpClient
private OkHttpClient mOkHttpClient;
// WebSocket
private WebSocket mWebSocket;
private WebSocketCallback mWebSocketCallback;
public WebSocketAgent() {
mOkHttpClient = new OkHttpClient.Builder()
.writeTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.connectTimeout(30, TimeUnit.SECONDS)
// 每隔 30s 客户端主动发送ping保活消息
.pingInterval(30, TimeUnit.SECONDS)
.build();
Log.d(TAG, "newWebSocket connection");
}
public void connect() {
//建立连接
Request request = new Request.Builder()
.url(AgentConstant.WEBSOCKET_APP_URL)
.build();
mWebSocket = mOkHttpClient.newWebSocket(request, new OkHttpWebSocketListener());
}
public void sendMessage(String message) {
Log.d(TAG, "sendMessage text: " + message);
if (mWebSocket != null) {
mWebSocket.send(message);
} else {
Log.e(TAG, "mWebSocket is null, please call connect first.", null);
}
}
public void sendMessage(byte... data) {
Log.d(TAG, "sendMessage byte: " + data);
if (mWebSocket != null) {
ByteString bs = ByteString.of(data);
mWebSocket.send(bs);
} else {
Log.e(TAG, "mWebSocket is null, please call connect first.", null);
}
}
public void close(int code, String reason) {
if (mWebSocket != null) {
mWebSocket.close(code, reason);
} else {
Log.e(TAG, "mWebSocket is null, please call connect first.", null);
}
}
public void setWebSocketCallback(WebSocketCallback callback) {
mWebSocketCallback = callback;
}
public class OkHttpWebSocketListener extends WebSocketListener {
@Override
public void onOpen(WebSocket webSocket, Response response) {
// 注意:这里回到的是异步线程
if (response.code() == 101) {
// 连接成功
}
}
// 当收到文本(类型{@code 0x1})消息时调用
@Override
public void onMessage(WebSocket webSocket, String text) {
// 注意:这里回到的是异步线程
if (mWebSocketCallback != null) {
mWebSocketCallback.onWebSocketMessage(text);
}
}
// 当收到二进制(类型为{@code 0x2})消息时调用。
@Override
public void onMessage(WebSocket webSocket, ByteString bytes) {
// 注意:这里回到的是异步线程
}
// 当远程对等体指示不再有传入的消息将被传输时调用。
@Override
public void onClosing(WebSocket webSocket, int code, String reason) {
// 注意:这里回到的是异步线程
if (mWebSocketCallback != null) {
mWebSocketCallback.onWebSocketClosing(code, reason);
}
}
// 当两个对等方都表示不再传输消息并且连接已成功释放时调用。 没有进一步的电话给这位听众。
@Override
public void onClosed(WebSocket webSocket, int code, String reason) {
// 注意:这里回到的是异步线程
if (mWebSocketCallback != null) {
mWebSocketCallback.onWebSocketClosed(code, reason);
}
}
// 由于从网络读取或向网络写入错误而关闭Web套接字时调用。
@Override
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
// 注意:这里回到的是异步线程
if (mWebSocketCallback != null) {
mWebSocketCallback.onWebSocketError(t, response);
}
// TODO 切换到主线程中后,考虑一下断开连接后的重试逻辑
}
}
public abstract static class WebSocketCallback {
public void onWebsocketConnected() {}
public void onWebSocketMessage(String text) {}
public void onWebSocketClosing(int code, String reason) {}
public void onWebSocketClosed(int code, String reason) {}
public void onWebSocketError(Throwable t, Response response) {}
}
}
四、协议握手
WebSocket 协议握手复用了HTTP协议:
客户端通过HTTP请求与WebSocket服务端协商升级协议到 Websocket 协议。服务端通过HTTP响应数据回应客户端的升级请求,完成协议握手。
完成协议握手后,后续的数据交换则遵照 WebSocket 的协议进行。
4.1 握手请求
客户端通过 HTTP/1.1 协议 GET 请求发起协议升级请求:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
请求Header头域字段说明:
Connection: Upgrade:表示要升级协议;Upgrade: websocket:表示要升级到websocket协议;Sec-WebSocket-Version: 13:表示websocket的版本;Sec-WebSocket-Key:与服务端响应首部的Sec-WebSocket-Accept是配套的,提供基本的防护,比如恶意或者无意的连接;
Wireshark抓包如下:

4.2 握手响应
服务端通过HTTP Response 中 101状态码 返回响应数据,完成协议握手:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
响应数据说明:
101 Switching Protocols:HTTP协议切换为WebSocket协议。Sec-WebSocket-Accept:与请求数据中的Sec-WebSocket-Key数据对应,由请求数据中对应自己计算生成(计算方式后续详细说明)。
注:每个header都以\r\n结尾,并且最后一行加上一个额外的空行\r\n。
Wireshark抓包如下:

4.3 Sec-WebSocket-Accept计算
Sec-WebSocket-Accept根据客户端请求首部的Sec-WebSocket-Key计算出来。
计算公式为:
- 将
Sec-WebSocket-Key跟258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接。 - 通过
SHA1计算出摘要,并转成base64字符串。
// 伪代码举例
Base64( sha1( Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 ) )
标准文档中关于计算方式的表述:

五、数据帧格式
在完成协议握手后,后续客户端与服务端数据交换均需要遵循 WebSocket 协议进行。
这里,我用 Wireshark 抓包了一个服务端推送到客户端的消息数据,可以让大家先对 Wireshark 数据格式有一个大概的认识。

WebSocket客户端、服务端通信的最小单位是帧(frame),由1个或多个帧组成一条完整的消息(message)。
- 发送端:将消息切割成多个帧,发送给服务端;
- 服务端:接收消息帧,并将关联的帧重新组装成完整的消息数据;
RFC6455 定义了 WebSocket数据帧的统一格式。如下图所示,单位为比特,从左到右FIN、RSV等各占据1比特位,opcode 占据4比特。

FIN (Final):是否为最后一个分片(占1比特位)
如果是1,表示这是消息(message)的最后一个分片(fragment);
如果是0,表示不是是消息(message)的最后一个分片(fragment)。RSV1, RSV2, RSV3 (Reserved):扩展字段(共占3比特位)
一般情况下全为0。当客户端、服务端协商采用WebSocket扩展时,这三个标志位可以非0,且值的含义由扩展进行定义。
对于 Reserved 字段,这里不做详细说明,如有扩展需求,可自行查阅相关国际标准:
RFC6455: WebSocket协议
https://www.rfc-editor.org/rfc/rfc6455.txtOpcode:操作码(占4比特位)
Opcode的值决定了应该如何解析后续的数据载荷(data payload)。
可选的操作代码如下:
| Opcode | 含义 |
|---|---|
| 0x0 | 表示一个延续帧,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片 |
| 0x1 | 表示这是一个文本帧(frame) |
| 0x2 | 表示这是一个二进制帧(frame) |
| 0x3-7 | 保留的操作代码,用于后续定义的非控制帧 |
| 0x8 | 表示连接断开 |
| 0x9 | 表示这是一个ping操作 |
| 0xA | 表示这是一个pong操作 |
| 0xB-F | 保留的操作代码,用于后续定义的控制帧 |
Mask:掩码(占1比特位)
表示是否要对数据载荷进行掩码操作。
从客户端向服务端发送数据时,需要对数据进行掩码操作;从服务端向客户端发送数据时,不需要对数据进行掩码操作。
如果Mask是1,那么在Masking-key中会定义一个掩码键,并用这个掩码键来对数据载荷进行反掩码。

Payload length:数据载荷的长度,单位是字节(占 7、7+16 或7+64 比特位)
假设 Payload length 中前7 个比特位的值为x:
如果x为 0~125,则x即为载荷数据的有效长度;
如果x为 126,则后续16比特位代表一个16位的无符号整数,该无符号整数的值为载荷数据的有效长度;
如果x为 127,则后续64个比特位代表一个64位的无符号整数(最高位为0),该无符号整数的值为载荷数据的有效长度。Masking-key:掩码键(占0或32比特位)
所有从客户端传送到服务端的数据帧,数据载荷都进行了掩码操作(Mask值为1),会携带4字节的Masking-key。
对于 Masking-key 掩码算法,这里不做详细说明,如需了解,可自行查阅相关国际标准:
RFC6455: WebSocket协议
https://www.rfc-editor.org/rfc/rfc6455.txtPayload data:载荷数据(占x+y字节)
载荷数据有两部分组成:扩展数据占 x 字节,应用数据占 y 字节。
扩展数据:如果没有协商使用扩展的话,扩展数据数据为0字节。所有的扩展都必须声明扩展数据的长度,或者可以如何计算出扩展数据的长度。此外,扩展如何使用必须在握手阶段就协商好。如果扩展数据存在,那么载荷数据长度必须将扩展数据的长度包含在内。
应用数据:任意的应用数据,在扩展数据之后(如果存在扩展数据),占据了数据帧剩余的位置。
WebSocket数据帧举例
WebSocket客户端向服务端发送的数据抓包举例如下:

WebSocket服务端向客户端推送的数据抓包举例如下:

六、关闭连接
上边说道了发送消息时,WebSocket数据帧格式。
当WebSocket不再需要时,客户端或服务端可以选择关闭 WebSocket 连接。
WebSocket客户端关闭连接抓包如下:

WebSocket服务端关闭连接抓包如下:

可以看到以上抓包数据中,涉及到一个状态码Status code :
| 连接关闭状态码 | 含义 |
|---|---|
| 1000 | 正常关闭连接 |
| 1001 | 表示某个端“正在离开”,例如服务器关闭或客户端已离开页面 |
| 1002 | websocket协议错误 |
| 1003 | 正在关闭连接,某个端接受了不支持数据格式 |
| 1004~1006 | 保留字段 |
| 1007 | 正在关闭连接,某个端接受了无效数据格式(文本消息编码不是utf-8) |
| 1008 | 正在关闭连接,某个端接收到了违反政策的消息 |
| 1009 | 正在关闭连接,传输的数据量过大 |
| 1010 | 表示客户端正在关闭连接,因为它期望服务器协商一个或多个扩展,但服务器没有做出响应 |
| 1011 | 表示服务器正在关闭连接,因为它遇到了意外情况 |
七、心跳消息
WebSocket 是客户端与服务端的长链接,需要间隔一段发送Ping、Pong心跳,以抵挡运营商的Nat超时,来维持TCP连接不断开。
- 客户端需每隔一段时间(心跳间隔时间)发送一个
Ping消息,到远端服务器侧; - 服务端收到客户端的
Ping消息后,需在10s内回复一个Pong消息;
客户端发送的Ping消息抓包举例如下:

服务端发送的Pong消息抓包举例如下:

八、参考:
RFC6455: WebSocket协议
https://www.rfc-editor.org/rfc/rfc6455.txt
RFC7936: WebSocket补充
https://www.rfc-editor.org/rfc/rfc7936.txt
维基百科:WebSocket
https://zh.m.wikipedia.org/zh-hans/WebSocket
阮一峰: WebSocket教程
https://www.ruanyifeng.com/blog/2017/05/websocket.html
WebSocket协议:
https://www.cnblogs.com/chyingp/p/websocket-deep-in.html
= THE END =
文章首发于公众号”CODING技术小馆“,如果文章对您有帮助,欢迎关注我的公众号。

一文详解 WebSocket 网络协议的更多相关文章
- 一文详解 Linux 系统常用监控工一文详解 Linux 系统常用监控工具(top,htop,iotop,iftop)具(top,htop,iotop,iftop)
一文详解 Linux 系统常用监控工具(top,htop,iotop,iftop) 概 述 本文主要记录一下 Linux 系统上一些常用的系统监控工具,非常好用.正所谓磨刀不误砍柴工,花点时间 ...
- 一文详解Hexo+Github小白建站
作者:玩世不恭的Coder时间:2020-03-08说明:本文为原创文章,未经允许不可转载,转载前请联系作者 一文详解Hexo+Github小白建站 前言 GitHub是一个面向开源及私有软件项目的托 ...
- 『TCP/IP详解——卷一:协议』读书笔记——10
2013-08-22 22:57:17 3.8 ifconfig命令 这个命令在Linux系统下可以通过下面的指令阅读说明文档: ifconfig 由于书中作者用的系统比较早的某Unix系统,所以我的 ...
- 『TCP/IP详解——卷一:协议』读书笔记——01
从今日起开始认真研读TCP/IP详解这本经典制作,一是巩固我薄弱的计算机网络知识,二来提高我的假期的时间利用率.将心得与思考记录下来,防止白看-哦耶 2013-08-14 18:47:06 第一章 概 ...
- 【TCP/IP详解 卷一:协议】TCP的小结
前言:TCP学习的综述 在学习TCP/IP协议的大头:TCP协议 的过程中,遇到了很多机制和知识点,详解中更是用了足足8章的内容介绍它. TCP协议作为 应用层 和 网络层 中间的 传输层协议,既要为 ...
- 【TCP/IP详解 卷一:协议】第十一章 UDP 用户数据报协议
11.1 引言 UDP 是一个简单的 面向数据报 的运输层协议:进程的每个 输出操作 都正好产生一个 UDP数据报,并且组装成一份待发送的IP数据报. 这与 TCP 不一样,它是 面向流字符 的协议, ...
- 【TCP/IP详解 卷一:协议】第二章:链路层
2.1 引言 链路层的三个目的: (1)为IP模块发送和接收IP数据报. (2)为ARP模块发送ARP请求和接收ARP应答.地址解析协议:ARP. (3)为RARP模块发送RARP请求和接收RARP应 ...
- 【TCP/IP详解 卷一:协议】第六章:DHCP 和自动配置
简介 为了使用 TCP/IP 协议族,每台主机or路由器都需要一定的配置信息: IP地址 子网掩码 广播地址 路由或转发表 DNS 协议配置方法: 手动 通过使用网络服务来获得 使用一些算法来自动确定 ...
- Oracle10g数据泵impdp参数详解--摘自网络
Oracle10g数据泵impdp参数详解 2011-6-30 12:29:05 导入命令Impdp • ATTACH 连接到现有作业, 例如 ATTACH [=作业名]. • C ...
随机推荐
- node-webkit文档翻译#package.json
title: node-webkit文档翻译#package.json date: 2013-12-07 21:38:25 tags: node-webkit 基本示例 { "main&qu ...
- [FireshellCTF2020]ScreenShooter 1
此题关键在于理清逻辑,本地将url发送给服务器,服务器请求sereenshooter以后将结果返回 所以应该在服务器查看日志. 发现了PhantomJS 引擎一下 <!DOCTYPE html& ...
- java中接口interface有什么用呢?举例!
接口只有方法的定义,没有方法的任何实现.那这有什么意义呢?马克-to-win: 接口就像一个服务合同.接口只关心必须得干什么而不关心如何去实现它.有 意义吗?有意义.马克-to-win:比如我们的软件 ...
- java中封装encapsulate的概念
封装encapsulate的概念:就是把一部分属性和方法非公有化,从而控制谁可以访问他们. https://blog.csdn.net/qq_44639795/article/details/1018 ...
- Java中switch语句+例题输出当前月份
学习目标: 掌握switch的使用 学习内容: 1.switch语法 <font color=#000000 size=3> switch(表达式) { case 常量1: 语句体1; b ...
- java集合(arraylist详解)
一.ArrayList概述 ArrayList是实现List接口的动态数组,所谓动态就是它的大小是可变的.实现了所有可选列表操作,并允许包括 null 在内的所有元素.除了实现 List 接口外,此类 ...
- dev分支代码覆盖master分支代码
将develop分支上的代码完全覆盖master分支, 1. 切换到master分支 git checkout master 2. 执行以下命令 git reset --hard origin/dev ...
- uni-app中实现图片左滑的效果
template: 1 <view class="my-reg"> 2 <view class="my-regs"> 3 <ima ...
- Visual Studio 2022 git error Unable to negotiate with xx.xxx.xxxx port 22: no matching host key type found. Their offer: ssh-rsa
前言 前两天因为升级了Git导致git提交拉取的时候都提示下面这个异常,然后经过一番折腾以后终于把这个问题解决了.但是今天我升级了下Visual Studio 2022将其升级到了17.1.3版本然后 ...
- centos7源码安装mysql5.7.19
centos7源码包安装mysql5.7 5.7.20安装方法和5.7.19的一样. 1.安装前准备 清空环境.安装相应的软件包 1>关闭防火墙和SELinux 2>配置yum源(阿里云, ...