okhttp之websocket源碼解析(未完)

本文要求:大概了解使用方法,知道如何通過okhttp建立websocket,四個回調(diào)的名字最好大概記住,因為后面會直接使用习寸。

websocket協(xié)議有兩種消息類型, 被稱為frame器仗, 幀融涣,一種是消息類童番, 就是我們普通通信使用的精钮, 一種是控制通信用的威鹿,比如關(guān)閉用的close幀,心跳相關(guān)的ping幀和pong幀轨香。

 if (isControlFrame) {
      readControlFrame();
    } else {
      readMessageFrame();
    }

而發(fā)射的概念就是write a frame忽你,到服務(wù)端, 接受呢就是響應(yīng)的read a frame

兩者最為關(guān)鍵的源碼就是runWriter()loopReader()這兩個函數(shù).

本文先從程序的入口開始查看設(shè)計和邏輯臂容, 最后再從最貼近業(yè)務(wù)的科雳,暴露給應(yīng)用層使用的 發(fā)射,接受脓杉,以及回調(diào) 來總結(jié)歸納一下糟秘。

可以說市面上大部分的博客都是寫的人都是一知半解,寫出來的更是千篇一律球散,大部分僅僅講解了demo級別的使用方法尿赚,缺失了很多情況的處理, 以及重要的注意點蕉堰。

1. 入口

val client = OkHttpClient.Builder()
client.newWebSocket(request, object : WebSocketListener() {}    

還是從OkHttpClient開始的

/**
   * Uses {@code request} to connect a new web socket.
   */
  @Override public WebSocket newWebSocket(Request request, WebSocketListener listener) {
    RealWebSocket webSocket = new RealWebSocket(request, listener, new Random(), pingInterval);
    webSocket.connect(this);
    return webSocket;
  }

newWebSocket()函數(shù)里完成的事情:

  • 初始化RealWebSocket
  • 調(diào)用RealWebSocket$connect()

1.1初始化

public RealWebSocket(Request request, WebSocketListener listener, Random random,
      long pingIntervalMillis) {
    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;

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

這里request和random用來建立連接凌净,把http1升級成websocket。

this.pingIntervalMillis是指定心跳的間隔屋讶, OkHttpClient.Builder()構(gòu)建的時候默認(rèn)是0冰寻,默認(rèn)的話,就不會發(fā)射ping幀了皿渗,后面會提到斩芭。

還有重要的一步是初始化了this.writerRunnable,這個runnable乐疆,是后續(xù)所有write操作(write就是客戶端發(fā)送的操作划乖,read就是接受服務(wù)端發(fā)來的操作)最終調(diào)用的任務(wù)(runWriter)

這個任務(wù)是無限循環(huán)writeOneFrame()函數(shù)

注釋基本寫的非常的清楚了:

嘗試從queue里取出一幀,然后send他诀拭,pong幀優(yōu)先級高于message幀和close幀迁筛,會優(yōu)先發(fā)射,如果一個調(diào)用者入隊了一個被pong幀跟隨的message幀耕挨,那么會發(fā)射pong幀细卧,讓message幀跟隨, pong幀當(dāng)他們?nèi)腙牭臅r候筒占, 永遠(yuǎn)會被下一個執(zhí)行贪庙。

如果queue已經(jīng)空了, 或者websokcet斷開連接了翰苫, 就不能send幀了止邮。

這樣會什么都不做这橙,只是返回false,否則會返回true且立刻再次調(diào)用本方法直到返回false导披。

這個方法只能唄writter thread調(diào)用屈扎, 可能只有一個線程在同一時間調(diào)用這個方法(應(yīng)該是一定的吧, 因為會檢查是否有鎖)撩匕。

看下源碼其實pongQueue和messageAndCloseQueue是兩個queue鹰晨,先處理pongQueue的邏輯

  • 如果poll出了pong幀,就直接
    writer.writePong(pong);發(fā)射

  • 如果沒有poll出pong幀止毕,那么處理messageAndCloseQueue

1.1.1處理messageAndCloseQueue

如果沒有pong的話模蜡, messageAndCloseQueue.poll()取出消息messageOrClose

if(messageOrClose is null)
        return false隊列空了, 這個while死循環(huán)就斷掉了
if(messageOrClose is Close幀)
    if(receivedCloseCode扁凛!= -1忍疾,也就這是從服務(wù)器接收到的close幀)
        關(guān)閉excutor
    else 證明是自己發(fā)射的close幀
        優(yōu)雅的關(guān)閉,60秒后調(diào)用call.cancel 
        
    不管優(yōu)不優(yōu)雅是不是自己enqueue的Close幀谨朝,都會執(zhí)行下面的代碼
    發(fā)射close幀;
    如果是服務(wù)端接受到的close幀那么會回調(diào)onClosed()方法卤妒,通知調(diào)用者;

最后return true
            
  /** The close code from the peer, or -1 if this web socket has not yet read a close frame. */
  private int receivedCloseCode = -1;

初始化的代碼基本就這些了,但卻非常的重要叠必,因為他定義了所有與發(fā)射相關(guān)的邏輯荚孵。

1.2connect()函數(shù)

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);
    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, streams);
          streamAllocation.connection().socket().setSoTimeout(0);
          loopReader();
        } catch (Exception e) {
          failWebSocket(e, null);
        }
      }

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

connect函數(shù)完成了websocket協(xié)議要求的升級過程(如果不了解,請簡單了解下websocket協(xié)議)纬朝,可以看到他添加了一些請求頭收叶, 并且調(diào)用了一次http1的握手協(xié)議, 如果失敗的話共苛,回調(diào)failWebSocket給用戶通知判没。

主要看下升級成功的邏輯, 此時已經(jīng)是websocket協(xié)議了隅茎, 可以看到上來就檢查response澄峰,確認(rèn)無誤后,回調(diào)onOpen通知用戶辟犀。下面是幾件重要的步驟

  • 初始化reader和writer
  • loopReader()死循環(huán)讀服務(wù)端發(fā)來的消息
  • 設(shè)置超時時間為0俏竞,證明是阻塞IO
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市堂竟,隨后出現(xiàn)的幾起案子魂毁,更是在濱河造成了極大的恐慌,老刑警劉巖出嘹,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件席楚,死亡現(xiàn)場離奇詭異,居然都是意外死亡税稼,警方通過查閱死者的電腦和手機(jī)烦秩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進(jìn)店門垮斯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人只祠,你說我怎么就攤上這事兜蠕。” “怎么了铆农?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵牺氨,是天一觀的道長狡耻。 經(jīng)常有香客問我墩剖,道長,這世上最難降的妖魔是什么夷狰? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任岭皂,我火速辦了婚禮,結(jié)果婚禮上沼头,老公的妹妹穿的比我還像新娘爷绘。我一直安慰自己,他們只是感情好进倍,可當(dāng)我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布土至。 她就那樣靜靜地躺著,像睡著了一般猾昆。 火紅的嫁衣襯著肌膚如雪陶因。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天垂蜗,我揣著相機(jī)與錄音楷扬,去河邊找鬼。 笑死贴见,一個胖子當(dāng)著我的面吹牛烘苹,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播片部,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼镣衡,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了档悠?” 一聲冷哼從身側(cè)響起廊鸥,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎站粟,沒想到半個月后黍图,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡奴烙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年助被,在試婚紗的時候發(fā)現(xiàn)自己被綠了剖张。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡揩环,死狀恐怖搔弄,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情丰滑,我是刑警寧澤顾犹,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站褒墨,受9級特大地震影響炫刷,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜郁妈,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一浑玛、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧噩咪,春花似錦顾彰、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至仆百,卻和暖如春厕隧,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背儒旬。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工栏账, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人栈源。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓挡爵,卻偏偏與公主長得像,于是被迫代替她去往敵國和親甚垦。 傳聞我的和親對象是個殘疾皇子茶鹃,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,619評論 2 354

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

  • 前言 最近公司有項目需要用WebSocket完成及時通信的需求,這里來學(xué)習(xí)一下艰亮。 WebScoket簡介 在以前的...
    Misery_Dx閱讀 5,420評論 0 15
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對...
    cosWriter閱讀 11,097評論 1 32
  • <!DOCTYPE html> 查看源 window.WRM=window.WRM||{};window....
    SMSM閱讀 878評論 1 0
  • 每天的千言萬語闭翩,想說的不過一句“我想你了” 你能狠心放下我,我就能狠心不打擾你迄埃,這次我一定做得到疗韵,放心好了~ 祝你...
    李巖心閱讀 146評論 0 0
  • 文/葉棲兒 1. 得了一場重感冒。 2. 沒人心疼你侄非。 不久前看到一篇寫《北京遇上西雅圖之不二情書...
    葉棲兒閱讀 769評論 2 1