語音即時通訊要求低延遲、高吞吐、容忍丟包,適合使用 UDP (SOCK_DGRAM)。本文以 Visual C++ / Winsock 2 為例,說明雙 Socket 架構 (分離送收)、非阻塞模式、WSAAsyncSelect 事件驅動、緩衝區調校、埠重用等關鍵技術。
| 特性 | TCP | UDP |
|---|---|---|
| 連線導向 | 是 | 否 |
| 可靠性 | 保證送達、有序、重傳 | 不保證、可能亂序/重複/丟失 |
| 延遲 | 較高 (擁塞控制、確認機制) | 極低 (無握手、無確認) |
| 適合場景 | 檔案傳輸、郵件、Web、資料庫 | 即時語音/視訊、遊戲、廣播、DNS |
語音封包遲到比丟失更糟 (造成卡頓),UDP 丟棄遲到封包、無隊頭阻塞,配合前向錯誤修正 (FEC)、抖動緩衝、編碼器容錯 (Opus, G.722, iLBC) 可獲得良好品質。
// 全域變數
SOCKET g_sSend = INVALID_SOCKET; // 發送 Socket
SOCKET g_sRecv = INVALID_SOCKET; // 接收 Socket
SOCKADDR_IN g_remoteAddr; // 對方地址
HWND g_hWnd; // 接收訊息視窗
UINT g_uMsg = WM_USER + 1; // 自訂訊息
// 緩衝區大小 (4KB ~ 64KB,視 MTU 與抖動需求)
const int BUF_SIZE = 4096;
char g_sendBuf[BUF_SIZE];
char g_recvBuf[BUF_SIZE];
bool InitSockets(HWND hWnd, const char* localIP, u_short localSendPort, u_short localRecvPort,
const char* remoteIP, u_short remoteSendPort, u_short remoteRecvPort)
{
g_hWnd = hWnd;
// 建立發送 Socket
g_sSend = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (g_sSend == INVALID_SOCKET) return false;
// 建立接收 Socket
g_sRecv = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (g_sRecv == INVALID_SOCKET) { closesocket(g_sSend); return false; }
// 允許埠重用 (SO_REUSEADDR) - 讓兩個 Socket 可綁定同一 IP 不同埠
BOOL bReuse = TRUE;
setsockopt(g_sSend, SOL_SOCKET, SO_REUSEADDR, (char*)&bReuse, sizeof(bReuse));
setsockopt(g_sRecv, SOL_SOCKET, SO_REUSEADDR, (char*)&bReuse, sizeof(bReuse));
// 設定發送/接收緩衝區
int bufSize = BUF_SIZE;
setsockopt(g_sSend, SOL_SOCKET, SO_SNDBUF, (char*)&bufSize, sizeof(bufSize));
setsockopt(g_sRecv, SOL_SOCKET, SO_RCVBUF, (char*)&bufSize, sizeof(bufSize));
// 綁定本地地址
SOCKADDR_IN localSend = {0}, localRecv = {0};
localSend.sin_family = AF_INET;
localSend.sin_addr.s_addr = inet_addr(localIP);
localSend.sin_port = htons(localSendPort);
if (bind(g_sSend, (SOCKADDR*)&localSend, sizeof(localSend)) == SOCKET_ERROR) return false;
localRecv.sin_family = AF_INET;
localRecv.sin_addr.s_addr = inet_addr(localIP);
localRecv.sin_port = htons(localRecvPort);
if (bind(g_sRecv, (SOCKADDR*)&localRecv, sizeof(localRecv)) == SOCKET_ERROR) return false;
// 設定遠端地址 (發送目標)
g_remoteAddr.sin_family = AF_INET;
g_remoteAddr.sin_addr.s_addr = inet_addr(remoteIP);
g_remoteAddr.sin_port = htons(remoteSendPort); // 對方接收埠
// 註冊非阻塞事件通知
WSAAsyncSelect(g_sSend, g_hWnd, g_uMsg, FD_WRITE | FD_CLOSE);
WSAAsyncSelect(g_sRecv, g_hWnd, g_uMsg, FD_READ | FD_CLOSE);
return true;
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
if (msg == g_uMsg) {
SOCKET s = (SOCKET)wParam;
int event = WSAGETSELECTEVENT(lParam);
int error = WSAGETSELECTERROR(lParam);
if (error) { /* 處理錯誤 */ return 0; }
if (s == g_sRecv) {
switch (event) {
case FD_READ: {
// 接收語音資料
SOCKADDR_IN from;
int fromLen = sizeof(from);
int n = recvfrom(g_sRecv, g_recvBuf, BUF_SIZE, 0, (SOCKADDR*)&from, &fromLen);
if (n > 0) {
// 送入音訊解碼/播放佇列
AudioPlaybackQueue_Push(g_recvBuf, n);
}
break;
}
case FD_CLOSE:
// 對方關閉
break;
}
} else if (s == g_sSend) {
switch (event) {
case FD_WRITE:
// 發送緩衝區可寫,從編碼器取資料送出
int n = AudioEncodeQueue_Pop(g_sendBuf, BUF_SIZE);
if (n > 0) {
sendto(g_sSend, g_sendBuf, n, 0, (SOCKADDR*)&g_remoteAddr, sizeof(g_remoteAddr));
}
break;
case FD_CLOSE:
break;
}
}
return 0;
}
return DefWindowProc(hWnd, msg, wParam, lParam);
}
SO_SNDBUF/SO_RCVBUF 雙向設定。recvfrom/sendto 會卡住 UI,無法處理視窗訊息,導致視窗無回應甚至無法正常關閉。WSAAsyncSelect 自動設為非阻塞並發送視窗訊息。| 技術 | 優點 |
|---|---|
| WebRTC (libwebrtc) | 完整 ICE/STUN/TURN、SRTP 加密、Opus/VP8/H.264、抖動緩衝、頻寬估測、跨平台 |
| QUIC (msquic, quiche) | 0-RTT、多路復用、內建 TLS 1.3、可靠串流 + 不可靠資料報 |
| ENet / RakNet / yojimbo | 遊戲導向可靠 UDP 庫、序號/ACK/分片/加密 |
| GStreamer / FFmpeg + RTP | 媒體管線、編解碼、RTP/RTCP、SRTP、硬體加速 |
自行實作 UDP 語音僅適合教學、極簡嵌入式、或特殊定制需求。生產環境強烈建議使用 WebRTC 或成熟媒體框架。
Winsock UDP 全雙工語音核心在於:雙 Socket 分離送收、非阻塞事件驅動、適當緩衝區與封包大小、抖動緩衝與編碼器容錯。掌握 WSAAsyncSelect/IOCP、SO_RCVBUF/SNDBUF、NAT 穿透、編碼器整合,即可構建基礎語音通訊雛形。