okhttp之旅(十三)--websocket數據發送與接收

系統學習詳見OKhttp源碼解析詳解系列

1 數據發送

1.1 四種類型幀

在 WebSocket 協議中,客戶端需要發送 四種類型 的幀:
1.PING 幀
PING幀用于連接保活,它的發送是在 PingRunnable 中執行的,在初始化 Reader 和 Writer 的時候,就會根據設置調度執行或不執行。
除PING 幀外的其它 三種 幀,都在 writeOneFrame() 中發送。
2.PONG 幀
PONG 幀是對服務器發過來的 PING 幀的響應,同樣用于保活連接。
PONG 幀具有最高的發送優先級
3.CLOSE 幀
CLOSE 幀用于關閉連接
4.MESSAGE 幀

1.2 send()

  • 通過 WebSocket 接口的 send(String text) 和 send(ByteString bytes) 分別發送文本的和二進制格式的消息。
  • 調用發送數據的接口時,做的事情主要是構造消息,放進一個消息隊列,然后調度 writerRunnable 執行。
  • 當消息隊列中的未發送數據超出最大大小限制,WebSocket 連接會被直接關閉。對于發送失敗過或被關閉了的 WebSocket,將無法再發送信息。
public final class RealWebSocket implements WebSocket, WebSocketReader.FrameCallback {
    @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) {
        //對于發送失敗過或被關閉了的 WebSocket,將無法再發送信息。
        // Don't send new frames after we've failed or enqueued a close frame.
        if (failed || enqueuedClose) return false;

        //當消息隊列中的未發送數據超出最大大小限制,WebSocket 連接會被直接關閉。
        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);
        }
    }
}

1.3 writerRunnable

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

public final class RealWebSocket implements WebSocket, WebSocketReader.FrameCallback {
    public RealWebSocket(Request request, WebSocketListener listener, Random random,
                         long pingIntervalMillis) {
        ...
        this.writerRunnable = new Runnable() {
            @Override
            public void run() {
                try {
                    while (writeOneFrame()) {
                    }
                } catch (IOException e) {
                    failWebSocket(e, null);
                }
            }
        };
    }
}

1.4 writeOneFrame()

  • 在沒有PONG 幀需要發送時,writeOneFrame() 從消息隊列中取出一條消息,如果消息不是 CLOSE 幀,則主要通過如下的過程進行發送:
  • 1.隊列中取出消息
  • 2.創建一個 BufferedSink 用于數據發送。
  • 3.將數據寫入前面創建的 BufferedSink 中。
  • 4.關閉 BufferedSink。
  • 5.更新 queueSize 以正確地指示未發送數據的長度。

PONG 幀具有最高的發送優先級

public final class RealWebSocket implements WebSocket, WebSocketReader.FrameCallback {
    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; // websocket連接失敗,跳出循環
            }

            writer = this.writer;
            pong = pongQueue.poll();
            //判斷是否有pong消息
            if (pong == null) {
                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; //消息隊列為空,跳出循環
                }
            }
        }

        try {
            if (pong != null) {
                writer.writePong(pong);

            } else if (messageOrClose instanceof Message) {
                ByteString data = ((Message) messageOrClose).data;
                //這里將數據轉化為可供websocket交互的格式
                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) {
                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 {
                throw new AssertionError();
            }

            return true;
        } finally {
            //釋放資源
            closeQuietly(streamsToClose);
        }
    }
}

1.5 數據格式化及發送

if (messageOrClose instanceof Message) {
    ByteString data = ((Message) messageOrClose).data;
    //這里將數據轉化為可供websocket交互的格式
    BufferedSink sink = Okio.buffer(writer.newMessageSink(
            ((Message) messageOrClose).formatOpcode, data.size()));
    sink.write(data);
    sink.close();
    synchronized (this) {
        queueSize -= data.size();
    }

} 

newMessageSink-返回的是FrameSink

final class WebSocketWriter {
  final FrameSink frameSink = new FrameSink();
  Sink newMessageSink(int formatOpcode, long contentLength) {
    if (activeWriter) {
      throw new IllegalStateException("Another message writer is active. Did you call close()?");
    }
    activeWriter = true;

    // Reset FrameSink state for a new writer.
    frameSink.formatOpcode = formatOpcode;
    frameSink.contentLength = contentLength;
    frameSink.isFirstFrame = true;
    frameSink.closed = false;

    return frameSink;
  }
}

sink.write(data)

  • FrameSink 的 write() 會先將數據寫如一個 Buffer 中,然后再從這個 Buffer 中讀取數據來發送。
  • 如果是第一次發送數據,同時剩余要發送的數據小于 8192 字節時,會延遲執行實際的數據發送,等 close() 時刷新。
  • 在 write() 時,總是寫入整個消息的所有數據,在 FrameSink 的 write() 中總是不會發送數據的(交給close)。
final class WebSocketWriter {
  final class FrameSink implements Sink {
    @Override public void write(Buffer source, long byteCount) throws IOException {
      if (closed) throw new IOException("closed");

      buffer.write(source, byteCount);

      // Determine if this is a buffered write which we can defer until close() flushes.
      boolean deferWrite = isFirstFrame
          && contentLength != -1
          && buffer.size() > contentLength - 8192 /* segment size */;

      long emitCount = buffer.completeSegmentByteCount();
      if (emitCount > 0 && !deferWrite) {
        writeMessageFrame(formatOpcode, emitCount, isFirstFrame, false /* final */);
        isFirstFrame = false;
      }
    }

    @Override public void flush() throws IOException {
      if (closed) throw new IOException("closed");

      writeMessageFrame(formatOpcode, buffer.size(), isFirstFrame, false /* final */);
      isFirstFrame = false;
    }

    @Override public Timeout timeout() {
      return sink.timeout();
    }

    @SuppressWarnings("PointlessBitwiseExpression")
    @Override public void close() throws IOException {
      if (closed) throw new IOException("closed");

      writeMessageFrame(formatOpcode, buffer.size(), isFirstFrame, true /* final */);
      closed = true;
      activeWriter = false;
    }
  }
}

writeMessageFrame()將用戶數據格式化并發送出去。

final class WebSocketWriter {
  void writeMessageFrame(int formatOpcode, long byteCount, boolean isFirstFrame,
      boolean isFinal) throws IOException {
    if (writerClosed) throw new IOException("closed");

    int b0 = isFirstFrame ? formatOpcode : OPCODE_CONTINUATION;
    if (isFinal) {
      b0 |= B0_FLAG_FIN;
    }
    sinkBuffer.writeByte(b0);

    int b1 = 0;
    if (isClient) {
      b1 |= B1_FLAG_MASK;
    }
    if (byteCount <= PAYLOAD_BYTE_MAX) {
      b1 |= (int) byteCount;
      sinkBuffer.writeByte(b1);
    } else if (byteCount <= PAYLOAD_SHORT_MAX) {
      b1 |= PAYLOAD_SHORT;
      sinkBuffer.writeByte(b1);
      sinkBuffer.writeShort((int) byteCount);
    } else {
      b1 |= PAYLOAD_LONG;
      sinkBuffer.writeByte(b1);
      sinkBuffer.writeLong(byteCount);
    }

    if (isClient) {
      random.nextBytes(maskKey);
      sinkBuffer.write(maskKey);

      if (byteCount > 0) {
        long bufferStart = sinkBuffer.size();
        sinkBuffer.write(buffer, byteCount);

        sinkBuffer.readAndWriteUnsafe(maskCursor);
        maskCursor.seek(bufferStart);
        toggleMask(maskCursor, maskKey);
        maskCursor.close();
      }
    } else {
      sinkBuffer.write(buffer, byteCount);
    }

    sink.emit();
  }
}

規范中定義的數據格式如下:

      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 ...                |
     +---------------------------------------------------------------+

2 數據接收

2.1 讀取內容

  • 在握手的HTTP請求返回之后,會在HTTP請求的回調里,啟動消息讀取循環 loopReader():
  • 不斷通過 WebSocketReader 的 processNextFrame() 讀取消息,直到收到了關閉連接的消息。
  • processNextFrame() 先讀取 Header 的兩個字節,然后根據 Header 的信息,讀取數據內容。
public final class RealWebSocket implements WebSocket, WebSocketReader.FrameCallback {
    /**
     * Receive frames until there are no more. Invoked only by the reader thread.
     * 接收幀,直到沒有更多。 僅由讀者線程調用。
     */
    public void loopReader() throws IOException {
        while (receivedCloseCode == -1) {
            // This method call results in one or more onRead* methods being called on this thread.
            //此方法調用將導致在此線程上調用一個或多個onRead *方法
            reader.processNextFrame();
        }
    }
}
final class WebSocketReader {
    void processNextFrame() throws IOException {
        //先讀取Header
        readHeader();
        //是控制幀要先讀取
        if (isControlFrame) {
            readControlFrame();
        } else {
            readMessageFrame();
        }
    }
}

2.2 讀取header

  • WebSocketReader 從 Header 中,獲取到這個幀是不是消息的最后一幀,消息的類型,是否有掩碼字節,保留位,幀的長度,以及掩碼字節等信息。
  • WebSocket 通過掩碼位和掩碼字節來區分數據是從客戶端發送給服務器的,還是服務器發送給客戶端的。
    通過幀的 Header 確定了是數據幀,則會執行 readMessageFrame() 讀取消息幀:會讀取一條消息包含的所有數據幀。
final class WebSocketReader {
    private void readHeader() throws IOException {
        if (closed) throw new IOException("closed");

        //讀的第一個字節是同步的不計超時時間的
        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;

        // 控制幀必須是最終幀(不能包含延續)。
        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) {
            // 保留標志用于我們目前不支持的擴展。
            throw new ProtocolException("Reserved flags are unsupported.");
        }

        int b1 = source.readByte() & 0xff;

        boolean 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");
            }
        }

        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);
        }
    }
}

2.3 readMessageFrame() 讀取消息幀并回調 FrameCallback 將讀取的內容通知出去

  • 按照 WebSocket 的標準,包含用戶數據的消息數據幀可以和控制幀交替發送;但消息之間的數據幀不可以。因而在這個過程中,若遇到了控制幀,則會先讀取控制幀進行處理,然后繼續讀取消息的數據幀,直到讀取了消息的所有數據幀。
final class WebSocketReader {
    private void readMessageFrame() throws IOException {
        int opcode = this.opcode;
        if (opcode != OPCODE_TEXT && opcode != OPCODE_BINARY) {
            throw new ProtocolException("Unknown opcode: " + toHexString(opcode));
        }

        readMessage();

        //回調 FrameCallback 將讀取的內容通知出去
        if (opcode == OPCODE_TEXT) {
            frameCallback.onReadMessage(messageFrameBuffer.readUtf8());
        } else {
            frameCallback.onReadMessage(messageFrameBuffer.readByteString());
        }
    }
    /**
     * Reads a message body into across one or more frames. Control frames that occur between
     * fragments will be processed. If the message payload is masked this will unmask as it's being
     * processed.
     * <p>
     * 通過一個或多個幀讀取消息正文。 處理片段之間發生的控制幀。
     * 如果消息有效負載被屏蔽,則在處理它時將取消屏蔽。
     */
    private void readMessage() throws IOException {
        while (true) {
            if (closed) throw new IOException("closed");

            if (frameLength > 0) {
                source.readFully(messageFrameBuffer, frameLength);

                if (!isClient) {
                    messageFrameBuffer.readAndWriteUnsafe(maskCursor);
                    maskCursor.seek(messageFrameBuffer.size() - frameLength);
                    toggleMask(maskCursor, maskKey);
                    maskCursor.close();
                }
            }

            if (isFinalFrame) break; // We are exhausted and have no continuations.

            readUntilNonControlFrame();
            if (opcode != OPCODE_CONTINUATION) {
                throw new ProtocolException("Expected continuation opcode. Got: " + toHexString(opcode));
            }
        }
    }
}

參考

http://www.lxweimin.com/p/13ceb541ade9

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內容

  • 前言 最近公司有項目需要用WebSocket完成及時通信的需求,這里來學習一下。 WebScoket簡介 在以前的...
    Misery_Dx閱讀 5,437評論 0 15
  • 每個人,都在爭取一個完滿的人生。然而,世界上沒有絕對完滿的東西。太陽一到中間,馬上就會偏西;月圓,馬上就會月虧。所...
    雪蓉閱讀 215評論 0 0