一种基于Go 语言的SIP 协议栈设计与实现
2023-08-22邓杨凡
邓杨凡
(中国电子科技集团公司第三十研究所,四川 成都 610041)
0 引 言
会话初始协议(Session Initialization Protocol,SIP)是由互联网工程任务组(Internet Engineering Task Force,IETF)制定的多媒体通信协议,具有结构灵活、易于实现、便于扩展等特点[1]。利用SIP 协议可以实现用户定位,检查终端用户的位置,用于通信。此外,还可以检查用户参与会话的意愿程度,在呼叫方和被叫方同时建立会话参数,实现会话管理,包括会话的传输和终止、修改会话参数以及请求服务。
目前主流的开源SIP 协议栈均采用C 语言开发,随着版本不断迭代,其功能越来越强大,同时也存在一定不便之处,如功能、结构越来越复杂,初学者的学习成本增加。同时,由于C 语言在各平台版本库之间存在差别,针对每个平台需要进行独立的交叉编译。文章基于Go 语言设计并实现了一种可以快速部署实现的SIP协议栈,由于采用原生Go语言进行开发,不采用任何C 语言的库,跨平台部署时仅需修改几行配置文件即可,无须安装各种编译器和库文件。
1 SIP 协议
1.1 SIP 协议栈结构
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 实体都是事务用户。当一个事务用户希望发送请求时,就创建一个客服端事务实例用于发送此请求。
1.2 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 响应消息类型
2 协议栈模块设计与实现
根据对SIP 层级结构和消息结构的分析,结合工程实际需求,按照逻辑功能将SIP 协议栈分为不同的模块,如图2 所示。
图2 SIP 协议栈模块划分
2.1 传输模块
传输模块逻辑上处于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 流媒体传输的过程。
2.2 编解码模拟
编解码模块提供针对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 消息组包的编码实现
2.3 数据处理与数据存储模块
数据处理模块接收编解码模块解析的数据并对其进行处理,以供编解码模块生成响应消息时使用。数据处理器根据编解码模块解析出的消息类型,调用对应的处理函数对对方支持的媒体格式和本协议栈支持的媒体格式进行对比处理,选择其中的交集作为对端通信的媒体格式。
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”对应的结构体。
2.4 有限状态机
有限状态机是整个协议栈的控制核心,该模块提供对协议栈运行的总体调度能力[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 响应并释放会话,此时一个会话结束。
3 实验验证
采用端到端的形式模拟一次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 数据流
4 结 论
文章基于Go 语言设计并实现了一种轻型的SIP协议栈,在跨平台部署时无须安装各种编译器和库文件,应用简单便捷。通过在目标主机上进行实验验证,该协议栈具备主动发起呼叫并处理相关RTP 媒体流的能力,具有较好的应用价值。