从http到websocket

阅读本文之前,你最好已经做过一些websocket的简单应用

从http到websocket

  • HTTP101
  • HTTP 轮询、长轮询和流化
  • 其他技术
    • 1. 服务器发送事件
    • 2. SPDY
    • 3. web实时通信
  • 互联网简史
  • web和http
  • Websocket协议
    • 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或者反向AJAXComet延长HTTP响应的完成,直到服务器有需要发送给客户端的内容,这种技术常常称作“挂起GET”或“搁置POST”。但是当信息量很大的时候,长轮询相对于传统轮询并没有明显的性能优势,因为客户端必须频繁的重连到服务器以读取新信息,造成网络的表现和快速轮询相同。长轮询的另一个问题是缺乏标准实现
  • 在流化技术中,客户端发送一个请求,服务器发送并维护一个持续更新和保持打开(可以是无限或者规定的时间段)的开放响应。每当服务器有需要交付给客户端的信息时,它就更新响应。似乎这是一种能够适应不可预测的信息交付的极佳方案,但是服务器从不发出完成HTTP响应的请求,从而使连接一直保持打开。在这种情况下,代理和防火墙可能缓存响应,导致信息交付的延迟增加。因此,许多流化的尝试对于存在防火墙和代理的网络时不友好的
  • 上述几种方法还有一些问题,例如冗余的HTTP首标数据和延迟、客户端必须等待请求返回才能发出后续的请求,这会显著增加延迟

其他技术

1. 服务器发送事件

  • 如果你的服务主要向其客户端广播或者推送消息,而不需要任何交互,可能使用服务器发送事件(Server-Sent Events SSE)提供的EventSource API是个好的选择。SSEHTML5规范的一部分,加强了某些Comet技术,可以将SSE当作一种HTTP轮询、长轮询和流化的公用可互操作语法使用。利用SSE,你可以得到自动重连、事件ID等功能,但是这种方式只支持文本数据

2. SPDY

  • SPDY(音同"Speedy")是Google开发的一种网络协议,本质上扩充了HTTP协议,通过压缩HTTP首标和多路复用等手段改进HTTP请求性能,也就是说,它相当于对于HTTP进行的增量改进,改正了许多HTTP的非本质问题,增加了多路复用、工作管道(working pipling)和其他有用的改进。而websocketHTTP之间的不同是架构性的,不是增量的,可以将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读取整个资源
  • 利用JavaScriptXMLHttpRequest API,人们开发出了一组称为AJAX的技术,这项技术能够使应用程序在每次交互期间不会有不连贯的过渡。AJAX使应用程序只读取感兴趣去的资源数据,并在没有导航的情况下更新页面。AJAX使用的网络协议仍然是HTTP;尽管名为XMLHttpRequest,数据也只是有时使用XML格式,而不是始终使用该格式
  • 本质上,HTTP用其内置的文本支持、URLHTTPS使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 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

下面详细介绍

  1. 操作码(opcode)
  • 每条Websocket消息都有一个指定消息载荷类型的操作码。操作码由帧头的第一个字节中最后4 bit组成,如下表所示
操作码消息载荷类型描述
1文本消息的数据类型为文本
2二进制消息的数据类型为二进制
8关闭客户端或者服务器向对方发送关闭握手
9ping客户端或者服务器向对方发送ping
10pong客户端或者服务器向对方发送pong
  • 4 bit的操作码有16种可能取值,WebSocket协议只定义了5种操作码,剩余的操作码保留用于未来的扩展
  1. 长度
  • WebSocket协议使用可变位数来变码帧长度,这样,小的消息就能使用紧凑的编码,协议仍然可以携带中型甚至非常大的消息。对于小于126字节的消息,长度用帧头前两个字节之一来表示。对于126~216字节的消息,使用额外的两个字节表示长度。对于大于216字节的消息,长度为8字节。该长度编码保存于帧头第二个字节的最后7位。该字段种126和127两个值被当作特殊的信号,表示需要后面的字节才能完成长度编码
  1. 编码文本
  • WebSocket文本消息用8位UCS转换格式(UTF-8)编码。UTF-8是用于Unicode的变长编码,向后兼容7位的ASCII,也是WebSocket文本消息允许的唯一编码。坚持使用UTF-8编码避免了大量的“普通文本”格式以及协议中的不同编码对互操作性的妨害
  1. 屏蔽(或者叫掩码)
  • 从浏览器向服务器发送的WebSocket帧内容进行了“屏蔽”,以混淆其内容。屏蔽的目的不是阻止窃听,而是为了不常见的安全原因,以及改进和现有的HTTP代理的兼容性。
  • 帧头的第二个字节的第一位表示该帧是否进行了屏蔽,WebSocket协议要求客户端屏蔽发送的所有帧。如果有屏蔽,所用的掩码将占据帧头扩展长度部分后的4个字节
  • WebSocket服务器接收的每个载荷在处理之前首先被解除屏蔽。解除屏蔽之后,服务器得到原始消息内容:二进制消息可以直接交付;文本消息将进行UTF-8编码,并通过服务器API输出字符串
  1. 多帧消息
  • 帧格式中的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 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 = 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.readErrCount++if 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 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 2641,所以一般不会有超过一个帧的数据
  • 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// 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 []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将当前读取器缓冲区中上一个帧的残留数据全部清除
  • 后面的代码都是对协议的实现,理解上面的帧格式表之后,不难理解代码内容

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/711833.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Redis 缓存数据库

redis 中文网 http://www.redis.cn/ redis.net.cn 两种数据库阵营 1.关系型数据库 MySQL Oracle DB2 SQL Server 等基于二维表结构存储数据的文件型磁盘数据库 缺点: 因为数据库的特征是磁盘文件型数据库, 就造成每次查询都有IO操作, 海量数据查询速度较慢 2.NoSQL数据库 …

lv20 QT 常用控件 2

1 QT GUI 类继承简介 布局管理器 输出控件 输入控件 按钮 容器 2 按钮示例 #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QCheckBox> #include <QLineEdit> #include <QPushButton>class Widget : public QWidget {Q_OBJECTpublic…

击鼓传花游戏

有N个小朋友围成一圈玩击鼓传花游戏&#xff0c;将小朋友编号为1-N&#xff0c;从1号开始传花&#xff0c;每次传3个&#xff0c;拿到花的小朋友表演节目后退出。任给N&#xff0c;问最后一个表演的小朋友编号是多少&#xff1f;例如&#xff1a;输入5&#xff0c;从1号开始传花…

基于springboot+vue的共享汽车管理系统(前后端分离)

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、阿里云专家博主、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战&#xff0c;欢迎高校老师\讲师\同行交流合作 ​主要内容&#xff1a;毕业设计(Javaweb项目|小程序|Pyt…

3d模型版本转换器注意事项---模大狮模型网

在使用3D模型版本转换器时&#xff0c;有一些注意事项可以帮助您顺利完成模型转换并避免不必要的问题&#xff1a; 数据完整性&#xff1a;在进行模型转换之前&#xff0c;确保您的原始3D模型文件没有损坏或缺失数据。损坏的文件可能导致转换器无法正常处理或输出错误的结果。 …

力扣经典题目解析--滑动窗口最大值

原题地址: . - 力扣&#xff08;LeetCode&#xff09; 给你一个整数数组 nums&#xff0c;有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。 返回 滑动窗口中的最大值 。 示例 1&#xff1a;…

小程序自定义组件

自定义组件 1. 创建-注册-使用组件 组件介绍 小程序目前已经支持组件化开发&#xff0c;可以将页面中的功能模块抽取成自定义组件&#xff0c;以便在不同的页面中重复使用&#xff1b; 也可以将复杂的页面拆分成多个低耦合的模块&#xff0c;有助于代码维护。 开发中常见的…

111790-37-5 ,生物素-氨基,一种生物素化合物,可与-NHS、-COOH反应

您好&#xff0c;欢迎来到新研之家 文章关键词&#xff1a;111790-37-5 &#xff0c;生物素-氨基&#xff0c;生物素氨基&#xff0c;Biotin-NH2&#xff0c;Biotin-amine 一、基本信息 【产品简介】&#xff1a;Biotin-NH2 provides a convenient biotinylation method for…

OSCP靶场--DVR4

OSCP靶场–DVR4 考点(1.windows&#xff1a;路径遍历获取私钥getshell 2.ssh shell中runas切换用户) 1.nmap扫描 ┌──(root㉿kali)-[~/Desktop] └─# nmap -sV -sC -p- 192.168.161.179 --min-rate 2000 Starting Nmap 7.92 ( https://nmap.org ) at 2024-02-29 07:14 EST…

Springboot接口参数校验

在设计接口时我们通常需要对接口中的非法参数做校验&#xff0c;以降低在程序运行时因为一些非法参数而导致程序发生异常的风险&#xff0c;例如登录的时候需要校验用户名密码是否为空&#xff0c;创建用户的时候需要校验邮件、手机号码格式是否准确。如果在代码中对接口参数一…

系统集成Prometheus+Grafana

根据产品需求在自己的系统中添加一个系统监控的页面&#xff0c;其中有主机信息的显示&#xff0c;也有一些业务信息的显示。调研后的方案是 主机信息通过Prometheus采集和存储&#xff0c;业务信息通过自己系统的调度任务统计后存储在Mysql中&#xff0c;使用Grafana对接Prome…

ICLR 2024|ReLU激活函数的反击,稀疏性仍然是提升LLM效率的利器

论文题目&#xff1a; ReLU Strikes Back: Exploiting Activation Sparsity in Large Language Models 论文链接&#xff1a; https://arxiv.org/abs/2310.04564 参数规模超过十亿&#xff08;1B&#xff09;的大型语言模型&#xff08;LLM&#xff09;已经彻底改变了现阶段人工…

gcc和g++的区别,如何看自己的编译器支持的C++的版本

gcc和g的区别 用一句话来说&#xff0c;就是gcc将程序视为c语言的&#xff0c;g将程序视为C的 gcc和g的区别主要在于它们处理不同后缀的文件类型、编译和连接阶段的不同调用方式&#xff0c;以及它们对C特性的支持方式。以下是详细介绍&#xff1a;123 文件类型。gcc将后缀为…

Pinia使用

官方地址&#xff1a;Pinia | The intuitive store for Vue.js (vuejs.org)https://pinia.vuejs.org/ 1.安装 npm install pinia npm install pinia-plugin-persistedstate Pinia是一个基于Vue 3的状态管理库&#xff0c;它使得管理Vue的全局状态变得更加容易和直观。 而…

自定义el-dialog的样式

实现效果&#xff1a; 样式代码如下&#xff1a;&#xff08;可以写在common.scss文件夹中&#xff09; .el-dialog__header {padding: 16px 20px;border-bottom: 1px solid #DCDFE6;display: flex;align-items: center;.el-dialog__title {font-size: 16px;position: relativ…

utniy urp shinyssrr插件使用

文章目录 前言步骤1首先在URP的配置文件里添加SSR后处理2 修改RenderingPath为延迟渲染3 启用深度纹理4 为物体添加脚本 插件下载 前言 用来实现屏幕空间反射效果 unity 版本为2021.3.8LTS&#xff0c;低版本的untiy URP的参数设置位置z可能会不同 步骤 1首先在URP的配置文件…

1028. 从先序遍历还原二叉树(三种方法:栈+递归+集合)

文章目录 1028. 从先序遍历还原二叉树&#xff08;三种方法&#xff1a;栈递归集合&#xff09;一、栈 while迭代1.思路2.代码 二、递归法1.思路2.代码 三、集合存储1.思路2.代码 1028. 从先序遍历还原二叉树&#xff08;三种方法&#xff1a;栈递归集合&#xff09; 一、栈 wh…

hive报错:FAILED: NullPointerException null

发现问题 起因是我虚拟机的hive不管执行什么命令都报空指针异常的错误 我也在网上找了很多相关问题的资料&#xff0c;发现都不是我这个问题的解决方法&#xff0c;后来在hive官网上与hive 3.1.3版本相匹配的hadoop版本是3.x的版本&#xff0c;而我的hadoop版本还是2.7.2的版本…

HTTPS的加密过程

文章目录 前言一、为什么需要加密&#xff1f;二、只用对称加密可以吗&#xff1f;三、只使用非对称加密四、双方都使用非对称加密五、使用非对称加密对称加密六、引入证书1.如何放防止数字证书被篡改&#xff1f;2.中间人有可能篡改该证书吗&#xff1f;3.中间人有可能掉包该证…

【YOLO系列】YOLOv9论文超详细解读(翻译 +学习笔记)

前言 时隔一年&#xff0c;YOLOv8还没捂热&#xff0c;YOLO系列最新版本——YOLOv9 终于闪亮登场&#xff01; YOLOv9的一作和v7一样。v4也有他。 他于2017年获得台湾省National Central University计算机科学与信息工程博士学位&#xff0c;现在就职于该省Academia Sinica的…