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。

相关推荐
xiangpanf3 小时前
Laravel 10.x重磅升级:五大核心特性解析
android
robotx6 小时前
安卓线程相关
android
消失的旧时光-19437 小时前
Android 面试高频:JSON 文件、大数据存储与断电安全(从原理到工程实践)
android·面试·json
dalancon8 小时前
VSYNC 信号流程分析 (Android 14)
android
dalancon8 小时前
VSYNC 信号完整流程2
android
dalancon8 小时前
SurfaceFlinger 上帧后 releaseBuffer 完整流程分析
android
用户69371750013849 小时前
不卷AI速度,我卷自己的从容——北京程序员手记
android·前端·人工智能
程序员Android9 小时前
Android 刷新一帧流程trace拆解
android
墨狂之逸才10 小时前
解决 Android/Gradle 编译报错:Comparison method violates its general contract!
android
阿明的小蝴蝶11 小时前
记一次Gradle环境的编译问题与解决
android·前端·gradle