1. 程式人生 > >聊聊OkHttp實現WebSocket細節,包括鑑權和長連線保活及其原理!

聊聊OkHttp實現WebSocket細節,包括鑑權和長連線保活及其原理!

一、序

OkHttp 應該算是 Android 中使用最廣泛的網路庫了,我們通常會利用它來實現 HTTP 請求,但是實際上它還可以支援 WebSocket,並且使用起來還非常的便捷。

那本文就來聊聊,利用 OkHttp 實現 WebSocket 的一些細節,包括對 WebSocket 的介紹,以及在傳輸前如何做到鑑權、長連線保活及其原理。

二、WebSocket 簡介

2.1 為什麼使用 WebSocket?

我們做客戶端開發時,接觸最多的應用層網路協議,就是 HTTP 協議,而今天介紹的 WebSocket,下層和 HTTP 一樣也是基於 TCP 協議,這是一種輕量級網路通訊協議,也屬於應用層協議。

WebSocket 與 HTTP/2 一樣,其實都是為了解決 HTTP/1.1 的一些缺陷而誕生的,而 WebSocket 針對的就是「請求-應答」這種"半雙工"的模式的通訊缺陷。

「請求-應答」是"半雙工"的通訊模式,資料的傳輸必須經過一次請求應答,這個完整的通訊過程,通訊的同一時刻資料只能在一個方向上傳遞。它最大的問題在於,HTTP 是一種被動的通訊模式,服務端必須等待客戶端請求才可以返回資料,無法主動向客戶端傳送資料。

這也導致在 WebSocket 出現之前,一些對實時性有要求的服務,通常是基於輪詢(Polling)這種簡單的模式來實現。輪詢就是由客戶端定時發起請求,如果服務端有需要傳遞的資料,可以藉助這個請求去響應資料。

輪詢的缺點也非常明顯,大量空閒的時間,其實是在反覆傳送無效的請求,這顯然是一種資源的損耗。

雖然在之後的 HTTP/2、HTTP/3 中,針對這種半雙工的缺陷新增了 Stream、Server Push 等特性,但是「請求-應答」依然是 HTTP 協議主要的通訊方式。

WebSocket 協議是由 HTML5 規範定義的,原本是為了瀏覽器而設計的,可以避免同源的限制,瀏覽器可以與任意服務端通訊,現代瀏覽器基本上都已經支援 WebSocket。

雖然 WebSocket 原本是被定義在 HTML5 中,但它也適用於移動端,儘管移動端也可以直接通過 Socket 與服務端通訊,但藉助 WebSocket,可以利用 80(HTTP) 或 443(HTTPS)埠通訊,有效的避免一些防火牆的攔截。

WebSocket 是真正意義上的全雙工模式,也就是我們俗稱的「長連線」。當完成握手連線後,客戶端和服務端均可以主動的發起請求,回覆響應,並且兩邊的傳輸都是相互獨立的。

2.2 WebSocket 的特點

WebSocket 的資料傳輸,是基於 TCP 協議,但是在傳輸之前,還有一個握手的過程,雙方確認過眼神,才能夠正式的傳輸資料。

WebSocket 的握手過程,符合其 "Web" 的特性,是利用 HTTP 本身的 "協議升級" 來實現。

在建立連線前,客戶端還需要知道服務端的地址,WebSocket 並沒有另闢蹊徑,而是沿用了 HTTP 的 URL 格式,但協議識別符號變成了 "ws" 或者 "wss",分別表示明文和加密的 WebSocket 協議,這一點和 HTTP 與 HTTPS 的關係類似。

以下是一些 WebSocket 的 URL 例子:

ws://cxmydev.com/some/path
ws://cxmydev.com:8080/some/path
wss://cxmydev.com:443?uid=xxx

而在連線建立後,WebSocket 採用二進位制幀的形式傳輸資料,其中常用的包括用於資料傳輸的資料幀 MESSAGE 以及 3 個控制幀:

  • PING:主動保活的 PING 幀;
  • PONG:收到 PING 幀後回覆;
  • CLOSE:主動關閉 WebSocket 連線;

更多 WebSocket 的協議細節,可以參考《WebSocket Protocol 規範》,具體細節,有機會為什麼再開單篇文章講解。

瞭解這些基本知識,我們基本上就可以把 WebSocket 使用起來,並且不會掉到坑裡。

我們再小結一下 WebSocket 的特性:

  1. WebSocket 建立在 TCP 協議之上,對伺服器端友好;
  2. 預設埠採用 80 或 443,握手階段採用 HTTP 協議,不容易被防火牆遮蔽,能夠通過各種 HTTP 代理伺服器;
  3. 傳輸資料相比 HTTP 更輕量,少了 HTTP Header,效能開銷更小,通訊更高效;
  4. 通過 MESSAGE 幀傳送資料,可以傳送文字或者二進位制資料,如果資料過大,會被分為多個 MESSAGE 幀傳送;
  5. WebSocket 沿用 HTTP 的 URL,協議識別符號是 "ws" 或 "wss"。

那接下來我們就看看如何利用 OkHttp 使用 WebSocket。

三、WebSocket之OkHttp

3.1 建立 WebSocket 連線

藉助 OkHttp 可以很輕易的實現 WebSocket,它的 OkHttpClient 中,提供了 newWebSocket() 方法,可以直接建立一個 WebSocket 連線並完成通訊。

fun connectionWebSockt(hostName:String,port:Int){
  val httpClient = OkHttpClient.Builder()
      .pingInterval(40, TimeUnit.SECONDS) // 設定 PING 幀傳送間隔
      .build()
  val webSocketUrl = "ws://${hostName}:${port}"
  val request = Request.Builder()
      .url(webSocketUrl)
      .build()
  httpClient.newWebSocket(request, object:WebSocketListener(){
    // ...
  })
}

我想熟悉 OkHttp 的朋友,對上面這端程式碼不會有疑問,只是 URL 換成了 "ws" 協議識別符號。另外,還需要配置 pingInterval(),這個細節後文會講解。

呼叫 newWebSocket() 後,就會開始 WebSocket 連線,但是核心操作都在 WebSocketListener 這個抽象類中。

3.2 使用 WebSocketListener

WebSocketListener 是一個抽象類,其中定義了比較多的方法,藉助這些方法回撥,就可以完成對 WebSocket 的所有操作。

var mWebSocket : WebSocket? = null
fun connectionWebSockt(hostName:String,port:Int){
  // ...
  httpClient.newWebSocket(request, object:WebSocketListener(){
    override fun onOpen(webSocket: WebSocket, response: Response) {
      super.onOpen(webSocket, response)
      // WebSocket 連線建立
      mWebSocket = webSocket
    }

    override fun onMessage(webSocket: WebSocket, text: String) {
      super.onMessage(webSocket, text)
      // 收到服務端傳送來的 String 型別訊息
    }

    override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
      super.onClosing(webSocket, code, reason)
      // 收到服務端發來的 CLOSE 幀訊息,準備關閉連線
    }

    override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
      super.onClosed(webSocket, code, reason)
      // WebSocket 連線關閉
    }

    override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
      super.onFailure(webSocket, t, response)
      // 出錯了
    }
  })
}

WebSocketListener 的所有方法回撥中,都包含了 WebSocket 型別的物件,它就是當前建立的 WebSocket 連線實體,通過它就可以向服務端傳送 WebSocket 訊息。

如果需要在其他時機發送訊息,可以在回撥 onOpen() 這個建立連線完成的時機,儲存 webSocket 物件,以備後續使用。

OkHttp 中的 WebSocket 本身是一個介面,它的實現類是 RealWebSocket,它定義了一些傳送訊息和關閉連線的方法:

  • send(text):傳送 String 型別的訊息;
  • send(bytes):傳送二進位制型別的訊息;
  • close(code, reason):主動關閉 WebSocket 連線;

利用這些回撥和 WebSocket 提供的方法,我們就可以完成 WebSocket 通訊了。

3.3 Mock WebSocket

有時候為了方便我們測試,OkHttp 還提供了擴充套件的 MockWebSocket 服務,來模擬服務端。

MockWebSocket 需要新增額外的 Gradle 引用,最好和 OkHttp 版本保持一致:

api 'com.squareup.okhttp3:okhttp:3.9.1'
api 'com.squareup.okhttp3:mockwebserver:3.9.1'

MockWebServer 的使用也非常簡單,只需要利用 MockWebSocket 類即可。

var mMockWebSocket: MockWebServer? = null
fun mockWebSocket() {
  if (mMockWebSocket != null) {
    return
  }
  mMockWebSocket = MockWebServer()
  mMockWebSocket?.enqueue(MockResponse().withWebSocketUpgrade(object : WebSocketListener() {

    override fun onOpen(webSocket: WebSocket, response: Response) {
      super.onOpen(webSocket, response)
      // 有客戶端連線時回撥
    }

    override fun onMessage(webSocket: WebSocket, text: String) {
      super.onMessage(webSocket, text)
      // 收到新訊息時回撥
    }

    override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
      super.onClosing(webSocket, code, reason)
      // 客戶端主動關閉時回撥
    }

    override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
      super.onClosed(webSocket, code, reason)
      // WebSocket 連線關閉
    }

    override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
      super.onFailure(webSocket, t, response)
      // 出錯了
    }
  }))
}

Mock WebSocket 服務端,依然需要用到我們前面講到的 WebSocketListener,這個就比較熟悉,不再贅述了。

之後就可以通過 mMockWebSocket 獲取到這個 Mock 的服務的 IP 和埠。

val hostName = mMockWebSocket?.getHostName()
val port = mMockWebSocket?.getPort()
val url = "ws:${hostName}:${port}"

需要注意的是,這兩個方法需要在子執行緒中呼叫,否者會收到一個異常。

雖然有時候在服務端完善的情況下,我們並不需要使用 Mock 的手段,但是在學習階段,依然推薦大家在本地 Mock 一個服務端,打一些日誌,觀察一個完整的 WebSocket 連線和傳送訊息的過程。

3.4 WebSocket 如何鑑權

接下來我們聊聊 WebSocket 連線的鑑權問題。

所謂鑑權,其實就是為了安全考慮,避免服務端啟動 WebSocket 的連線服務後,任誰都可以連線,這肯定會引發一些安全問題。其次,服務端還需要將 WebSocket 的連線實體與一個真是的使用者對應起來,否者業務無法保證了。

那麼問題就回到了,WebSocket 通訊的完整過程中,如何以及何時將一些業務資料傳遞給服務端?當然在 WebSocket 連線建立之後,立即給服務端傳送一些鑑權的資料,必然是可以做到業務實現的,但是這樣明顯是不夠優雅的。

前文提到,WebSocket 在握手階段,使用的是 HTTP 的 "協議升級",它本質上還是 HTTP 的報文頭髮送一些特殊的頭資料,來完成協議升級。

例如在 RealWebSocket 中,就有構造 Header 的過程,如 Upgrade、Connection 等等。

public void connect(OkHttpClient client) {
  // ...
  final Request request = originalRequest.newBuilder()
    .header("Upgrade", "websocket")
    .header("Connection", "Upgrade")
    .header("Sec-WebSocket-Key", key)
    .header("Sec-WebSocket-Version", "13")
    .build();
  //....
}

那麼實際我們在 WebSocket 階段,也可以通過 Header 傳輸一些鑑權的資料,例如 uid、token 之類,具體方法就是在構造 Request 的時候,為其增加 Header,這裡就不舉例說明了。

另外 WebSocket 的 URL 也是可以攜帶引數的。

wss://cxmydev.com:443?uid=xxx&token=xxx

3.5 WebSocket 保活

WebSocket 建立的連線就是我們所謂的長連線,每個連線對於伺服器而言,都是資源。但伺服器傾向於在一個連線長時間沒有訊息往來的時候,將其關閉。而 WebSocket 的保活,實際上就是定時向服務端傳送一個空訊息,來保證連線不會被服務端主動斷開。

那麼我們自己寫個定時器,固定間隔向服務端 mWebSocket.send() 一個訊息,就可以達到保活的目的,但這樣傳送的其實是 MESSAGE 幀資料,如果使用 WebSocket 還有更優雅的方式。

前文我們提到,WebSocket 採用二進位制幀的形式傳輸資料,其中就包括了用於保活的 PING 幀,而 OkHttp 只需要簡單的配置,就可以自動的間隔傳送 PING 幀和資料。

我們只需要在構造 OkHttpClient 的時候,通過 pingInterval() 設定 PING 幀傳送的時間間隔,它的預設值為 0,所以不設定不傳送。

val httpClient = OkHttpClient.Builder()
      .pingInterval(40, TimeUnit.SECONDS) // 設定 PING 幀傳送間隔
      .build()

這裡設定的時長,需要和服務端商議,通常建議最好設定一個小於 60s 的值。

具體的邏輯在 RealWebSocket 類中。

public void initReaderAndWriter(String name, Streams streams) throws IOException {
  synchronized (this) {
    // ...
    if (pingIntervalMillis != 0) {
      executor.scheduleAtFixedRate(
        new PingRunnable(), pingIntervalMillis, pingIntervalMillis, MILLISECONDS);
    }
    // ...
  }
  // ...
}

PingRunnabel 最終會去間隔呼叫 writePingFrame() 用以向 WebSocketWriter 中寫入 PING 幀,來達到服務端長連線保活的效果。

四、小結

到這裡本文就介紹清楚 WebSocket 以及如何使用 OkHttp 實現 WebSocket 支援。

這裡還是簡單小結一下:

  1. WebSocket 是一個全雙工的長連線應用層協議,可以通過它實現服務端到客戶端主動的推送通訊。
  2. OkHttp 中使用 WebSocket 的關鍵在於 newWebSocket() 方法以及 WebSocketListener 這個抽象類,最終連線建立完畢後,可以通過 WebSocket 物件向對端傳送訊息;
  3. WebSocket 鑑權,可以利用握手階段的 HTTP 請求中,新增 Header 或者 URL 引數來實現;
  4. WebSocket 的保活,需要定時傳送 PING 幀,傳送的時間間隔,在 OkHttp 中可以通過 pingInterval() 方法設定;

額外提一句,OkHttp 在 v3.4.1 中新增的 WebSocket 的支援,之前的版本需要 okhttp-ws 擴充套件庫來支援,但是那畢竟已經是 2016 年的事了,我想現在應該沒有人在用那麼老版本的 OkHttp 了。

本文對你有幫助嗎?留言、轉發、收藏是最大的支援,謝謝!如果本文各項資料好,之後會再分享一篇 OkHttp 中針對 WebSocket 的實現以及 WebSocket 協議的講解。

參考:

  • WebSocket教程:http://www.ruanyifeng.com/blog/2017/05/websocket.html
  • The WebSocket Protocol:https://tools.ietf.org/html/rfc6455#page-37

熱文推薦:

  • 取代安卓?谷歌新系統Fuchsia OS即將殺青,詳解程式語言的選擇
  • AS 3.6 穩定版釋出,新版本帶來了哪些變化?
  • 漫畫:聊聊執行緒池中,執行緒的增長/回收策略

公眾號後臺回覆成長『成長』,將會得到我準備的學習資料。

相關推薦

聊聊OkHttp實現WebSocket細節包括連線及其原理

一、序 OkHttp 應該算是 Android 中使用最廣泛的網路庫了,我們通常會利用它來實現 HTTP 請求,但是實際上它還可以支援 WebSocket,並且使用起來還非常的便捷。 那本文就來聊聊,利用 OkHttp 實現 WebSocket 的一些細節,包括對 WebSocket 的介紹,以及在傳輸前

高效 實現連線:手把手教你實現 自適應的心跳機制

以下內容轉載自http://blog.csdn.net/carson_ho/article/details/79522975前言當實現具備實時性需求時,我們一般會選擇長連線的通訊方式而在實現長連線方式時,存在很多效能問題,如 長連線保活今天,我將 手把手教大家實現自適應的心跳

很全面的登陸註冊介面實現包括頁面顯示後臺資料庫互動(寫了我一下午)

1.login.jsp <%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> <%@ taglib uri="http://java.sun.com/jsp/jstl/c

Retrofit+OkHttp實現Cookie持久化RxJava方式

通過Interceptor實現cookie持久化,相關三方庫:PersistentCookieJar 將cookie儲存到本地 public class ReceivedCookiesInter

android 實現視訊選取功能包括視訊錄製從相簿選取

最近在做一個程式,需要實現視訊上傳的功能,其中有一個地方需要選取視訊,選取視訊的方法有兩種,一種是視訊錄製,一種是從相簿中選取,我現在要實現的功能就是選取這個視訊,其中有一個關鍵的要點就是使用調相簿的方式來選取素材的時候,原本uri返回的是file:///...,但是and

線上選課案例—通過js實現全選全不選多選效果。順便談談理解的半吊子flag這個變數

要點: 1.首先分為兩個業務邏輯的模組,首先 全選/取消全選 的按鈕會的選中的狀態或者沒有選中,他的返回值是Boolean型別,也就是說通過通過這個通過全選按鈕將其Boolean型別的值,通過迴圈賦值給全選框下面所有的單選按鈕 2.再單選按鈕執行之前,將所有的單選按鈕狀態做一次判斷,判斷是否

htmlcssjs實現音樂播放含音訊特效歌詞

前端播放器樣例  有需要的小夥伴直接用就行:https://download.csdn.net/download/qq_34042417/10669205 實現思路: 1.載入完頁面後請求等到歌曲,歌詞檔案,要實現歌詞跟歌曲滾動則要求歌詞是lrc格式。 2.對歌詞處理,處理

(websocket)協議中Ping PongSocket通訊ping pong(連線)

- websocket協議,長連線;Http短連線 WebSocket如何建立連線、交換資料的細節,以及資料幀的格式。  WebSocket複用了HTTP的握手通道。具體指的是,客戶端通過HTTP請求與WebSocket服務端協商升級協議。協議升級完成後,後續的資料交換則遵照WebSock

如果類a繼承類b實現介面c而類b介面c中定義了同名變數請問會出現什麼問題?(瞬聯)

如果類a繼承類b,實現介面c,而類b和介面c中定義了同名變數,請問會出現什麼問題?(瞬聯)interface      A{       int x = 0;}class B{       int x =1;}class C extends B implements A{  

在Javaweb中poi實現資料匯入支援03版07版Excel匯入

注意資料型別的轉換,另外由於在後面的sid我不需要插入資料庫,所以最後就沒有set到實體物件 哪些不明白可以直問! import java.io.File; import java.io.FileInputStream; import java.io.IOExceptio

Xstream解析XML包括對陣列List的處理

使用Xstream須要引入xstream-1.4.jar和xpp3-1.1.4c.jar import com.thoughtworks.xstream.XStream; import com.hikvision.bms.main.Person; /** * 使用Xstr

C#實現圖片縮放(包括縮圖旋轉)

using System; using System.Collections; using System.Drawing; using System.Drawing.Imaging; using System.IO; using System.Web; using Syst

bash腳本之case語句應用while、untilselect循環應用及其示例

bash腳本bash腳本編程: case選擇分支結構: case: case 詞 in [模式 [| 模式]...) 命令 ;;]... esac 在腳本中使用case的結構: case ${VAR_NAME} in PATTERN1) COMMAND ... ;; PATTE

ZYNQ U-BOOT 解密方法(authenticationdecryption)

轉至:https://blog.csdn.net/cph77777/article/details/79708982  ZYNQ U-BOOT 鑑權和解密方法 [email protected] 技術交流QQ群:691976956 ZYNQ 支援最新

TCP/IPhttpRPC、SOA、連線連線

TCP/IP建立TCP需要三次握手才能建立(客戶端發起SYN,服務端SYN+ACK,客戶端ACK),斷開連線則需要四次握手(客戶端和服務端都可以發起,FIN-ACK-FIN-ACK)。為什麼連線的時候是三次握手,關閉的時候卻是四次握手?答:因為當Server端收到Client

Nginx代理webSocket時60s自動斷開, 怎麼保持連線

利用nginx代理websocket的時候,發現客戶端和伺服器握手成功後,如果在60s時間內沒有資料互動,連線就會自動斷開,如下圖:為了保持長連線,可以採取來兩種方式.1.nginx.conf 檔案裡l

TCP/IPHTTPRPC、SOA、連線連線等的區別

一、TCP/IP 建立TCP需要三次握手才能建立(客戶端發起SYN,服務端SYN+ACK,客戶端ACK), 斷開連線則需要四次握手(客戶端和服務端都可以發起,FIN-ACK-FIN-ACK)。 1、為什麼連線的時候是三次握手,關閉的時候卻是四次握手? 答

MySQL 併發測試中執行緒數資料庫連線池的實驗

   第5次的失敗原因: Cannot create PoolableConnectionFactory (Communications link failure The last packet sent successfully to the server was 0 mill

如果服務端重啟那麼客戶端的連線會怎麼樣

這裡記錄一次服務端重啟時,使用winshark的抓包過程; 場景是:SDK 建立對 服務端的長連線,客戶端連線策略是: 失活判斷: 一條連線 180s都沒有read到資料; 保活判斷: 每秒檢查一次,連續60次檢查都為空閒,那麼傳送一次keeplive包。 重連邏輯:

DeepLab:深度卷積網路多孔卷積 連線條件隨機場 的影象語義分割 Semantic Image Segmentation with Deep Convolutional Nets, Atro

深度卷積網路,多孔卷積 和全連線條件隨機場 的影象語義分割 Taylor Guo, 2017年5月03日 星期三 摘要 本文的主要任務是深度學習的影象語義分割,主要有3個方面的貢獻,有重要的實踐價值。首先, 用上取樣濾波器進行卷積,或“多孔卷積”,