从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或者反向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帧头

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
下面详细介绍
- 操作码(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结构的定义
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 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// 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到websocket
阅读本文之前,你最好已经做过一些websocket的简单应用 从http到websocket HTTP101HTTP 轮询、长轮询和流化其他技术1. 服务器发送事件2. SPDY3. web实时通信 互联网简史web和httpWebsocket协议1. 简介2. 初始握手3. 计算响应健值4. 消息格式5. WebSocket关闭握手 实…...
UE5 C++ Widget练习 Button 和 ProgressBar创建血条
一. 1.C创建一个继承Widget类的子类, 命名为MyUserWidget 2.加上Button 和 UserWidget的头文件 #include "CoreMinimal.h" #include "Components/Button.h" #include "Blueprint/UserWidget.h" #include "MyUserWidget.genera…...
抖店无货源违规频发,不能入驻?这个是真的吗?
我是电商珠珠 还没有踏入抖店这个电商行业的新手,单从别人的口中,听说了抖店无货源特别容易违规,还会被扣除全部的保证金,得不偿失之类的话。有的还专门劝诫新手不要做抖店,做了就会亏本之类的话,这搞得人…...
HarmonyOS—开发云数据库
您可以在云侧工程下开发云数据库资源,包括创建对象类型、在对象类型中添加数据条目、部署云数据库。 创建对象类型 对象类型(即ObjectType)用于定义存储对象的集合,不同的对象类型对应的不同数据结构。每创建一个对象类型&#…...
mysql查询某个数据库的数量有多少GB
要查询MySQL数据库中某个数据库(或称为“schema”)所占用的磁盘空间大小(以GB为单位),你可以使用information_schema数据库中的TABLES和DATA_LENGTH、INDEX_LENGTH字段来获取每个表的数据和索引的大小,然后…...
table展示子级踩坑
##elemenui中table通过row中是否有children进行判断是否展示子集,通过设置tree-prop的属性进行设置,子级的children的名字可以根据自己的子级名字进行替换,当然同样可以对数据处理成含有chilren的子级list。 问题: 1.如果是根据后…...
xss过waf的小姿势
今天看大佬的视频学到了几个操作 首先是拆分发可以用self将被过滤的函数进行拆分 如下图我用self将alert拆分成两段依然成功执行 然后学习另一种姿势 <svg id"YWxlcnQoIlhTUyIp"><img src1 οnerrοr"window[eval](atob(document.getElementsByTagNa…...
【六袆 - MySQL】MySQL 5.5及更高版本中,InnoDB是新表的默认存储引擎;
InnoDB 这是一个MySQL组件,结合了高性能和事务处理能力,以确保可靠性、健壮性和并发访问。它体现了ACID设计哲学。它作为一个存储引擎存在,处理使用ENGINEINNODB子句创建的或修改的表。请参阅第14章“InnoDB存储引擎”以获取有关架构细节和管…...
可移植性(兼容性)测试指南
可移植性是指应用程序能够安装到不同的环境中,在不同的环境中使用,甚至可以移动到不同的环境中。当然,前两者对所有系统都很重要。就PC软件而言,鉴于操作系统、共存和互操作应用程序、硬件、带宽可用性等方面的快速变化࿰…...
软件更新快讯-Obsidian更新-1.5.8 linux Appimage直装
更新内容 1.5.8: 从具有相同属性的文件导航时,固定属性不会显示。 修复了Home和End在导航文档顶部和底部时不总是起作用的问题。 Fixed properties not appearing when navigating from a file that has the same properties.Fixed Home and End not a…...
Android Gradle开发与应用 (二) : Groovy基础语法
1. Groovy是什么 Groovy是基于JVM虚拟机的一种动态语言,语法和Java非常相似,并能够无缝地与Java代码集成和互操作,增加了很多动态类型和灵活的特性。(闭包、DSL) 语法和Java非常相似这个特点,意味着,如果我们完全不懂…...
iptables学习
iptables的4表5链的处理流程 一:业务地址请求服务时,首先经过iptables服务,iptables通过校验规则,通过校验是否同意业务访问,规则从上到下,匹配规则都失败了的话,走默认规则 (1&…...
kafka 集群搭建
kafka集群搭建 1. kafka介绍 Apache Kafka是一个开源的流处理平台,由Scala和Java编写1。它是一个分布式、支持分区的(partition)、多副本的(replica),基于zookeeper协调的分布式消息系统2。Kafka的最大特…...
springboot-基础-eclipse配置+helloword示例
备份笔记。所有代码都是2019年测试通过的,如有问题请自行搜索解决! 下一篇:springboot-基础-添加model和controller的简单例子常用注解含义 目录 配置helloword示例新建项目创建文件 配置 spring boot官方有定制版eclipse,也就是…...
关于HTML标签应用教程
简介 HTML(HyperText Markup Language)是用于创建网页结构的标记语言。在本教程中,我们将介绍一些常用的HTML标签,以及它们的用法和示例。 1. HTML基础结构 <!DOCTYPE html> <html> <head><title>页面…...
3. 台阶问题
数楼梯 题目描述 楼梯有 N N N 阶,上楼可以一步上一阶,也可以一步上二阶。 编一个程序,计算共有多少种不同的走法。 输入格式 一个数字,楼梯数。 输出格式 输出走的方式总数。 样例 #1 样例输入 #1 4样例输出 #1 5提示…...
推荐一个 Obsidian 的 ChatGPT 插件
源码地址:https://github.com/nhaouari/obsidian-textgenerator-plugin Text Generator 是目前我使用过的最好的 Obsidian 中的 ChatGPT 功能插件。它旨在智能生成内容,以便轻松记笔记。它不仅可以在 Obsidian 中直接使用 ChatGPT,还提供了优…...
aws的RDS数据库开启慢查询日志
#开启慢日志两个参数 slow_query_log 1 设置为1,来启用慢查询日志 long_query_time 5 (单位秒) sql执行多长时间被定义为慢日志1. 点击RDS然后点击参数组,选择slow_query_log,设置为1【表示开启慢日志】点击保存…...
一文读懂 Python 值传递和引用传递
文章目录 版本前言形参和实参值传递和引用传递Python 变量存储值语义和引用语义值语义引用语义 探讨 Python 值传递和引用传递不可变(immutable)类型可变(mutable)类型案例一案例二 拓展:不可变类型真的不可变…...
Linux进阶——系统安全,重要文件,加固系统的相关配置
目录 引出Linux系统安全一、重要文件二、帐户口令三、权限管理四、日志配置五、服务安全六、其他配置 缓存三兄弟:缓存击穿、穿透、雪崩缓存击穿缓存穿透缓存雪崩 总结 引出 Linux进阶——系统安全,重要文件,加固系统的相关配置 Linux系统安…...
SpringBoot-17-MyBatis动态SQL标签之常用标签
文章目录 1 代码1.1 实体User.java1.2 接口UserMapper.java1.3 映射UserMapper.xml1.3.1 标签if1.3.2 标签if和where1.3.3 标签choose和when和otherwise1.4 UserController.java2 常用动态SQL标签2.1 标签set2.1.1 UserMapper.java2.1.2 UserMapper.xml2.1.3 UserController.ja…...
基于FPGA的PID算法学习———实现PID比例控制算法
基于FPGA的PID算法学习 前言一、PID算法分析二、PID仿真分析1. PID代码2.PI代码3.P代码4.顶层5.测试文件6.仿真波形 总结 前言 学习内容:参考网站: PID算法控制 PID即:Proportional(比例)、Integral(积分&…...
shell脚本--常见案例
1、自动备份文件或目录 2、批量重命名文件 3、查找并删除指定名称的文件: 4、批量删除文件 5、查找并替换文件内容 6、批量创建文件 7、创建文件夹并移动文件 8、在文件夹中查找文件...
【快手拥抱开源】通过快手团队开源的 KwaiCoder-AutoThink-preview 解锁大语言模型的潜力
引言: 在人工智能快速发展的浪潮中,快手Kwaipilot团队推出的 KwaiCoder-AutoThink-preview 具有里程碑意义——这是首个公开的AutoThink大语言模型(LLM)。该模型代表着该领域的重大突破,通过独特方式融合思考与非思考…...
将对透视变换后的图像使用Otsu进行阈值化,来分离黑色和白色像素。这句话中的Otsu是什么意思?
Otsu 是一种自动阈值化方法,用于将图像分割为前景和背景。它通过最小化图像的类内方差或等价地最大化类间方差来选择最佳阈值。这种方法特别适用于图像的二值化处理,能够自动确定一个阈值,将图像中的像素分为黑色和白色两类。 Otsu 方法的原…...
OkHttp 中实现断点续传 demo
在 OkHttp 中实现断点续传主要通过以下步骤完成,核心是利用 HTTP 协议的 Range 请求头指定下载范围: 实现原理 Range 请求头:向服务器请求文件的特定字节范围(如 Range: bytes1024-) 本地文件记录:保存已…...
Linux-07 ubuntu 的 chrome 启动不了
文章目录 问题原因解决步骤一、卸载旧版chrome二、重新安装chorme三、启动不了,报错如下四、启动不了,解决如下 总结 问题原因 在应用中可以看到chrome,但是打不开(说明:原来的ubuntu系统出问题了,这个是备用的硬盘&a…...
k8s业务程序联调工具-KtConnect
概述 原理 工具作用是建立了一个从本地到集群的单向VPN,根据VPN原理,打通两个内网必然需要借助一个公共中继节点,ktconnect工具巧妙的利用k8s原生的portforward能力,简化了建立连接的过程,apiserver间接起到了中继节…...
使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台
🎯 使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台 📌 项目背景 随着大语言模型(LLM)的广泛应用,开发者常面临多个挑战: 各大模型(OpenAI、Claude、Gemini、Ollama)接口风格不统一;缺乏一个统一平台进行模型调用与测试;本地模型 Ollama 的集成与前…...
Device Mapper 机制
Device Mapper 机制详解 Device Mapper(简称 DM)是 Linux 内核中的一套通用块设备映射框架,为 LVM、加密磁盘、RAID 等提供底层支持。本文将详细介绍 Device Mapper 的原理、实现、内核配置、常用工具、操作测试流程,并配以详细的…...
