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

269 阅读9分钟

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)作为通信的基本单位,每个帧的结构如下:

 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类
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类
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接口
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
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
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. 应用调用发送方法

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

    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编码消息

    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读取数据

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

    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. 读取帧内容

    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处理消息

    // 在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接收消息
    webSocket.listener().onMessage(webSocket, message);
    

4. 掩码机制详解

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

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

掩码算法

// 应用掩码的代码示例
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包装过程

// 在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. 建立连接

// 创建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. 发送和接收消息

// 发送文本消息
webSocket.send("Hello, WebSocket!");

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

// 接收消息通过WebSocketListener回调

4. 心跳机制

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

// 设置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. 关闭连接

// 正常关闭
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。