WebSocket——OKHttp實現

WebSocket協議概述

  • Webscoket是Web瀏覽器和服務器之間的一種全雙工通信協議。比如說,服務器可以在任意時刻發送消息給瀏覽器。

  • WebSocket並不是全新的協議,而是利用了HTTP協議來建立連接。

  • WebSocket連接由瀏覽器發起,請求協議是一個標準的HTTP請求,格式如下:

    GET ws://localhost:3000/ws/chat HTTP/1.1
    Host: localhost
    Upgrade: websocket
    Connection: Upgrade
    Origin: http://localhost:3000
    Sec-WebSocket-Key: client-random-string
    Sec-WebSocket-Version: 13
    

該請求和普通的HTTP請求有幾點不同:

  • GET請求的地址不是類似/path/,而是以ws://開頭的地址;
  • 請求頭Upgrade: websocket和Connection: Upgrade表示這個連接將要被轉換爲WebSocket連接;
  • Sec-WebSocket-Key是用於標識這個連接,並非用於加密數據;
  • Sec-WebSocket-Version指定了WebSocket的協議版本。

服務器如果接受該請求,就會返回如下響應:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: server-random-string

響應代碼101表示本次連接的HTTP協議即將被更改,更改後的協議就是Upgrade: websocket指定的WebSocket協議。

OKHttp實現

連接握手

public class OkHttpClient implements Cloneable, Call.Factory, WebSocket.Factory {
  . . . . . .
  @Override public WebSocket newWebSocket(Request request, WebSocketListener listener) {
    RealWebSocket webSocket = new RealWebSocket(request, listener, new SecureRandom());
    webSocket.connect(this);
    return webSocket;
  }

這裏會創建一個 RealWebSocket 對象,然後執行其 connect() 方法建立連接。

RealWebSocket 構造方法如下

  public RealWebSocket(Request request, WebSocketListener listener, Random random) {
    if (!"GET".equals(request.method())) {
      throw new IllegalArgumentException("Request must be GET: " + request.method());
    }
    this.originalRequest = request;
    this.listener = listener;
    this.random = random;
    byte[] nonce = new byte[16];
    random.nextBytes(nonce);
    this.key = ByteString.of(nonce).base64();
    this.writerRunnable = new Runnable() {
      @Override public void run() {
        try {
          while (writeOneFrame()) {
          }
        } catch (IOException e) {
          failWebSocket(e, null);
        }
      }
    };
  }

主要的是初始化了 key,以備後續連接建立及握手之用。Key 是一個16字節長的隨機數經過 Base64 編碼得到的。此外還初始化了 writerRunnable 等。

連接建立及握手過程如下:

  public void connect(OkHttpClient client) {
    client = client.newBuilder()
        .protocols(ONLY_HTTP1)
        .build();
    final int pingIntervalMillis = client.pingIntervalMillis();
    final Request request = originalRequest.newBuilder()
        .header("Upgrade", "websocket")
        .header("Connection", "Upgrade")
        .header("Sec-WebSocket-Key", key)
        .header("Sec-WebSocket-Version", "13")
        .build();
    call = Internal.instance.newWebSocketCall(client, request);
    call.enqueue(new Callback() {
      @Override public void onResponse(Call call, Response response) {
        try {
          checkResponse(response);
        } catch (ProtocolException e) {
          failWebSocket(e, response);
          closeQuietly(response);
          return;
        }
        // Promote the HTTP streams into web socket streams.
        StreamAllocation streamAllocation = Internal.instance.streamAllocation(call);
        streamAllocation.noNewStreams(); // Prevent connection pooling!
        Streams streams = streamAllocation.connection().newWebSocketStreams(streamAllocation);
        // Process all web socket messages.
        try {
          listener.onOpen(RealWebSocket.this, response);
          String name = "OkHttp WebSocket " + request.url().redact();
          initReaderAndWriter(name, pingIntervalMillis, streams);
          streamAllocation.connection().socket().setSoTimeout(0);
          loopReader();
        } catch (Exception e) {
          failWebSocket(e, null);
        }
      }
      @Override public void onFailure(Call call, IOException e) {
        failWebSocket(e, null);
      }
    });
  }

其主要完成的動作:

向服務器發送一個HTTP請求,HTTP 請求的特別之處在於,它包含了如下的一些Headers:

Upgrade: WebSocket
Connection: Upgrade
Sec-WebSocket-Key: 7wgaspE0Tl7/66o4Dov2kw==
Sec-WebSocket-Version: 13

其中 Upgrade 和 Connection header 向服務器表明,請求的目的就是要將客戶端和服務器端的通訊協議從 HTTP 協議升級到 WebSocket 協議,同時在請求處理完成之後,連接不要斷開。

Sec-WebSocket-Key header 值正是我們前面看到的key,它是 WebSocket 客戶端發送的一個 base64 編碼的密文,要求服務端必須返回一個對應加密的 “Sec-WebSocket-Accept” 應答,否則客戶端會拋出 “Error during WebSocket handshake” 錯誤,並關閉連接。

來自於 HTTP 服務器的響應到達的時,則表明連接已建立。

儘管連接已經建立,還要爲數據的收發做一些準備。

這些準備中的第一步就是檢查 HTTP 響應:

  void checkResponse(Response response) throws ProtocolException {
    if (response.code() != 101) {
      throw new ProtocolException("Expected HTTP 101 response but was '"
          + response.code() + " " + response.message() + "'");
    }
    String headerConnection = response.header("Connection");
    if (!"Upgrade".equalsIgnoreCase(headerConnection)) {
      throw new ProtocolException("Expected 'Connection' header value 'Upgrade' but was '"
          + headerConnection + "'");
    }
    String headerUpgrade = response.header("Upgrade");
    if (!"websocket".equalsIgnoreCase(headerUpgrade)) {
      throw new ProtocolException(
          "Expected 'Upgrade' header value 'websocket' but was '" + headerUpgrade + "'");
    }
    String headerAccept = response.header("Sec-WebSocket-Accept");
    String acceptExpected = ByteString.encodeUtf8(key + WebSocketProtocol.ACCEPT_MAGIC)
        .sha1().base64();
    if (!acceptExpected.equals(headerAccept)) {
      throw new ProtocolException("Expected 'Sec-WebSocket-Accept' header value '"
          + acceptExpected + "' but was '" + headerAccept + "'");
    }
  }
  . . . . . .
  public void failWebSocket(Exception e, Response response) {
    Streams streamsToClose;
    synchronized (this) {
      if (failed) return; // Already failed.
      failed = true;
      streamsToClose = this.streams;
      this.streams = null;
      if (cancelFuture != null) cancelFuture.cancel(false);
      if (executor != null) executor.shutdown();
    }
    try {
      listener.onFailure(this, e, response);
    } finally {
      closeQuietly(streamsToClose);
    }
  }

其中的檢測如下:

  • 響應碼是 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請求的時候。這裏會阻止在這個連接上再創建新的流。

  public RealWebSocket.Streams newWebSocketStreams(final StreamAllocation streamAllocation) {
    return new RealWebSocket.Streams(true, source, sink) {
      @Override public void close() throws IOException {
        streamAllocation.streamFinished(true, streamAllocation.codec());
      }
    };
  }

Streams是一個 BufferedSource 和 BufferedSink 的holder:

  public abstract static class Streams implements Closeable {
    public final boolean client;
    public final BufferedSource source;
    public final BufferedSink sink;
    public Streams(boolean client, BufferedSource source, BufferedSink sink) {
      this.client = client;
      this.source = source;
      this.sink = sink;
    }
  }

第三步是調用回調 onOpen()。

第四步是初始化 Reader 和 Writer:

  public void initReaderAndWriter(
      String name, long pingIntervalMillis, Streams streams) throws IOException {
    synchronized (this) {
      this.streams = streams;
      this.writer = new WebSocketWriter(streams.client, streams.sink, random);
      this.executor = new ScheduledThreadPoolExecutor(1, Util.threadFactory(name, false));
      if (pingIntervalMillis != 0) {
        executor.scheduleAtFixedRate(
            new PingRunnable(), pingIntervalMillis, pingIntervalMillis, MILLISECONDS);
      }
      if (!messageAndCloseQueue.isEmpty()) {
        runWriter(); // Send messages that were enqueued before we were connected.
      }
    }
    reader = new WebSocketReader(streams.client, streams.source, this);
  }

OkHttp使用 WebSocketReader 和 WebSocketWriter 來處理數據的收發。在發送數據時將數據組織成幀。

  • WebSocket 的所有數據發送動作,都會在單線程線程池的線程中,通過 WebSocketWriter 執行。
  • 在這裏會創建 ScheduledThreadPoolExecutor 用於跑數據的發送操作。
  • WebSocket 協議中主要會傳輸兩種類型的幀,一是控制幀,主要是用於連接保活的 Ping 幀等;
  • 二是用戶數據載荷幀。在這裏會根據用戶的配置,調度 Ping 幀週期性地發送。
  • 我們在調用 WebSocket 的接口發送數據時,數據並不是同步發送的,而是被放在了一個消息隊列中。
  • 發送消息的 Runnable 從消息隊列中讀取數據發送。這裏會檢查消息隊列中是否有數據,如果有的話,會調度發送消息的 Runnable 執行。

第五步是配置socket的超時時間爲0,也就是阻塞IO。

第六步執行 loopReader()。這實際上是進入了消息讀取循環了,也就是數據接收的邏輯了。

數據發送

可以通過 WebSocket 接口的 send(String text) 和 send(ByteString bytes) 分別發送文本的和二進制格式的消息。

  @Override public boolean send(String text) {
    if (text == null) throw new NullPointerException("text == null");
    return send(ByteString.encodeUtf8(text), OPCODE_TEXT);
  }
  @Override public boolean send(ByteString bytes) {
    if (bytes == null) throw new NullPointerException("bytes == null");
    return send(bytes, OPCODE_BINARY);
  }
  private synchronized boolean send(ByteString data, int formatOpcode) {
    // Don't send new frames after we've failed or enqueued a close frame.
    if (failed || enqueuedClose) return false;
    // If this frame overflows the buffer, reject it and close the web socket.
    if (queueSize + data.size() > MAX_QUEUE_SIZE) {
      close(CLOSE_CLIENT_GOING_AWAY, null);
      return false;
    }
    // Enqueue the message frame.
    queueSize += data.size();
    messageAndCloseQueue.add(new Message(formatOpcode, data));
    runWriter();
    return true;
  }
  . . . . . .
  private void runWriter() {
    assert (Thread.holdsLock(this));
    if (executor != null) {
      executor.execute(writerRunnable);
    }
  }
  • 調用發送數據的接口時,做的事情主要是將數據格式化,構造消息,放進一個消息隊列,然後調度 writerRunnable 執行。

  • 當消息隊列中的未發送數據超出最大大小限制,WebSocket 連接會被直接關閉。對於發送失敗過或被關閉了的 WebSocket,將無法再發送信息。

  • 在 writerRunnable 中會循環調用 writeOneFrame() 逐幀發送數據,直到數據發完,或發送失敗。

在 WebSocket 協議中,客戶端需要發送 四種類型 的幀:

  • PING 幀——PING幀用於連接保活,它的發送是在 PingRunnable 中執行的,在初始化 Reader 和 Writer 的時候,就會根據設置調度執行或不執行。
  • PONG 幀——PONG 幀是對服務器發過來的 PING 幀的響應,同樣用於保活連接。
  • CLOSE 幀——CLOSE 幀用於關閉連接
  • MESSAGE 幀

除PING 幀外的其它 三種 幀,都在 writeOneFrame() 中發送。

我們主要關注用戶數據發送的部分。PONG 幀具有最高的發送優先級。在沒有PONG 幀需要發送時,writeOneFrame() 從消息隊列中取出一條消息,如果消息不是 CLOSE 幀,則主要通過如下的過程進行發送:

  boolean writeOneFrame() throws IOException {
    WebSocketWriter writer;
    ByteString pong;
    Object messageOrClose = null;
    int receivedCloseCode = -1;
    String receivedCloseReason = null;
    Streams streamsToClose = null;
    synchronized (RealWebSocket.this) {
      if (failed) {
        return false; // Failed web socket.
      }
      writer = this.writer;
      pong = pongQueue.poll();
      if (pong == null) {
        messageOrClose = messageAndCloseQueue.poll();
  . . . . . .
      } else if (messageOrClose instanceof Message) {
        ByteString data = ((Message) messageOrClose).data;
        BufferedSink sink = Okio.buffer(writer.newMessageSink(
            ((Message) messageOrClose).formatOpcode, data.size()));
        sink.write(data);
        sink.close();
        synchronized (this) {
          queueSize -= data.size();
        }
      } else if (messageOrClose instanceof Close) {

數據發送可以總結如下:

  • 創建一個 BufferedSink 用於數據發送。
  • 將數據寫入前面創建的 BufferedSink 中。
  • 關閉 BufferedSink。
  • 更新 queueSize 以正確地指示未發送數據的長度。

創建的 Sink 是一個 FrameSink,可以開出是FrameSink 的 write() 方法將數據發送出去。

數據接收

在握手的HTTP請求返回之後,會在HTTP請求的回調裏,啓動消息讀取循環 loopReader():

  public void loopReader() throws IOException {
    while (receivedCloseCode == -1) {
      // This method call results in one or more onRead* methods being called on this thread.
      reader.processNextFrame();
    }
  }

在這個循環中,不斷通過 WebSocketReader 的 processNextFrame() 讀取消息,直到收到了關閉連接的消息。

final class WebSocketReader {
  public interface FrameCallback {
    void onReadMessage(String text) throws IOException;
    void onReadMessage(ByteString bytes) throws IOException;
    void onReadPing(ByteString buffer);
    void onReadPong(ByteString buffer);
    void onReadClose(int code, String reason);
  }
  . . . . . .
  void processNextFrame() throws IOException {
    readHeader();
    if (isControlFrame) {
      readControlFrame();
    } else {
      readMessageFrame();
    }
  }
  private void readHeader() throws IOException {
    if (closed) throw new IOException("closed");
    // Disable the timeout to read the first byte of a new frame.
    int b0;
    long timeoutBefore = source.timeout().timeoutNanos();
    source.timeout().clearTimeout();
    try {
      b0 = source.readByte() & 0xff;
    } finally {
      source.timeout().timeout(timeoutBefore, TimeUnit.NANOSECONDS);
    }
    opcode = b0 & B0_MASK_OPCODE;
    isFinalFrame = (b0 & B0_FLAG_FIN) != 0;
    isControlFrame = (b0 & OPCODE_FLAG_CONTROL) != 0;
    // Control frames must be final frames (cannot contain continuations).
    if (isControlFrame && !isFinalFrame) {
      throw new ProtocolException("Control frames must be final.");
    }
    boolean reservedFlag1 = (b0 & B0_FLAG_RSV1) != 0;
    boolean reservedFlag2 = (b0 & B0_FLAG_RSV2) != 0;
    boolean reservedFlag3 = (b0 & B0_FLAG_RSV3) != 0;
    if (reservedFlag1 || reservedFlag2 || reservedFlag3) {
      // Reserved flags are for extensions which we currently do not support.
      throw new ProtocolException("Reserved flags are unsupported.");
    }
    int b1 = source.readByte() & 0xff;
    isMasked = (b1 & B1_FLAG_MASK) != 0;
    if (isMasked == isClient) {
      // Masked payloads must be read on the server. Unmasked payloads must be read on the client.
      throw new ProtocolException(isClient
          ? "Server-sent frames must not be masked."
          : "Client-sent frames must be masked.");
    }
    // Get frame length, optionally reading from follow-up bytes if indicated by special values.
    frameLength = b1 & B1_MASK_LENGTH;
    if (frameLength == PAYLOAD_SHORT) {
      frameLength = source.readShort() & 0xffffL; // Value is unsigned.
    } else if (frameLength == PAYLOAD_LONG) {
      frameLength = source.readLong();
      if (frameLength < 0) {
        throw new ProtocolException(
            "Frame length 0x" + Long.toHexString(frameLength) + " > 0x7FFFFFFFFFFFFFFF");
      }
    }
    frameBytesRead = 0;
    if (isControlFrame && frameLength > PAYLOAD_BYTE_MAX) {
      throw new ProtocolException("Control frame must be less than " + PAYLOAD_BYTE_MAX + "B.");
    }
    if (isMasked) {
      // Read the masking key as bytes so that they can be used directly for unmasking.
      source.readFully(maskKey);
    }
  }

processNextFrame() 先讀取 Header 的兩個字節,然後根據 Header 的信息,讀取數據內容。

通過幀的 Header 確定了是數據幀,則會執行 readMessageFrame() 讀取消息幀:

  private void readMessageFrame() throws IOException {
    int opcode = this.opcode;
    if (opcode != OPCODE_TEXT && opcode != OPCODE_BINARY) {
      throw new ProtocolException("Unknown opcode: " + toHexString(opcode));
    }
    Buffer message = new Buffer();
    readMessage(message);
    if (opcode == OPCODE_TEXT) {
      frameCallback.onReadMessage(message.readUtf8());
    } else {
      frameCallback.onReadMessage(message.readByteString());
    }
  }

在一個消息讀取完成之後,會通過回調 FrameCallback 將讀取的內容通知出去。

然後這一事件會通知到 RealWebSocket。

public final class RealWebSocket implements WebSocket, WebSocketReader.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);
  }

在 RealWebSocket 中,這一事件又被通知到我們在應用程序中創建的回調 WebSocketListener。

連接保活

保活通過PING幀和PONG來實現。

若用戶設置了PING幀的發送週期,在握手的HTTP請求返回時,消息讀取循環會調度PingRunnable週期性的向服務器發送PING幀:

  private final class PingRunnable implements Runnable {
    PingRunnable() {
    }
    @Override public void run() {
      writePingFrame();
    }
  }
  void writePingFrame() {
    WebSocketWriter writer;
    synchronized (this) {
      if (failed) return;
      writer = this.writer;
    }
    try {
      writer.writePing(ByteString.EMPTY);
    } catch (IOException e) {
      failWebSocket(e, null);
    }
  }

通過 WebSocket 通信的雙方,在收到對方發來的 PING 幀時,需要用PONG幀來回復。

在 WebSocketReader 的 readControlFrame() 中可以看到這一點:

  private void readControlFrame() throws IOException {
    Buffer buffer = new Buffer();
    if (frameBytesRead < frameLength) {
      if (isClient) {
        source.readFully(buffer, frameLength);
      } else {
        while (frameBytesRead < frameLength) {
          int toRead = (int) Math.min(frameLength - frameBytesRead, maskBuffer.length);
          int read = source.read(maskBuffer, 0, toRead);
          if (read == -1) throw new EOFException();
          toggleMask(maskBuffer, read, maskKey, frameBytesRead);
          buffer.write(maskBuffer, 0, read);
          frameBytesRead += read;
        }
      }
    }
    switch (opcode) {
      case OPCODE_CONTROL_PING:
        frameCallback.onReadPing(buffer.readByteString());
        break;
      case OPCODE_CONTROL_PONG:
        frameCallback.onReadPong(buffer.readByteString());
        break;
  @Override public synchronized void onReadPing(ByteString payload) {
    // Don't respond to pings after we've failed or sent the close frame.
    if (failed || (enqueuedClose && messageAndCloseQueue.isEmpty())) return;
    pongQueue.add(payload);
    runWriter();
    pingCount++;
  }
  @Override public synchronized void onReadPong(ByteString buffer) {
    // This API doesn't expose pings.
    pongCount++;
  }

可見在收到 PING 幀的時候,總是會發一個 PONG 幀出去。在收到一個 PONG 幀時,則通常只是記錄一下,然後什麼也不做。如我們前面所見,PONG 幀在 writerRunnable 中被髮送出去:

public final class RealWebSocket implements WebSocket, WebSocketReader.FrameCallback {
  . . . . . .
      if (pong != null) {
        writer.writePong(pong);
      } else if (messageOrClose instanceof Message) {

PONG 幀的發送與 PING 幀的非常相似:

final class WebSocketWriter {
  . . . . . .
  /** Send a pong with the supplied {@code payload}. */
  void writePong(ByteString payload) throws IOException {
    synchronized (this) {
      writeControlFrameSynchronized(OPCODE_CONTROL_PONG, payload);
    }
  }

連接關閉

連接的關閉可以通過WebSocket 接口的 close(int code, String reason)方法關閉。

public final class RealWebSocket implements WebSocket, WebSocketReader.FrameCallback {
  . . . . . .
  @Override public boolean close(int code, String reason) {
    return close(code, reason, CANCEL_AFTER_CLOSE_MILLIS);
  }
  synchronized boolean close(int code, String reason, long cancelAfterCloseMillis) {
    validateCloseCode(code);
    ByteString reasonBytes = null;
    if (reason != null) {
      reasonBytes = ByteString.encodeUtf8(reason);
      if (reasonBytes.size() > CLOSE_MESSAGE_MAX) {
        throw new IllegalArgumentException("reason.size() > " + CLOSE_MESSAGE_MAX + ": " + reason);
      }
    }
    if (failed || enqueuedClose) return false;
    // Immediately prevent further frames from being enqueued.
    enqueuedClose = true;
    // Enqueue the close frame.
    messageAndCloseQueue.add(new Close(code, reasonBytes, cancelAfterCloseMillis));
    runWriter();
    return true;
  }

在執行關閉連接動作前,會先檢查一下 close code 的有效性在合法範圍內。

檢查完了之後,會構造一個 Close 消息放入發送消息隊列,並調度 writerRunnable 執行。Close 消息可以帶有不超出 123 字節的字符串,以作爲 Close message,來說明連接關閉的原因。

連接的關閉分爲主動關閉和被動關閉。客戶端先向服務器發送一個 CLOSE 幀,然後服務器回覆一個 CLOSE 幀,對於客戶端而言,這個過程爲主動關閉;反之則爲對客戶端而言則爲被動關閉。

在 writerRunnable 執行的 writeOneFrame() 實際發送 CLOSE 幀:

public final class RealWebSocket implements WebSocket, WebSocketReader.FrameCallback {
  . . . . . .
        messageOrClose = messageAndCloseQueue.poll();
        if (messageOrClose instanceof Close) {
          receivedCloseCode = this.receivedCloseCode;
          receivedCloseReason = this.receivedCloseReason;
          if (receivedCloseCode != -1) {
            streamsToClose = this.streams;
            this.streams = null;
            this.executor.shutdown();
          } else {
            // When we request a graceful close also schedule a cancel of the websocket.
            cancelFuture = executor.schedule(new CancelRunnable(),
                ((Close) messageOrClose).cancelAfterCloseMillis, MILLISECONDS);
          }
        } else if (messageOrClose == null) {
          return false; // The queue is exhausted.
        }
      }
  . . . . . .
      } else if (messageOrClose instanceof Close) {
        Close close = (Close) messageOrClose;
        writer.writeClose(close.code, close.reason);
        // We closed the writer: now both reader and writer are closed.
        if (streamsToClose != null) {
          listener.onClosed(this, receivedCloseCode, receivedCloseReason);
        }
      } else {

發送 CLOSE 幀也分爲主動關閉的發送還是被動關閉的發送。

  • 對於被動關閉,在發送完 CLOSE 幀之後,連接被最終關閉,因而,發送 CLOSE 幀之前,這裏會停掉髮送消息用的 executor。而在發送之後,則會通過 onClosed() 通知用戶。
  • 而對於主動關閉,則在發送前會調度 CancelRunnable 的執行,發送後不會通過 onClosed() 通知用戶。

而對於主動關閉,則在發送前會調度 CancelRunnable 的執行,發送後不會通過 onClosed() 通知用戶。

final class WebSocketWriter {
  . . . . . .
  void writeClose(int code, ByteString reason) throws IOException {
    ByteString payload = ByteString.EMPTY;
    if (code != 0 || reason != null) {
      if (code != 0) {
        validateCloseCode(code);
      }
      Buffer buffer = new Buffer();
      buffer.writeShort(code);
      if (reason != null) {
        buffer.write(reason);
      }
      payload = buffer.readByteString();
    }
    synchronized (this) {
      try {
        writeControlFrameSynchronized(OPCODE_CONTROL_CLOSE, payload);
      } finally {
        writerClosed = true;
      }
    }
  }

將 CLOSE 幀發送到網絡的過程與 PING 和 PONG 幀的頗爲相似,僅有的差別就是 CLOSE 幀有載荷。關於掩碼位和掩碼自己的規則,同樣適用於 CLOSE 幀的發送。


CLOSE 的讀取在 WebSocketReader 的 readControlFrame()中,讀到 CLOSE 幀時,WebSocketReader 會將這一事件通知出去:

final class WebSocketReader {
  . . . . . .
  private void readControlFrame() throws IOException {
    Buffer buffer = new Buffer();
    if (frameBytesRead < frameLength) {
      if (isClient) {
        source.readFully(buffer, frameLength);
      } else {
        while (frameBytesRead < frameLength) {
          int toRead = (int) Math.min(frameLength - frameBytesRead, maskBuffer.length);
          int read = source.read(maskBuffer, 0, toRead);
          if (read == -1) throw new EOFException();
          toggleMask(maskBuffer, read, maskKey, frameBytesRead);
          buffer.write(maskBuffer, 0, read);
          frameBytesRead += read;
        }
      }
    }
    switch (opcode) {
  . . . . . .
      case OPCODE_CONTROL_CLOSE:
        int code = CLOSE_NO_STATUS_CODE;
        String reason = "";
        long bufferSize = buffer.size();
        if (bufferSize == 1) {
          throw new ProtocolException("Malformed close payload length of 1.");
        } else if (bufferSize != 0) {
          code = buffer.readShort();
          reason = buffer.readUtf8();
          String codeExceptionMessage = WebSocketProtocol.closeCodeExceptionMessage(code);
          if (codeExceptionMessage != null) throw new ProtocolException(codeExceptionMessage);
        }
        frameCallback.onReadClose(code, reason);
        closed = true;
        break;
      default:
        throw new ProtocolException("Unknown control opcode: " + toHexString(opcode));
    }
  }

讀到 CLOSE 幀時,WebSocketReader 會將這一事件通知出去。

public final class RealWebSocket implements WebSocket, WebSocketReader.FrameCallback {
  . . . . . .
  @Override public void onReadClose(int code, String reason) {
    if (code == -1) throw new IllegalArgumentException();
    Streams toClose = null;
    synchronized (this) {
      if (receivedCloseCode != -1) throw new IllegalStateException("already closed");
      receivedCloseCode = code;
      receivedCloseReason = reason;
      if (enqueuedClose && messageAndCloseQueue.isEmpty()) {
        toClose = this.streams;
        this.streams = null;
        if (cancelFuture != null) cancelFuture.cancel(false);
        this.executor.shutdown();
      }
    }
    try {
      listener.onClosing(this, code, reason);
      if (toClose != null) {
        listener.onClosed(this, code, reason);
      }
    } finally {
      closeQuietly(toClose);
    }
  }

對於收到的 CLOSE 幀處理同樣分爲主動關閉的情況和被動關閉的情況。與 CLOSE 發送時的情形正好相反,若是主動關閉,則在收到 CLOSE 幀之後,WebSocket 連接最終斷開,因而需要停掉executor,被動關閉則暫時不需要。

收到 CLOSE 幀,總是會通過 onClosing() 將事件通知出去。

對於主動關閉的情形,最後還會通過 onClosed() 通知用戶,連接已經最終關閉。

生命週期概括

  • 連接通過一個HTTP請求握手並建立連接。WebSocket 連接可以理解爲是通過HTTP請求建立的普通TCP連接。
  • WebSocket 做了二進制分幀。WebSocket 連接中收發的數據以幀爲單位。主要有用於連接保活的控制幀 PING 和 PONG,用於用戶數據發送的 MESSAGE 幀,和用於關閉連接的控制幀 CLOSE。
  • 連接建立之後,通過 PING 幀和 PONG 幀做連接保活。
  • 一次 send 數據,被封爲一個消息,通過一個或多個 MESSAGE幀進行發送。一個消息的幀和控制幀可以交叉發送,不同消息的幀之間不可以。
  • WebSocket 連接的兩端相互發送一個 CLOSE 幀以最終關閉連接。
發佈了91 篇原創文章 · 獲贊 63 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章