[Android]Okhttp心跳策略研究

Cang_Wang發表於2019-03-13

Android元件化架構

我是蒼王,以下是我這個系列的相關文章,有興趣可以參考一下,可以給個喜歡或者關注我的文章。 [Android]如何做一個崩潰率少於千分之三噶應用app--章節列表

這一張非常經典的心跳策略圖示

心跳pingpong機制

現在一般的心跳策略,都是從客戶端傳送一個ping訊號給伺服器,告訴伺服器是長連線存活。 伺服器會返回一個pong訊號給客戶端,讓其更新心跳執行緒時間。如果超時沒有接收到訊號,那麼客戶端考慮重連機制。

這裡說一下okhttp有提供了WebSocket的封裝,我們的應用也是使用了WebSocket,那就直接看一下WebSocket對pingpong的封裝

 public Builder() {
      ……
      //這裡預設的ping的時間間隔為0,因為okhttp也可以有如http短連線
      pingInterval = 0;
    }

    Builder(OkHttpClient okHttpClient) {
       ……
      //builder函式提供封裝
      this.pingInterval = okHttpClient.pingInterval;
    }
    //設定間隔
public Builder pingInterval(long interval, TimeUnit unit) {
      pingInterval = checkDuration("interval", interval, unit);
      return this;
    }
複製程式碼

在RealWebSocket中啟動迴圈傳送ping訊號

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


private final class PingRunnable implements Runnable {
    PingRunnable() {
    }

    @Override public void run() {
      //寫入ping訊號
      writePingFrame();
    }
  }

void writePingFrame() {
    WebSocketWriter writer;
    int failedPing;
    synchronized (this) {
      if (failed) return;
      writer = this.writer;
      //是否等待pong訊號
      failedPing = awaitingPong ? sentPingCount : -1;
      //等待ping計數
      sentPingCount++;
      //等待pong
      awaitingPong = true;
    }
    
    //ping失敗,長連線失效
    if (failedPing != -1) {
      failWebSocket(new SocketTimeoutException("sent ping but didn't receive pong within "
          + pingIntervalMillis + "ms (after " + (failedPing - 1) + " successful ping/pongs)"),
          null);
      return;
    }

    try {
      //寫入空支付到websocket頭部
      writer.writePing(ByteString.EMPTY);
    } catch (IOException e) {
      failWebSocket(e, null);
    }
  }
複製程式碼

在RealWebSocket的call中執行loopReader監聽讀取接收到的資訊

/** 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.
      reader.processNextFrame();
    }
  }

  void processNextFrame() throws IOException {
   //讀取頭部
    readHeader();
    if (isControlFrame) {
      //讀取頭部資訊體
      readControlFrame();
    } else {
      readMessageFrame();
    }
  }

複製程式碼

讀取到是頂部資訊

private void readControlFrame() throws IOException {
    if (frameLength > 0) {
      source.readFully(controlFrameBuffer, frameLength);

      if (!isClient) {
        controlFrameBuffer.readAndWriteUnsafe(maskCursor);
        maskCursor.seek(0);
        toggleMask(maskCursor, maskKey);
        maskCursor.close();
      }
    }

    switch (opcode) {
      //讀取ping訊號
      case OPCODE_CONTROL_PING:
        frameCallback.onReadPing(controlFrameBuffer.readByteString());
        break;
      //讀取pong訊號
      case OPCODE_CONTROL_PONG:
        frameCallback.onReadPong(controlFrameBuffer.readByteString());
        break;
     //讀取到關閉連線訊號
      case OPCODE_CONTROL_CLOSE:
        int code = CLOSE_NO_STATUS_CODE;
        String reason = "";
        long bufferSize = controlFrameBuffer.size();
        if (bufferSize == 1) {
          throw new ProtocolException("Malformed close payload length of 1.");
        } else if (bufferSize != 0) {
          code = controlFrameBuffer.readShort();
          reason = controlFrameBuffer.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));
    }
  }
複製程式碼

讀取到pong訊號,等待pong置為false

@Override public synchronized void onReadPong(ByteString buffer) {
    // This API doesn't expose pings.
    receivedPongCount++;
    awaitingPong = false;
  }
複製程式碼

這就是使用OkHttp的WebSocket keepAlive的流程,而基本的okhttp的socket連線也是通過類似傳送這種pingpong訊號來維持,之需要設定維護的時間。 然後keepAlive的經驗以前的經驗值是59秒,微信的大神的方案是通過記錄socket連線和斷開時間,適配出最適當的傳送長連結時間,有興趣可以自己實驗寫一個演算法。 微信的智慧心跳方案

這邊做IM的應用的,說一下這邊方案,僅供參考。 手機進入後臺後十秒後主動關閉長連線,通過推送來維護訊息,這裡有個問題就是國內的Umeng推送可達率大家懂的,小米和華為還好點,如果是其他山寨機,到8.0後後臺很難保活了。如果是國外FCM送達率是非常高的,如果是最推國外平臺,直接依靠推送也非常可靠。

Android元件化群2

相關文章