2024年最新源码角度,分析OkHttp实现WebSocket---握手-保活-数据处理---,java程序员面试常问的问题

总结

虽然我个人也经常自嘲,十年之后要去成为外卖专员,但实际上依靠自身的努力,是能够减少三十五岁之后的焦虑的,毕竟好的架构师并不多。

架构师,是我们大部分技术人的职业目标,一名好的架构师来源于机遇(公司)、个人努力(吃得苦、肯钻研)、天分(真的热爱)的三者协作的结果,实践+机遇+努力才能助你成为优秀的架构师。

如果你也想成为一名好的架构师,那或许这份Java成长笔记你需要阅读阅读,希望能够对你的职业发展有所帮助。

image

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

示例代码如下:

这个过程与发送 HTTP 请求的过程有许多相似之处,它们都需要创建 OkHttpClient 和 Request。然而它们不同的地方更多:

  1. WebSocket 请求通过 WebSocketListener 来接收连接的状态和活动,而 HTTP 请求则通过 Callback。同时请求的 URL 的 scheme 是 “ws” 或者是 “wss” (TLS 之上的 WebSocket),而不是 HTTP 的 “http” 和 “https”;
  2. HTTP 请求的连接建立及执行需要基于 Request 和回调创建 Call,并调用 Call 的方法手动进行;而对于 WebSocket 请求,则在基于 Request 和回调创建 WebSocket 的时候,OkHttp 会自动发起连接建立的过程;
  3. 这也是 WebSocket 与 HTTP 最大的不同。对于 WebSocket,我们可以保存 WebSocket 对象,并在后续多次通过该对象向服务器发送数据;
  4. 通过回调可以获得更多 WebSocket 的状态变化。在连接建立、收到服务器发送回来的消息、服务器要关闭连接,以及出现 error 时,都能得到通知。不像 HTTP 请求那样,只在最后得到一个请求成功或者失败的结果;

后两点正是 WebSocket 全双工连接的体现。

二、OkHttp 的 WebSocket 实现

接着我们来看 OkHttp 的 WebSocket 实现。

WebSocket 包含两个部分,分别是握手和数据传输,数据传输又包括数据的发送,数据的接收,连接的保活,以及连接的关闭等,我们将分别分析这些过程。

2.1 连接握手

创建 WebSocket 的过程如下:

在这里会创建一个 RealWebSocket 对象,然后执行其 connect() 方法建立连接。RealWebSocket 对象的创建过程如下:

这里最主要的是初始化了 key,以备后续连接建立及握手之用。Key 是一个 16 字节长的随机数经过 Base64 编码得到的。此外还初始化了 writerRunnable 等。

连接建立及握手过程如下:

连接建立及握手的过程主要是向服务器发送一个 HTTP 请求。这个 HTTP 请求的特别之处在于,它包含了如下的一些 Headers:

其中 Upgrade 和 Connection header 向服务器表明,请求的目的就是要将客户端和服务器端的通讯协议从 HTTP 协议升级到 WebSocket 协议,同时在请求处理完成之后,连接不要断开。

Sec-WebSocket-Key header 值正是我们前面看到的 key,它是 WebSocket 客户端发送的一个 base64 编码的密文,要求服务端必须返回一个对应加密的 “Sec-WebSocket-Accept” 应答,否则客户端会抛出 “Error during WebSocket handshake” 错误,并关闭连接。

来自于 HTTP 服务器的响应到达的时候,即是连接建立大功告成的时候,也就是热豆腐熟了的时候。

然而,响应到达时,尽管连接已经建立,还要为数据的收发做一些准备。这些准备中的第一步就是检查 HTTP 响应:


根据 WebSocket 的协议,服务器端用如下响应,来表示接受建立 WebSocket 连接的请求:

  1. 响应码是 101;
  2. “Connection” header 的值为 “Upgrade”,以表明服务器并没有在处理完请求之后把连接断开;
  3. “Upgrade” header 的值为 “websocket”,以表明服务器接受后面使用 WebSocket 来通信;
  4. “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 协议中主要会传输两种类型的帧:

  1. 控制帧,主要是用于连接保活的 Ping 帧等;
  2. 数据载荷帧。在这里会根据用户的配置,调度 Ping 帧周期性地发送。我们在调用 WebSocket 的接口发送数据时,数据并不是同步发送的,而是被放在了一个消息队列中。发送消息的 Runnable 从消息队列中读取数据发送。其中会检查消息队列中是否有数据,如果有的话,会调度发送消息的 Runnable 执行。

第五步是配置 socket 的超时时间为 0,也就是阻塞 IO。

第六步执行 loopReader()。这实际上是进入了消息读取循环了,也就是数据接收的逻辑了。

2.2 数据发送

我们可以通过 WebSocket 接口的 send(String text) 和 send(ByteString bytes) 分别发送文本的和二进制格式的消息。

可以看到我们调用发送数据的接口时,做的事情主要是将数据格式化,构造消息,放进一个消息队列,然后调度 writerRunnable 执行。

此外,值得注意的是,当消息队列中的未发送数据,超出最大大小限制,WebSocket 连接会被直接关闭。对于发送失败过或被关闭了的 WebSocket,将无法再发送信息。

在 writerRunnable 中会循环调用 writeOneFrame() 逐帧发送数据,直到数据发完,或发送失败。在 WebSocket 协议中,客户端需要发送 4 种类型的帧:

  1. PING 帧
  2. PONG 帧
  3. CLOSE 帧
  4. MESSAGE 帧

PING 帧用于连接保活,它的发送是在 PingRunnable 中执行的,在初始化 Reader 和 Writer 的时候,就会根据设置调度执行或不执行。

除 PING 帧外的其它三种帧,都在 writeOneFrame() 中发送。PONG 帧是对服务器发过来的 PING 帧的响应,同样用于保活连接。

后面我们在分析连接的保活时会更详细的分析 PING 和 PONG 这两种帧。CLOSE 帧用于关闭连接,稍后我们在分析连接关闭过程时再来详细地分析。

这里我们主要关注用户数据发送的部分。PONG 帧具有最高的发送优先级。在没有 PONG 帧需要发送时,writeOneFrame() 从消息队列中取出一条消息,如果消息不是 CLOSE 帧,则主要通过如下的过程进行发送:

数据发送的过程可以总结如下:

  1. 创建一个 BufferedSink 用于数据发送;
  2. 将数据写入前面创建的 BufferedSink 中;
  3. 关闭 BufferedSink;
  4. 更新 queueSize 以正确地指示未发送数据的长度;

这里面的玄机主要在创建的 BufferedSink。创建的 Sink 是一个 FrameSink

image.png

FrameSink 的 write() 会先将数据写入一个 Buffer 中,然后再从这个 Buffer 中读取数据来发送。如果是第一次发送数据,同时剩余要发送的数据小于 8192 字节时,会延迟执行实际的数据发送,等 close() 时刷新。

根据 RealWebSocket 的 writeOneFrame() 的逻辑,在 write() 时,总是写入整个消息的所有数据,因而在 FrameSink 的 write() 中总是不会发送数据的。

writeMessageFrameSynchronized() 将用户数据格式化并发送出去。规范中定义的数据格式如下:

基本结构为:

  1. 第一个字节是 meta data 控制位,包括 4 位的操作码,用于指明这是否是消息的最后一帧的 FIN 位及三个保留位;
  2. 第二个字节包括掩码位,和载荷长度或载荷长度指示。只有载荷长度比较小,在 127 以内时,载荷长度才会包含在这个字节中。否则这个字节中将包含载荷长度指示的位;
  3. 可选的载荷长度。载荷长度大于 127 时,帧中会专门有一些字节来描述载荷的长度。载荷长度具体占用几个字节,因载荷的实际长度而异;
  4. 可选的掩码字节。客户端发送的帧,设置掩码指示位,并包含四个字节的掩码字节;
  5. 载荷数据。客户端发送的数据,会将原始的数据与掩码字节做异或之后再发送;

关于帧格式的更详细信息,可以参考 《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 连接的保活

笔者福利

以下是小编自己针对马上即将到来的金九银十准备的一套“面试宝典”,不管是技术还是HR的问题都有针对性的回答。

有了这个,面试踩雷?不存在的!

回馈粉丝,诚意满满!!!




本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

29497)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值