通信科技网站设计,网站开发技术公司,软件开发工程师访谈报告,静态网站有什么用阅读本文之前#xff0c;你最好已经做过一些websocket的简单应用 从http到websocket HTTP101HTTP 轮询、长轮询和流化其他技术1. 服务器发送事件2. SPDY3. web实时通信 互联网简史web和httpWebsocket协议1. 简介2. 初始握手3. 计算响应健值4. 消息格式5. WebSocket关闭握手 实… 阅读本文之前你最好已经做过一些websocket的简单应用 从http到websocket HTTP101HTTP 轮询、长轮询和流化其他技术1. 服务器发送事件2. SPDY3. web实时通信 互联网简史web和httpWebsocket协议1. 简介2. 初始握手3. 计算响应健值4. 消息格式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客户端
特性TCPHTTPWebSocket寻址IP地址和端口URLURL并发传输全双工半双工全双工内容字节流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帧头 0 1 2 30 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 len126/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关闭客户端或者服务器向对方发送关闭握手9ping客户端或者服务器向对方发送ping10pong客户端或者服务器向对方发送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意外情况当应用程序由于不可预见的原因无法继续处理连接时发送这一代码1015TLS失败(保留)不要发送这个代码。WebSocket API用这个代码表示TLS在WebSocket握手之前失败
实现
我们分析一下golang的websocket包https://github.com/gorilla/websocket来看此协议是如何实现的重点关注conn.go文件我们先看一下Conn结构的定义
type Conn struct {conn net.Conn // 底层网络连接isServer bool // 如果这个连接作为服务器端的连接则为true如果是客户端则为falsesubprotocol 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 overflowreadRemaining 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 // 用于创建新的解压缩读取器
}之后我们来看一个关键的函数ReadMessage
// 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.ReadermessageType, r, err c.NextReader()if err ! nil {return messageType, nil, err}p, err io.ReadAll(r)return messageType, p, err
}ReadMessage函数是我们经常会用到的函数它用来接收WebSocket消息一般接收到来自浏览器端的每条消息我们会从p数组中获取可以看到这个函数的核心功能是NextReader()方法实现的下面是这个方法
// 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 applications 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 nilc.readLength 0for c.readErr nil {frameType, err : c.advanceFrame()if err ! nil {c.readErr errbreak}if frameType TextMessage || frameType BinaryMessage {c.messageReader messageReader{c}c.reader c.messageReaderif 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.readErrCountif c.readErrCount 1000 {panic(repeated read on failed websocket connection)}return noFrame, nil, c.readErr
}核心是advanceFrame函数这个函数内部实现了WebSocket协议的内容此处可以对照帧格式来阅读 0 1 2 30 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 len126/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使得中间人无法进行持续解密接下来的部分就都是传输的数据了
// Read methodsfunc (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 []stringp, err : c.read(2)if err ! nil {return noFrame, err}frameType : int(p[0] 0xf)final : p[0]finalBit ! 0rsv1 : p[0]rsv1Bit ! 0rsv2 : p[0]rsv2Bit ! 0rsv3 : p[0]rsv3Bit ! 0mask : p[1]maskBit ! 0if err : c.setReadRemaining(int64(p[1] 0x7f)); err ! nil {return noFrame, err}c.readDecompress falseif 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 finalcase continuationFrame:if c.readFinal {errors append(errors, continuation after FIN)}c.readFinal finaldefault: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 0p, 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// Dont 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 []byteif 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 : CloseNoStatusReceivedcloseText : 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将当前读取器缓冲区中上一个帧的残留数据全部清除后面的代码都是对协议的实现理解上面的帧格式表之后不难理解代码内容