阅读本文之前,你最好已经做过一些websocket的简单应用
从http到websocket
- HTTP101
- [HTTP 轮询、长轮询和流化](#HTTP 轮询、长轮询和流化)
- 其他技术
-
- [1. 服务器发送事件](#1. 服务器发送事件)
- [2. SPDY](#2. SPDY)
- [3. web实时通信](#3. web实时通信)
- 互联网简史
- web和http
- Websocket协议
-
- [1. 简介](#1. 简介)
- [2. 初始握手](#2. 初始握手)
- [3. 计算响应健值](#3. 计算响应健值)
- [4. 消息格式](#4. 消息格式)
- [5. WebSocket关闭握手](#5. WebSocket关闭握手)
- 实现
HTTP101
- 在HTTP/1.0中,每个服务器请求需要一个单独的链接,这种方法至少可以说没有太好的伸缩性。在HTTP的下一个修订版本,也就是HTTP/1.1中增加了可重用连接。由于可重用连接的推出,浏览器可以初始化一个到web服务器的连接,以读取HTML页面,然后重用该连接读取图片、脚本等资源。HTTP/1.1通过减少客户端到服务器的连接数量,降低了请求的延迟
- HTTP是无状态的,也就是说,它将每个请求当成唯一和独立的。无状态协议具有一些优势,例如,服务器不需要保存有关会话的信息,从而不需要存储数据。但是这也意味着在每次HTTP请求和响应中都会发送关于请求的冗余信息
- 从根本上讲,HTTP还是半双工的协议,也就是说,在同一时刻,流量只能单向流动:客户端向服务器发送请求,然后服务器响应请求。之后出现了轮询、长轮询和HTTP流化(streaming)等技术
HTTP 轮询、长轮询和流化
- 很多提供实时web应用程序的尝试多半是围绕轮询
(polling)
技术进行的,这是一种定时的同步调用,客户端向服务器发送请求查看是否有可用的新信息。请求以固定的时间间隔发出,不管是否有信息,客户端都会得到响应:如果有可用信息,服务器发送这些信息,否则服务器返回一个拒绝响应,客户端关闭连接,这种技术的问题在于我们无法事先预知信息交付的精确间隔,从而导致打开或者关闭很多不必要的连接
- 长轮询
(long polling)
是另一种流行的通信方法,客户端向服务器请求信息,并且在设定的时间段内打开一个连接。服务器如果没有任何信息,会保持请求打开,直到有客户端可用的信息,或者直到指定的超时时间用完为止。这时,客户端重新向服务器请求信息。长轮询也称作Comet
或者反向AJAX
。Comet
延长HTTP
响应的完成,直到服务器有需要发送给客户端的内容,这种技术常常称作"挂起GET"或"搁置POST"。但是当信息量很大的时候,长轮询相对于传统轮询并没有明显的性能优势,因为客户端必须频繁的重连到服务器以读取新信息,造成网络的表现和快速轮询相同。长轮询的另一个问题是缺乏标准实现
- 在流化技术中,客户端发送一个请求,服务器发送并维护一个持续更新和保持打开(可以是无限或者规定的时间段)的开放响应。每当服务器有需要交付给客户端的信息时,它就更新响应。似乎这是一种能够适应不可预测的信息交付的极佳方案,但是服务器从不发出完成HTTP响应的请求,从而使连接一直保持打开。在这种情况下,代理和防火墙可能缓存响应,导致信息交付的延迟增加。因此,许多流化的尝试对于存在防火墙和代理的网络时不友好的
- 上述几种方法还有一些问题,例如冗余的HTTP首标数据和延迟、客户端必须等待请求返回才能发出后续的请求,这会显著增加延迟
其他技术
1. 服务器发送事件
- 如果你的服务主要向其客户端广播或者推送消息,而不需要任何交互,可能使用服务器发送事件
(Server-Sent Events SSE)
提供的EventSource API
是个好的选择。SSE
是HTML5
规范的一部分,加强了某些Comet
技术,可以将SSE
当作一种HTTP
轮询、长轮询和流化的公用可互操作语法使用。利用SSE
,你可以得到自动重连、事件ID等功能,但是这种方式只支持文本数据
2. SPDY
SPDY
(音同"Speedy")是Google
开发的一种网络协议,本质上扩充了HTTP
协议,通过压缩HTTP
首标和多路复用等手段改进HTTP
请求性能,也就是说,它相当于对于HTTP
进行的增量改进,改正了许多HTTP
的非本质问题,增加了多路复用、工作管道(working pipling)
和其他有用的改进。而websocket
与HTTP
之间的不同是架构性的,不是增量的,可以将SPDY
扩充的HTTP
连接升级为Websocket
,从而在两个领域获得利益
3. web实时通信
- 这是一种浏览器之间的点对点技术,不借助服务器传输数据,目前尚未完善
互联网简史
- 一开始,互联网主机之间采用
TCP/IP
通信。在这种情况下,任意一台主机都可以建立新的连接,一旦TCP连接建立,两台主机都可以在任何时候发送数据
- 你想在网络协议中实现的其他功能必须在传输协议基础上构建,这些更高的层次被称为应用层协议。例如,在web之前主线的用于聊天的IRC和用于远程终端访问的telnet就是两个重要的应用层协议,他们显然需要异步的双向通信,客户端必须在另一个用户发送聊天消息或者远程应用程序打印一行输出时接收到提示通知。由于这些协议一般在TCP之上运行,异步双向通信总是可用
- TCP/IP还是http和websocket协议的基础,我们先简单介绍一下http协议
web和http
- 1991年,万维网(World Wide Web)项目第一次公布。Web是使用统一资源定位符 (URL)链接的超文本文档系统。当时,URL是一个重大的发明。URL的U是universal(统一)的缩写,说明了当时的一个革命性想法 ,所有超文本文档的相互连接。Web上的HTML文档通过URL相互连接。更有意义的事Web洗可以经过裁剪,用于读取资源。HTTP是一个用于文档传输的简单同步请求 --- \text{---} --- 响应式协议
- 最早的web应用程序使用表单和全页刷新。每当用户提交信息,浏览器将提交一个表单并读取新页面。每当有需要显示的更新信息,用户或者浏览器必须刷新整个页面,使用HTTP读取整个资源
- 利用
JavaScript
和XMLHttpRequest API
,人们开发出了一组称为AJAX
的技术,这项技术能够使应用程序在每次交互期间不会有不连贯的过渡。AJAX
使应用程序只读取感兴趣去的资源数据,并在没有导航的情况下更新页面。AJAX
使用的网络协议仍然是HTTP
;尽管名为XMLHttpRequest
,数据也只是有时使用XML
格式,而不是始终使用该格式
- 本质上,
HTTP
用其内置的文本支持、URL
和HTTPS
使Web成为可能,然而,在某种程度上,HTTP
的流行也造成了互联网的退化。因为HTTP
不需要可寻址的客户端,Web世界的寻址变成不对称的。浏览器能够通过URL
寻找服务器资源,但是服务器端应用程序却无法主动的向客户端发送资源。客户端只能发起请求,而服务器只能响应未决的请求,在这个非对称的世界中,要求全双工通信的协议无法正常工作
- 解决这一局限性的方法之一是由客户端发出
HTTP
请求,以防服务器有需要共享的更新。使用HTTP
请求颠倒通知流程的这一过程用一个伞形术语Comet
来表示。正如前面所说,Comet
本质是一组利用轮询、长轮询和流化开发HTTP
潜力的技术。这些技术实际上模拟了TCP
的一些功能。因为同步的HTTP
和这些异步应用程序之间不匹配,Comet
复杂、不标准且低效
Websocket协议
1. 简介
- Websocket是定义服务器和客户端如何通过Web通信的一种网络协议。在万维网以及其基础技术HTML、HTTP等推出之前,互联网和现在完全不同。一方面,它比现在小的多;另一方面,它实际上是一个对等网络。当时互联网主机之间通信的两个流行协议现在仍然盛行:互联网协议(
Internet Protocol, IP
)和传输控制协议(Transmission Control Protocol, TCP
)
,前者负责在互联网的两台主机之间传送数据封包,后者可以看做跨越互联网,在两个端点之间可靠地双向传输字节流的一个管道。两者结合起来的TCP/IP
在历史上是无数网络应用程序使用的和核心传输层协议,这种情况仍在持续
- WebSocket为Web应用程序保留了我们所喜欢的HTTP特性(URL、HTTP安全性、更简单的基于数据模型的消息和内置的文本支持),同时提供了其他网络架构和通信模式。和
TCP
一样,WebSocket是异步的,可以用作高级协议的传输层。WebSocket是消息协议、聊天、服务器通知、管道和多路复用协议、自定义协议、紧凑二进制协议和用于互联网服务器互操作的其他标准协议的很好基础
- WebSocket为Web应用程序提供了TCP风格的网络能力。寻址仍然是单向的,服务器可以异步发送客户端数据,但是只在WebSocket连接打开时才能做到。在客户端和服务器之间WebSocket连接始终打开。WebSocket服务器也可以作为WebSocket客户端
特性 |
TCP |
HTTP |
WebSocket |
寻址 |
IP地址和端口 |
URL |
URL |
并发传输 |
全双工 |
半双工 |
全双工 |
内容 |
字节流 |
MIME信息 |
文本和二进制数据 |
消息定界 |
否 |
是 |
是 |
连接定向 |
是 |
否 |
是 |
TCP
只能传送字节流,所以消息边界只能由更高层的协议来表现。对于TCP
来说,它唯一可以保证的是到达接收端的单个字节将会按顺序到达。和TCP
不同,WebSocket传输一序列单独的消息,在WebSocket中,和HTTP
一样,多字节的消息作为整体,按照顺序到达。因为WebSocket协议内置了消息边界,所以它能够发送和接收单独的消息并避免常见的碎片错误
IP
处于互联网层,而TCP
处于IP
之上的传输层。WebSocket的层次在TCP/IP
之上,因为你可以在WebSocket上构建应用级协议,所以它也被看作是传输层协议
2. 初始握手
- 每个WebSocket连接都始于一个HTTP请求,该请求与其他请求很相似,但是包含一个特殊的首标 --- \text{---} ---
Upgrade
,这个首标表示客户端将把连接升级到不同的协议。在这种情况下,这种特殊的协议就是WebSocket
- 从客户端发往服务器升级为WebSocket的HTTP请求称为WebSocket的初始握手,在成功升级之后,连接的语法切换为用于表示WebSocket消息的数据帧格式。除非服务器响应 101代码、
Upgrade
首标和Sec-WebSocket-Accept
首标,否则WebSocket连接不能成功。Sec-WebSocket-Accept
响应首标的值从Sec-WebSocket-Key
请求首标继承而来,包含一个特殊的响应健值,必须与客户端的预期精确匹配
3. 计算响应健值
- 为了成功地完成握手,WebSocket服务器必须响应一个计算出来的健值。这个响应说明服务器理解WebSocket协议。这个响应说明服务器理解WebSocket协议。。没有精确的响应,就可能哄骗一些轻信的HTTP服务器意外的升级一个连接
- 响应函数从客户端发送的
Sec-WebSocket-Key
首标中取得键值,并在Sec-WebSocket- Accept
首标中返回根据客户端预期计算的键值
首标 |
描述 |
Sec-WebSocket-Key |
只能在HTTP请求中出现一次,用于从客户端到服务器的WebSocket初始握手,避免跨协议攻击 |
Sec-WebSocket-Accept |
只能在HTTP请求中出现一次,用于从客户端到服务器的WebSocket初始握手,确认服务器理解WebSocket协议 |
Sec-WebSocket-Extensions |
可能在HTTP请求中出现多次,但是在HTTP响应中只能出现一次。用于从客户端到服务器的WebSocket初始握手,然后用于从服务器到客户端的响应。这个首标帮助客户端和服务器商定一组连接期间使用的协议级扩展 |
Sec-WebSocket-Protocol |
用于从客户端到服务器的WebSocket初始握手,然后用于从服务器到客户端的响应。这个首标通告客户端应用程序可使用的协议。服务器使用相同的首标,在这些协议中最多选择一个 |
Sec-WebSocket-Version |
用于从客户端到服务器的WebSocket初始握手,表示版本兼容性。RFC 6455的版本总是13。服务器如果不支持客户端请求的协议版本,则用这个首标响应。在那种情况下,服务器发送的首标中列出了它支持的版本。这只发生在RFC 6455之前的客户端中 |
4. 消息格式
- 当WebSocket连接打开时,客户端和服务器可以在任何时候相互发送消息。这些消息在网络上用于标记消息之间边界并包括简洁的类型消息的二进制语法表示。更准确地说,这些二进制首标标记另一个单位 --- \text{---} ---帧(
frame
)之间的边界。帧是可以合并组成消息的部分数据。你可能在WebSocket的相关讨论中将"帧"和"消息"互换使用,这是因为很少有一个消息使用超过一个帧的(至少目前如此)。而且在协议帧的早期草案中,帧就是消息,消息在线路上的表示被称作"组帧"(framing
)
- WebSocket API没有向应用程序暴露帧级别的信息。尽管API按照消息工作,但是可以在协议级别上处理子消息数据单元。虽然消息一般只有一个帧,但是它可以由任意数量的帧组成。服务器可以使用不同数量的帧,在全体数据可用之前开始交付数据
- 下面是一个WebSocket帧头
bash
复制代码
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 ... |
+---------------------------------------------------------------+
参考https://datatracker.ietf.org/doc/html/rfc6455
下面详细介绍
- 操作码(opcode)
- 每条Websocket消息都有一个指定消息载荷类型的操作码。操作码由帧头的第一个字节中最后4 bit组成,如下表所示
操作码 |
消息载荷类型 |
描述 |
1 |
文本 |
消息的数据类型为文本 |
2 |
二进制 |
消息的数据类型为二进制 |
8 |
关闭 |
客户端或者服务器向对方发送关闭握手 |
9 |
ping |
客户端或者服务器向对方发送ping |
10 |
pong |
客户端或者服务器向对方发送pong |
- 4 bit的操作码有16种可能取值,WebSocket协议只定义了5种操作码,剩余的操作码保留用于未来的扩展
- 长度
- WebSocket协议使用可变位数来变码帧长度,这样,小的消息就能使用紧凑的编码,协议仍然可以携带中型甚至非常大的消息。对于小于126字节的消息,长度用帧头前两个字节之一来表示。对于
126~216
字节的消息,使用额外的两个字节表示长度。对于大于216字节的消息,长度为8字节。该长度编码保存于帧头第二个字节的最后7位。该字段种126和127两个值被当作特殊的信号,表示需要后面的字节才能完成长度编码
- 编码文本
- WebSocket文本消息用8位UCS转换格式(UTF-8 )编码。UTF-8 是用于Unicode的变长编码,向后兼容7位的ASCII,也是WebSocket文本消息允许的唯一编码。坚持使用UTF-8编码避免了大量的"普通文本"格式以及协议中的不同编码对互操作性的妨害
- 屏蔽(或者叫掩码)
- 从浏览器向服务器发送的WebSocket帧内容进行了"屏蔽",以混淆其内容。屏蔽的目的不是阻止窃听,而是为了不常见的安全原因,以及改进和现有的HTTP代理的兼容性。
- 帧头的第二个字节的第一位表示该帧是否进行了屏蔽,WebSocket协议要求客户端屏蔽发送的所有帧。如果有屏蔽,所用的掩码将占据帧头扩展长度部分后的4个字节
- WebSocket服务器接收的每个载荷在处理之前首先被解除屏蔽。解除屏蔽之后,服务器得到原始消息内容:二进制消息可以直接交付;文本消息将进行UTF-8编码,并通过服务器API输出字符串
- 多帧消息
- 帧格式中的
fin
位考虑了多帧消息或者部分可用消息的流化,这些消息可能不连续或者不完整。要发送一条不完整的消息,你可以发送一个fin
位设置为0的帧。最后一个帧的fin
位设置为1,表示消息以这一帧的载荷作为结束
5. WebSocket关闭握手
- WebSocket连接总是以初始握手开始,因为这是初始化互联网和其他可靠网上对话的唯一手段,连接可以在任何时候关闭,所以不可能总是以关闭握手结束。有时候,底层的TCP套接字可能突然关闭。关闭握手优雅地关闭连接,使应用程序能够知道有意中断和意外终止连接之间的差异
- 当WebSocket关闭时,终止连接的端点可以发送一个数字代码,以及一个表示选择关闭套接字原因的字符串。代码和原因编码为具有关闭操作码的一个帧的载荷。数字代码 用一个16位无符号整数表示,原因则是一个UTF-8 编码的短字符串。
RFC 6455
定义了多种特殊的关闭代码。代码1000~1015
规定用于WebSocket连接层。这戏的代码表示网络中或者协议中的某些故障,下表是关闭代码
代码 |
描述 |
何时使用 |
1000 |
正常关闭 |
当你的会话成功完成时发送这个代码 |
1001 |
离开 |
因应用程序离开且不希望后续的连接尝试而关闭连接时,发送这一代码。服务器可能关闭,或者客户端应用程序可能关闭 |
1002 |
协议错误 |
当因协议错误而关闭连接时发送这一代码 |
1003 |
不可接受的数据类型 |
当应用程序接收到一条无法处理的意外类型消息时发送这一代码 |
1004 |
保留 |
不要发送这一代码。根据RFC 6455,这个状态码保留,可能在未来定义 |
1005 |
保留 |
不要发送这一代码。WebSocket API用这个代码表示没有接收到任何代码 |
1006 |
保留 |
不要发送这一代码。WebSocket API用这个代码表示连接异常关闭 |
1007 |
无效数据 |
在接收一个格式与消息类型不匹配的消息之后发送这一代码。如果文本消息包含错误格式的UTF-8数据,连接应该用这个代码关闭 |
1008 |
违反消息政策 |
当应用程序由于其他代码所不包含的原因终止连接,或者不希望泄露消息无法处理的原因时,发送这一代码 |
1009 |
消息过大 |
当接收的消息过大,应用程序无法处理时发送这一代码(帧的载荷长度最多为64字节,即使你有一个大服务器,有些消息也仍然太大) |
1010 |
需要扩展 |
当应用程序需要一个或职责多个服务器无法协商的特殊扩展时,从客户端(浏览器)发送这一代码 |
1011 |
意外情况 |
当应用程序由于不可预见的原因,无法继续处理连接时,发送这一代码 |
1015 |
TLS失败(保留) |
不要发送这个代码。WebSocket API用这个代码表示TLS在WebSocket握手之前失败 |
实现
- 我们分析一下golang的websocket包
https://github.com/gorilla/websocket
来看此协议是如何实现的,重点关注conn.go
文件,我们先看一下Conn
结构的定义
go
复制代码
type Conn struct {
conn net.Conn // 底层网络连接
isServer bool // 如果这个连接作为服务器端的连接则为true,如果是客户端则为false
subprotocol string // 代表WebSocket连接中协商的子协议
// Write fields (写操作相关字段)
mu chan struct{} // used as mutex to protect write to conn(用作互斥锁保护对连接的写操作)
writeBuf []byte // frame is constructed in this buffer.(字节切片,用于构造要写入的帧)
writePool BufferPool // 提供和管理写缓冲区的池
writeBufSize int // 写缓冲区的大小
writeDeadline time.Time // 写操作的截止时间
writer io.WriteCloser // the current writer returned to the application(当前返回给应用程序的写入器)
isWriting bool // for best-effort concurrent write detection(用于尽最大努力检测并发写操作)
writeErrMu sync.Mutex // 用于保护写操作错误的互斥锁
writeErr error // 保存写操作中发生的错误
enableWriteCompression bool // 指示是否启用写操作压缩
compressionLevel int // 写操作压缩的级别
newCompressionWriter func(io.WriteCloser, int) io.WriteCloser // 用于创建新的压缩写入器
// Read fields(读操作相关字段)
reader io.ReadCloser // the current reader returned to the application (当前 返回给应用程序的读取器)
readErr error // 保存读操作中发生的错误
br *bufio.Reader // 带缓冲的读取器
// bytes remaining in current frame.
// set setReadRemaining to safely update this value and prevent overflow
readRemaining int64 // 当前帧剩余的字节数
readFinal bool // true the current message has more frames.(指示当前消息是否有更多帧)
readLength int64 // Message size. // 消息大小
readLimit int64 // Maximum message size. // 消息的最大大小
readMaskPos int // 掩码在消息中的位置
readMaskKey [4]byte // 用于WebSocket消息掩码
handlePong func(string) error // 处理Pong帧的回调函数
handlePing func(string) error // 处理Ping帧的回调函数
handleClose func(int, string) error // 处理关闭帧的回调函数
readErrCount int // 记录读取错误的次数
messageReader *messageReader // the current low-level reader(当前的底层消息读取器)
readDecompress bool // whether last read frame had RSV1 set(指示最后读取的帧是否设置了RSV1(用于压缩))
newDecompressionReader func(io.Reader) io.ReadCloser // 用于创建新的解压缩读取器
}
go
复制代码
// ReadMessage is a helper method for getting a reader using NextReader and
// reading from that reader to a buffer.
func (c *Conn) ReadMessage() (messageType int, p []byte, err error) {
var r io.Reader
messageType, r, err = c.NextReader()
if err != nil {
return messageType, nil, err
}
p, err = io.ReadAll(r)
return messageType, p, err
}
ReadMessage
函数是我们经常会用到的函数,它用来接收WebSocket消息,一般接收到来自浏览器端的每条消息,我们会从p
数组中获取,可以看到这个函数的核心功能是NextReader()
方法实现的,下面是这个方法
go
复制代码
// NextReader returns the next data message received from the peer. The
// returned messageType is either TextMessage or BinaryMessage.
//
// There can be at most one open reader on a connection. NextReader discards
// the previous message if the application has not already consumed it.
//
// Applications must break out of the application's read loop when this method
// returns a non-nil error value. Errors returned from this method are
// permanent. Once this method returns a non-nil error, all subsequent calls to
// this method return the same error.
func (c *Conn) NextReader() (messageType int, r io.Reader, err error) {
// Close previous reader, only relevant for decompression.
if c.reader != nil {
_ = c.reader.Close()
c.reader = nil
}
c.messageReader = nil
c.readLength = 0
for c.readErr == nil {
frameType, err := c.advanceFrame()
if err != nil {
c.readErr = err
break
}
if frameType == TextMessage || frameType == BinaryMessage {
c.messageReader = &messageReader{c}
c.reader = c.messageReader
if c.readDecompress {
c.reader = c.newDecompressionReader(c.reader)
}
return frameType, c.reader, nil
}
}
// Applications that do handle the error returned from this method spin in
// tight loop on connection failure. To help application developers detect
// this error, panic on repeated reads to the failed connection.
c.readErrCount++
if c.readErrCount >= 1000 {
panic("repeated read on failed websocket connection")
}
return noFrame, nil, c.readErr
}
- 核心是
advanceFrame
函数,这个函数内部实现了WebSocket协议的内容,此处可以对照帧格式来阅读
sh
复制代码
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 ... |
+---------------------------------------------------------------+
- 前两行展示了帧格式的布局和结构,0到3表示列的索引,好像没什么实际含义,只是标记0的位置,第二行有32个数字,表示32个比特位,一共4个字节
FIN
表示这是消息的最后一个帧
RSV1,RSV2,RSV3
用于扩展协议
OpCode
指示数据帧的类型,例如文本、二进制、连接关闭等
Mask
指示是否有掩码
Payload length
表示负载数据的长度,也就是实际传输的数据长度
Extended payload length
表示扩展的负载长度,如果Payload len小于等于125个字节,则使用一个字节来表示长度;如果长度在126个字节到65535个字节之间,则使用两个字节来表示长度;如果长度超过65535个字节,则使用8个字节来表示长度。因此,负载数据的长度最大可以达到 2 64 − 1 2^{64}-1 264−1,所以一般不会有超过一个帧的数据
Masking-Key
是发送方随机生成的4字节掩码,它会对负载数据进行加密,接收方使用这个掩码对负载数据进行加密,以获取原始数据内容。它能够防止一些网络攻击,如中间人攻击,每次随机生成的Masking-Key
使得中间人无法进行持续解密
- 接下来的部分就都是传输的数据了
go
复制代码
// Read methods
func (c *Conn) advanceFrame() (int, error) {
// 1. Skip remainder of previous frame.
if c.readRemaining > 0 {
if _, err := io.CopyN(io.Discard, c.br, c.readRemaining); err != nil {
return noFrame, err
}
}
// 2. Read and parse first two bytes of frame header.
// To aid debugging, collect and report all errors in the first two bytes
// of the header.
var errors []string
p, err := c.read(2)
if err != nil {
return noFrame, err
}
frameType := int(p[0] & 0xf)
final := p[0]&finalBit != 0
rsv1 := p[0]&rsv1Bit != 0
rsv2 := p[0]&rsv2Bit != 0
rsv3 := p[0]&rsv3Bit != 0
mask := p[1]&maskBit != 0
if err := c.setReadRemaining(int64(p[1] & 0x7f)); err != nil {
return noFrame, err
}
c.readDecompress = false
if rsv1 {
if c.newDecompressionReader != nil {
c.readDecompress = true
} else {
errors = append(errors, "RSV1 set")
}
}
if rsv2 {
errors = append(errors, "RSV2 set")
}
if rsv3 {
errors = append(errors, "RSV3 set")
}
switch frameType {
case CloseMessage, PingMessage, PongMessage:
if c.readRemaining > maxControlFramePayloadSize {
errors = append(errors, "len > 125 for control")
}
if !final {
errors = append(errors, "FIN not set on control")
}
case TextMessage, BinaryMessage:
if !c.readFinal {
errors = append(errors, "data before FIN")
}
c.readFinal = final
case continuationFrame:
if c.readFinal {
errors = append(errors, "continuation after FIN")
}
c.readFinal = final
default:
errors = append(errors, "bad opcode "+strconv.Itoa(frameType))
}
if mask != c.isServer {
errors = append(errors, "bad MASK")
}
if len(errors) > 0 {
return noFrame, c.handleProtocolError(strings.Join(errors, ", "))
}
// 3. Read and parse frame length as per
// https://tools.ietf.org/html/rfc6455#section-5.2
//
// The length of the "Payload data", in bytes: if 0-125, that is the payload
// length.
// - If 126, the following 2 bytes interpreted as a 16-bit unsigned
// integer are the payload length.
// - If 127, the following 8 bytes interpreted as
// a 64-bit unsigned integer (the most significant bit MUST be 0) are the
// payload length. Multibyte length quantities are expressed in network byte
// order.
switch c.readRemaining {
case 126:
p, err := c.read(2)
if err != nil {
return noFrame, err
}
if err := c.setReadRemaining(int64(binary.BigEndian.Uint16(p))); err != nil {
return noFrame, err
}
case 127:
p, err := c.read(8)
if err != nil {
return noFrame, err
}
if err := c.setReadRemaining(int64(binary.BigEndian.Uint64(p))); err != nil {
return noFrame, err
}
}
// 4. Handle frame masking.
if mask {
c.readMaskPos = 0
p, err := c.read(len(c.readMaskKey))
if err != nil {
return noFrame, err
}
copy(c.readMaskKey[:], p)
}
// 5. For text and binary messages, enforce read limit and return.
if frameType == continuationFrame || frameType == TextMessage || frameType == BinaryMessage {
c.readLength += c.readRemaining
// Don't allow readLength to overflow in the presence of a large readRemaining
// counter.
if c.readLength < 0 {
return noFrame, ErrReadLimit
}
if c.readLimit > 0 && c.readLength > c.readLimit {
if err := c.WriteControl(CloseMessage, FormatCloseMessage(CloseMessageTooBig, ""), time.Now().Add(writeWait)); err != nil {
return noFrame, err
}
return noFrame, ErrReadLimit
}
return frameType, nil
}
// 6. Read control frame payload.
var payload []byte
if c.readRemaining > 0 {
payload, err = c.read(int(c.readRemaining))
if err := c.setReadRemaining(0); err != nil {
return noFrame, err
}
if err != nil {
return noFrame, err
}
if c.isServer {
maskBytes(c.readMaskKey, 0, payload)
}
}
// 7. Process control frame payload.
switch frameType {
case PongMessage:
if err := c.handlePong(string(payload)); err != nil {
return noFrame, err
}
case PingMessage:
if err := c.handlePing(string(payload)); err != nil {
return noFrame, err
}
case CloseMessage:
closeCode := CloseNoStatusReceived
closeText := ""
if len(payload) >= 2 {
closeCode = int(binary.BigEndian.Uint16(payload))
if !isValidReceivedCloseCode(closeCode) {
return noFrame, c.handleProtocolError("bad close code " + strconv.Itoa(closeCode))
}
closeText = string(payload[2:])
if !utf8.ValidString(closeText) {
return noFrame, c.handleProtocolError("invalid utf8 payload in close frame")
}
}
if err := c.handleClose(closeCode, closeText); err != nil {
return noFrame, err
}
return noFrame, &CloseError{Code: closeCode, Text: closeText}
}
return frameType, nil
}
- 第一步丢弃之前的帧的数据的原因是我们希望尽可能的从一个干净的状态开始,这些帧既然现在还存在就说明上一次接收的数据已经决定丢弃这些帧,那么这些帧是要被这次的数据忽略的,在这个函数中用到了
io.Discard
结构体,这个结构体很简单,写入它的所有数据都会被丢弃,通过io.CopyN
将当前读取器缓冲区中上一个帧的残留数据全部清除
- 后面的代码都是对协议的实现,理解上面的帧格式表之后,不难理解代码内容