OkHttp WebSocket 实现详解:数据传输、帧结构与组件关系

OkHttp WebSocket 实现详解:数据传输、帧结构与组件关系

1. 整体架构与数据流

WebSocket通信在OkHttp中由多个组件协同完成,下图展示了这些组件之间的关系和数据流向:

flowchart TD A["应用层\n(用户代码)"] <-->|发送/接收消息| B[RealWebSocket] B <-->|控制| D[Socket] B -->|编码/解码| C subgraph 编码解码层 C[["WebSocketWriter\nWebSocketReader"]] end C -->|写入帧| D D -->|读取帧| C C -->|使用| E[Okio\nSource/Sink/Buffer] D -->|字节流| E F[WebSocket服务器] --> D

数据发送流程

  1. 应用层调用 → 用户调用WebSocket.send(String/ByteString)
  2. RealWebSocket处理 → 将消息排队并安排写入
  3. WebSocketWriter编码 → 将消息转换为WebSocket帧
  4. Okio写入 → 通过BufferedSink将帧写入Socket
  5. 网络传输 → 数据通过网络发送到服务器

数据接收流程

  1. 网络接收 → Socket接收来自服务器的数据
  2. Okio读取 → 通过BufferedSource从Socket读取数据
  3. WebSocketReader解码 → 将字节流解析为WebSocket帧
  4. RealWebSocket处理 → 处理帧并触发相应回调
  5. 应用层接收 → 用户通过WebSocketListener接收消息

2. 核心数据结构

2.1 WebSocket帧结构

WebSocket协议使用帧(Frame)作为通信的基本单位,每个帧的结构如下:

lua 复制代码
 0                   1                   2                   3
 0 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 ...                |
+---------------------------------------------------------------+

字段说明:

  • FIN (1 bit): 标识这是消息的最后一个片段
  • RSV1-3 (3 bits): 保留位,通常为0,RSV1用于压缩标志
  • Opcode (4 bits) : 操作码,定义帧的类型
    • 0x0: 继续帧
    • 0x1: 文本帧
    • 0x2: 二进制帧
    • 0x8: 关闭帧
    • 0x9: Ping帧
    • 0xA: Pong帧
  • MASK (1 bit): 标识是否使用掩码
  • Payload length (7+16/64 bits): 有效载荷长度
  • Masking-key (32 bits): 掩码密钥(如果MASK=1)
  • Payload Data: 实际数据内容

2.2 Okio核心组件

Buffer类
java 复制代码
public final class Buffer implements BufferedSource, BufferedSink {
  // 链表头节点
  Segment head;
  // 当前缓冲区大小
  long size;

  // 主要方法
  public Buffer write(byte[] source) { ... }
  public Buffer writeUtf8(String string) { ... }
  public byte readByte() { ... }
  public String readUtf8() { ... }
  // ...更多读写方法
}

Buffer使用分段链表结构存储数据:

flowchart LR subgraph Segment1["Segment"] S1D["data[8KB]"] S1P["pos: 0"] S1L["limit: 8K"] end subgraph Segment2["Segment"] S2D["data[8KB]"] S2P["pos: 0"] S2L["limit: 4K"] end subgraph Segment3["Segment"] S3D["data[8KB]"] S3P["pos: 0"] S3L["limit: 2K"] end Segment1 --> Segment2 Segment2 --> Segment3
Segment类
java 复制代码
final class Segment {
  // 每个段的大小(8KB)
  static final int SIZE = 8192;

  // 数据数组
  final byte[] data;

  // 有效数据起始位置
  int pos;

  // 有效数据结束位置
  int limit;

  // 是否共享(用于写时复制)
  boolean shared;

  // 是否可被其他Buffer所有
  boolean owner;

  // 链表指针
  Segment next;
  Segment prev;
}
Source和Sink接口
java 复制代码
public interface Source extends Closeable {
  // 从此源读取字节到sink
  long read(Buffer sink, long byteCount) throws IOException;
  // 设置超时
  Timeout timeout();
  // 关闭源
  void close() throws IOException;
}

public interface Sink extends Closeable, Flushable {
  // 从source写入字节到此接收器
  void write(Buffer source, long byteCount) throws IOException;
  // 刷新缓冲数据
  void flush() throws IOException;
  // 设置超时
  Timeout timeout();
  // 关闭接收器
  void close() throws IOException;
}

2.3 WebSocketReader/Writer类

WebSocketReader
java 复制代码
final class WebSocketReader {
  // 用于读取数据的缓冲源
  private final BufferedSource source;
  // 是否是客户端模式
  private final boolean isClient;
  // 消息回调接口
  private final FrameCallback frameCallback;
  // WebSocket扩展
  private final WebSocketExtensions extensions;
  // 消息解压缩器
  private final MessageInflater messageInflater;

  // 帧状态变量
  private boolean closed;
  private int opcode;
  private long frameLength;
  private boolean isFinalFrame;
  private boolean isMasked;
  private boolean isCompressed;

  // 缓冲区
  private final Buffer controlFrameBuffer = new Buffer();
  private final Buffer messageFrameBuffer = new Buffer();

  // 主要方法
  void processNextFrame() throws IOException { ... }
  private void readHeader() throws IOException { ... }
  private void readControlFrame() throws IOException { ... }
  private void readMessageFrame() throws IOException { ... }
}
WebSocketWriter
java 复制代码
final class WebSocketWriter {
  // 用于写入数据的缓冲接收器
  private final BufferedSink sink;
  // 是否是客户端模式
  private final boolean isClient;
  // 随机数生成器(用于掩码)
  private final Random random;
  // 写入锁
  private final Object writerLock = new Object();
  // 是否已关闭
  private boolean closed;
  // 消息压缩器
  private final MessageDeflater messageDeflater;

  // 缓冲区
  private final Buffer buffer = new Buffer();
  private final Buffer messageBuffer = new Buffer();
  private final byte[] maskKey;
  private final Buffer maskBuffer;

  // 主要方法
  void writeMessageFrame(int formatOpcode, ByteString data) throws IOException { ... }
  void writeControlFrame(int opcode, ByteString payload) throws IOException { ... }
  void writePing(ByteString payload) throws IOException { ... }
  void writePong(ByteString payload) throws IOException { ... }
  void writeClose(int code, ByteString reason) throws IOException { ... }
}

3. 详细数据传输流程

3.1 发送消息流程

sequenceDiagram participant App as 应用代码 participant RWS as RealWebSocket participant WSW as WebSocketWriter participant Socket App->>RWS: send(message) RWS->>WSW: writeMessageFrame() Note over WSW: 1. 准备帧头 Note over WSW: 2. 可选压缩 Note over WSW: 3. 应用掩码(如果是客户端) WSW->>Socket: 4. 写入Socket Note over Socket: 5. 网络传输

详细步骤:

  1. 应用调用发送方法

    java 复制代码
    webSocket.send("Hello, WebSocket!");
    // 或
    webSocket.send(ByteString.of("binary data".getBytes()));
  2. RealWebSocket处理发送请求

    java 复制代码
    public boolean send(String text) {
      if (text == null) throw new NullPointerException("text == null");
      return send(ByteString.encodeUtf8(text), OPCODE_TEXT);
    }
    
    private boolean send(ByteString data, int formatOpcode) {
      // 检查状态和大小限制
      synchronized (this) {
        if (closed) return false;
        // 将消息加入队列
        messageQueue.add(new Message(formatOpcode, data));
        // 检查队列大小
        long queueSize = queueSize + data.size();
        if (queueSize > MAX_QUEUE_SIZE) {
          close(CLOSE_CLIENT_GOING_AWAY, null);
          return false;
        }
        queueSize = queueSize;
      }
      // 安排写入操作
      runWriter();
      return true;
    }
  3. WebSocketWriter编码消息

    java 复制代码
    void writeMessageFrame(int formatOpcode, ByteString data) throws IOException {
      synchronized (writerLock) {
        // 准备缓冲区
        messageBuffer.write(data);
    
        // 可选压缩
        boolean compress = false;
        if (messageDeflater != null && data.size() >= MIN_COMPRESS_SIZE) {
          Buffer compressedMessage = new Buffer();
          messageDeflater.deflate(messageBuffer, compressedMessage);
          if (compressedMessage.size() < data.size()) {
            compress = true;
            messageBuffer = compressedMessage;
          }
        }
    
        // 计算帧大小
        long frameSize = messageBuffer.size();
    
        // 写入帧头
        byte b0 = (byte) (FIN_FLAG | formatOpcode);
        if (compress) {
          b0 |= RSV1_FLAG; // 设置压缩标志
        }
        buffer.writeByte(b0);
    
        // 写入长度和掩码标志
        byte b1 = isClient ? (byte) MASK_FLAG : 0;
    
        // 根据长度选择合适的编码方式
        if (frameSize <= PAYLOAD_SHORT_MAX) {
          buffer.writeByte(b1 | (byte) frameSize);
        } else if (frameSize <= PAYLOAD_MEDIUM_MAX) {
          buffer.writeByte(b1 | PAYLOAD_SHORT);
          buffer.writeShort((int) frameSize);
        } else {
          buffer.writeByte(b1 | PAYLOAD_LONG);
          buffer.writeLong(frameSize);
        }
    
        // 如果是客户端,生成并写入掩码密钥
        if (isClient) {
          random.nextBytes(maskKey);
          buffer.write(maskKey);
    
          // 应用掩码
          WebSocketProtocol.toggleMask(messageBuffer.data(), 0, frameSize, maskKey, 0);
        }
    
        // 写入消息内容
        buffer.write(messageBuffer, frameSize);
    
        // 刷新到底层Socket
        sink.write(buffer, buffer.size());
        sink.flush();
      }
    }
  4. 数据通过Socket发送

    • 编码后的WebSocket帧通过BufferedSink写入Socket
    • Socket将数据发送到网络层
    • 数据通过网络传输到服务器

3.2 接收消息流程

sequenceDiagram participant App as 应用代码 participant RWS as RealWebSocket participant WSR as WebSocketReader participant Socket Note over Socket: 1. 接收数据 Socket->>WSR: 2. 读取数据 Note over WSR: 3. 解析帧头 Note over WSR: 4. 读取帧内容 Note over WSR: 5. 移除掩码(如果有) Note over WSR: 6. 可选解压缩 WSR->>RWS: 7. 回调消息 RWS->>App: 8. 通知应用

详细步骤:

  1. Socket接收数据

    • 服务器发送WebSocket帧
    • Socket接收网络数据
  2. WebSocketReader读取数据

    java 复制代码
    void processNextFrame() throws IOException {
      // 读取帧头
      readHeader();
    
      if (isControlFrame()) {
        // 处理控制帧(PING, PONG, CLOSE)
        readControlFrame();
      } else {
        // 处理数据帧(TEXT, BINARY)
        readMessageFrame();
      }
    }
  3. 解析帧头

    java 复制代码
    private void readHeader() throws IOException {
      if (closed) throw new IOException("closed");
    
      // 读取第一个字节,包含FIN标志和操作码
      int b0 = source.readByte() & 0xff;
      isFinalFrame = (b0 & 0x80) != 0;
      isCompressed = (b0 & 0x40) != 0;
    
      // 检查保留位
      if ((b0 & 0x20) != 0 || (b0 & 0x10) != 0) {
        throw new ProtocolException("Reserved flags are unsupported.");
      }
    
      // 提取操作码
      opcode = b0 & 0xf;
    
      // 读取第二个字节,包含MASK标志和有效载荷长度
      int b1 = source.readByte() & 0xff;
      isMasked = (b1 & 0x80) != 0;
    
      // 提取有效载荷长度
      frameLength = b1 & 0x7f;
    
      // 处理扩展长度
      if (frameLength == 126) {
        frameLength = source.readShort() & 0xffffL; // 2字节长度
      } else if (frameLength == 127) {
        frameLength = source.readLong(); // 8字节长度
      }
    
      // 如果帧使用掩码,读取掩码密钥
      if (isMasked) {
        maskKey = source.readByteArray(4);
      }
    }
  4. 读取帧内容

    java 复制代码
    private void readMessageFrame() throws IOException {
      // 准备缓冲区
      Buffer message = messageFrameBuffer;
    
      // 读取帧内容
      if (frameLength > 0) {
        source.readFully(message, frameLength);
    
        // 如果使用掩码,应用掩码解码
        if (isMasked) {
          WebSocketProtocol.toggleMask(message.data(), message.size() - frameLength, frameLength, maskKey, 0);
        }
      }
    
      // 处理最终帧
      if (isFinalFrame) {
        // 如果消息被压缩,进行解压缩
        if (isCompressed) {
          Buffer uncompressedMessage = new Buffer();
          messageInflater.inflate(message, uncompressedMessage);
          message = uncompressedMessage;
        }
    
        // 根据操作码回调不同类型的消息
        if (opcode == OPCODE_TEXT) {
          frameCallback.onReadMessage(message.readUtf8());
        } else {
          frameCallback.onReadMessage(message.readByteString());
        }
      }
    }
  5. RealWebSocket处理消息

    java 复制代码
    // 在RealWebSocket中实现的FrameCallback接口
    @Override public void onReadMessage(String text) throws IOException {
      listener.onMessage(this, text);
    }
    
    @Override public void onReadMessage(ByteString bytes) throws IOException {
      listener.onMessage(this, bytes);
    }
  6. 应用层接收消息

    • 用户通过WebSocketListener接收消息
    java 复制代码
    webSocket.listener().onMessage(webSocket, message);

4. 掩码机制详解

WebSocket协议要求客户端发送的所有帧必须使用掩码,而服务器发送的帧不能使用掩码。这是一种安全措施,目的是:

  1. 防止缓存投毒攻击:掩码可以防止恶意代理或中间人预测和缓存WebSocket流量
  2. 防止协议混淆:掩码使WebSocket流量看起来是随机的,减少与其他协议混淆的可能性

掩码算法

java 复制代码
// 应用掩码的代码示例
static void toggleMask(byte[] buffer, long byteCount, byte[] key) {
    for (int i = 0; i < byteCount; i++) {
        buffer[i] = (byte) (buffer[i] ^ key[i % 4]);
    }
}

掩码过程图解:

flowchart LR subgraph 原始数据 A[A] --- B[B] --- C[C] --- D[D] --- E[E] --- F[F] --- G[G] --- H[H] end subgraph 掩码密钥 K1[K1] --- K2[K2] --- K3[K3] --- K4[K4] --- K1'[K1] --- K2'[K2] --- K3'[K3] --- K4'[K4] end A -- XOR --> M1[A^K1] B -- XOR --> M2[B^K2] C -- XOR --> M3[C^K3] D -- XOR --> M4[D^K4] E -- XOR --> M5[E^K1] F -- XOR --> M6[F^K2] G -- XOR --> M7[G^K3] H -- XOR --> M8[H^K4] subgraph 掩码后数据 M1 --- M2 --- M3 --- M4 --- M5 --- M6 --- M7 --- M8 end

5. 压缩扩展机制

WebSocket协议支持通过扩展机制来压缩消息,OkHttp实现了permessage-deflate扩展(RFC 7692)。

压缩协商过程

  1. 握手阶段

    • 客户端在HTTP升级请求中包含Sec-WebSocket-Extensions: permessage-deflate
    • 服务器在响应中确认并可能修改参数
  2. 压缩参数

    • client_max_window_bits:客户端压缩窗口大小
    • server_max_window_bits:服务器压缩窗口大小
    • client_no_context_takeover:每条消息后重置客户端压缩上下文
    • server_no_context_takeover:每条消息后重置服务器压缩上下文

压缩流程

flowchart LR A[原始消息] --> B[DEFLATE压缩] B --> C[设置RSV1=1] C --> D[发送WebSocket帧]

解压缩流程

flowchart LR A[接收帧] --> B[检查RSV1=1] B --> C[INFLATE解压] C --> D[处理解压后的消息]

6. Socket与Okio的关系

OkHttp使用Okio库来处理底层I/O操作,它在Java标准Socket API之上提供了更高效的抽象。

Socket包装过程

java 复制代码
// 在RealConnection中
private void connectSocket(int connectTimeout, int readTimeout, Call call,
    EventListener eventListener) throws IOException {
  // 创建原始Socket
  Socket rawSocket = socketFactory.createSocket();

  // 连接到服务器
  rawSocket.connect(address, connectTimeout);

  // 设置超时
  rawSocket.setSoTimeout(readTimeout);

  // 获取输入输出流
  source = Okio.buffer(Okio.source(rawSocket));
  sink = Okio.buffer(Okio.sink(rawSocket));
}

Okio与Socket的关系图

复制代码
┌───────────────────┐
│      输入流        │
├───────────────────┤
│ Socket InputStream → Okio Source → BufferedSource
│ 
└───────────────────┘

┌───────────────────┐
│      输出流        │
├───────────────────┤
│ Socket OutputStream → Okio Sink → BufferedSink
└───────────────────┘

7. 完整的WebSocket生命周期

1. 建立连接

java 复制代码
// 创建OkHttpClient
OkHttpClient client = new OkHttpClient.Builder()
    .pingInterval(30, TimeUnit.SECONDS) // 设置心跳间隔
    .build();

// 创建请求
Request request = new Request.Builder()
    .url("ws://example.com/websocket")
    .build();

// 创建WebSocket监听器
WebSocketListener listener = new WebSocketListener() {
    @Override
    public void onOpen(WebSocket webSocket, Response response) {
        // 连接已建立
    }

    @Override
    public void onMessage(WebSocket webSocket, String text) {
        // 接收到文本消息
    }

    @Override
    public void onMessage(WebSocket webSocket, ByteString bytes) {
        // 接收到二进制消息
    }

    @Override
    public void onClosing(WebSocket webSocket, int code, String reason) {
        // 连接正在关闭
        webSocket.close(code, reason); // 确认关闭
    }

    @Override
    public void onClosed(WebSocket webSocket, int code, String reason) {
        // 连接已关闭
    }

    @Override
    public void onFailure(WebSocket webSocket, Throwable t, Response response) {
        // 连接失败
    }
};

// 建立WebSocket连接
WebSocket webSocket = client.newWebSocket(request, listener);

2. 内部连接流程

sequenceDiagram participant App as 应用代码 participant Client as OkHttpClient participant RWS as RealWebSocket participant Server as 服务器 App->>Client: newWebSocket() Client->>RWS: 创建RealWebSocket RWS->>Server: HTTP升级请求 Note over Server: 处理请求 Server->>RWS: 101 Switching Note over RWS: 创建读写器 Note over RWS: 启动读取线程 RWS->>App: onOpen回调

3. 发送和接收消息

java 复制代码
// 发送文本消息
webSocket.send("Hello, WebSocket!");

// 发送二进制消息
webSocket.send(ByteString.of("binary data".getBytes()));

// 接收消息通过WebSocketListener回调

4. 心跳机制

OkHttp的WebSocket实现包含自动心跳机制,通过pingInterval配置:

java 复制代码
// 设置30秒的心跳间隔
OkHttpClient client = new OkHttpClient.Builder()
    .pingInterval(30, TimeUnit.SECONDS)
    .build();

心跳流程:

sequenceDiagram participant RWS as RealWebSocket participant WSW as WebSocketWriter participant Server as 服务器 Note over RWS: 定时器触发 RWS->>WSW: writePing() WSW->>Server: PING帧 Note over Server: 处理PING Server->>WSW: PONG帧 Note over RWS: 更新最后活动时间

5. 关闭连接

java 复制代码
// 正常关闭
webSocket.close(1000, "Goodbye!");

// 异常关闭
webSocket.cancel();

关闭流程:

sequenceDiagram participant RWS as RealWebSocket participant WSW as WebSocketWriter participant Server as 服务器 RWS->>WSW: writeClose() WSW->>Server: CLOSE帧 Note over Server: 处理CLOSE Server->>WSW: CLOSE帧 Note over RWS: 关闭连接 Note over RWS: onClosed回调

8. 总结

OkHttp的WebSocket实现是一个复杂而高效的系统,它通过以下关键组件协同工作:

  1. RealWebSocket:核心协调类,管理WebSocket的生命周期和消息队列
  2. WebSocketReader/Writer:处理WebSocket协议的编码和解码
  3. Okio:提供高效的I/O抽象,处理底层数据传输
  4. Socket:提供与网络的实际连接

数据流通过这些组件在不同层次之间传递:

  • 应用层协议层I/O抽象层网络层

这种分层设计使得OkHttp的WebSocket实现既高效又可靠,能够处理各种网络条件和边缘情况,同时为开发者提供简洁的API。

相关推荐
amy_jork6 分钟前
android studio打包vue
android·vue.js·android studio
编程乐学11 分钟前
网络资源模板--基于Android Studio 实现的校园心里咨询预约App
android·android studio·预约系统·大作业·移动端开发·安卓移动开发·心理咨询预约
涵涵子RUSH15 分钟前
android studio(NewsApiDemo)100%kotlin
android·kotlin·android studio
九鼎创展科技34 分钟前
直播一体机技术方案解析:基于RK3588S的硬件架构特性
android·嵌入式硬件·硬件架构
峥嵘life2 小时前
Android14 锁屏密码修改为至少6位
android·安全
2501_9159184110 小时前
iOS WebView 调试实战 localStorage 与 sessionStorage 同步问题全流程排查
android·ios·小程序·https·uni-app·iphone·webview
Digitally11 小时前
如何永久删除安卓设备中的照片(已验证)
android·gitee
hmywillstronger12 小时前
【Settlement】P1:整理GH中的矩形GRID角点到EXCEL中
android·excel
lvronglee12 小时前
如何编译RustDesk(Unbuntu 和Android版本)
android·rustdesk
byadom_IT12 小时前
【Android】Popup menu:弹出式菜单
android