SIP Server 第一版原理说明
1. 目标
这一版系统的目标不是做一个完整的运营级 SIP 平台,而是先把最小链路跑通:
- 监听 SIP TCP 5060 和 SIP UDP 5060
- 接收终端发来的 INVITE
- 从本地动态分配一个 RTP 端口
- 返回带 SDP answer 的
200 OK - 等待对端发送 ACK
- 对端开始往协商好的 RTP 端口发送语音
- 服务端收到 RTP 后,把语音原样回送给对端
- 对端因此能听到自己的回声
- 通话结束时处理 BYE,关闭 RTP,释放端口
这一版验证的是三件事:
- SIP 信令链路可用
- SDP 媒体协商可用
- RTP 媒体通道可用
2. 三层协议分别负责什么
2.1 SIP:负责呼叫控制
SIP 是信令协议,负责“通话怎么建立、怎么结束、媒体端口是多少”。
它关心的是:
- 谁呼叫谁
- 呼叫是否接通
- 呼叫何时结束
- 双方媒体参数通过什么内容协商
它本身不承载真正的语音流。
2.2 SDP:负责媒体协商
SDP 通常放在 SIP 消息体里,用来表达媒体参数。
它关心的是:
- 音频还是视频
- 媒体发往哪个 IP 和端口
- 使用什么编码
- 采样率是多少
- 负载类型是什么
SIP 负责“打电话”,SDP 负责“电话接通后语音往哪发、按什么格式发”。
2.3 RTP:负责承载语音
RTP 负责实际的音频数据传输。
它关心的是:
- 序列号
- 时间戳
- 负载类型
- 音频 payload
在第一版里,RTP 服务端做的是最简单的处理:
- 收到一个 RTP 包
- 不解析音频语义
- 直接把数据回发给发送端
因此,终端会听到自己的声音回环。
3. 整体架构
第一版系统可以理解为三部分:
3.1 SIP TCP 服务
监听 5060/TCP,处理基于 TCP 的 SIP 请求。
特点:
- 需要做粘包拆包
- 不能简单按“本次读到多少字节就是一个 SIP 包”
- 必须按
\r\n\r\n + Content-Length切出完整 SIP 消息
3.2 SIP UDP 服务
监听 5060/UDP,处理基于 UDP 的 SIP 请求。
特点:
- UDP 一次收到的 datagram 基本就是一个完整 SIP 报文
- 不需要像 TCP 那样做流式切包
- 但要处理重传场景,尤其是
INVITE重传
3.3 RTP UDP 动态端口服务
不固定监听整个端口范围,而是:
- 收到一个新的 INVITE
- 从
30000-40000中分配一个 RTP 端口 - 在这个端口上启动一个 UDP 监听
- 将这个端口写入 SDP answer
- 对端以后把语音发到这个端口
- 通话结束后关闭该端口监听并回收端口
这也是常规 VoIP 的做法。
4. 为什么 RTP 不需要监听整个 30000-40000
在 VoIP 场景里,通常不需要一次性监听全部 RTP 端口范围。
正确做法是:
- 每次呼叫到来时分配一个可用端口
- 把这个端口通过 SDP 告诉对端
- 对端只会往这个端口发送 RTP
- 通话结束后关闭并回收
所以,系统并不是“被动等对端随便往 30000-40000 某个端口发”,而是“先通过 SDP 明确告诉对端该发到哪个端口”。
5. SIP 交互流程
下面按一次完整呼叫来说明。
5.1 第一步:终端发送 INVITE
终端向服务器的 5060 端口发送 INVITE。 这个 INVITE 可以走 TCP,也可以走 UDP。
作用:
- 表示发起呼叫
- 携带基本 SIP 头
- 常见情况下还会在消息体里携带一个 SDP offer
一个典型 INVITE 的结构如下:
INVITE sip:1001@192.168.3.219:5060 SIP/2.0
Via: SIP/2.0/UDP 192.168.3.10:5062;branch=z9hG4bK-123456
From: <sip:caller@192.168.3.10>;tag=from-tag-001
To: <sip:1001@192.168.3.219>
Call-ID: call-0001
CSeq: 1 INVITE
Contact: <sip:caller@192.168.3.10:5062>
Content-Type: application/sdp
Content-Length: xxx
v=0
o=- 1000 1000 IN IP4 192.168.3.10
s=-
c=IN IP4 192.168.3.10
t=0 0
m=audio 40002 RTP/AVP 0 8 101
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
a=rtpmap:101 telephone-event/8000
a=ptime:20
a=sendrecv
这里分两部分看:
SIP 头部的含义
INVITE sip:... SIP/2.0:发起呼叫Via:请求经过的传输路径From:主叫方To:被叫方Call-ID:本次呼叫的全局标识CSeq:请求序号Contact:对端联系地址Content-Type: application/sdp:消息体是 SDPContent-Length:消息体字节长度
SDP offer 的含义
c=IN IP4 192.168.3.10:对端媒体 IPm=audio 40002 RTP/AVP 0 8 101:对端希望在40002端口收音频,支持的 payload type 有0、8、101a=rtpmap:0 PCMU/8000:PT 0 表示 PCMU,8kHza=rtpmap:8 PCMA/8000:PT 8 表示 PCMA,8kHza=rtpmap:101 telephone-event/8000:DTMF 事件a=ptime:20:每包 20msa=sendrecv:双向收发
5.2 第二步:服务端解析 INVITE
服务端收到 INVITE 后,会做这些动作:
- 解析 SIP 起始行和头部
- 读取
Call-ID - 检查是否是重复的 INVITE
- 解析 SDP offer
- 提取对端的 RTP IP 和 RTP 端口
- 从本地动态分配一个 RTP 端口
- 启动该 RTP 端口的 UDP 监听
如果是 UDP 模式,还会处理 INVITE 重传问题。 如果同一个 Call-ID 的 INVITE 已经处理过,就可以直接重发之前的 200 OK。
5.3 第三步:服务端返回 100 Trying(UDP 场景常见)
在 UDP 模式下,服务端可以先返回一个临时响应:
SIP/2.0 100 Trying
Via: SIP/2.0/UDP 192.168.3.10:5062;branch=z9hG4bK-123456
From: <sip:caller@192.168.3.10>;tag=from-tag-001
To: <sip:1001@192.168.3.219>
Call-ID: call-0001
CSeq: 1 INVITE
Content-Length: 0
含义是:
- 请求已经收到
- 正在处理
第一版里它的作用主要是更贴近 SIP 的基本处理流程。
5.4 第四步:服务端返回 200 OK + SDP answer
这是建立通话的关键响应。
一个典型的 200 OK 结构如下:
SIP/2.0 200 OK
Via: SIP/2.0/UDP 192.168.3.10:5062;branch=z9hG4bK-123456
From: <sip:caller@192.168.3.10>;tag=from-tag-001
To: <sip:1001@192.168.3.219>;tag=java123456789
Call-ID: call-0001
CSeq: 1 INVITE
Contact: <sip:java@192.168.3.219:5060>
Content-Type: application/sdp
Content-Length: xxx
v=0
o=- 1 1 IN IP4 192.168.3.219
s=JavaSip
c=IN IP4 192.168.3.219
t=0 0
m=audio 31234 RTP/AVP 0
a=rtpmap:0 PCMU/8000
a=ptime:20
a=sendrecv
这个 200 OK 做了什么
在 SIP 层
- 表示呼叫已经被接受
To头里追加了一个tag- 携带本端
Contact
在 SDP 层
c=IN IP4 192.168.3.219:告诉对端媒体发往这个 IPm=audio 31234 RTP/AVP 0:告诉对端音频发往31234端口a=rtpmap:0 PCMU/8000:本端当前第一版只回 PCMUa=sendrecv:双向收发
这意味着:
- 信令继续走
5060 - 语音以后不再走
5060 - 语音改走
192.168.3.219:31234
5.5 第五步:终端发送 ACK
对端收到 200 OK 后,会发送 ACK,表示这次会话确认建立。
典型报文如下:
ACK sip:1001@192.168.3.219:5060 SIP/2.0
Via: SIP/2.0/UDP 192.168.3.10:5062;branch=z9hG4bK-ack-001
From: <sip:caller@192.168.3.10>;tag=from-tag-001
To: <sip:1001@192.168.3.219>;tag=java123456789
Call-ID: call-0001
CSeq: 1 ACK
Content-Length: 0
服务端对 ACK 一般不再回响应。 它的意义是把会话状态从“已发 200 OK”推进到“已建立”。
6. SDP 协商在第一版里的含义
第一版虽然还没做完整的 codec 协商,但已经具备最基本的 SDP 作用:
对端 offer 提供的信息
- 对端 RTP IP
- 对端 RTP 端口
- 对端支持哪些编码
本端 answer 返回的信息
- 本端 RTP IP
- 本端 RTP 端口
- 本端选用哪个编码
第一版的策略是:
- 读取对端 offer
- 拿到对端 RTP 目的地址
- 本端固定回答
PCMU/8000 - 这样双方就在 PCMU 上达成最小一致
7. RTP 交互流程
一旦 ACK 完成,会话建立后,媒体流就开始工作。
7.1 对端往服务端 RTP 端口发包
对端根据 200 OK 中的 SDP answer,把 RTP 发到:
- IP:
192.168.3.219 - 端口:
31234
一个 RTP 包一般由两部分组成:
- RTP Header
- Payload
第一版使用的音频通常是 PCMU,每个 20ms 一包。 在 8kHz 单声道场景下,20ms 通常对应 160 个采样点。
因此很常见的 RTP 包结构是:
- 12 字节 RTP 头
- 160 字节 PCMU payload
总长度大约 172 字节。
7.2 RTP 头的基本字段
RTP 头里最关键的字段有:
- Version:通常是 2
- Payload Type:例如
0表示 PCMU - Sequence Number:序列号,每发一包加 1
- Timestamp:时间戳,音频每包按采样数递增
- SSRC:同步源标识
例如:
- 第一包序列号:1
- 第二包序列号:2
- 每包 20ms、8kHz,则时间戳每次增加 160
7.3 第一版的 RTP 处理方式
第一版的媒体逻辑非常简单:
- 收到对端 RTP 包
- 不做语音识别
- 不做重采样
- 不做重新编码
- 直接把收到的二进制包回发给对端
这样对端就会收到一份“自己刚刚发出的音频包的回环”。
因此,用户会听见自己的回声。
7.4 为什么能听到回声
因为对端发送路径大致是:
- 终端采集麦克风声音
- 编码成 PCMU
- 封装成 RTP
- 发给服务端
服务端处理路径是:
- 收到 RTP
- 原样发回
终端接收路径是:
- 收到来自服务端的 RTP
- 按 PCMU 解码
- 播放到扬声器
于是就形成回声。
8. 通话结束流程
当任一方要结束通话时,会发送 BYE。
典型报文如下:
BYE sip:1001@192.168.3.219:5060 SIP/2.0
Via: SIP/2.0/UDP 192.168.3.10:5062;branch=z9hG4bK-bye-001
From: <sip:caller@192.168.3.10>;tag=from-tag-001
To: <sip:1001@192.168.3.219>;tag=java123456789
Call-ID: call-0001
CSeq: 2 BYE
Content-Length: 0
服务端收到后,会做这些动作:
- 根据
Call-ID找到对应 session - 停止对应的 RTP UDP 服务
- 回收本地 RTP 端口
- 删除会话状态
- 返回
200 OK
响应报文如下:
SIP/2.0 200 OK
Via: SIP/2.0/UDP 192.168.3.10:5062;branch=z9hG4bK-bye-001
From: <sip:caller@192.168.3.10>;tag=from-tag-001
To: <sip:1001@192.168.3.219>;tag=java123456789
Call-ID: call-0001
CSeq: 2 BYE
Content-Length: 0
到这里,本次会话结束。
9. TCP 与 UDP 版的差别
第一版同时支持 SIP over TCP 和 SIP over UDP,但两者关注点不同。
9.1 TCP 版重点
TCP 是字节流,没有天然报文边界。 因此必须做:
- 找到
\r\n\r\n - 解析
Content-Length - 等 body 收满后再交给 SIP parser
所以 TCP 版的核心难点是 解帧。
9.2 UDP 版重点
UDP 一次 datagram 基本就是一条完整 SIP 消息。 所以重点不在切包,而在:
- 重传处理
- 幂等处理
- 重复 INVITE 时重复发送上次
200 OK
所以 UDP 版的核心难点是 事务和重传。
10. 会话状态在第一版里的作用
第一版已经引入了最小会话概念。 每次 INVITE 建立一个 session,session 至少保存这些信息:
Call-IDFrom tagTo tag- SIP 对端 IP/端口
- RTP 对端 IP/端口
- 本地 RTP 端口
- 最近一次
200 OK - ACK 是否已收到
- 是否已终止
会话对象的作用是把三件事关联起来:
- SIP 请求
- SDP 协商结果
- RTP 端口实例
没有会话管理,就无法在 BYE 时准确关闭对应 RTP,也无法正确处理 ACK 和重传。
11. 第一版的价值
第一版虽然功能简单,但技术意义很大,因为它已经证明了完整的 VoIP 最小闭环:
11.1 在信令层
- 可以收 INVITE
- 可以回 200 OK
- 可以收 ACK
- 可以收 BYE 并回 200 OK
11.2 在协商层
- 可以通过 SDP 告诉对端本端的媒体地址和端口
- 可以让对端据此发起语音流
11.3 在媒体层
- 可以动态监听 RTP 端口
- 可以接收 RTP
- 可以把 RTP 回送给对端
这说明整个链路已经打通了。
12. 第一版的边界
这一版是“能跑通”的版本,不是“完整 SIP 平台”版本。 它目前仍有这些边界:
12.1 SDP 还不是完整协商
当前 answer 仍以最小可用为目标,实际还可以继续增强:
- 真正从 offer 中选择双方共同支持的 codec
- 更完整处理
telephone-event - 处理更多属性行
12.2 RTP 只是原包回显
当前 RTP 处理还没有进入真正的媒体处理链路:
- 没有解码成 PCM
- 没有做 ASR
- 没有接 LLM
- 没有重新编码生成新音频
12.3 会话回收还可以继续增强
后面还应补:
- ACK 超时回收
- 异常断开回收
- 长时间无 RTP 回收
13. 后续演进方向
这一版之后,通常会往两个方向升级。
13.1 把 SIP / SDP 做扎实
包括:
- 更完整的 parser
- 更严谨的事务状态
- 更完整的 SDP offer/answer 协商
13.2 把 RTP 从“回显”升级为“媒体处理”
路径通常是:
- 收到 RTP
- 取出 payload
- 按 PCMU 解码成 PCM16
- 送入语音识别或大模型
- 生成回复音频
- 再编码成 PCMU
- 重新封装 RTP
- 发回终端
到了这一步,系统就从“echo server”变成“语音机器人 server”。
14. 一次完整交互的时序总结
下面用最简洁的顺序把整条链路串起来。
阶段一:建立呼叫
- 终端发送
INVITE + SDP offer - 服务端解析 SIP 和 SDP
- 服务端分配本地 RTP 端口,例如
31234 - 服务端启动
31234/UDP的 RTP 监听 - 服务端返回
200 OK + SDP answer - 终端发送
ACK
阶段二:传输语音
- 终端把 RTP 发到
31234 - 服务端收到 RTP
- 服务端原样回发 RTP
- 终端播放回来的语音,听到回声
阶段三:结束呼叫
- 终端发送
BYE - 服务端关闭对应 RTP 监听
- 服务端释放 RTP 端口
- 服务端返回
200 OK - 会话结束
15. 一句话概括第一版
第一版的本质是:
用 SIP 建立会话、用 SDP 协商媒体地址、用 RTP 承载音频,并通过最简单的 RTP 回环验证整条语音链路是通的。
