首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> C++ >> 關於C++

Winsock UDP 實作語音全雙工通訊:架構、非阻塞與緩衝區調校

     編輯:關於C++

前言

語音即時通訊要求低延遲、高吞吐、容忍丟包,適合使用 UDP (SOCK_DGRAM)。本文以 Visual C++ / Winsock 2 為例,說明雙 Socket 架構 (分離送收)、非阻塞模式、WSAAsyncSelect 事件驅動、緩衝區調校、埠重用等關鍵技術。

為何選 UDP?

特性TCPUDP
連線導向是否
可靠性保證送達、有序、重傳不保證、可能亂序/重複/丟失
延遲較高 (擁塞控制、確認機制)極低 (無握手、無確認)
適合場景檔案傳輸、郵件、Web、資料庫即時語音/視訊、遊戲、廣播、DNS

語音封包遲到比丟失更糟 (造成卡頓),UDP 丟棄遲到封包、無隊頭阻塞,配合前向錯誤修正 (FEC)、抖動緩衝、編碼器容錯 (Opus, G.722, iLBC) 可獲得良好品質。

雙 Socket 全雙工架構


// 全域變數
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];

1. 建立 Socket 並設定選項


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;
}

2. 訊息迴圈處理 (視窗程式)


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);
}

3. 關鍵細節補充

  • 埠號規劃:本地發送埠 (如 2000) ≠ 本地接收埠 (如 2001);遠端對應相反。
  • 緩衝區大小:系統預設 ~8KB,語音建議 4KB~64KB。過大增加延遲,過小易溢位丟包。SO_SNDBUF/SO_RCVBUF 雙向設定。
  • 非阻塞必要性:阻塞模式下 recvfrom/sendto 會卡住 UI,無法處理視窗訊息,導致視窗無回應甚至無法正常關閉。WSAAsyncSelect 自動設為非阻塞並發送視窗訊息。
  • 封包大小:建議 ≤ 1400 bytes (乙太網 MTU 1500 - IP/UDP 標頭 28),避免 IP 分片導致丟包率倍增。
  • 時間同步:雙方需協商取樣率 (8k/16k/48k Hz)、幀長 (20ms/30ms)、編碼器參數。可在啟動階段用 TCP 信令交換 SDP。
  • NAT 穿透:真實部署需 STUN/TURN/ICE 或 UPnP 映射埠口。

進階:可靠性增強 (RUDP / QUIC 概念)

  • 序號 + ACK + 選擇性重傳 (SACK):僅重傳關鍵控制封包 (啟動/結束/參數變更),語音資料封包不重傳 (時效性)。
  • 前向錯誤修正 (FEC):每 N 個封包產生 1 個 parity 封包 (XOR 或 Reed-Solomon),接收端可恢復單個丟包。
  • 抖動緩衝器:接收端緩衝 50~200ms 動態調整,平滑網路抖動。
  • 頻寬自適應:監測 RTT、丟包率、抖動,動態切換編碼器位元率 (Opus 6~510 kbps)。

現代替代方案

技術優點
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 穿透、編碼器整合,即可構建基礎語音通訊雛形。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved