IP 組播 讓數據包高效分發給多個接收者,適用於股票行情、視頻會議、服務發現等場景。本文基於 Winsock2,詳細講解 組播地址選擇、Socket 選項設置 (TTL/Loopback/Interface)、加入/離開組、發送/接收循環、多線程架構,提供可在跨子網 AD 域環境運行的完整代碼。
| 概念 | 說明 |
|---|---|
| 組播地址範圍 | 224.0.0.0 ~ 239.255.255.255 (D 類地址) |
| 本地鏈路組播 | 224.0.0.0 ~ 224.0.0.255 (TTL=1,不穿越路由器,適合單網段) |
| 管理範圍組播 | 239.0.0.0 ~ 239.255.255.255 (機構內部管理,可配置路由範圍) |
| IGMP 協議 | Internet Group Management Protocol,主機向路由器報告組成員身份 |
| Socket 類型 | 必須為 SOCK_DGRAM (UDP),組播不可用 TCP |
// 1. 加入組播組 (必須)
struct ip_mreq mreq;
mreq.imr_multiaddr.s_addr = inet_addr("224.0.0.99"); // 組播地址
mreq.imr_interface.s_addr = INADDR_ANY; // 本地接口 (任意)
setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, (char*)&mreq, sizeof(mreq));
// 2. 離開組播組
setsockopt(sock, IPPROTO_IP, IP_DROP_MEMBERSHIP, (char*)&mreq, sizeof(mreq));
// 3. 設置 TTL (生存時間/跳數) - 控制數據包傳播範圍
int ttl = 10; // 1=本地鏈路, 32=同站點, 64=地區, 128=洲際, 255=全球
setsockopt(sock, IPPROTO_IP, IP_MULTICAST_TTL, (char*)&ttl, sizeof(ttl));
// 4. 禁用回環 (本機發送不接收自己發的包)
int loopback = 0; // 1=啟用(默認), 0=禁用
setsockopt(sock, IPPROTO_IP, IP_MULTICAST_LOOP, (char*)&loopback, sizeof(loopback));
// 5. 指定發送接口 (多網卡環境)
struct in_addr iface;
iface.s_addr = inet_addr("192.168.1.100"); // 本機具體 IP
setsockopt(sock, IPPROTO_IP, IP_MULTICAST_IF, (char*)&iface, sizeof(iface));
// 6. 允許端口復用 (同機多實例監聽同一端口)
int reuse = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (char*)&reuse, sizeof(reuse));
#include
#include
#include
#include // _beginthread
#include
#pragma comment(lib, "ws2_32.lib")
const char* MCAST_GROUP = "224.0.0.99";
const int MCAST_PORT = 2002;
const int BUF_SIZE = 1024;
SOCKET g_sock = INVALID_SOCKET;
volatile bool g_running = true;
void SenderThread(void*) {
sockaddr_in dest = {0};
dest.sin_family = AF_INET;
dest.sin_port = htons(MCAST_PORT);
dest.sin_addr.s_addr = inet_addr(MCAST_GROUP);
char line[BUF_SIZE];
while (g_running) {
std::cout << "> ";
if (!std::cin.getline(line, BUF_SIZE)) break;
if (strcmp(line, "quit") == 0) { g_running = false; break; }
int sent = sendto(g_sock, line, strlen(line), 0, (sockaddr*)&dest, sizeof(dest));
if (sent == SOCKET_ERROR) {
std::cerr << "Send error: " << WSAGetLastError() << std::endl;
}
}
// 觸發接收線程退出
closesocket(g_sock);
}
void ReceiverThread(void*) {
sockaddr_in from;
int fromLen = sizeof(from);
char buf[BUF_SIZE + 1];
while (g_running) {
int recvLen = recvfrom(g_sock, buf, BUF_SIZE, 0, (sockaddr*)&from, &fromLen);
if (recvLen == SOCKET_ERROR) {
int err = WSAGetLastError();
if (err == WSAEINTR || err == WSAENOTSOCK) break; // Socket 關閉導致的正常退出
std::cerr << "Recv error: " << err << std::endl;
continue;
}
if (recvLen == 0) continue;
buf[recvLen] = '\0';
char fromIp[16];
inet_ntop(AF_INET, &from.sin_addr, fromIp, 16);
std::cout << "\r[" << fromIp << "] " << buf << "\n> " << std::flush;
}
}
int main() {
// 1. 初始化 Winsock
WSADATA wsa;
if (WSAStartup(MAKEWORD(2,2), &wsa) != 0) {
std::cerr << "WSAStartup failed" << std::endl;
return 1;
}
// 2. 創建 UDP Socket
g_sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (g_sock == INVALID_SOCKET) {
WSACleanup(); return 1;
}
// 3. 設置選項 (關鍵順序)
int reuse = 1;
setsockopt(g_sock, SOL_SOCKET, SO_REUSEADDR, (char*)&reuse, sizeof(reuse));
int ttl = 10;
setsockopt(g_sock, IPPROTO_IP, IP_MULTICAST_TTL, (char*)&ttl, sizeof(ttl));
int loop = 0; // 禁用回環
setsockopt(g_sock, IPPROTO_IP, IP_MULTICAST_LOOP, (char*)&loop, sizeof(loop));
// 4. 綁定本地端口 (接收必須)
sockaddr_in local = {0};
local.sin_family = AF_INET;
local.sin_port = htons(MCAST_PORT);
local.sin_addr.s_addr = INADDR_ANY;
if (bind(g_sock, (sockaddr*)&local, sizeof(local)) == SOCKET_ERROR) {
std::cerr << "Bind failed: " << WSAGetLastError() << std::endl;
closesocket(g_sock); WSACleanup(); return 1;
}
// 5. 加入組播組
ip_mreq mreq = {0};
mreq.imr_multiaddr.s_addr = inet_addr(MCAST_GROUP);
mreq.imr_interface.s_addr = INADDR_ANY;
if (setsockopt(g_sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, (char*)&mreq, sizeof(mreq)) == SOCKET_ERROR) {
std::cerr << "Join group failed: " << WSAGetLastError() << std::endl;
closesocket(g_sock); WSACleanup(); return 1;
}
std::cout << "已加入組播組 " << MCAST_GROUP << ":" << MCAST_PORT << std::endl;
std::cout << "輸入消息發送,輸入 quit 退出。" << std::endl;
// 6. 啟動收發線程
HANDLE hSender = (HANDLE)_beginthread(SenderThread, 0, nullptr);
HANDLE hReceiver = (HANDLE)_beginthread(ReceiverThread, 0, nullptr);
// 7. 等待發送線程結束 (輸入 quit)
WaitForSingleObject(hSender, INFINITE);
g_running = false;
// 8. 清理
Sleep(100); // 給接收線程退出時間
setsockopt(g_sock, IPPROTO_IP, IP_DROP_MEMBERSHIP, (char*)&mreq, sizeof(mreq));
closesocket(g_sock);
WSACleanup();
std::cout << "程序結束。" << std::endl;
return 0;
}
IP_MULTICAST_IF 指定發送網卡 IP,否則系統按路由表選擇可能不符合預期。bind(INADDR_ANY) 監聽所有接口;若需綁定特定接口,設置 local.sin_addr.s_addr = inet_addr("192.168.x.x")。| 現象 | 可能原因 | 排查步驟 |
|---|---|---|
| 本機收不到自己發的包 | IP_MULTICAST_LOOP=0 (已禁用回環) | 測試時設置 loop=1;生產環境通常禁用避免自處理 |
| 同網段其他機收不到 | 防火牆攔截 / TTL=1 但跨了路由器 / IGMP Snooping 未開 | 關閉防火牆測試;抓包確認 IGMP Report 包;檢查交換機配置 |
| bind 失敗 10048 | 端口被佔用 / SO_REUSEADDR 未設置 | 加 SO_REUSEADDR;檢查是否有殘留進程 |
| IP_ADD_MEMBERSHIP 失敗 10042 | 地址非組播地址 / 接口 IP 錯誤 | 確認 224.x.x.x;imr_interface 用 INADDR_ANY 或具體本機 IP |
| recvfrom 返回 WSAEINTR | Socket 被關閉 (closesocket) | 正常退出信號,線程循環中判斷 break |
Pragmatic General Multicast (PGM) (Windows 支持 IPPROTO_PGM)。ff00::/8,Socket AF_INET6,選項 IPV6_JOIN_GROUP/IPV6_MULTICAST_HOPS。Winsock 實現 UDP 組播的核心在於 正確設置 Socket 選項 (TTL/Loopback/Interface/Reuse) + IP_ADD_MEMBERSHIP 加入組 + bind 本地端口。本文提供的雙線程收發模型簡潔可靠,可直接用於局域網廣播通知、行情分發、集群心跳等場景。跨子網部署時,網絡設備 (路由器/交換機) 的 IGMP/PIM 配置是成敗關鍵,務必與網絡團隊協同調試。