最近研讀《Unix 網路程式設計》一書,深感這本經典著作極適合 Socket 程式設計入門者。書中關於多播的介紹讓我豁然開朗:原來在程式碼中實作基本的多播功能竟然如此直觀簡單。本文不打算照本宣科地探討理論概念,而是專注於實際程式碼中如何落地多播功能。
針對區域網路,可用的多播位址範圍為 224.0.0.0 至 224.0.0.255(詳細原因請參考專業網路程式設計書籍)。
多播功能透過設定 UDP Socket 選項來實現,主要涉及以下選項:
| 選項 | 功能說明 |
|---|---|
IP_ADD_MEMBERSHIP | 加入多播群組 |
IP_DROP_MEMBERSHIP | 離開多播群組 |
IP_MULTICAST_IF | 指定輸出多播資料報的介面 |
IP_MULTICAST_TTL | 設定 TTL (Time To Live) 值 |
IP_MULTICAST_LOOP | 控制是否接收自己發送的多播資料(回環) |
設定 IP_ADD_MEMBERSHIP 和 IP_DROP_MEMBERSHIP 時需使用 ip_mreq 結構體,須包含 <Ws2tcpip.h> 並確保其在 <winsock2.h> 之後引入。
程式讀取使用者輸入的文字行並發送至多播群組;所有加入該群組的客戶端均可接收並顯示來自群組的資料。
// 專案設定:C/C++ -> Code Generation -> 勾選 "Debug Multithreaded"
#include <iostream>
#include <winsock2.h> // 注意引入順序
#include <Ws2tcpip.h>
#include <process.h> // _beginthread 需求
#pragma comment(lib, "ws2_32.lib")
using namespace std;
const char* MULTICAST_IP = "224.0.0.99"; // 多播群組位址
const int MULTICAST_PORT = 2002; // 多播群組埠號
const int BUFFER_SIZE = 1024;
void do_send(void* arg); // 讀取使用者輸入並發送至多播群組的執行緒函式
void do_read(void* arg); // 讀取多播群組資料的函式
int main() {
WSAData wsaData;
if (WSAStartup(MAKEWORD(2,2), &wsaData) != 0) {
cout << "Error in WSAStartup" << endl;
return 0;
}
SOCKET server = socket(AF_INET, SOCK_DGRAM, 0); // 建立 UDP Socket
cout << "create socket: " << server << endl;
int ret;
const int on = 1; // 允許同一機器執行多個程式實例
ret = setsockopt(server, SOL_SOCKET, SO_REUSEADDR, (char*)&on, sizeof(on));
if (ret == SOCKET_ERROR) {
WSACleanup();
cout << "Error in setsockopt(SO_REUSEADDR): " << WSAGetLastError() << endl;
return 0;
}
// 設定 TTL
const int routenum = 10;
ret = setsockopt(server, IPPROTO_IP, IP_MULTICAST_TTL, (char*)&routenum, sizeof(routenum));
if (ret == SOCKET_ERROR) {
WSACleanup();
cout << "Error in setsockopt(IP_MULTICAST_TTL): " << WSAGetLastError() << endl;
return 0;
}
// 禁止回環
const int loopback = 0;
ret = setsockopt(server, IPPROTO_IP, IP_MULTICAST_LOOP, (char*)&loopback, sizeof(loopback));
if (ret == SOCKET_ERROR) {
WSACleanup();
cout << "Error in setsockopt(IP_MULTICAST_LOOP): " << WSAGetLastError() << endl;
return 0;
}
// 綁定本地位址
sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(MULTICAST_PORT);
local.sin_addr.S_un.S_addr = INADDR_ANY;
ret = bind(server, (sockaddr*)(&local), sizeof(local));
if (ret == SOCKET_ERROR) {
WSACleanup();
cout << "Error in bind: " << WSAGetLastError() << endl;
return 0;
}
// 加入多播群組
ip_mreq mreq;
memset(&mreq, 0, sizeof(mreq));
mreq.imr_interface.S_un.S_addr = INADDR_ANY;
mreq.imr_multiaddr.S_un.S_addr = inet_addr(MULTICAST_IP);
ret = setsockopt(server, IPPROTO_IP, IP_ADD_MEMBERSHIP, (char*)&mreq, sizeof(mreq));
if (ret == SOCKET_ERROR) {
WSACleanup();
cout << "Error in setsockopt(IP_ADD_MEMBERSHIP): " << WSAGetLastError() << endl;
return 0;
}
// 建立兩個執行緒:一個負責發送,一個負責接收
HANDLE hHandle[2];
hHandle[0] = (HANDLE)_beginthread(do_send, 0, (void*)server);
hHandle[1] = (HANDLE)_beginthread(do_read, 0, (void*)server);
// 等待發送執行緒結束(使用者輸入 "end" 時)
WaitForSingleObject(hHandle[0], INFINITE);
WSACleanup();
return 0;
}
// 發送執行緒:讀取使用者輸入直到 "end"
void do_send(void* arg) {
SOCKET server = (SOCKET)arg;
char sendline[BUFFER_SIZE + 1];
sockaddr_in remote;
memset(&remote, 0, sizeof(remote));
remote.sin_addr.s_addr = inet_addr(MULTICAST_IP);
remote.sin_family = AF_INET;
remote.sin_port = htons(MULTICAST_PORT);
for (;;) {
cin.getline(sendline, BUFFER_SIZE);
if (strncmp(sendline, "end", 3) == 0) break;
sendto(server, sendline, strlen(sendline), 0, (sockaddr*)(&remote), sizeof(remote));
}
cout << "do_send end..." << endl;
}
// 接收執行緒:持續讀取多播資料
void do_read(void* arg) {
SOCKET server = (SOCKET)arg;
char buf[BUFFER_SIZE + 1];
int ret;
sockaddr_in client;
int clientLen;
for (;;) {
clientLen = sizeof(client);
memset(&client, 0, clientLen);
ret = recvfrom(server, buf, BUFFER_SIZE, 0, (sockaddr*)(&client), &clientLen);
if (ret == 0) continue; // 空字串
else if (ret == SOCKET_ERROR) {
if (WSAGetLastError() == WSAEINTR) break; // 主執行緒終止導致的錯誤
cout << "Error in recvfrom: " << WSAGetLastError() << endl;
break;
}
buf[ret] = '\0';
cout << "received: " << buf << endl;
}
cout << "do_read end..." << endl;
}
雖然實際專案中仍需更多錯誤處理與健壯性設計,但核心多播功能的實作確實相當簡潔明瞭。掌握 Socket 選項與 ip_mreq 結構體的運用,即可快速建立區域網路多播通訊應用。