在 Windows 95 時代,Winsock 已集成進系統,為進程間通信提供了強大支持。本文針對實時語音全雙工傳輸需求,採用 UDP 協議與雙套接字技術,詳細闡述關鍵 API 使用要點、套接字緩衝區配置、地址重用技術、異步選擇機制應用,並給出完整實現代碼片段。
| 特性 | TCP | UDP |
|---|---|---|
| 連接模式 | 面向連接,三次握手 | 無連接,即發即收 |
| 可靠性 | 可靠,有序,無重複 | 不可靠,可能丟包/亂序/重複 |
| 流控/擁塞控制 | 有,引入延遲 | 無,延遲極低 |
| 適用場景 | 文件傳輸、郵件、Web | 實時語音、視頻、廣播、遊戲 |
結論:語音通信對實時性要求極高,容忍少量丟包,但不能容忍 TCP 重傳帶來的延迟抖動,UDP 是最佳選擇。
為實現同時發送與接收互不干擾,設計雙套接字架構:
sendto 發送語音數據包recvfrom 接收語音數據包兩套接字獨立綁定、獨立緩衝區、獨立異步事件,實現真正的並行全雙工。
SOCKET s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
// 或
SOCKET s = socket(AF_INET, SOCK_DGRAM, 0); // 協議為0時系統自動選擇UDP
關鍵難點:兩個套接字需綁定同一本地 IP 但不同端口。第二個套接字綁定前必須設置 SO_REUSEADDR 選項:
// 接收套接字設置地址重用
int optval = 1;
setsockopt(recvSock, SOL_SOCKET, SO_REUSEADDR, (char*)&optval, sizeof(optval));
綁定代碼:
SOCKADDR_IN sendAddr = {AF_INET, htons(2000), inet_addr(LOCAL_IP)};
SOCKADDR_IN recvAddr = {AF_INET, htons(2001), inet_addr(LOCAL_IP)};
bind(sendSock, (LPSOCKADDR)&sendAddr, sizeof(sendAddr));
bind(recvSock, (LPSOCKADDR)&recvAddr, sizeof(recvAddr));
語音數據流連續,需足夠緩衝區防止丟包。系統默認較小(8KB),建議設置 4KB-64KB:
// 發送緩衝區
int sendBufSize = 4 * 1024;
setsockopt(sendSock, SOL_SOCKET, SO_SNDBUF, (char*)&sendBufSize, sizeof(sendBufSize));
// 接收緩衝區(同時設置 REUSEADDR)
int recvBufSize = 4 * 1024;
int optval = 1;
setsockopt(recvSock, SOL_SOCKET, SO_RCVBUF, (char*)&recvBufSize, sizeof(recvBufSize));
setsockopt(recvSock, SOL_SOCKET, SO_REUSEADDR, (char*)&optval, sizeof(optval));
緩衝區分配技巧:使用 GlobalAlloc(GMEM_MOVEABLE|GMEM_SHARE, size) + GlobalLock 獲取指針,傳給 setsockopt。
// 發送
int sendto(SOCKET s, const char* buf, int len, int flags, const struct sockaddr* to, int tolen);
// 接收
int recvfrom(SOCKET s, char* buf, int len, int flags, struct sockaddr* from, int* fromlen);
關鍵差異:sendto 最後參數為值(地址長度),recvfrom 最後參數為指針(輸入緩衝區長度,輸出實際地址長度)。
UDP 數據報特性:單次 recvfrom 讀取一個完整數據報。若緩衝區小於數據報,多餘數據被丟棄;若緩衝區大於數據報,返回實際長度。
void CloseSockets() {
// 1. 註銷異步選擇
WSAAsyncSelect(sendSock, hWnd, 0, 0);
WSAAsyncSelect(recvSock, hWnd, 0, 0);
// 2. 釋放緩衝區內存
GlobalUnlock(hSendBuf); GlobalFree(hSendBuf);
GlobalUnlock(hRecvBuf); GlobalFree(hRecvBuf);
// 3. 關閉套接字並清理 Winsock
closesocket(sendSock);
closesocket(recvSock);
WSACleanup();
}
網絡擁塞或大數據發送導致 sendto/recvfrom 短暫阻塞。Windows 非搶佔式多任務下,阻塞會凍結應用。Winsock 採用阻塞鉤子機制:
GetMessage/DispatchMessage 處理 Windows 消息Sleep(0) 讓出 CPU 時間片現代建議:避免阻塞模式,改用異步選擇或重疊 I/O。
// 為雙套接字註冊異步事件
WSAAsyncSelect(sendSock, hWnd, WM_VOICE_SEND, FD_WRITE | FD_CLOSE);
WSAAsyncSelect(recvSock, hWnd, WM_VOICE_RECV, FD_READ | FD_CLOSE);
消息處理分發:
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) {
if (msg == WM_VOICE_RECV) {
SOCKET s = (SOCKET)wParam;
int event = WSAGETSELECTEVENT(lParam);
if (event == FD_READ) {
char voiceBuf[4096];
SOCKADDR_IN fromAddr;
int fromLen = sizeof(fromAddr);
int nRecv = recvfrom(s, voiceBuf, sizeof(voiceBuf), 0, (SOCKADDR*)&fromAddr, &fromLen);
if (nRecv > 0) PlayVoiceData(voiceBuf, nRecv); // 送入音頻播放隊列
}
}
else if (msg == WM_VOICE_SEND) {
SOCKET s = (SOCKET)wParam;
int event = WSAGETSELECTEVENT(lParam);
if (event == FD_WRITE) {
SendNextVoicePacket(s); // 從編碼隊列取數據發送
}
}
return DefWindowProc(hWnd, msg, wParam, lParam);
}
bind 前端口設 0,系統從 1024-5000 分配htons() 主機序→網絡序,ntohs() 反向BOOL InitVoiceSockets(HWND hWnd) {
// 1. 啟動 Winsock
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2,2), &wsaData) != 0) return FALSE;
// 2. 創建雙套接字
sendSock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
recvSock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sendSock == INVALID_SOCKET || recvSock == INVALID_SOCKET) return FALSE;
// 3. 配置發送套接字
SOCKADDR_IN sendAddr = {0};
sendAddr.sin_family = AF_INET;
sendAddr.sin_addr.s_addr = inet_addr(LOCAL_IP);
sendAddr.sin_port = htons(2000);
int sendBuf = 4096;
setsockopt(sendSock, SOL_SOCKET, SO_SNDBUF, (char*)&sendBuf, sizeof(sendBuf));
if (bind(sendSock, (LPSOCKADDR)&sendAddr, sizeof(sendAddr)) == SOCKET_ERROR) return FALSE;
// 4. 配置接收套接字(關鍵:SO_REUSEADDR)
SOCKADDR_IN recvAddr = {0};
recvAddr.sin_family = AF_INET;
recvAddr.sin_addr.s_addr = inet_addr(LOCAL_IP);
recvAddr.sin_port = htons(2001);
int optval = 1;
setsockopt(recvSock, SOL_SOCKET, SO_REUSEADDR, (char*)&optval, sizeof(optval));
int recvBuf = 4096;
setsockopt(recvSock, SOL_SOCKET, SO_RCVBUF, (char*)&recvBuf, sizeof(recvBuf));
if (bind(recvSock, (LPSOCKADDR)&recvAddr, sizeof(recvAddr)) == SOCKET_ERROR) return FALSE;
// 5. 註冊異步事件
WSAAsyncSelect(sendSock, hWnd, WM_VOICE_SEND, FD_WRITE | FD_CLOSE);
WSAAsyncSelect(recvSock, hWnd, WM_VOICE_RECV, FD_READ | FD_CLOSE);
return TRUE;
}
基於 UDP 雙套接字 + 異步選擇模型實現的語音全雙工通信,具有架構清晰、延遲低、資源佔用少的優點。生產環境可進一步優化:
掌握雙套接字綁定技巧、SO_REUSEADDR 使用、緩衝區調優、異步事件分發等核心要點,是構建高質量實時通信應用的關鍵基礎。