為什麼聲音需要自己的信封
到現在你已經知道問題的樣貌了。一通電話是對話式即時媒體:端到端大約只有 150 毫秒的嚴格預算,超過了對話就開始覺得彆扭;它容得下一點點遺失、卻容不下延遲;而它真正的敵人是抵達時間不均勻,也就是抖動。從上一篇你也知道解法:一個播放緩衝,把早到的封包短暫扣住,讓每個封包都在它該被聽到的那一刻播出。但緩衝要能做這件事,每個封包就得告訴它兩件事——這是第幾個、以及它原本該在什麼時候被聽到。普通的網路封包這兩樣都沒有。我們需要一個小小的額外標頭來補上它們。
那個標頭就是 RTP,即時傳輸協定。別被這個堂皇的名字騙了:RTP 並不是 UDP 或 TCP 那種意義下的傳輸協定。它並不負責把位元送過網路——那是 UDP 在做的事。RTP 只是一層薄薄的包裝,搭在一個 UDP 資料報裡頭,補上媒體播放器真正需要的那幾個欄位。可以把它想成:傳送端把你的聲音切成一段段 20 毫秒的小塊之後,在交給郵差之前,先把每一塊塞進一個貼了標籤的小信封。信封是 RTP;郵差是 UDP 和 IP。
RTP 標頭裡面:一個編號和一個時鐘
RTP 標頭很小,而它最重要的兩個欄位,恰好就是播放緩衝苦苦盼望的那兩個。序號為封包計數:4011、4012、4013……每次加一往上跳。有了它,接收端就能把封包排回正確順序,並立刻發現有誰不見了——如果 4012 一直沒出現,那裡就有個缺口。時間戳則是一個時鐘讀數,說明這一塊的第一個取樣是在什麼時候錄下的。它讓緩衝能替每一塊安排它該被播出的確切時刻,於是即使封包抖動著抵達,你的聲音出來時依然均勻分布。
另外兩個欄位也值得一瞥。酬載型別說明裡面裝的是什麼——這一塊是哪一種音訊編解碼器編出來的,好讓接收端知道怎麼把它解碼回聲音。而 SSRC,一個隨機的來源識別碼,標明是誰在說話;在一場有好幾位發言者的會議裡,每個人都有自己的 SSRC,他們的串流才不會纏在一起。注意 RTP 沒有承諾什麼:它對「準時送達」隻字不提,對「不遺失」也隻字不提。它根本不帶任何保證。RTP 只負責提供標籤;重新排序、安排時程、修補缺口這些聰明活,全由接收端去做。
One UDP datagram carrying one RTP packet of voice: +-----------------------------------------+ | IP header (source + dest IP address) | +-----------------------------------------+ | UDP header (source + dest port) | +-----------------------------------------+ | RTP header | | payload type : 0 (a voice codec) | | sequence # : 4012 (+1 per packet) | | timestamp : 642880 (a clock value) | | SSRC : 0x5A3F (who is talking)| +-----------------------------------------+ | payload: ~20 ms of compressed audio | +-----------------------------------------+ The envelopes nest: audio inside RTP inside UDP inside IP.
RTCP:那張安靜的成績單
在 RTP 媒體串流旁邊,還並行著第二條、安靜得多的串流,叫做 RTCP,即 RTP 控制協定。它完全不帶聲音。取而代之,每隔幾秒,雙方就互寄一張小小的成績單,描述通話進行得如何:我送出了多少封包、我這邊遺失了你多少封包、我看到多少抖動、以及我們之間的來回延遲。它是那條回饋通道,讓每一端能真正去「量」那三個敵人——延遲、遺失、抖動——而不是用猜的。
何必費這個事?因為聰明的應用程式可以根據這些數字行動。如果 RTCP 回報遺失正在攀升,傳送端就能切換到一個更耐操、位元率更低的編解碼器,減輕它對一條壅塞路徑施加的負擔——很像自適應影片播放器在網路吃力時降到較低的解析度。RTCP 本身什麼也修不了;它純粹是個傳信人。但它把一條盲目的、單向噴灑的封包流,變成了一個迴圈,讓每一端都能看見通話的健康狀況並隨之調整。為了不讓自己的流量擠掉語音,RTCP 客氣地把自己限制在一小份——按慣例,大約是媒體所用頻寬的百分之五。
SIP:那位找到對方話機的媒人
RTP 和 RTCP 負責承載媒體、回報媒體狀況,但它們不回答任何接通前的問題。我要打的那個人在哪裡?他的話機到底有沒有開?我們兩邊都會講哪一種編解碼器?我該把音訊送到哪個 IP 位址和埠號?解決這一切,是另一個協定的工作,那就是 SIP,工作階段發起協定。SIP 是媒人,也是門鈴;一旦它把雙方湊在一起、談妥條件,它就退到一旁,讓 RTP 媒體在話機之間直接流動。
兩個想法讓 SIP 運轉起來。第一,找到你的是一個名字,而不是一個固定地點:SIP 位址長得像電子郵件位址,比方說 sip:[email protected],而一個「註冊」步驟讓網路隨時知道這個名字目前坐在哪一台裝置上——這正是 網域名稱系統替網站伺服器解決的同一個問題,在這裡是替「會在話機、筆電、app 之間遊走的人」解決。第二,SIP 看起來令人安心地熟悉:它是一個以文字為基礎的「請求—回應」協定,有 INVITE、BYE 之類的方法和數字狀態碼,緊貼著 HTTP 的樣子設計。如果你讀過前面幾個單元,讀一則 SIP 訊息會像在讀一個只是把動詞換了名字的 HTTP 請求。
整通通話,從頭到尾
讓我們追蹤一通 VoIP 通話,從頭到尾,看每個部件輪流上場。坐在筆電前的 Mia,打給隔著一片海洋、拿著手機的 Theo。注意 SIP 怎麼負責引介,接著 RTP 和 RTCP 承載對話,而上一篇談的播放緩衝又是怎麼安靜地把這一切撫平。
- Mia 的筆電送出一則寄給 sip:[email protected] 的 SIP INVITE。它穿過她供應商的 SIP 伺服器,這些伺服器查出 Theo 的名字目前註冊在哪裡,並把這份邀請轉發到他的手機。塞在 INVITE 裡頭的是 Mia 的提議:她會講的編解碼器、以及她希望音訊被送到的 IP 位址和埠號。
- Theo 的手機響了(一則 SIP 180 Ringing 流回 Mia,這就是她聽到回鈴音的原因)。當 Theo 按下接聽,他的手機回覆 200 OK,並在裡頭送回他自己的回答:他從她的清單裡挑的那個編解碼器,加上他希望音訊送達的 IP 與埠號。Mia 的筆電以一則 SIP ACK 確認。條件至此談妥。
- 現在 SIP 退到一旁。每支話機把它的麥克風打包成約 20 毫秒一塊,每塊裹進一個 RTP 信封,配上一個遞增的序號和一個時間戳,丟進一個 UDP 資料報,直接送往對方指定的位址。兩條 RTP 串流此刻平行流動,一邊一條——這就是真正的聲音。
- 封包抖動著抵達時,接收端並不在它們落地的那一瞬間就播出。它把封包丟進自己的播放緩衝、依序號排好,並在每個封包的時間戳所規定的那一刻把它放出來——用幾十毫秒的固定延遲為代價,把參差不齊的抵達,換成平順、間隔均勻的話語。
- 整個過程中,RTCP 成績單每隔幾秒就在他們之間往返。如果 Theo 的網路開始遺失封包,他的手機會察覺,並可能即時切換到一個更耐操、位元率更低的編解碼器。若真有封包消失,接收端會用遺失掩蓋把缺口藏起來,而不是癡等一次注定無用的重傳。
- 當 Mia 掛斷,她的筆電送出一則 SIP BYE。Theo 的手機回覆 200 OK,雙方停下各自的 RTP 串流,工作階段結束。訊令開啟了通話、訊令也關閉它;媒體只是單純地不再流動。
退一步,欣賞一下這些部件分工得多乾淨。SIP 找到對方、談妥條件,然後鞠躬退場。RTP 替每一小段音訊貼上編號與時鐘,好讓它能依序、準時地重新組裝。RTCP 低聲遞交品質報告,好讓每一端都能調整。UDP 和 IP 做真正的搬運,不問問題、也不給承諾。而這每一樣工具,都只是疊在一個盡力而為網路之上的、聰明的端系統把戲——而那個網路,一如既往,依然什麼都不保證。奇蹟不在於網路變好了——它沒有。奇蹟在於端點變聰明了。