Skip to content

SIP Server 第一版原理说明 ​

1. 目标 ​

这一版系统的目标不是做一个完整的运营级 SIP 平台,而是先把最小链路跑通:

  1. 监听 SIP TCP 5060 和 SIP UDP 5060
  2. 接收终端发来的 INVITE
  3. 从本地动态分配一个 RTP 端口
  4. 返回带 SDP answer 的 200 OK
  5. 等待对端发送 ACK
  6. 对端开始往协商好的 RTP 端口发送语音
  7. 服务端收到 RTP 后,把语音原样回送给对端
  8. 对端因此能听到自己的回声
  9. 通话结束时处理 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 端口范围。

正确做法是:

  1. 每次呼叫到来时分配一个可用端口
  2. 把这个端口通过 SDP 告诉对端
  3. 对端只会往这个端口发送 RTP
  4. 通话结束后关闭并回收

所以,系统并不是“被动等对端随便往 30000-40000 某个端口发”,而是“先通过 SDP 明确告诉对端该发到哪个端口”。


5. SIP 交互流程 ​

下面按一次完整呼叫来说明。


5.1 第一步:终端发送 INVITE ​

终端向服务器的 5060 端口发送 INVITE。 这个 INVITE 可以走 TCP,也可以走 UDP。

作用:

  • 表示发起呼叫
  • 携带基本 SIP 头
  • 常见情况下还会在消息体里携带一个 SDP offer

一个典型 INVITE 的结构如下:

text
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:消息体是 SDP
  • Content-Length:消息体字节长度

SDP offer 的含义 ​

  • c=IN IP4 192.168.3.10:对端媒体 IP
  • m=audio 40002 RTP/AVP 0 8 101:对端希望在 40002 端口收音频,支持的 payload type 有 0、8、101
  • a=rtpmap:0 PCMU/8000:PT 0 表示 PCMU,8kHz
  • a=rtpmap:8 PCMA/8000:PT 8 表示 PCMA,8kHz
  • a=rtpmap:101 telephone-event/8000:DTMF 事件
  • a=ptime:20:每包 20ms
  • a=sendrecv:双向收发

5.2 第二步:服务端解析 INVITE ​

服务端收到 INVITE 后,会做这些动作:

  1. 解析 SIP 起始行和头部
  2. 读取 Call-ID
  3. 检查是否是重复的 INVITE
  4. 解析 SDP offer
  5. 提取对端的 RTP IP 和 RTP 端口
  6. 从本地动态分配一个 RTP 端口
  7. 启动该 RTP 端口的 UDP 监听

如果是 UDP 模式,还会处理 INVITE 重传问题。 如果同一个 Call-ID 的 INVITE 已经处理过,就可以直接重发之前的 200 OK。


5.3 第三步:服务端返回 100 Trying(UDP 场景常见) ​

在 UDP 模式下,服务端可以先返回一个临时响应:

text
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 结构如下:

text
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:告诉对端媒体发往这个 IP
  • m=audio 31234 RTP/AVP 0:告诉对端音频发往 31234 端口
  • a=rtpmap:0 PCMU/8000:本端当前第一版只回 PCMU
  • a=sendrecv:双向收发

这意味着:

  • 信令继续走 5060
  • 语音以后不再走 5060
  • 语音改走 192.168.3.219:31234

5.5 第五步:终端发送 ACK ​

对端收到 200 OK 后,会发送 ACK,表示这次会话确认建立。

典型报文如下:

text
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 包一般由两部分组成:

  1. RTP Header
  2. 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 处理方式 ​

第一版的媒体逻辑非常简单:

  1. 收到对端 RTP 包
  2. 不做语音识别
  3. 不做重采样
  4. 不做重新编码
  5. 直接把收到的二进制包回发给对端

这样对端就会收到一份“自己刚刚发出的音频包的回环”。

因此,用户会听见自己的回声。


7.4 为什么能听到回声 ​

因为对端发送路径大致是:

  • 终端采集麦克风声音
  • 编码成 PCMU
  • 封装成 RTP
  • 发给服务端

服务端处理路径是:

  • 收到 RTP
  • 原样发回

终端接收路径是:

  • 收到来自服务端的 RTP
  • 按 PCMU 解码
  • 播放到扬声器

于是就形成回声。


8. 通话结束流程 ​

当任一方要结束通话时,会发送 BYE。

典型报文如下:

text
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

服务端收到后,会做这些动作:

  1. 根据 Call-ID 找到对应 session
  2. 停止对应的 RTP UDP 服务
  3. 回收本地 RTP 端口
  4. 删除会话状态
  5. 返回 200 OK

响应报文如下:

text
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-ID
  • From tag
  • To tag
  • SIP 对端 IP/端口
  • RTP 对端 IP/端口
  • 本地 RTP 端口
  • 最近一次 200 OK
  • ACK 是否已收到
  • 是否已终止

会话对象的作用是把三件事关联起来:

  1. SIP 请求
  2. SDP 协商结果
  3. 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 从“回显”升级为“媒体处理” ​

路径通常是:

  1. 收到 RTP
  2. 取出 payload
  3. 按 PCMU 解码成 PCM16
  4. 送入语音识别或大模型
  5. 生成回复音频
  6. 再编码成 PCMU
  7. 重新封装 RTP
  8. 发回终端

到了这一步,系统就从“echo server”变成“语音机器人 server”。


14. 一次完整交互的时序总结 ​

下面用最简洁的顺序把整条链路串起来。

阶段一:建立呼叫 ​

  1. 终端发送 INVITE + SDP offer
  2. 服务端解析 SIP 和 SDP
  3. 服务端分配本地 RTP 端口,例如 31234
  4. 服务端启动 31234/UDP 的 RTP 监听
  5. 服务端返回 200 OK + SDP answer
  6. 终端发送 ACK

阶段二:传输语音 ​

  1. 终端把 RTP 发到 31234
  2. 服务端收到 RTP
  3. 服务端原样回发 RTP
  4. 终端播放回来的语音,听到回声

阶段三:结束呼叫 ​

  1. 终端发送 BYE
  2. 服务端关闭对应 RTP 监听
  3. 服务端释放 RTP 端口
  4. 服务端返回 200 OK
  5. 会话结束

15. 一句话概括第一版 ​

第一版的本质是:

用 SIP 建立会话、用 SDP 协商媒体地址、用 RTP 承载音频,并通过最简单的 RTP 回环验证整条语音链路是通的。

MIT Licensed