系統學習詳見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));
}
}
}
}