OkHttp使用分析—WebSocket篇

weixin_34198453發表於2017-04-30

OkHttp使用分析—WebSocket篇

我們先看一下怎麼使用OKhtttp完成WebSocket的請求:

  //設定連線超時時間
        mOkHttpClient = new OkHttpClient.Builder().connectTimeout(9 * 10, TimeUnit.SECONDS).build();
        Request request = new Request.Builder().url(BASE_URL).build();
        mWebSocket = mOkHttpClient.newWebSocket(request, this);

重點在這裡,開啟OkHttpClient.class查詢newWebSocket()方法:

  /**
   * 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());
    webSocket.connect(this);
    return webSocket;
  }

這裡傳入request物件和websocket的專用監聽WebSocketListener,WebSocketListener 物件稍後再做贅述,主流程還是看RealWebSocket.class的connect()方法:
步驟1:

 client = client.newBuilder()
        .protocols(ONLY_HTTP1)
        .build();

我們都知道普通的請求時client是需要被bulid的,這裡拿到OkHttpClient又重新建立了一遍,一開始就建立好了幹嘛還要建立建立呢?看這個方法:protocols(ONLY_HTTP1),

 private static final List<Protocol> ONLY_HTTP1 = Collections.singletonList(Protocol.HTTP_1_1);

步驟2:

 final Request request = originalRequest.newBuilder()
        .header("Upgrade", "websocket")
        .header("Connection", "Upgrade")
        .header("Sec-WebSocket-Key", key)
        .header("Sec-WebSocket-Version", "13")
        .build();

對request物件的頭部加工,

步驟3:

 call = Internal.instance.newWebSocketCall(client, request);

從OkHttpClient中 獲取WebSocket的call物件(回撥使用),這個Internal.instance雖然是介面方法,其實現是在OkHttpClient中,直接看對應方法:

 @Override public Call newWebSocketCall(OkHttpClient client, Request originalRequest) {
        return new RealCall(client, originalRequest, true);
      }

步驟4:搜嘎 原來enqueue()方法是使用RealCall.class的enqueue()方法,這是一個入隊的方法,而且是個非同步的方法。這就說明webSocket建立連線後才響應回撥。而且如果是長連線那麼這個執行緒就一直線上程池裡不會被釋放掉。

call.enqueue(new Callback() {
      @Override public void onResponse(Call call, Response response) {
        try {
          checkResponse(response);
        } catch (ProtocolException e) {
          failWebSocket(e, response);
          closeQuietly(response);
          return;
        }

照現在的進度已經到了設定好的回撥要開始執行了,那就轉戰RealCall

 @Override public void enqueue(Callback responseCallback) {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    captureCallStackTrace();
    client.dispatcher().enqueue(new AsyncCall(responseCallback));
  }

其實我對okhttp同步請求有幾點疑惑:
1一開始我沒有建立執行緒,那麼這個請求就是在主執行緒中嗎?
2如果是同步請求那麼如果同時多次請求是不是如果前面的請求在執行後面的請求在進入等待的狀態了呢?
其實這些問題就需要從dispatcher()的執行緒池入手了。

這個dispatcher在一開始介紹ok的時候已經介紹過了,我們來看dispatcher中的enqueue()方法:
嘿嘿嘿,又到了OkHttp請求裡了 而且 這時候realCall內部建立了AsyncCall(非同步的Call),其實看方法名就應該知道的,ok的webSocket都是使用非同步的,而且我們要明白現在只是一個最初的socket,之後的通訊,都會在該執行緒池的一個執行緒中進行。

問題1:ok的websocket是非同步的,並不會阻塞主執行緒,而且也不需要單獨開闢一個子執行緒來建立連線。
問題2:會不會阻塞首先我們再次看看這個executorService的執行緒池結構。雖然在同步篇對dispatcher的執行緒池做過介紹,但是在我看來還是很解釋不夠清晰的地方:
首先 這個是dispatcher執行緒池的結構

executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
          new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));

我在這裡做一個詳細的說明:首先,SynchronousQueue是一個無快取的阻塞的佇列,什麼意思呢?我們可以理解為當這個佇列中有元素的時候,這個元素沒有被取走(take方法)之前是不允許繼續對之後的內容進行操作。

注意1:它一種阻塞佇列,其中每個 put 必須等待一個 take,反之亦然。同步佇列沒有任何內部容量,甚至連一個佇列的容量都沒有。
注意2:它是執行緒安全的,是阻塞的。
注意3:不允許使用 null 元素。
注意4:公平排序策略是指呼叫put的執行緒之間,或take的執行緒之間。公平排序策略可以查考ArrayBlockingQueue中的公平策略。
所以這又解決了一個困擾我多年的難題:
okhttp的能同時執行多少個請求?
這個執行緒池的配置其實就是Executors提供的執行緒池配置方案之一,構造一個緩衝功能的執行緒池,配置corePoolSize=0,maximumPoolSize=Integer.MAX_VALUE,keepAliveTime=60s,以及一個無容量的阻塞佇列 SynchronousQueue,因此任務提交之後,將會建立新的執行緒執行;執行緒空閒超過60s將會銷燬:

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }


用一個形象的比喻就是一個傳球手,當從主執行緒傳進了任務,就建立一個runnable來接收。


2916442-bdd2d06701c3ee8c.jpg
ThreadPoolExecutor.jpg

這裡是Dispatcher的非同步啟動方法:

 synchronized void enqueue(AsyncCall call) {
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
      runningAsyncCalls.add(call);
      executorService().execute(call);
    } else {
      readyAsyncCalls.add(call);
    }
  }

在這裡專門用runningAsyncCalls來記錄在執行的Call,每次執行都會記錄,當向executor新增call的時候,根據2,將任務放入SynchronousQueue中等待前面的request被取出才能執行之後的request,這裡maxRequests 被定為64.超出64的將會被放入readyAsyncCalls。
ready和running之間怎麼傳遞呢?
這就需要我們對比分析下RealCall這個類:
同步的時候是呼叫RealCall的:@Override public Response execute() throws IOException
非同步的時候是呼叫AsyncCall的:

@Override protected void execute() {
      boolean signalledCallback = false;
      try {
        Response response = getResponseWithInterceptorChain();
        if (retryAndFollowUpInterceptor.isCanceled()) {
          signalledCallback = true;
          responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
        } else {
          signalledCallback = true;
          responseCallback.onResponse(RealCall.this, response);
        }
      } catch (IOException e) {
        if (signalledCallback) {
          // Do not signal the callback twice!
          Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
        } else {
          responseCallback.onFailure(RealCall.this, e);
        }
      } finally {
        client.dispatcher().finished(this);
      }
    }

事件的回撥已經具備了,回收需要看這裡.finished(this)方法,最終會呼叫這個:

private void promoteCalls() {
    if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
    if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.

    for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
      AsyncCall call = i.next();

      if (runningCallsForHost(call) < maxRequestsPerHost) {
        i.remove();
        runningAsyncCalls.add(call);
        executorService().execute(call);
      }

      if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
    }
  }

那麼問題又來了,
請對比分析Ok與Volley的優缺點。

websocket篇:

此前我先宣告一點,一個websocket連結的建立是在一個子執行緒當中,如果連結不關閉這個子執行緒一直存在,
在連結前 我們建立了一個RealWebSocket.class我們進它的構造裡看看也許有個驚喜:

public RealWebSocket(Request request, WebSocketListener listener, Random random) {
//省略部分程式碼  
this.writerRunnable = new Runnable() {
  @Override public void run() {
try {
  while (writeOneFrame()) {
  }
} catch (IOException e) {
  failWebSocket(e, null);
}
  }
};
  }

在這裡建立了一個寫的執行緒,writerRunnable
再看connect()方法:這次只需要看call的回撥就可以。根據現在的流程,連結成功,走了成功的回撥,Call的onResponse方法:

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

核心程式碼在這裡:
1.initReaderAndWriter()初始化讀寫者。這是為同伺服器互動進行準備?

 this.writer = new WebSocketWriter(streams.client, streams.sink, random);
 this.executor = new ScheduledThreadPoolExecutor(1, Util.threadFactory(name, false));

準備了Writer,準備了定時任務(心跳連結ping——pong)
runWriter();方法都做了什麼呢?
private void runWriter() {
assert (Thread.holdsLock(this));

if (executor != null) {
  executor.execute(writerRunnable);
}
  }

哈哈 原來是為心跳連結做準備啊,定時進行通知伺服器 我還在哈。

2.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();
}
  }

這不,一直迴圈呼叫reader.processNextFrame();

 /**
   * Process the next protocol frame.
   *
   * <ul>
   * <li>If it is a control frame this will result in a single call to {@link FrameCallback}.
   * <li>If it is a message frame this will result in a single call to {@link
   * FrameCallback#onReadMessage}. If the message spans multiple frames, each interleaved
   * control frame will result in a corresponding call to {@link FrameCallback}.
   * </ul>
   */
  void processNextFrame() throws IOException {
readHeader();
if (isControlFrame) {
  readControlFrame();
} else {
  readMessageFrame();
}
  }

沒辦法 註釋寫的太好了,我忍不住都貼上了進來:
1如果是控制幀將會有一個單一的callback:FrameCallback
2如果是訊息幀也會有一個單一的callback:FrameCallback#onReadMessage

看到這裡websocket基本上已經完了,剩下的就是呼叫監聽了。
~~~~~~~~~~~~~~ 補充部分 ~~~~~~~~~~~~~~~

感謝網友朋友細心指導,因為寫這篇文章比較早(細節忘了很多,尷尬)還原問題:
“框架會自動傳送ping包嗎? 怎麼設定傳送間隔時間呢?”

真的會,而且在而且OkHttpClient也支援設定心跳間隔:

 // Promote the HTTP streams into web socket streams.
        StreamAllocation streamAllocation = Internal.instance.streamAllocation(call);

還對 ping pong的次數進行了記錄:至於怎麼傳送ping 需要看這個:

  initReaderAndWriter(name, pingIntervalMillis, streams);

沒錯 又追蹤到了初始化讀寫者,在初始化讀寫者的時候有這樣一句(多看一句就能回答 讀者的問題了 甚是慚愧):

      if (pingIntervalMillis != 0) {
        executor.scheduleAtFixedRate(
            new PingRunnable(), pingIntervalMillis, pingIntervalMillis, MILLISECONDS);
      }

由此可見:
1 如果pingIntervalMillis 設定為0的時候 心跳executor是不會執行的。
2 executor 原來也負責心跳包的定時任務

讓我們看看 pingrunnable裡都做了什麼吧:

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

果然簡單實用:
一個runnable 呼叫writer的writePing方法。想一想還是很合理啊,畢竟傳送訊息就是需要 writer來做,所以 writer有這些方法也不足為其。具體writer怎麼寫 我們看下:

 /** Send a ping with the supplied {@code payload}. */
  void writePing(ByteString payload) throws IOException {
    synchronized (this) {
      writeControlFrameSynchronized(OPCODE_CONTROL_PING, payload);
    }
  }

  /** Send a pong with the supplied {@code payload}. */
  void writePong(ByteString payload) throws IOException {
    synchronized (this) {
      writeControlFrameSynchronized(OPCODE_CONTROL_PONG, payload);
    }
  }

順便一瞅 就在下邊有個pong的傳送方法,分析一下:
1 入參payload 是ByteString.EMPTY 就是一個空的位元組,
2 最終都是相同的方法writeControlFrameSynchronized,
3 對於訊息的區分:依靠writeControlFrameSynchronized的第一個入參opcode,
4 writeControlFrameSynchronized這個方法雖然沒有註釋 但是 即然寫訊息都需要呼叫這個方法,相比這個方法才是writer的實力擔當:

  private void writeControlFrameSynchronized(int opcode, ByteString payload) throws IOException {
    assert Thread.holdsLock(this);

    if (writerClosed) throw new IOException("closed");

    int length = payload.size();
    if (length > PAYLOAD_BYTE_MAX) {
      throw new IllegalArgumentException(
          "Payload size must be less than or equal to " + PAYLOAD_BYTE_MAX);
    }

    int b0 = B0_FLAG_FIN | opcode;
    sink.writeByte(b0);

    int b1 = length;
    if (isClient) {
      b1 |= B1_FLAG_MASK;
      sink.writeByte(b1);

      random.nextBytes(maskKey);
      sink.write(maskKey);

      byte[] bytes = payload.toByteArray();
      toggleMask(bytes, bytes.length, maskKey, 0);
      sink.write(bytes);
    } else {
      sink.writeByte(b1);
      sink.write(payload);
    }

    sink.flush();
  }

操作太6 ,表示職能看懂個大概 , 都被寫入這個sink中了!!!

問題來了:sink是什麼東西?

 /** Writes must be guarded(被守護的) by synchronizing on 'this'. */
  final BufferedSink sink;

沒有交代,但是有這樣一個提醒,對sink寫的時候必須是被synchronizing保護的 這樣我算是明白為嘛ping和pong的方法都會加鎖了(他說咋做就咋做 嘻嘻 稍後看)。

我們先從單詞上理解這個變數的意義吧:sink,水槽,洗滌池,什麼鬼?看不懂。。。我還是看BufferedSink吧:

  • A sink that keeps a buffer internally so that callers can do small writes
  • 在內部保留緩衝區的接收器,以便呼叫方可以執行小的寫入操作。
  • without a performance penalty.

都說了是個小型的緩衝池,因此在寫的時候會對大小進行限制:
static final long PAYLOAD_BYTE_MAX = 125L;

雖然是個介面但是已經給了我們足夠多的有效資訊,讓我們看看在建立的時候是怎麼實現這個BufferedSink,回到最初writer建立的地方:

  this.writer = new WebSocketWriter(streams.client, streams.sink, random);

哦?在初始化的時候從Stream中獲取的。在向上找當初的stream是怎麼建立的:
當連結成功後就會 返回一個Call:

   @Override public void onResponse(Call call, Response response) 

  // Promote the HTTP streams into web socket streams.
  // 促進 http流初始化這個socket流
  StreamAllocation streamAllocation = Internal.instance.streamAllocation(call);
   // Prevent connection pooling!
   // 防止連線共用
        streamAllocation.noNewStreams(); 
  //建立 Stream
   Streams streams = streamAllocation.connection().newWebSocketStreams(streamAllocation);

看來一切的謎底都在 RealConnection的newWebSockerStreams裡:

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

呵呵,看到真相我有點想放棄, new RealWebSocket.Streams(true, source, sink) sink就是這樣被賦予的,讓我回想一下,RealConnection還是挺熟悉的,是在什麼時候建立的呢?
今天先研究到這裡我容我仔細研究一番。。。

相關文章