鄧楊凡
(中國電子科技集團公司第三十研究所,四川 成都 610041)
會話初始協議(Session Initialization Protocol,SIP)是由互聯網工程任務組(Internet Engineering Task Force,IETF)制定的多媒體通信協議,具有結構靈活、易于實現、便于擴展等特點[1]。利用SIP 協議可以實現用戶定位,檢查終端用戶的位置,用于通信。此外,還可以檢查用戶參與會話的意愿程度,在呼叫方和被叫方同時建立會話參數,實現會話管理,包括會話的傳輸和終止、修改會話參數以及請求服務。
目前主流的開源SIP 協議棧均采用C 語言開發,隨著版本不斷迭代,其功能越來越強大,同時也存在一定不便之處,如功能、結構越來越復雜,初學者的學習成本增加。同時,由于C 語言在各平臺版本庫之間存在差別,針對每個平臺需要進行獨立的交叉編譯。文章基于Go 語言設計并實現了一種可以快速部署實現的SIP協議棧,由于采用原生Go語言進行開發,不采用任何C 語言的庫,跨平臺部署時僅需修改幾行配置文件即可,無須安裝各種編譯器和庫文件。
SIP 是一種應用層協議,它獨立于傳輸層和物理層,可以通過不同的傳輸協議進行傳輸,如用戶數據報協議(User Datagram Protocol,UDP)、傳輸控制協議(Transmission Control Protocol,TCP)以及安全傳輸層(Transport Layer Security,TLS)協議。其中,UDP 是目前最常用的協議。
SIP 協議采用松耦合的分層關系結構,如圖1所示。

圖1 SIP 協議棧層級結構
SIP 協議棧最底層是語法編碼層,該層的編碼方式是增強型的巴克斯范式(Augmented Backus-Naur Form,ABNF)。第2 層是傳輸層,它定義了客戶端與服務端之間的請求與響應。第3 層是事務層,主要完成會話事務的管理。事務層采用有限狀態機機制,包括客戶端事務和服務端事務,一個事務由客戶端事務發送給服務端事務的請求和服務端事務發送對應該請求的響應組成。SIP 協議棧的最上層是事務用戶層,除了無狀態的代理,每個SIP 實體都是事務用戶。當一個事務用戶希望發送請求時,就創建一個客服端事務實例用于發送此請求。
SIP 消息是基于文本的消息,采用UTF-8 字符集。SIP 將消息分為請求和響應2 種類型,請求消息和響應消息都采用RFC2822 規定的通用消息格式,包括起始行、一個或多個消息頭、一個空行以及一個可選消息體。SIP 消息的起始行分為請求行(Request-Line)和狀態行(Status-Line)。其中,請求行是請求消息的起始行,狀態行是響應消息的起始行。請求消息包含請求行、消息頭、空行以及消息體,響應消息包括狀態行、消息頭、空行以及消息體。RFC3261 規定的6 種請求消息如表1 所示。

表1 SIP 請求消息
SIP 響應消息的第一行由狀態碼構成,表示服務端的響應狀態。RFC3261 規定了nXX(n從1 到6)的狀態碼定義,XX 用于對響應狀態的進一步描述。相關的響應狀態編碼及其含義如表2 所示。

表2 SIP 響應消息類型
根據對SIP 層級結構和消息結構的分析,結合工程實際需求,按照邏輯功能將SIP 協議棧分為不同的模塊,如圖2 所示。

圖2 SIP 協議棧模塊劃分
傳輸模塊邏輯上處于SIP 協議棧的最底層,主要實現信令傳輸和媒體傳輸。傳輸模塊能夠收發SIP信令和實時傳輸協議(Real Time Transport Protocol,RTP)流媒體數據,保證SIP 消息的完整性和正確的消息順序,根據消息類型進行分發及錯誤處理[2]。
傳輸模塊將編解碼模塊組包后的有效請求載荷通過特定端口進行發送,并接收相應的響應消息,提取有效載荷后交由編解碼模塊進行解析。SIP 消息的傳輸流程如圖3 所示。

圖3 消息傳輸流程
SIP 可以采用UDP、TCP 以及TLS 等傳輸協議作為傳輸承載。文章設計的SIP 協議棧采用UDP 作為傳輸協議,Go 語言原生提供net 包實現UDP 通信。以主叫方為例,上層用戶接口調用模塊發起一次呼叫時,即客戶端向服務端發送“Invite”消息前,程序使用net 包下的Dial 函數發起與SIP 服務端的連接,收到連接響應后,調用net 包中的“Listen Packet”創建一個UDP 服務,并提取返回結果中的UDP 服務端口值,將其作為Invite 消息中“media.port”參數的值,然后通過建立的UDP 通道發送該消息。服務端用于RTP 傳輸的UDP 服務端口的創建與之類似。UDP 傳輸建立的相關流程如圖4 所示。

圖4 UDP 傳輸典型流程
圖4 中,①和②分別是SIP 協議棧作為客戶端與服務端進行RTP 傳輸協商并分配端口等待被叫端接聽后進行RTP 流媒體傳輸的過程。
編解碼模塊提供針對SIP消息的解析與編碼能力。SIP 協議類似于超文本傳輸協議(Hypertext Transfer Protocol,HTTP)協議,都是按行解析。SIP 消息的第一行標識該消息的類型、消息目的地及協議版本。從第二行開始,每一行的格式為“字段名:空格 字段值”(冒號后固定有一個或多個空格)[3]。
根據以上分析,為了驗證平臺的實際情況,設計一套多級流水線的文本消息處理流程。第1 級以空行為分割符,將消息分割為消息頭(Message Header)和消息體(Message Body)。第2 級解析消息頭與消息體中的回車換行符,以此標志作為分割符,將消息分割為多條字段。第3 級以“:”作為分割符,獲取字段名稱。第4 級以“:”“@”等標志作為分割符,過濾出字段的參數值。4 級流水線結構的解碼模型如圖5 所示。

圖5 4 級流水線結構的解碼模型
針對SIP(包含SDP 和RTP)的編碼可以看作解析的逆向過程,編碼模塊根據接收消息的內容,結合當前網絡狀態,按照SIP 消息的標準生成消息頭和消息體,組合成相應的載荷。文章設計的協議棧支持INVITE、BYE、RING 以及ACK 等多種消息的生成和解析,其消息編解碼模塊根據消息頭判斷消息類型,同時采用多級流水的模式進行解析。
實現上述編解碼時,接收消息為16 進制的數組,此時使用Go 語言原生包“bytes”中的“splite”函數,利用“0x0d,0x0a”作為分割關鍵數組,將收到的消息進行分割,分割后的消息為一段包含關鍵字的字符串。此外,分割后第一段為消息類型或“Status Line”的值。獲取到接收消息的類型后,調用相應的函數對相關消息進行解析,獲取消息中“Call-ID”“From”“To”等關鍵參數的值進行處理存儲。
消息編碼利用SIP 協議文本性質的特點,采用關鍵字填充模式進行消息組包,通過對關鍵字的填充來生成消息載荷。SIP 消息組包的典型編程實現如圖6所示。

圖6 SIP 消息組包的編碼實現
數據處理模塊接收編解碼模塊解析的數據并對其進行處理,以供編解碼模塊生成響應消息時使用。數據處理器根據編解碼模塊解析出的消息類型,調用對應的處理函數對對方支持的媒體格式和本協議棧支持的媒體格式進行對比處理,選擇其中的交集作為對端通信的媒體格式。
SIP 消息通過文本形式進行信息交互,收發雙方每次交付都會產生大量參數,其中大部分參數在整個會話過程中都要使用。在支持多線程的情況下,相關參數的存儲量大幅增加,這就要求協議棧能夠實時創建一個存儲空間來存儲上述數據,同時相關函數可以方便地對其訪問[4]。
結合Go 語言的特點,會話中的數據采用結構體的形式進行存儲。通過HashMap 的形式,采用一次會話中的唯一值“Call-ID”作為鍵值進行檢索。會話開始時創建一條索引關系,會話結束或者會話中止時則刪除。中間可采用指針形式對索引中的值進行修改。數據存儲模塊除了存儲一次會話中的SIP 參數信息,還存儲一次SIP 會話中的會話狀態信息。狀態信息采用SIP 消息中的“Status Code”作為主要參數,同時加入部分自定義狀態。例如,協議棧作為客服端發送INVITE 消息后,則將狀態設置為“Wait-100”,同時設置一個計時器,開始等待“100 Trying”(INVITE消息的響應消息)。
數據處理存儲采用HashMap 的形式實現,Go 語言提供了“sync.map”庫用作并發環境下實現效率高且線程安全的map。map 采用“Call ID”作為鍵值,索引內容以結構體的形式存在。存儲呼叫信息的結構體如圖7 所示。

圖7 存儲呼叫信息的結構體
存儲數據的結構體采用值類型的形式進行更新,每收到一條消息,就執行一次判斷。如果當前消息不是“BYE”,就提取“Call-ID”值,并同步更新上述的“sync.map”列表;如果收到的消息為“BYE”消息,就形成相應的log,并刪除當前消息“Call-ID”對應的結構體。
有限狀態機是整個協議棧的控制核心,該模塊提供對協議棧運行的總體調度能力[5]。該模塊通過存儲模塊中存儲的“Status”作為狀態轉換的狀態值,通過一次會話中的唯一鍵值“Call ID”作為索引,獲取每次消息的“Status Code”或關鍵字(如BYE、ACK 等)作為狀態變換的更新。典型的有限狀態機結構如圖8 所示。

圖8 有限狀態機結構
有限狀態機的初始狀態為WAITING,若無主叫請求或收到被叫請求,則保持此狀態。此時根據主動發出會話請求和收到會話請求,狀態機會有2 種狀態走向。
若由SIP 協議棧發起會話,則狀態機由WAITING轉為INVITE,并開啟一個定時器。若在一定時間內沒有收到響應消息,則重新跳轉至WAITING 狀態,并拋出一個異常記錄;若在一定時間內收到“100 Trying”回復,則進入下一個狀態,等待被叫方回復“180 Ring”響應消息;若收到“180 Ring”消息后,狀態機轉為RINGING 狀態并等待200 OK 消息,狀態機轉為ACK 狀態并向對端發送“ACK”響應,同時打開會話并啟動RTP 多媒體傳輸;若收不到相應的響應,則重新轉到WAITING 狀態,重新等待會話開始。
若協議棧收到會話請求時,狀態機由WAITING轉為RINGING。此時協議棧開啟定時機制,等待用戶接收或中止該請求。若目標用戶接受請求,則將狀態機置為OK 狀態,回復“200 OK”響應,等待對方最終請求為ACK 后,狀態機由OK 狀態轉換為ACK,開啟會話和RTP 媒體傳輸;若用戶放棄該請求,則將狀態重置為WAITING 狀態,等待新的呼叫。
終止會話時,請求方或響應方發送BYE 請求,等待200 OK 消息,此時將狀態機由ACK 置為BYE狀態。當一方接受BYE 請求后,狀態機重新復位到WAITING 狀態,同時發送200 OK 響應并釋放會話,此時一個會話結束。
采用端到端的形式模擬一次SIP 呼叫與語音通話過程,利用虛擬機搭建服務端與客戶端。服務端系統采用Linphone,客戶端采用文章設計的協議棧系統,讀取已經生成的WAV 文件作為傳輸話音。實驗簡易部署結構如圖9 所示。

圖9 實驗簡易部署結構
圖9 中,主叫端為基于SIP 協議棧開發的SIP 客戶端軟件,其IP 地址為“192.168.46.147”,分配的主叫號碼為“22222222222”;被叫端采用Linphone軟件,其IP 地址為“192.168.46.142”,分配的被叫號碼為“11111111111”。實驗中SIP 信令抓包結果如圖10 所示。

圖10 實驗中SIP 信令抓包結果
由圖10 可知,基于SIP 協議棧開發的SIP 客戶端成功發起并處理了一次呼叫的全部信令流程。本次呼叫中傳輸的RTP 數據流如圖11 所示。

圖11 實驗中傳輸的RTP 數據流
文章基于Go 語言設計并實現了一種輕型的SIP協議棧,在跨平臺部署時無須安裝各種編譯器和庫文件,應用簡單便捷。通過在目標主機上進行實驗驗證,該協議棧具備主動發起呼叫并處理相關RTP 媒體流的能力,具有較好的應用價值。