11-2. SIP 呼叫流程詳解
深入解析 SIP 各種呼叫場景、轉接、保留與會議功能
📱 SIP 呼叫流程詳解
🎯 基本呼叫流程
💡 比喻:打電話的完整過程
1. 撥號(INVITE)
2. 電話響鈴(180 Ringing)
3. 對方接聽(200 OK)
4. 確認接通(ACK)
5. 開始通話(RTP)
6. 掛斷電話(BYE)最簡單的場景(無 Proxy)
Alice Bob
(192.168.1.100) (192.168.1.200)
│ │
├─ INVITE ───────────────────────>│ 撥號
│ │
│<─ 100 Trying ─────────────────────┤ 處理中
│ │
│<─ 180 Ringing ────────────────────┤ 響鈴
│ │
│<─ 200 OK ─────────────────────────┤ 接聽
│ │
├─ ACK ───────────────────────────>│ 確認
│ │
│<══════ RTP 媒體流 ═══════════════>│ 通話中
│ │
├─ BYE ───────────────────────────>│ 掛斷
│ │
│<─ 200 OK ─────────────────────────┤ 確認掛斷詳細訊息內容:
1. INVITE(Alice → Bob):
INVITE sip:bob@192.168.1.200:5060 SIP/2.0
Via: SIP/2.0/UDP 192.168.1.100:5060;branch=z9hG4bK776
Max-Forwards: 70
To: Bob <sip:bob@192.168.1.200>
From: Alice <sip:alice@192.168.1.100>;tag=9fxced76sl
Call-ID: 3848276298220188511@192.168.1.100
CSeq: 1 INVITE
Contact: <sip:alice@192.168.1.100:5060>
Content-Type: application/sdp
Content-Length: 142
v=0
o=alice 2890844526 2890844526 IN IP4 192.168.1.100
s=-
c=IN IP4 192.168.1.100
t=0 0
m=audio 49170 RTP/AVP 0
a=rtpmap:0 PCMU/80002. 180 Ringing(Bob → Alice):
SIP/2.0 180 Ringing
Via: SIP/2.0/UDP 192.168.1.100:5060;branch=z9hG4bK776
To: Bob <sip:bob@192.168.1.200>;tag=314159
From: Alice <sip:alice@192.168.1.100>;tag=9fxced76sl
Call-ID: 3848276298220188511@192.168.1.100
CSeq: 1 INVITE
Contact: <sip:bob@192.168.1.200:5060>
Content-Length: 03. 200 OK(Bob → Alice):
SIP/2.0 200 OK
Via: SIP/2.0/UDP 192.168.1.100:5060;branch=z9hG4bK776
To: Bob <sip:bob@192.168.1.200>;tag=314159
From: Alice <sip:alice@192.168.1.100>;tag=9fxced76sl
Call-ID: 3848276298220188511@192.168.1.100
CSeq: 1 INVITE
Contact: <sip:bob@192.168.1.200:5060>
Content-Type: application/sdp
Content-Length: 131
v=0
o=bob 2890844730 2890844730 IN IP4 192.168.1.200
s=-
c=IN IP4 192.168.1.200
t=0 0
m=audio 38060 RTP/AVP 0
a=rtpmap:0 PCMU/80004. ACK(Alice → Bob):
ACK sip:bob@192.168.1.200:5060 SIP/2.0
Via: SIP/2.0/UDP 192.168.1.100:5060;branch=z9hG4bK777
Max-Forwards: 70
To: Bob <sip:bob@192.168.1.200>;tag=314159
From: Alice <sip:alice@192.168.1.100>;tag=9fxced76sl
Call-ID: 3848276298220188511@192.168.1.100
CSeq: 1 ACK
Content-Length: 05. BYE(Alice → Bob):
BYE sip:bob@192.168.1.200:5060 SIP/2.0
Via: SIP/2.0/UDP 192.168.1.100:5060;branch=z9hG4bK778
Max-Forwards: 70
To: Bob <sip:bob@192.168.1.200>;tag=314159
From: Alice <sip:alice@192.168.1.100>;tag=9fxced76sl
Call-ID: 3848276298220188511@192.168.1.100
CSeq: 2 BYE
Contact: <sip:alice@192.168.1.100:5060>
Content-Length: 0🔀 透過 Proxy 的呼叫流程
💡 比喻:透過總機轉接
Alice 打給總機,總機找到 Bob 並轉接Alice Proxy Server Bob
│ │ │
├─ 1. INVITE ────────>│ │
│ ├─ 2. INVITE ───────>│
│ │ │
│<─ 3. 100 Trying ─────┤ │
│ │ │
│ │<─ 4. 180 Ringing ───┤
│<─ 5. 180 Ringing ────┤ │
│ │ │
│ │<─ 6. 200 OK ────────┤
│<─ 7. 200 OK ─────────┤ │
│ │ │
├─ 8. ACK ─────────────────────────────────>│
│ │ │
│<══════════ 9. RTP 媒體流(直連)═════════>│
│ │ │
├─ 10. BYE ────────────────────────────────>│
│<─ 11. 200 OK ──────────────────────────────┤重點:
- ✅ Proxy 只轉發「訊號」(SIP 訊息)
- ✅ RTP 媒體流「直連」(不經過 Proxy)
- ✅ ACK 和 BYE 直接發送給對方(使用 Contact header)
Via Header 追蹤路徑:
Alice 發送 INVITE:
Via: SIP/2.0/UDP alice.com:5060;branch=z9hG4bK776
Proxy 轉發時加上自己的 Via:
Via: SIP/2.0/UDP proxy.com:5060;branch=z9hG4bK123
Via: SIP/2.0/UDP alice.com:5060;branch=z9hG4bK776
Bob 回應時,按照 Via 順序回傳(先回給 Proxy,再回給 Alice)📍 Redirect Server 流程
💡 比喻:查號台
總機告訴你正確的號碼,但不幫你轉接Alice Redirect Server Bob
│ │ │
├─ INVITE ───────────>│ │
│ To: bob@old.com │ │
│ │ │
│<─ 302 Moved ─────────┤ │
│ Contact: bob@new.com│ │
│ │ │
├─ ACK ───────────────>│ │
│ │ │
├─ INVITE ─────────────────────────────────>│
│ To: bob@new.com │ │
│ │ │
│<─ 200 OK ───────────────────────────────────┤
├─ ACK ─────────────────────────────────────>│
│<══════════ RTP 媒體流 ═══════════════════>│302 Moved Temporarily:
SIP/2.0 302 Moved Temporarily
Via: SIP/2.0/UDP alice.com:5060;branch=z9hG4bK776
To: Bob <sip:bob@old.com>;tag=314159
From: Alice <sip:alice@example.com>;tag=9fxced76sl
Call-ID: 123@alice.com
CSeq: 1 INVITE
Contact: <sip:bob@new.com> ← 新的聯絡地址
Content-Length: 0⏸️ Call Hold(通話保留)
💡 比喻:讓對方等一下
就像打電話時說「請稍等」,然後放下聽筒去做事保留通話流程
Alice Bob
│ │
│<═══ RTP ═══════>│ 通話中
│ │
├─ re-INVITE ────>│ (保留通話)
│ a=sendonly │ 只發送,不接收(靜音)
│ │
│<─ 200 OK ───────┤
│ a=recvonly │ 只接收,不發送
│ │
├─ ACK ──────────>│
│ │
│──> RTP ─────────>│ Alice 單向發送保留音樂
│ │
...(Alice 去處理其他事情)
│ │
├─ re-INVITE ────>│ (恢復通話)
│ a=sendrecv │ 雙向通話
│ │
│<─ 200 OK ───────┤
│ a=sendrecv │
│ │
├─ ACK ──────────>│
│<═══ RTP ═══════>│ 恢復正常通話SDP 範例:
保留通話(a=sendonly):
v=0
o=alice 2890844526 2890844527 IN IP4 192.168.1.100
s=-
c=IN IP4 192.168.1.100
t=0 0
m=audio 49170 RTP/AVP 0
a=rtpmap:0 PCMU/8000
a=sendonly ← 只發送(播放保留音樂),不接收恢復通話(a=sendrecv):
m=audio 49170 RTP/AVP 0
a=rtpmap:0 PCMU/8000
a=sendrecv ← 雙向通話JavaScript 實作:
// 保留通話
async function holdCall(session) {
const pc = session.connection;
// 設定只發送
const senders = pc.getSenders();
senders.forEach(sender => {
if (sender.track) {
sender.track.enabled = false; // 停止發送媒體
}
});
// 發送 re-INVITE
await session.renegotiate({
rtcOfferConstraints: {
offerToReceiveAudio: false, // 不接收音訊
offerToReceiveVideo: false
}
});
console.log('通話已保留');
}
// 恢復通話
async function resumeCall(session) {
const pc = session.connection;
// 恢復發送
const senders = pc.getSenders();
senders.forEach(sender => {
if (sender.track) {
sender.track.enabled = true;
}
});
// 發送 re-INVITE
await session.renegotiate({
rtcOfferConstraints: {
offerToReceiveAudio: true,
offerToReceiveVideo: true
}
});
console.log('通話已恢復');
}🔀 Call Transfer(轉接)
💡 比喻:把電話轉給別人
Alice 和 Bob 通話中,Alice 想把 Bob 轉給 Charlie1️⃣ Blind Transfer(盲目轉接)
不確認 Charlie 是否接聽,直接轉過去Alice Bob Charlie
│ │ │
│<══RTP════>│ │ Alice ↔ Bob 通話中
│ │ │
├─ REFER ─>│ │ (轉給 Charlie)
│ Refer-To: Charlie │
│ │ │
│<─ 202 Accepted ────────┤
│ │ │
│ ├─ INVITE ──>│ Bob 打給 Charlie
│ │ │
│ │<─ 200 OK ──┤
│ ├─ ACK ─────>│
│ │<══RTP═════>│ Bob ↔ Charlie 通話中
│ │ │
├─ BYE ───>│ │ Alice 掛斷
│<─ 200 OK ─┤ │REFER 訊息:
REFER sip:bob@example.com SIP/2.0
Via: SIP/2.0/UDP alice.com:5060;branch=z9hG4bK776
To: Bob <sip:bob@example.com>;tag=314159
From: Alice <sip:alice@example.com>;tag=9fxced76sl
Call-ID: 123@alice.com
CSeq: 101 REFER
Refer-To: <sip:charlie@example.com> ← 轉給 Charlie
Referred-By: <sip:alice@example.com>
Content-Length: 0JavaScript 實作:
// 盲目轉接
function blindTransfer(session, targetURI) {
session.refer(targetURI);
session.on('refer', (event) => {
console.log('轉接請求已發送');
session.terminate(); // 掛斷與 Alice 的通話
});
}
// 使用
blindTransfer(currentSession, 'sip:charlie@example.com');2️⃣ Attended Transfer(咨詢轉接)
先打給 Charlie 確認,再轉接Alice Bob Charlie
│ │ │
│<═RTP═>│ │ Alice ↔ Bob 通話中
│ │ │
├─ INVITE ────────>│ Alice 打給 Charlie(保留 Bob)
│ │ │
│<─ 200 OK ────────┤
├─ ACK ──────────>│
│<═RTP═══════════>│ Alice ↔ Charlie 通話中
│ │ │
│ │ │ Alice 咨詢 Charlie 是否接受轉接
│ │ │
├─ REFER ────────>│ (告訴 Charlie 接受 Bob 的來電)
│ Refer-To: Bob │
│ │ │
│<─ 202 Accepted ──┤
│ │ │
│ ├─ INVITE >│ Charlie 打給 Bob
│ │ │
│ │<─ 200 OK┤
│ ├─ ACK ──>│
│ │<══RTP══>│ Bob ↔ Charlie 通話中
│ │ │
├─ BYE ──────────>│ Alice 掛斷與 Charlie 的通話
├─ BYE ──>│ │ Alice 掛斷與 Bob 的通話JavaScript 實作:
// 咨詢轉接
async function attendedTransfer(sessionWithBob, targetURI) {
// 1. 保留與 Bob 的通話
await holdCall(sessionWithBob);
// 2. 打給 Charlie
const sessionWithCharlie = ua.call(targetURI, options);
sessionWithCharlie.on('accepted', async () => {
console.log('Charlie 接聽了');
// 3. 詢問 Charlie 是否接受轉接
const accept = confirm('將 Bob 轉給 Charlie?');
if (accept) {
// 4. 執行轉接
sessionWithBob.refer(targetURI, {
replaces: sessionWithCharlie
});
// 5. 掛斷兩個通話
sessionWithBob.terminate();
sessionWithCharlie.terminate();
} else {
// 恢復與 Bob 的通話
await resumeCall(sessionWithBob);
sessionWithCharlie.terminate();
}
});
}
// 使用
attendedTransfer(currentSession, 'sip:charlie@example.com');🎪 Conference Call(三方通話)
💡 比喻:開會
多人同時在線上討論架構選擇
1. Mixing(混音):
Alice ─┐
Bob ───┼──> Conference Server(MCU)
Charlie┘ ↓
混合音訊
↓
廣播給所有人(一個流)2. Full Mesh(完全網狀):
Alice ←→ Bob
↓ ╲ ↗
↓ Charlie
↓ ↗
↓↗
每個人都連線到其他所有人
3 人 = 3 條連線
10 人 = 45 條連線(n*(n-1)/2)Conference 建立流程(使用 MCU)
Alice Conference Server Bob
│ │ │
├─ INVITE ──────────────>│ │
│ Conference-ID: 123 │ │
│ │ │
│<─ 200 OK ───────────────┤ │
├─ ACK ─────────────────>│ │
│<══════ RTP ═══════════>│ │
│ │ │
│ │<─ INVITE ─────────┤
│ │ Conference-ID: 123│
│ │ │
│ ├─ 200 OK ─────────>│
│ │<─ ACK ─────────────┤
│ │<══════ RTP ═══════>│
│ │ │
│ Conference Server 混合音訊 │
│<══════════ 混合音訊 ═══════════════════════>│JavaScript 實作(使用 SFU):
// 建立會議室
class ConferenceCall {
constructor(conferenceURI) {
this.conferenceURI = conferenceURI;
this.sessions = [];
}
// 加入會議
async join() {
const session = ua.call(this.conferenceURI, {
mediaConstraints: {
audio: true,
video: true
}
});
session.on('accepted', () => {
console.log('已加入會議');
this.sessions.push(session);
});
session.on('ended', () => {
console.log('已離開會議');
const index = this.sessions.indexOf(session);
this.sessions.splice(index, 1);
});
return session;
}
// 邀請其他人
async invite(targetURI) {
// 發送 REFER 給會議伺服器
const referSession = this.sessions[0];
referSession.refer(targetURI);
}
// 離開會議
leave() {
this.sessions.forEach(session => {
session.terminate();
});
this.sessions = [];
}
}
// 使用
const conference = new ConferenceCall('sip:conf123@conference.example.com');
await conference.join();
// 邀請 Bob
conference.invite('sip:bob@example.com');
// 邀請 Charlie
conference.invite('sip:charlie@example.com');🎵 Early Media(早期媒體)
💡 比喻:響鈴音、語音提示
打電話時聽到的「嘟嘟嘟」或「您撥打的電話正在通話中」問題:
一般流程:
INVITE → 180 Ringing → 200 OK → ACK → RTP
問題:在 200 OK 之前無法播放媒體(響鈴音、提示音)解決:使用 183 Session Progress
Alice Bob
│ │
├─ INVITE ───────────────────────>│
│ │
│<─ 183 Session Progress ───────────┤
│ (包含 SDP) │
│ │
│<══════ Early RTP ═══════════════>│ 播放響鈴音
│ │
│<─ 180 Ringing ────────────────────┤
│ │
│<─ 200 OK ─────────────────────────┤
│ │
├─ ACK ───────────────────────────>│
│<══════ RTP ═══════════════════════>│ 正常通話183 Session Progress:
SIP/2.0 183 Session Progress
Via: SIP/2.0/UDP alice.com:5060;branch=z9hG4bK776
To: Bob <sip:bob@example.com>;tag=314159
From: Alice <sip:alice@example.com>;tag=9fxced76sl
Call-ID: 123@alice.com
CSeq: 1 INVITE
Content-Type: application/sdp
Content-Length: 147
v=0
o=bob 2890844730 2890844730 IN IP4 192.168.1.200
s=-
c=IN IP4 192.168.1.200
t=0 0
m=audio 38060 RTP/AVP 0
a=rtpmap:0 PCMU/8000
a=sendonly ← 只發送(播放響鈴音)應用場景:
- 📞 自訂響鈴音
- 🎙️ 語音提示(「請撥分機號碼」)
- 📢 廣告(「本通話由 XX 贊助」)
🎓 常見面試題
Q1:SIP 的三次握手是哪三次?
答案:
INVITE → 200 OK → ACK
就像 TCP 三次握手:
SYN → SYN-ACK → ACK為什麼需要 ACK?
問題場景:
Alice ─ INVITE ──> Bob
Alice <─ 200 OK ─── Bob
如果 Alice 沒收到 200 OK,會重傳 INVITE
Bob 會重傳 200 OK
但如果 Alice 收到了 200 OK,如何讓 Bob 知道?
→ 發送 ACK 確認
Alice ─ ACK ──> Bob
(Bob 收到 ACK,停止重傳 200 OK)ACK 的特殊性:
- ✅ ACK 不會被回應(沒有 200 OK for ACK)
- ✅ ACK 直接發送給對方(不經過 Proxy)
- ✅ 使用 Contact header 中的 URI
記憶技巧:「邀答認」(INVITE、200 OK、ACK)
Q2:re-INVITE 用在哪些場景?
答案:
re-INVITE 用於修改現有會話的參數(不是建立新通話)。
常見場景:
1. Call Hold(保留通話):
// 修改 SDP:a=sendonly
session.renegotiate();2. 新增/移除媒體:
// 原本只有音訊,新增視訊
session.renegotiate({
mediaConstraints: {
audio: true,
video: true // 新增視訊
}
});3. 更換編碼器:
m=audio 49170 RTP/AVP 0 8
原本:PCMU (0), PCMA (8)
re-INVITE:
m=audio 49170 RTP/AVP 99
更換為:Opus (99)4. 切換網路(IP 變更):
WiFi 切換到 4G,IP 改變
發送 re-INVITE 更新 SDP 中的 IP5. NAT Keep-Alive:
定期發送 re-INVITE 保持 NAT 通道開啟re-INVITE 流程:
Alice Bob
│<══ RTP ═════════>│ 通話中
│ │
├─ re-INVITE ─────>│ 修改參數
│ (新的 SDP) │
│ │
│<─ 200 OK ────────┤
│ (新的 SDP) │
│ │
├─ ACK ──────────>│
│<══ 新的 RTP ════>│ 使用新參數Q3:Forking 是什麼?
答案:
Forking(分叉) 是指一個 INVITE 請求被轉發到多個目標。
💡 比喻:同時撥打多個號碼
打給 Bob,但 Bob 有手機、辦公室電話、家裡電話
全部同時響鈴,誰先接就是誰Parallel Forking(並行分叉)
Alice Proxy Bob's Mobile Bob's Office
│ │ │ │
├─ INVITE ─────>│ │ │
│ ├─ INVITE ───────>│ │
│ ├─ INVITE ─────────────────────────>│
│ │ │ │
│ │<─ 180 Ringing ───┤ │
│<─ 180 Ringing ┤ │ │
│ │<─ 180 Ringing ─────────────────────┤
│ │ │ │
│ │<─ 200 OK ────────┤ (手機先接聽)
│<─ 200 OK ─────┤ │ │
│ │ │ │
│ ├─ CANCEL ─────────────────────────>│ 取消其他
├─ ACK ────────────────────────────>│ │
│<═══════════ RTP ════════════════>│ │Proxy 配置(Kamailio):
# Parallel Forking
if (is_method("INVITE")) {
lookup("location"); # 查詢所有註冊位置
# Bob 註冊了 3 個裝置
# sip:bob@mobile.com
# sip:bob@office.com
# sip:bob@home.com
t_relay(); # 同時發送 INVITE 到所有裝置
}Sequential Forking(循序分叉)
先試手機,沒接再試辦公室,最後試家裡Alice Proxy Mobile Office Home
│ │ │ │ │
├─ INVITE ─────>│ │ │ │
│ ├─ INVITE ────>│ │ │
│ │ │ │ │
│ │<─ 180 ───────┤ │ │
│ │ │ │ │
│ │<─ 486 Busy ──┤ (忙線) │ │
│ │ │ │ │
│ ├─ INVITE ─────────────>│ │
│ │ │ │ │
│ │<─ 180 ─────────────────┤ │
│ │ │ │ │
│ │<─ 408 Timeout ─────────┤ (無人接聽)
│ │ │ │ │
│ ├─ INVITE ─────────────────────────>│
│ │ │ │ │
│ │<─ 200 OK ───────────────────────────┤
│<─ 200 OK ─────┤ │ │ │
├─ ACK ─────────────────────────────────────────────>│
│<═══════════ RTP ═════════════════════════════════>│Q4:如何處理無人接聽(No Answer)?
答案:
使用計時器 + 480 Temporarily Unavailable
Alice Proxy Bob
│ │ │
├─ INVITE ─────────>│ │
│ ├─ INVITE ───────>│
│ │ │
│ │<─ 180 Ringing ──┤
│<─ 180 Ringing ────┤ │
│ │ │
│ │ (等待 30 秒) │
│ │ │
│ ├─ CANCEL ───────>│ 超時取消
│ │<─ 200 OK ───────┤
│ │<─ 487 Request Terminated ─┤
│ │ │
│<─ 480 Temporarily Unavailable ──────┤
├─ ACK ────────────>│ │Proxy 配置:
# Kamailio
if (is_method("INVITE")) {
t_set_fr(30000); # 設定超時為 30 秒
t_on_failure("NO_ANSWER");
t_relay();
}
failure_route[NO_ANSWER] {
if (t_check_status("408")) { # Request Timeout
# 轉接到語音信箱
t_relay("sip:voicemail@example.com");
}
}JavaScript 實作:
const session = ua.call('sip:bob@example.com', options);
// 30 秒無人接聽,自動取消
const timer = setTimeout(() => {
if (session.isInProgress()) {
session.terminate(); // 發送 CANCEL
console.log('無人接聽,已取消');
}
}, 30000);
session.on('accepted', () => {
clearTimeout(timer); // 接聽了,取消計時器
});Q5:SIP 如何防止循環路由?
答案:
使用 Max-Forwards header
💡 比喻:TTL (Time To Live)
每經過一個 Proxy,Max-Forwards 減 1
減到 0 時,丟棄訊息並回應 483 Too Many Hops循環路由場景:
Proxy A ──> Proxy B ──> Proxy C ──> Proxy A ──> ...
(無限循環)Max-Forwards 機制:
Alice 發送 INVITE:
Max-Forwards: 70
經過 Proxy A:
Max-Forwards: 69
經過 Proxy B:
Max-Forwards: 68
...
經過 Proxy 70:
Max-Forwards: 0 → 回應 483 Too Many Hops483 Too Many Hops:
SIP/2.0 483 Too Many Hops
Via: SIP/2.0/UDP proxy70.com:5060;branch=z9hG4bK999
To: Bob <sip:bob@example.com>;tag=314159
From: Alice <sip:alice@example.com>;tag=9fxced76sl
Call-ID: 123@alice.com
CSeq: 1 INVITE
Content-Length: 0Proxy 實作(檢查 Max-Forwards):
def handle_invite(request):
max_forwards = int(request.headers.get('Max-Forwards', 70))
if max_forwards == 0:
# 已達上限,回應錯誤
send_response(request, 483, "Too Many Hops")
return
# 減 1 並轉發
request.headers['Max-Forwards'] = str(max_forwards - 1)
forward_request(request)📝 總結
SIP 呼叫流程重點:
- 基本流程:INVITE → 180 Ringing → 200 OK → ACK → RTP → BYE
- 保留:re-INVITE with a=sendonly
- 轉接:REFER(Blind/Attended)
- 會議:MCU/SFU 混音或 Full Mesh
- Early Media:183 Session Progress
記憶口訣:「基保轉會早」
- 基(基本流程)
- 保(保留)
- 轉(轉接)
- 會(會議)
- 早(Early Media)
關鍵差異:
INVITE vs re-INVITE:
- INVITE:建立新會話
- re-INVITE:修改現有會話
Blind vs Attended Transfer:
- Blind:直接轉,不確認
- Attended:先確認,再轉🔗 延伸閱讀
- 上一篇:06-1. SIP 協定基礎
- 下一篇:06-3. 視訊通話架構
- RFC 3515(REFER):https://tools.ietf.org/html/rfc3515
- RFC 4579(Conference):https://tools.ietf.org/html/rfc4579