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 幀以最終關閉連接。