okhttp之旅(十二)--websocket的使用及建立連接

系統(tǒng)學(xué)習(xí)詳見OKhttp源碼解析詳解系列

1. websocket

  • 1.客戶端與服務(wù)端建連接
  • 2.客戶端向服務(wù)端發(fā)送消息
  • 3.服務(wù)端在接受消息后以“response-接受的消息內(nèi)容“的形式返回給客戶端
  • 4.當(dāng)服務(wù)端收到第5條信息的時(shí)候,主動(dòng)關(guān)閉與客戶端的連接

客戶端代碼

    private void clientWebSocket(String url) {
        OkHttpClient client = new OkHttpClient.Builder().build();
        //構(gòu)造request對(duì)象
        Request request = new Request.Builder()
                .url(url)
                .build();
        //建立連接
        client.newWebSocket(request, new WebSocketListener() {
            //當(dāng)遠(yuǎn)程對(duì)等方接受Web套接字并可能開始傳輸消息時(shí)調(diào)用。
            @Override
            public void onOpen(WebSocket webSocket, Response response) {
                super.onOpen(webSocket, response);
                webSocket.send("發(fā)送消息");
            }

            //當(dāng)收到文本(類型{@code 0x1})消息時(shí)調(diào)用
            @Override
            public void onMessage(WebSocket webSocket, String text) {
                super.onMessage(webSocket, text);
            }

            //當(dāng)收到二進(jìn)制(類型為{@code 0x2})消息時(shí)調(diào)用。
            @Override
            public void onMessage(WebSocket webSocket, ByteString bytes) {
                super.onMessage(webSocket, bytes);
            }

            //當(dāng)遠(yuǎn)程對(duì)等體指示不再有傳入的消息將被傳輸時(shí)調(diào)用。
            @Override
            public void onClosing(WebSocket webSocket, int code, String reason) {
                super.onClosing(webSocket, code, reason);
            }

            //當(dāng)兩個(gè)對(duì)等方都表示不再傳輸消息并且連接已成功釋放時(shí)調(diào)用。 沒有進(jìn)一步的電話給這位聽眾。
            @Override
            public void onClosed(WebSocket webSocket, int code, String reason) {
                super.onClosed(webSocket, code, reason);
            }

            //由于從網(wǎng)絡(luò)讀取或向網(wǎng)絡(luò)寫入錯(cuò)誤而關(guān)閉Web套接字時(shí)調(diào)用。
            // 傳出和傳入的消息都可能丟失。 沒有進(jìn)一步的電話給這位聽眾。
            @Override
            public void onFailure(WebSocket webSocket, Throwable t, @Nullable Response response) {
                super.onFailure(webSocket, t, response);
            }
        });
    }

2 客戶端與服務(wù)端建連接

2.1創(chuàng)建一個(gè) RealWebSocket 對(duì)象,然后執(zhí)行其 connect() 方法建立連接。

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

2.2 真真的RealWebSocket

主要的是初始化了 key,以備后續(xù)連接建立及握手之用。Key 是一個(gè)16字節(jié)長的隨機(jī)數(shù)經(jīng)過 Base64 編碼得到的。此外還初始化了 writerRunnable 等。

public final class RealWebSocket implements WebSocket, WebSocketReader.FrameCallback
    public RealWebSocket(Request request, WebSocketListener listener, Random random,
                         long pingIntervalMillis) {
        //使用get的方式進(jìn)行握手
        if (!"GET".equals(request.method())) {
            throw new IllegalArgumentException("Request must be GET: " + request.method());
        }
        this.originalRequest = request;
        this.listener = listener;
        this.random = random;
        this.pingIntervalMillis = pingIntervalMillis;
        //初始化key,以備后續(xù)連接建立及握手之用。
        byte[] nonce = new byte[16];
        random.nextBytes(nonce);
        this.key = ByteString.of(nonce).base64();
        //初始化了 writerRunnable
        this.writerRunnable = new Runnable() {
            @Override
            public void run() {
                try {
                    while (writeOneFrame()) {
                    }
                } catch (IOException e) {
                    failWebSocket(e, null);
                }
            }
        };
    }
}

2.3 握手建立連接

  • 1.創(chuàng)建一個(gè) RealWebSocket 對(duì)象,然后執(zhí)行其 connect() 方法建立連接。
  • 2.連接建立及握手的過程主要是向服務(wù)器發(fā)送一個(gè)HTTP請(qǐng)求。這個(gè) HTTP 請(qǐng)求的特別之處在于,它包含了如下的一些Headers:
Upgrade: WebSocket
Connection: Upgrade
Sec-WebSocket-Key: 7wgaspE0Tl7/66o4Dov2kw==
Sec-WebSocket-Version: 13
  • 其中 Upgrade 和 Connection header 向服務(wù)器表明,請(qǐng)求的目的就是要將客戶端和服務(wù)器端的通訊協(xié)議從 HTTP 協(xié)議升級(jí)到 WebSocket 協(xié)議,同時(shí)在請(qǐng)求處理完成之后,連接不要斷開。
  • Sec-WebSocket-Key header 值正是我們前面看到的key,它是 WebSocket 客戶端發(fā)送的一個(gè) base64 編碼的密文,要求服務(wù)端必須返回一個(gè)對(duì)應(yīng)加密的 “Sec-WebSocket-Accept” 應(yīng)答,否則客戶端會(huì)拋出 “Error during WebSocket handshake” 錯(cuò)誤,并關(guān)閉連接。
  • 3.來自于 HTTP 服務(wù)器的響應(yīng)到達(dá)的時(shí)候,即是連接建立大功告成的時(shí)候
public final class RealWebSocket implements WebSocket, WebSocketReader.FrameCallback {
    public void connect(OkHttpClient client) {
        client = client.newBuilder()
                .eventListener(EventListener.NONE)
                .protocols(ONLY_HTTP1)
                .build();
        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);
        /**
         * WebSocket協(xié)議首先會(huì)通過發(fā)送一個(gè)http請(qǐng)求來完成一個(gè)握手的過程
         * 客戶端發(fā)送一個(gè)請(qǐng)求協(xié)議升級(jí)的get請(qǐng)求給服務(wù)端
         * 服務(wù)端如果支持的話會(huì)返回http code 為101,表示可以切換到對(duì)應(yīng)的協(xié)議
         */
        call.enqueue(new Callback() {
            @Override
            public void onResponse(Call call, Response response) {
                try {
                    //第一步就是檢查 HTTP 響應(yīng)
                    checkResponse(response);
                } catch (ProtocolException e) {
                    failWebSocket(e, response);
                    closeQuietly(response);
                    return;
                }


                //將HTTP流推廣到Web套接字流中
                StreamAllocation streamAllocation = Internal.instance.streamAllocation(call);
                streamAllocation.noNewStreams(); //阻止連接池

                //第二步:初始化用于輸入輸出的 Source 和 Sink。
                // Source 和 Sink 創(chuàng)建于之前發(fā)送HTTP請(qǐng)求的時(shí)候。這里會(huì)阻止在這個(gè)連接上再創(chuàng)建新的流。
                Streams streams = streamAllocation.connection().newWebSocketStreams(streamAllocation);

                // Process all web socket messages.
                try {
                    //第三步是調(diào)用回調(diào) onOpen()。
                    listener.onOpen(RealWebSocket.this, response);
                    //第四步是初始化 Reader 和 Writer:
                    String name = "OkHttp WebSocket " + request.url().redact();
                    initReaderAndWriter(name, streams);
                    //第五步是配置socket的超時(shí)時(shí)間為0,也就是阻塞IO。
                    streamAllocation.connection().socket().setSoTimeout(0);
                    //第六步執(zhí)行 loopReader()。這實(shí)際上是進(jìn)入了消息讀取循環(huán)了,也就是數(shù)據(jù)接收的邏輯了。
                    loopReader();
                } catch (Exception e) {
                    failWebSocket(e, null);
                }
            }

            @Override
            public void onFailure(Call call, IOException e) {
                failWebSocket(e, null);
            }
        });
    }
}

2.4 數(shù)據(jù)收發(fā)準(zhǔn)備

  • HTTP 服務(wù)器的響應(yīng)到達(dá)的時(shí)候,為數(shù)據(jù)收發(fā)做的準(zhǔn)備,如:2.3代碼onResponse;
  • 共有6步:
  • 第一步就是檢查 HTTP 響應(yīng):
  • 第二步:初始化用于輸入輸出的 Source 和 Sink。
  • 第三步是調(diào)用回調(diào) onOpen()。
  • 第四步是初始化 Reader 和 Writer。
  • 第五步是配置socket的超時(shí)時(shí)間為0,也就是阻塞IO。
  • 第六步執(zhí)行 loopReader()。

2.4.1 第一步就是檢查 HTTP 響應(yīng):

  • 根據(jù) WebSocket 的協(xié)議,服務(wù)器端用如下響應(yīng),來表示接受建立 WebSocket 連接的請(qǐng)求:
    1. 響應(yīng)碼是 101。
    1. "Connection" header 的值為 "Upgrade",以表明服務(wù)器并沒有在處理完請(qǐng)求之后把連接個(gè)斷開。
    1. "Upgrade" header 的值為 "websocket",以表明服務(wù)器接受后面使用 WebSocket 來通信。
    1. "Sec-WebSocket-Accept" header 的值為,key + WebSocketProtocol.ACCEPT_MAGIC 做 SHA1 hash,然后做 base64 編碼,來做服務(wù)器接受連接的驗(yàn)證。關(guān)于這部分的設(shè)計(jì)的詳細(xì)信息,可參考 WebSocket 協(xié)議規(guī)范
public final class RealWebSocket implements WebSocket, WebSocketReader.FrameCallback {
    void checkResponse(Response response) throws ProtocolException {
        //響應(yīng)碼是 101。
        if (response.code() != 101) {
            throw new ProtocolException("Expected HTTP 101 response but was '"
                    + response.code() + " " + response.message() + "'");
        }
        //"Connection" header 的值為 "Upgrade",以表明服務(wù)器并沒有在處理完請(qǐng)求之后把連接個(gè)斷開。
        String headerConnection = response.header("Connection");
        if (!"Upgrade".equalsIgnoreCase(headerConnection)) {
            throw new ProtocolException("Expected 'Connection' header value 'Upgrade' but was '"
                    + headerConnection + "'");
        }
        //"Upgrade" header 的值為 "websocket",以表明服務(wù)器接受后面使用 WebSocket 來通信。
        String headerUpgrade = response.header("Upgrade");
        if (!"websocket".equalsIgnoreCase(headerUpgrade)) {
            throw new ProtocolException(
                    "Expected 'Upgrade' header value 'websocket' but was '" + headerUpgrade + "'");
        }
        //"Sec-WebSocket-Accept" header 的值為,key + WebSocketProtocol.ACCEPT_MAGIC 做 SHA1 hash,然后做 base64 編碼,來做服務(wù)器接受連接的驗(yàn)證。
        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 + "'");
        }
    }
}

2.4.2 第二步:初始化用于輸入輸出的 Source 和 Sink。

Source 和 Sink 創(chuàng)建于之前發(fā)送HTTP請(qǐng)求的時(shí)候。這里會(huì)阻止在這個(gè)連接上再創(chuàng)建新的流。

public final class RealConnection extends Http2Connection.Listener implements Connection {
    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(), -1L, null);
            }
        };
    }
}

2.4.3 第三步是調(diào)用回調(diào) onOpen()

2.4.4 第四步是初始化 Reader 和 Writer:

  • OkHttp使用 WebSocketReaderWebSocketWriter 來處理數(shù)據(jù)的收發(fā)。
  • 在發(fā)送數(shù)據(jù)時(shí)將數(shù)據(jù)組織成幀,在接收數(shù)據(jù)時(shí)則進(jìn)行反向操作,同時(shí)處理 WebSocket 的控制消息。
  • WebSocket 的所有數(shù)據(jù)發(fā)送動(dòng)作,都會(huì)在單線程線程池的線程中,通過 WebSocketWriter 執(zhí)行。在這里會(huì)創(chuàng)建 ScheduledThreadPoolExecutor 用于跑數(shù)據(jù)的發(fā)送操作。
  • WebSocket 協(xié)議中主要會(huì)傳輸兩種類型的幀,一是控制幀,主要是用于連接保活的 Ping 幀等;二是用戶數(shù)據(jù)載荷幀
  • 在這里會(huì)根據(jù)用戶的配置,調(diào)度** Ping 幀**周期性地發(fā)送。
  • 在調(diào)用 WebSocket 的接口發(fā)送數(shù)據(jù)時(shí),數(shù)據(jù)并不是同步發(fā)送的,而是被放在了一個(gè)消息隊(duì)列中。發(fā)送消息的 Runnable 從消息隊(duì)列中讀取數(shù)據(jù)發(fā)送。這里會(huì)檢查消息隊(duì)列中是否有數(shù)據(jù),如果有的話,會(huì)調(diào)度發(fā)送消息的 Runnable 執(zhí)行。
public final class RealWebSocket implements WebSocket, WebSocketReader.FrameCallback {
    public void initReaderAndWriter(String name, 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);
    }
}

2.4.5第五步是配置socket的超時(shí)時(shí)間為0,也就是阻塞IO。

2.4.6第六步執(zhí)行 loopReader()。這實(shí)際上是進(jìn)入了消息讀取循環(huán)了,也就是數(shù)據(jù)接收的邏輯了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容