- 响应码是 101;
- “Connection” header 的值为 “Upgrade”,以表明服务器并没有在处理完请求之后把连接断开;
- “Upgrade” header 的值为 “websocket”,以表明服务器接受后面使用 WebSocket 来通信;
- “Sec-WebSocket-Accept” header 的值为,key + WebSocketProtocol.ACCEPT_MAGIC 做 SHA1 hash,然后做 base64 编码,来做服务器接受连接的验证。关于这部分的设计的详细信息,可参考 《WebSocket 协议规范》。
为数据收发做准备的第二步是,初始化用于输入输出的 Source 和 Sink。Source 和 Sink 创建于之前发送 HTTP 请求的时候。这里会阻止在这个连接上再创建新的流。
Streams 是一个 BufferedSource 和 BufferedSink 的 holder:
第三步是调用回调 onOpen()
。
第四步是初始化 Reader 和 Writer:
OkHttp 使用 WebSocketReader
和 WebSocketWriter
来处理数据的收发。在发送数据时将数据组织成帧,在接收数据时则进行反向擦除,同时处理 WebSocket 的控制消息。
WebSocket 的所有数据发送动作,都会在单线程线程池的线程中,通过 WebSocketWriter 执行。在这里会创建 ScheduledThreadPoolExecutor 用于跑数据的发送操作。
WebSocket 协议中主要会传输两种类型的帧:
- 控制帧,主要是用于连接保活的 Ping 帧等;
- 数据载荷帧。在这里会根据用户的配置,调度 Ping 帧周期性地发送。我们在调用 WebSocket 的接口发送数据时,数据并不是同步发送的,而是被放在了一个消息队列中。发送消息的 Runnable 从消息队列中读取数据发送。其中会检查消息队列中是否有数据,如果有的话,会调度发送消息的 Runnable 执行。
第五步是配置 socket 的超时时间为 0,也就是阻塞 IO。
第六步执行 loopReader()
。这实际上是进入了消息读取循环了,也就是数据接收的逻辑了。
2.2 数据发送
我们可以通过 WebSocket 接口的 send(String text)
和 send(ByteString bytes)
分别发送文本的和二进制格式的消息。
可以看到我们调用发送数据的接口时,做的事情主要是将数据格式化,构造消息,放进一个消息队列,然后调度 writerRunnable 执行。
此外,值得注意的是,当消息队列中的未发送数据,超出最大大小限制,WebSocket 连接会被直接关闭。对于发送失败过或被关闭了的 WebSocket,将无法再发送信息。
在 writerRunnable
中会循环调用 writeOneFrame()
逐帧发送数据,直到数据发完,或发送失败。在 WebSocket 协议中,客户端需要发送 4 种类型的帧:
- PING 帧
- PONG 帧
- CLOSE 帧
- MESSAGE 帧
PING 帧用于连接保活,它的发送是在 PingRunnable
中执行的,在初始化 Reader 和 Writer 的时候,就会根据设置调度执行或不执行。
除 PING 帧外的其它三种帧,都在 writeOneFrame()
中发送。PONG 帧是对服务器发过来的 PING 帧的响应,同样用于保活连接。
后面我们在分析连接的保活时会更详细的分析 PING 和 PONG 这两种帧。CLOSE 帧用于关闭连接,稍后我们在分析连接关闭过程时再来详细地分析。
这里我们主要关注用户数据发送的部分。PONG 帧具有最高的发送优先级。在没有 PONG 帧需要发送时,writeOneFrame()
从消息队列中取出一条消息,如果消息不是 CLOSE 帧,则主要通过如下的过程进行发送:
数据发送的过程可以总结如下:
- 创建一个 BufferedSink 用于数据发送;
- 将数据写入前面创建的 BufferedSink 中;
- 关闭 BufferedSink;
- 更新 queueSize 以正确地指示未发送数据的长度;
这里面的玄机主要在创建的 BufferedSink。创建的 Sink 是一个 FrameSink
:
FrameSink
的 write()
会先将数据写入一个 Buffer 中,然后再从这个 Buffer 中读取数据来发送。如果是第一次发送数据,同时剩余要发送的数据小于 8192 字节时,会延迟执行实际的数据发送,等 close()
时刷新。
根据 RealWebSocket
的 writeOneFrame()
的逻辑,在 write()
时,总是写入整个消息的所有数据,因而在 FrameSink
的 write()
中总是不会发送数据的。
writeMessageFrameSynchronized()
将用户数据格式化并发送出去。规范中定义的数据格式如下:
基本结构为:
- 第一个字节是 meta data 控制位,包括 4 位的操作码,用于指明这是否是消息的最后一帧的 FIN 位及三个保留位;
- 第二个字节包括掩码位,和载荷长度或载荷长度指示。只有载荷长度比较小,在 127 以内时,载荷长度才会包含在这个字节中。否则这个字节中将包含载荷长度指示的位;
- 可选的载荷长度。载荷长度大于 127 时,帧中会专门有一些字节来描述载荷的长度。载荷长度具体占用几个字节,因载荷的实际长度而异;
- 可选的掩码字节。客户端发送的帧,设置掩码指示位,并包含四个字节的掩码字节;
- 载荷数据。客户端发送的数据,会将原始的数据与掩码字节做异或之后再发送;
关于帧格式的更详细信息,可以参考 《WebSocket Protocol 规范》。
2.3 数据的接收
如我们前面看到的, 在握手的 HTTP 请求返回之后,会在 HTTP 请求的回调里,启动消息读取循环 loopReader()
:
在这个循环中,不断通过 WebSocketReader
的 processNextFrame()
读取消息,直到收到了关闭连接的消息。
processNextFrame()
先读取 Header 的两个字节,然后根据 Header 的信息,读取数据内容。
在读取 Header 时,读的第一个字节是同步的不计超时时间的。WebSocketReader
从 Header 中,获取到这个帧是不是消息的最后一帧,消息的类型,是否有掩码字节,保留位,帧的长度,以及掩码字节等信息。
WebSocket 通过掩码位和掩码字节来区分数据是从客户端发送给服务器的,还是服务器发送给客户端的。这里会根据协议,对这些信息进行有效性一致性检验,若不一致则会抛出 ProtocolException
。
WebSocketReader
同步读取时的调用栈如下:
通过帧的 Header 确定了是数据帧,则会执行 readMessageFrame()
读取消息帧:
这个过程中,会读取一条消息包含的所有数据帧。按照 WebSocket 的标准,包含用户数据的消息数据帧可以和控制帧交替发送。
但消息之间的数据帧不可以。因而在这个过程中,若遇到了控制帧,则会先读取控制帧进行处理,然后继续读取消息的数据帧,直到读取了消息的所有数据帧。
掩码位和掩码字节,对于客户端而言,发送的数据中包含这些东西,在接收的数据中不包含这些。对于服务器而言,则是在接收的数据中包含这些,发送的数据中不包含。OkHttp 既支持服务器开发,也支持客户端开发,因而可以看到对于掩码位和掩码字节完整的处理。
在一个消息读取完成之后,会通过回调 FrameCallback
将读取的内容通知出去。
这一事件会通知到 RealWebSocket
。
在 RealWebSocket
中,这一事件又被通知到我们在应用程序中创建的回调 WebSocketListener
。
2.4 连接的保活
连接的保活通过 PING 帧和 PONG 帧来实现。如我们前面看到的,若用户设置了 PING 帧的发送周期,在握手的 HTTP 请求返回时,消息读取循环开始前会调度 PingRunnable
周期性的向服务器发送 PING 帧:
在 PingRunnable
中,通过 WebSocketWriter
发送 PING 帧:
PING 帧是一个不包含载荷的控制帧。关于掩码位和掩码字节的设置,与消息的数据帧相同。即客户端发送的帧,设置掩码位,帧中包含掩码字节。服务器发送的帧,不设置掩码位,帧中不包含掩码字节。
通过 WebSocket 通信的双方,在收到对方发来的 PING 帧时,需要用 PONG 帧来回复。在 WebSocketReader
的 readControlFrame()
中可以看到这一点:
PING 帧和 PONG 帧都不带载荷,控制帧读写时对于载荷长度的处理,都是为 CLOSE 帧做的。因而针对 PING 帧和 PONG 帧,除了 Header 外, readControlFrame()
实际上无需再读取任何数据,但它会将这些事件通知出去:
可见在收到 PING 帧的时候,总是会发一个 PONG 帧出去,且通常其没有载荷数据。在收到一个 PONG 帧时,则通常只是记录一下,然后什么也不做。如我们前面所见,PONG 帧在 writerRunnable
中被发送出去:
PONG 帧的发送与 PING 帧的非常相似:
2.5 连接的关闭
连接的关闭,与数据发送的过程颇有几分相似之处。通过 WebSocket
接口的 close(int code, String reason)
我们可以关闭一个 WebSocket 连接:
在执行关闭连接动作前,会先检查一下 close code 的有效性在合法范围内。关于不同 close code 的详细说明,可以参考 《WebSocket 协议规范》。
检查完了之后,会构造一个 Close 消息放入发送消息队列,并调度 writerRunnable
执行。Close 消息可以带有不超出 123 字节的字符串,以作为 Close message,来说明连接关闭的原因。
连接的关闭分为主动关闭和被动关闭。客户端先向服务器发送一个 CLOSE 帧,然后服务器恢复一个 CLOSE 帧,对于客户端而言,这个过程为主动关闭。反之则为对客户端而言则为被动关闭。
在 writerRunnable
执行的 writeOneFrame()
实际发送 CLOSE 帧:
发送 CLOSE 帧也分为主动关闭的发送还是被动关闭的发送。
对于被动关闭,在发送完 CLOSE 帧之后,连接被最终关闭,因而,发送 CLOSE 帧之前,这里会停掉发送消息用的 executor。而在发送之后,则会通过 onClosed()
通知用户。
而对于主动关闭,则在发送前会调度 CancelRunnable
的执行,发送后不会通过 onClosed()
通知用户。
Spring全套教学资料
Spring是Java程序员的《葵花宝典》,其中提供的各种大招,能简化我们的开发,大大提升开发效率!目前99%的公司使用了Spring,大家可以去各大招聘网站看一下,Spring算是必备技能,所以一定要掌握。
目录:
部分内容:
Spring源码
- 第一部分 Spring 概述
- 第二部分 核心思想
- 第三部分 手写实现 IoC 和 AOP(自定义Spring框架)
- 第四部分 Spring IOC 高级应用
基础特性
高级特性 - 第五部分 Spring IOC源码深度剖析
设计优雅
设计模式
注意:原则、方法和技巧 - 第六部分 Spring AOP 应用
声明事务控制 - 第七部分 Spring AOP源码深度剖析
必要的笔记、必要的图、通俗易懂的语言化解知识难点
脚手框架:SpringBoot技术
它的目标是简化Spring应用和服务的创建、开发与部署,简化了配置文件,使用嵌入式web服务器,含有诸多开箱即用的微服务功能,可以和spring cloud联合部署。
Spring Boot的核心思想是约定大于配置,应用只需要很少的配置即可,简化了应用开发模式。
- SpringBoot入门
- 配置文件
- 日志
- Web开发
- Docker
- SpringBoot与数据访问
- 启动配置原理
- 自定义starter
微服务架构:Spring Cloud Alibaba
同 Spring Cloud 一样,Spring Cloud Alibaba 也是一套微服务解决方案,包含开发分布式应用微服务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务。
- 微服务架构介绍
- Spring Cloud Alibaba介绍
- 微服务环境搭建
- 服务治理
- 服务容错
- 服务网关
- 链路追踪
- ZipKin集成及数据持久化
- 消息驱动
- 短信服务
- Nacos Confifig—服务配置
- Seata—分布式事务
- Dubbo—rpc通信
Spring MVC
目录:
部分内容:
mg-TLp1DB88-1714753099775)]
Spring MVC
目录:
[外链图片转存中…(img-blqTHrPf-1714753099775)]
[外链图片转存中…(img-wnk2m5N7-1714753099775)]
[外链图片转存中…(img-sAosZlnt-1714753099776)]
部分内容:
[外链图片转存中…(img-zXBXnUpY-1714753099776)]
[外链图片转存中…(img-wuaEzZTJ-1714753099776)]