近幾年,基于WebRTC的電話終端工具在通訊行業中越來越流行,客戶服務可以直接通過瀏覽器撥打電話來實現。目前業內大多數Web電話工具僅支持單個頁面使用,無法支撐美團多業務復雜的外呼場景,美團在WebRTC領域不斷探索,實現了多頁面多域名共用的Web端電話SDK。在 RTE 2020 實時互聯網大會上,美團前端技術專家楊尚林分享了美團是如何通過共享線程來解決多頁面多域名下共享通話狀態的業界難題的。
??點擊「閱讀原文」可觀看視頻回放,獲取 PPT
以下為演講實錄:
大家好我是來自美團網的楊尚林,很高興來參加今年的實時互聯網大會,在這里我將也會跟大家交流一些我在美團這邊一些日常的工作。
今天我主要跟大家分享的題目是美團的webRTC電話終端工具實踐。下面開始我今天的分享,我今天的分享將會進行五個部分講解,分別是工具介紹、項目背景、項目目標、實現方案以及最后的項目成果的展示,希望跟大家一起進行探討。
首先我會先介紹一下我們的終端工具到底是個什么樣的東西,大家首先可以看到一個demo的展示,那么相信在很多做通信公司里面都有的,很多公司都會做自己內部電話的系統,它其實本身是包括外呼、接聽、轉接、掛斷、呼叫保持、通話計時、通話質量檢測,最后其實還包括一個很重要的多頁面的使用,我不知道各位公司里面會不會有這樣的功能??赐炅怂鞘裁粗?,我們來看一下它的項目背景是什么,首先目前來說各個目前各大公司其實都有自己的云呼叫中心,或者有自己內部自建的通信系統,那么它本身是需要提供一些基礎的通話能力和坐席能力,比如說最基本的語音通話能力、坐席、技能、隊列等等能力,包括智能IVR、機器人的能力,同時它還會提供一套客戶關系管理或者工單系統,那么會兼容其他的數據報表等系統,包括知識庫。
其實我們可以看到語音通信能力是語音呼叫中心最基礎最基本的能力,語音通話能力一般情況下怎么樣去做呢。
現在的做法是說去實現一個桌面的軟件化,比如說我們平時經常用的ESPhone/X-Lite,在PC端的情況下,那么在Web端我們一般依賴于WebRTC,比如說我們會實現一個電話條的工具或者一個單獨的頁面,給我們業務方去提供使用。
那么Web電話一般的使用場景是什么呢,首先我們會用一種電話條UI組建的方式去嵌入業務方的頁面中,這個頁面可能包括工單系統CRM等,它的功能包括電銷、回訪、客服這種常見的功能,因為它需要滿足我們正常的通話操作。比如說外呼、撥號、掛斷等等,它還需要去保持我們的通話狀態,我們的坐席是需要實時的感知到電話的狀態的,比如說通話中、掛斷、忙線等。其實還有一些非常必要的功能。
目前來說我發現市面上其實很多的電話功能是不具備的,比如說多頁面、多系統、同狀態,這三者分別是什么意思呢,第一點就是其實我們的業務方式需要在多個瀏覽器標簽里進行外呼和掛斷的,其實目前來說大多數的這種電話工具都只是在單個tab頁里去發起外呼和接聽,或者只支持單個tab頁的注冊動作。這個需求是重點,就是說業務方其實是不關心自己的tab頁的行為是什么樣的,用戶也不需要關心自己所做的操作是什么,他們所關心的其實只是日常的工作,比如說他們接打電話、處理工單、回訪用戶等一系列。同時這個業務方還可能使用多個系統,那么他們在真正接入的時候在對于同一個業務方的兩個系統來說,一個是工單系統,一個是CRM系統,他們所需要的接入信息是一樣的,因為這個坐席它可能是同時使用兩個系統的,那么當一通電話呼入的時候它不可能說只是某一個頁面進行響應,或者是兩個同時響應,它是需要同時兩個響應的話接起任意一個,所以說業務方式可能使用多個系統的,每個系統都需要具備外呼的能力,在這種狀態下這種同頁面多系統狀態下的,每一個標簽頁都需要保持統一的狀態。
比如說我們在A頁面進行外呼的時候,我們這時候切換到B頁面把它掛斷到,其實電話所做的操作是一樣的,A、B頁面的電話都會被掛斷掉。
像這里就是展示了一個我們平時內部的一個demo頁,我們可以在多個tab頁內保持通話狀態,并且在多個tab業同時進行通話。本次這個項目目標我們是針對前面我們前面我們需要的功能去做一些分析。
第一個我們必須支持多頁面的使用,當然除了基本的電話功能之外,那么它支持多頁面多系統同時注冊和使用,并且使用的注冊信息是相同的,比如說我們使用同一個分機號去進行注冊。
第二個多頁面、多系統的時候狀態是需要完全同步的。
第三個它的業務接入是需要非常簡單,因為我們的業務方它僅需要引入我們的組件就可以接入我們的工具了,而不是要關注我們為多頁面多系統開發的成本所帶來的變化。比如說它不需要為我們支持他們進行多頁面而進行任何的適配工作。
第四個就是我們的用戶體驗必須是良好的,首先保持良好的UI和交互,要有完美的錯誤回調和說明,也要提供很好的質量檢測與設備探測功能。
我們在實現這個電話條中所經歷的哪些過程,首先這個電話工具它其實是有三個難點的,第一個就是多Tab業注冊是很難的,第二個多連接管理是非常難的,第三個我們會給大家介紹一個新的方案,引入這個新的方案我們帶來了很多相關的問題。
首先我們看一下多Tab業為什么注冊難,我想這個問題大家都是非常清楚的,因為我們都是有共性的,我們知道在WebRTC SDP交換過程中,首先我們是需要對端的地址和端口號的,我們的媒體服務器其實會為每一個注冊的動作去事先分配好一個IP+端口號去用于日后的傳輸。如果說我們有多個Tab頁的時候,比如說當Tab A已經完成了注冊并且已經完成了SDP的交互,并且在建立語音的情況下,就是說我們的媒體服務器已經為他分配好了一個端口號去進行UDP的傳輸,我們的Tab B又發起了注冊,這個時候我們的媒體服務器又會為Tab B重新分配一個端口號去用于后續的傳輸,其實這個時候我們媒體服務器用于中轉語音流的端口其實是已經變化了,就會導致當前的通話直接被斷掉。
在我們日常場景中實際上是面臨這個問題,我理解其實我們日常生活工作中所面臨的問題是大同小異的,所以說其實我們的結論在我們美團內部的結論是顯而易見的,多個Tab頁其實我們只能為他進行一次的注冊動作。
那么其實我們怎么樣讓多個Tab只進行一次注冊動作呢,其實我們在這之間去引用sharedWorker,那首先我們先不去sharedWorker是什么,我們首先來看一下shared Worker在其中起到的作用是什么,我們目前來說,假如說我們有兩個Tab頁的話那么shared Worker在中間起到一個橋梁的作用,它會溝通兩個Tab頁,它會為兩個Tab頁之間建立一定的聯系,并且我們在Tab頁中間只保持一個sharedWorker,我們僅通過這個sharedWorker在我們的遠端發起單個連接,可以理解為TabA和TabB它們兩個共用了同一個連接。
這個時候這樣的話我們的注冊命令其實就很好實現,因為在我們看來我們的電話工具其實是只有一個,我們其實不需要關心我們有幾個Tab頁,Tab A和Tab B目前來說對我們來說都已經只是一個頁面了,我們只需要通過sharedWorker去發起同步的操作就可以了。
那么sharedWorker是什么呢,它是一個共享多線程,它本身代表了一種特定類型的Worker,可以從瀏覽器的上下文中訪問,比如說在幾個窗口內iFrame或者其他的Worker中,它本身是有自己的全局作用域的,并且它還使用Post Message進行通信的。
它其實是有很多限制,比如說同源限制,那么我們其實規定加載的Worker腳本必須與我們的宿主頁面是同源的,并且shared Worker在連接不同頁面的時候,這些頁面也必須是同源的,就是說天然規定的我們所接觸的業務方它的域名必須是同源的,請注意我們之前提到過,我們是支持業務方使用多系統的,并且這些系統可能是不同域名的,后面會介紹我們怎么去處理這種現象。
第三個就是作用域是無法訪問DOM的,并且它無法在共享線程中使用任何WebRTC的相關API。7
下面有一個sharedWorker使用的例子。它的兼容性其實在我們看來是非常好的,目前來說在不考慮IE的情況下它的兼容性基本上與WebRTC相關的生態是同步的,并且國產瀏覽器同樣是支持,在國內環境中。唯一的問題是隱身模式是無法使用的,但是這個其實不是問題,因為它和localStorage這些特性其實是一樣的。
首先我們來看同源限制,為什么我們需要去考慮這個同源限制呢,因為畢竟我們作為平臺方的話我們所有的資源腳本都是要去給業務方引用的,所以我們勢必會被為業務方提供一個npm包或是一個JS的腳本,這個時候勢必會與業務方的域起沖突,所以我們一定要想辦法解決這個同域的限制,我們這里研究出來兩個方法,分別一個是Data URL,一個是iFrame。
Data URL其實我們之前用過,比如說我們在引用Base64的時候其實都是去使用過的。我們怎么樣去使用它呢,我在下面也給出了一個例子,是通過本身的importScripts API去引入官方域下的sharedWorker腳本。
iFrame方案其實通過iFrame代理去進入sharedWorker腳本。舉個例子,我有兩個域名,分別是A域名和B域名,我其實是無法再兩個域名中間去建立使用同一個shared Worker的,但是我可以在A和B兩個域名中引用同樣域的iFrame,我們使用同樣域的iFrame去引入同樣的sharedWorker,那這個時候其實我們就間接的讓A和B共同的使用了shared Worker的腳本,之后我們再配合post Message進行消息中轉就可以了,相當于iFrame在中間起到代理層的作用。
我們看一下架構圖,針對單個域名的時候sharedWorker就是一個代理,我們是通過Data URL這種方案去引用的,意思就是我們使用sharedWorker的腳本地址是一個Data URL地址,之后它會為我們去建立連接,跟我們之前說的流程是完全一樣的。
其實很多人會擔心這種Data URL方式會不會更多的是一種hack,其實我們之前也是有擔心的,就是在我們比如嘗試摸索出Data URL這種方式之后,其實我們是專門去查證的原碼,最后是發現chromium是專門放開了Data URL這種跨域的限制,原碼也在這里,大家可以放心去使用的,因為這條commit是可以加上去的。
那么iFrame方案其實更多的是針對多個域名的情況,比如說像之前講到的我們存在兩個域名,我們可以同時引入相同域的官方iFrame腳本,并且引入同樣的sharedWorker去達到我們不同的域名不同頁面使用同一個sharedWorker的目的,并且通過這個sharedWorker幫我們做資源的分發連接,那么這里面其實與Data URL相比iFrame,我們為什么會有兩個方案呢。就是因為iFrame其實大家都是比較清楚的,它相對來說耗費的資源要相對多一點,它的速度要相對慢一點,但是在絕對速度上來說其實也是在毫秒級別的,所以說業務方其實可以是自行選擇去采用多域名或者單域名的配置,我們僅僅是通過一個配置項就可以給他們提供到。這里面相對來說我們如果說業務方只有單個域名的話使用Data URL這種方案可能會稍微快一點。
其實在這個方案的背后我們是有很多的問題的,這些問題其實是非常頭疼也是非常必要的,我們現在看一下我們為了支持我們的多頁面會引入哪些問題,并且我們是如何解決這些問題。
我目前總結的這些問題應該是支持多頁面里面并且是sharedWorker方案里面應該是最棘手的問題了,底下分成三部分去講解。
首先我們就會面臨一個通話頁異常關閉的現象,那么它的現象是什么呢,就是因為我們的sharedWorker,我們之前講過它其實是無法使用WebRTC相關生態里的任何對象的,比如說MediaDevices,也就是說明我們的WebRTC相關設備連接邏輯必須是存在于我們業務方系統的某一個頁面內。所以我們勢必會出現,當我們把那個頁面關閉掉的時候,如果業務方使用多個頁面,那其實多個頁面的整個這種呼叫都會被掛斷掉。
比如說我們關閉了發起外乎或者接聽的這種頁面的話,頁面的流就會被斷掉,下面有一個示意圖,其實我們任何的這種多頁面的情況下,每時每刻都只有一個頁面的Web RTC其實真正的邏輯是在工作中的。它的原因是很明顯的,比如說我們在進行呼入的時候其實我們會在直接外呼的頁面上建立peerConnection的連接。比如說我們存在呼入的時候其實我們會在接聽的那個頁面上去進行peerConnection的連接。
下面是有例子的,比如說我有三個訂單頁,其實當我的訂單A是真正的WebRTC承載頁,那如果說我不小心把它關掉了,我們大家想一下真實邏輯的處理流程,比如說我是一名客服,我要處理三個訂單,我很容易在跟一個用戶咨詢完電話之后我不小心,或者說在中途中,其實我切換了好多頁面,我是不小心把某個頁面關掉了,但是恰好這個頁面是我真正WebRTC承載的頁面,這個時候其實整個電話都會掛斷掉。這個只是我個人一個不小心的行為會導致整個頁面掛掉,但是我們不能把這個問題去歸咎去讓客服同學注意這種行為。
其實我們是需要給一定解決方案的,這個解決方案其實是比較簡單的,我們只需要去給到我們的開發同學一個正在通話的狀態就好了,我們讓他根據這個通話狀態去進行一個提示,比如說通話中我們不允許他關閉頁面,當然這種方法是非常簡單非常高效的。但是其實我們有另外一種方法去完全規避這種開發的問題,這個時候我們會增加一個設備頁面,設備頁面它其實是用來創建WebRTC實例,并且建立peerConnection連接的頁面,其實這個設備頁面在我們提出這個方案之前它是存在一個業務方的邏輯里面的,就是業務方的頁面里面的,雖然跟我們的SDK在一起,最終我們是希望我們增加這個設備頁面去始終通過這個設備頁面去管理我們的語音,并且承載我們的通話。
我們看打開這個設備頁面的邏輯,設備頁面建立的時機是什么呢,比如說我們有呼入或者來電的時候,我們首先會去檢查設備頁面是否存在了,如果沒存在我們會把設備頁面拉起并且建立起來,并且建立它的WebRTC相關的邏輯。
他打開的策略是這個設備頁面會以最小的窗口進行打開,比方說我們給它高100像素的一個大小,并且它會顯示電話的狀態,并且它會置于所有的tab頁之后,平時是不可見的。我們還會做一個邏輯,比如說當我們的業務頁面全部關閉的時候,我們會自動觸發關閉這個tab頁的操作,其實用戶他不會感知到我們這個設備頁面的存在。就好比他是運行在所有tab頁之后的一個幽靈一樣。所有的操作同學他其實可以自由的使用他的頁面,關閉任何一個頁面都不會影響到設備頁面,只有說當我們把所有的頁面全部關閉之后才會對設備頁面進行關閉的操作。
那么現在我們其實可以看一下整個的架構,因為我們其實是引入了sharedWorker,其實正是因為我們有sharedWorker我們才得以去實現這種設備頁面的這種操作,設備頁面其實與業務頁面他們之間都是通過sharedWorker去進行共享電話的狀態和操作的,比如說業務頁面進行電話的這種狀態的展示,設備頁面去執行語音的動作,其實我們可以理解為每一個頁面包括設備頁面包括業務頁面他們都是可以從sharedWorker中獲取一定信息的。
當然只是說他們執行不同的動作而已,而只有設備頁面去承載語音的質量,而且他是唯一承載語音質量的頁面。
那么我們還會面臨版本升級混亂的問題,怎么去理解這個問題呢,其實可能多數沒有使用過sharedWorker同學完全想象不到這個問題的,但是這個問題在我們的升級開發中是非常非常嚴重的問題,因為他會導致我們整個電話系統不可用,并且每次當我們發布新版本的時候都會存在這樣的問題,就是我們升級一個新版本,業務方卻完全不可用了,它是怎么回事呢,當我們的sharedWorker腳本進行升級的時候,也就是對我們的SDK進行升級的時候,我們是需要對版本進行區分的。
因為sharedWorker它原理是跟webWorker一樣的,就是我們如果想要對它進行替換的話,我們需要去改變它的URL,所以我們需要用不同的版本對它的資源加以區分才可以替換。
比如說我們想把0.0.1的Worker去換掉0.0.2的Worker,我們真正期望的是多個頁面共享的sharedWorker,它其實能與正常的前端發布一樣,用戶是不需要關注頁面刷新或者說資源發布時機的,它可以正常的使用,完全無憂無慮的去用,我們來看一下這個問題是怎么造成的呢,其實在升級版本之前用戶的版本都是統一的。
比如說Worker的狀態其實也可以共享一例,所有的狀態都是可以同步的,比如說我們可以看一下左圖,當我們沒有升級版本之前大家的版本都是0.0.1,包括sharedWorker它本身的腳本也是0.0.1。那我們升級版本之后用戶刷新了某個頁面會導致這個頁面版本升級,從而引入新的Worker腳本,這樣會導致多個頁面間狀態是無法同步的。
因為我們底層出現了兩個Worker腳本,它會與我們的遠端進行兩個連接,并且他們的上層所有這些業務頁面是無法再進行通信了,相當于我們又開辟出來一個新的業務頁面,他們之間再也沒有關系了。這個例子大家是可以理解的。
比如說我們三個頁面中某一個頁面刷新了它變成0.0.2了,它勢必會引入新的shared Worker的資源,也變成0.0.2,這個時候其實我們底層的shared Worker會創建兩個事例,因為它們不再是同樣的URL了。
我們怎么去解決這個問題呢,其實我們需要引入一套shared Worker這種本身的升級機制的,去避免這種多頁面版本升級時和這種刷新動作帶來的這種不可用的現象,所以說我們可以通過我們總結出來的流程,去實現Worker的這種自我檢測和自我升級的。首先其實我們在運行過程中,我們是需要時時刻刻知道我們當前的版本是什么。
比如說我們在我們打包的過程中,把我們當前的版本給打入到我們的包里面去,在我們運行的過程中,其實我們在加載的初期是時刻需要對我們的Worker版本進行探測的。首先我們需要感知到當前在活著所有的Worker它是一個什么樣的版本,而我們刷新之后我們新引入我們希望升級的Worker是一個什么樣的版本,其實我們怎么樣在我們刷新頁面的時候,知道我們當前活著的就是其他頁面引用的Worker是什么呢。
因為這個時候我們還沒有建立這個shared Worker連接,其實我們是無法進行交互的,所以我們是需要先引入原先的shared Worker首先進行一個探測,去探測我們其他的頁面,目前的shared Worker是一個什么樣的狀態,同時我們需要把這個狀態去存入我們的localStorage之中。就好比是,如果我們進來之后發現我們當前已經使用了localStorage里面某一個版本的版本號的話,那么我們會跟自己的狀態自身版本號進行對比,自身版本號發現不一致的情況下,我們會給它進行一個升級并且替換,這個升級替換規則是什么呢。
比如說我當前是0.0.1我需要把自己替換成0.0.2,首先我需要同樣先引入0.0.1的這個sharedWorker,這樣的話我才能跟其他的頁面引入的sharedWorker的版本是一致的,我們大家都引入0.0.1的版本,這個時候sharedWorker是保持一列的,之后我再傳入一系列命令,去告訴我的sharedWorker你要把你自己替換成0.0.2的版本,然后shared Worker會先所有業務頁面同步狀態,所有的業務頁面都會把自己對0.0.1版本的shared Worker的引用銷毀掉,并且把自己升級成0.0.2的版本。我們目前建立一種shared Worker自更新自升級的機制。
如果其實想實現一個Web電話工具,我們是需要有其他優化內容的,比如說我們需要去提供一些懶加載機制,避免業務方自身的懶加載。
第二我們需要一系列設備檢測,幫助我們客戶快速的定位問題,去檢測他們的設備當前是否可用的,語音質量是如何的。
第三個,我們需要去提供一些網絡檢測的工具或者說頁面,或者最好把它集成到我們的電話工具里面去,還需要對我們云監控質量進行一個通信的大盤,還需要給用戶在通話過程中進行一個Notification的提示,包括可視化的一個質量信號實時的抖動。
還有我們每通電話它通信過程中發生的一些問題,如何快速定位問題,進行一個可視化的鏈路的監控。
如果說在這個鏈路監控中出現問題的時候,我們還需要對我們的操作人員或者說我們的開發人員進行一個實時的告警。
這里其實為大家展示了一個整體的架構(如上圖所示),它其實會包括很多的東西,因為shared Worker是我們整個方案的核心,整個方案里面不光是有sharedWorker,其實它包含了很多內容,所以工程化的能力其實也是需要去思考的,這里面也有我們的架構圖可以給大家展示一下,最終我們其實是達成了一些成果的,有些成果是在我們意料之內,有些成果其實是在我們完成之后又去發現的,首先我們的業務收益其實是目前支撐了我們美團數千名坐席的日常電話服務。包括電銷、面試、銷售等各種各樣場景。
其實它是同時滿足業務方快速接入的,業務方其實只需要引入我們的資源就可以了,并且它執行一個初始化的方法,他是不需要關心多個頁面帶來的問題的,接入是非常簡單的。
其實它是有一些技術收益的,比如說我們去引入sharedWorker這個方案之后,大家知道我們是保持單個連接,單個連接大大的降低了我們后端系統復雜度的,比如說我們不需要再用MQ去同步機器之間的狀態,我們不需要再去維護多個鏈接同步狀態去進行復雜的邏輯。
第二個我們是大大降低了并發的,不光是前端的連接,包括后端需要同步狀態,MQ連接,包括數據庫查詢連接,包括我們Redis的連接等各個查詢連接,我們都是降低了很多并發流量的。
第三點我們知道,我們本次的方案是多個tab頁我們只引用了一個連接,其實我們的日志是只需要打印一路就可以了,多個tab頁其實對于我們來說更像是只存在一個電話,所以我們的日志也只需要一路了,否則的話我們需要打印多路日志,這些日志其實是有大量的冗余的,并且我們查起來其實所有日志的事件是交錯在一起的。
以上就是我本次的分享,本次分享里我也介紹了我們美團在電話業務上的一些實踐,為大家介紹了sharedWorker這種方案,其實sharedWorker這種方案不光是在電話的場景中,其實在日常工作中,比如IM系統中也是可以去使用的。
標簽:迪慶
鞍山
咸寧
游戲
內蒙古