WebSocket 在實時語音識別中的應用

因爲業務需求,接觸了 ws 協議以及在實時語音識別中的運用,總體的感覺還是挺有意思,並且瞭解到很多人其實是沒有用過這個協議的,所以還是值得分享記錄一下。

本文將分爲以下幾個模塊去講述 WebSocket,1. 以實際場景引入來分析傳統方式實現實時數據傳輸存在的弊端,由此引出 ws 的作用和特點。2. 介紹 ws 協議的工作流程,握手過程以及對於心跳機制的一些理解。3. 結合實際的業務場景去看看 ws 協議是怎麼使用的,並且實現了什麼樣的價值。

1. 實際場景

大家在玩股票的時候,不知道有沒有仔細的想過一個問題,它這個價格的實時變動是怎麼做的呢?說兩種比較常見的方式,第一種方式就是今天要講的 ws 協議,還有一種方式是 grpc 框架的服務端流模式,我們平時 soa 的接口其實是 grpc 一種最常見的簡單模式,他還有幾種方式,如:客戶端流模式,服務斷流模式,客戶端服務端流模式,這幾種方式後面大家都可以瞭解一下。那麼在沒有 ws 或者 grpc 服務端流模式的時候,最古老的方式是怎麼去做實時的響應呢?如圖示:

http 短輪詢:

以週期性的方式,去不斷地訪問服務端,問服務端是否有結果給返回,現在兩種情況,假設訪問的週期過長,那麼實際上已經對實時性產生了影響,假如訪問的週期很短,不斷的請求是很容易造成資源浪費的,因爲並不是每次的請求都能拿到實時的數據。

http 長輪詢:

作爲短輪詢的一種演進方式,讓請求在服務端去等待,直到拿到結果,或者請求時長達到了超時時間自動斷開,這種演進可以說是在一定程度上減少了請求的頻次,假設服務端無更新,就不會一直重新發起請求。但其實缺點依舊明顯,如果 sever 不更新的話,這個請求就會一直在服務端 hold 住,依舊是資源浪費。

總結下:其實這兩種方式在 client 數量很多的時候,是存在連接數容易被頂滿的風險的。

那麼我們希望有一種全雙工的通信協議,使得服務端和客戶端都能夠主動或者被動的發送或者接受消息,並且能夠減少類似於 http 請求的頻次,發送數據的時候又不會每次都需要重新建立連接上下文(如請求頭),那 ws 協議正好能滿足這些要求。

ws 協議的出現其實是爲了彌補 http 請求存在的缺陷的,主要解決了 http 的兩個痛點。1.http 協議的被動性,因爲 http 協議他是個請求 - 響應模型,一個請求觸發一個響應,在沒有請求的前提下,響應不會被觸發。這就是被動性。2.http 的無狀態性,所謂無狀態性,其實就是每次請求 - 響應完成,其實他的生命週期已經結束了,如果開始下一個生命週期,那必須要重新建立連接上下文。

很多人會問,http1.1 不是支持長連接嗎,爲何每次連接還是要重新建立連接上下文呢?其實 http 的長連接他是一個 "僞" 長連接,他的長連接只是基於一個 tcp 通道的複用而已,而應用層的 http 連接依舊是需要每次連接斷開,再連,斷開,再連。

其實說到這裏大家應該很明白,ws 的特點和優勢在於哪裏了,那下面就主要介紹下,ws 協議的基本工作流程,握手流程以及對於 ws 協議應用層心跳機制的一些理解。

2.ws 協議介紹

總體說下 ws 的工作流程,ws 協議需要利用 http 進行一次握手,那麼首先就得進行三次握手進行 tcp 連接,然後在進行 http 的握手操作,在握手完成之後其實就沒有 http 的什麼事情了,這裏 ws 連接就可被建立,sever 端和 client 端都可進行 OnMessage 的消息互傳,完成以後,server 和 client 都可以進行主動或者被動的關閉連接,觸發 tcp 四次揮手。

關於 ws 協議的握手:

關於 ws 協議心跳機制:

這裏拋兩個問題,其一,我們都知道 tcp 他自身是存在 keep-alive 機制的,那爲何我們還需要去做應用層的心跳檢測(原因如 123),這裏得解釋下,這個 keep-alive 和 http 那個 keep-alive 不是一個概念,tcp 的 keep-alive 是爲了檢測 client 和 server 是否處於存活狀態。

  1. 第一個原因是 tcp 本身默認的 keep-alive-time 是 2h,這也就以爲着,當兩端無數據傳輸之後,要等待兩個小時,然後 keep-alive-probe 告訴 tcp 檢測斷開的次數,並且中間必須要間隔 keep-alive-inval 的時間,就從這個過程來說是否流程太長又太複雜。

  2. 網絡本身其實是很不穩定的,當 client 遇到斷電,斷網的時候,就很容易出現 tcp 假死的情況,客戶端其實已經掛了,而服務端這個時候還認爲這個連接活着,於是依舊不斷去發送數據。

3.java 的封裝機制使得在應用層去操作傳輸層不太方便。

其二,當客戶端異常斷開的時候,各端的處理機制,如圖表:

0y2LiG

3.ws 在語音識別中的運用

先介紹下業務背景流程:

當用戶在打開頁面的時候,麥克風其實就已經在監聽的狀態了,當說出喚醒詞的時候,功能被喚醒,後面需要去指揮這個功能去幹點什麼事情,比如開鎖,關鎖,導航等命令詞,前端會進行音頻的分包切片傳輸,在通一個通道發送給 asr 識別引擎,後經過一個規則過濾或模型增強(針對特殊場景的過濾和優化),最後識別出指令,觸發相關的動作。

前期踩過的坑

前期是直接和服務端對接的,是希望以 soa 的方式去提供服務,前期嘗試的方式如下圖,兩種方式,一種方式是忽略了語音數據他其實是個連續的並且存在上下文關係特徵的數據,第二種方式是耗時太久了,這種架構一旦設計起來,問題也會一堆。

現在的問題是,假設我們要去用 soa 這種方式去調用接口,我們要做些什麼?第一,保證一個用戶的所有 pcm 切片是有序傳輸的。第二,要自己去做負載,保證一個用戶所有的 pcm 片是打到一臺 asr 引擎上的,綜合這兩點,成本其實很高,那其實就是以 soa 這種方式其實已經不合適了。

系統的整體設計

用戶首先經過一層的握手鑑權,然後進行錄音採集,並且分包發送,數據是在一個通道傳輸的,最後是經過 asr 引擎的識別,隨着輸入的不斷完善,識別的出的結果也在不斷完善,並且可以根據後面的語境去糾正之前的識別結果,最後我們會把相關的數據進行存儲,以便後期的模型強化。這裏以兩個用戶爲例的目的是爲了說明,兩個用戶是各自擁有自己的通道和 task 的,互不干擾。

WS-JAVA 服務端的實現方式 (兩種)

  1. 基於 tomcat 的 websocke 實現,註解 @ServerEndPoint("url"),幾個重要的方法:onopen,onerror,onclose,onmessage。缺點:握手攔截如鑑權,比較困難。相應的功能如圖:

  2. 基於 Springboot 的 WebSocket 實現,幾個重要的方法:afterConnectionEstablished,handleMessage,handleTransportError,afterConnectionClosed。優點:能夠在握手前後,拓展自己的業務邏輯。分別對應於 tomcat 的 onopen,onmessage,onerror,onclose。

各個方法的執行順序:

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/GUvC5Q2_KeBilWnNKiCHmw