隨著區域網(LAN)應用的廣泛使用,網路通訊已經成為軟體設計中不可或缺的一部分。區域網聊天軟體作為一種常見的網路應用,可以實現多個使用者之間的實時通訊,廣泛應用於企業內部溝通和小型網路環境中。本專案設計並實現一個基於C語言的區域網群聊程式,透過UDP廣播搜尋線上使用者,並在發現其他線上應用程式後,自動建立TCP連線,實現訊息的收發。本程式展示瞭如何在Windows環境下使用Winsock API進行網路程式設計,提供了對UDP和TCP協議的實際應用,體現了網路通訊中的多執行緒處理、廣播通訊和實時訊息傳遞的關鍵技術點。
二、好友探測功能
在區域網內探測並發現其他線上使用者是區域網聊天軟體最主要的核心功能。該過程涉及到區域網廣播(UDP廣播)和TCP連線兩個關鍵步驟。
下面將詳細介紹實現這一功能的方法和設計思路。
2.1 使用UDP廣播探測線上使用者
1.1 UDP廣播的概念
UDP(用戶數據報協議)是一種無連線的、輕量級的傳輸協議,適用於傳送小資料包。UDP廣播允許將資料包傳送到區域網內的所有裝置,所有在監聽特定埠的裝置都能夠接收到廣播訊息。這種特性使得UDP廣播非常適合用於探測和發現區域網內的線上裝置。
1.2 探測思路
在程式啟動時,客戶端會透過UDP廣播發送一個上線通知訊息,表示自己已線上。其他監聽同一埠的客戶端接收到這一訊息後,可以獲知該客戶端的IP地址,並識別出它線上。具體的實現步驟如下:
建立UDP套接字:為UDP通訊建立一個套接字,並配置為允許廣播。
傳送廣播訊息:程式向區域網內的廣播地址(通常為
255.255.255.255
)傳送一個訊息,例如"HELLO, I'M ONLINE"。這個訊息會被區域網內所有監聽相同埠的裝置接收。監聽UDP訊息:每個客戶端都持續監聽來自區域網內的UDP訊息。一旦接收到廣播訊息,客戶端會記錄傳送方的IP地址和埠,以確認該客戶端線上。
2.2 建立TCP連線實現通訊
2.1 TCP連線的必要性
UDP廣播雖然可以有效地發現線上使用者,但由於其無連線的特點,不適合用於長時間的可靠通訊。因此,在發現其他線上使用者後,程式需要透過TCP(傳輸控制協議)建立可靠的點對點連線。TCP是一種面向連接的協議,能夠確保資料的完整性和順序傳輸,非常適合用於聊天訊息的傳遞。
2.2 連線建立的流程
接收廣播後嘗試連線:當客戶端接收到來自其他使用者的UDP廣播後,會透過TCP連線到該使用者。客戶端會使用從UDP訊息中獲取的IP地址和預定義的TCP埠號,發起連線請求。
接受連線請求:已經線上的客戶端會開啟一個TCP監聽套接字,等待來自其他客戶端的連線請求。一旦有請求到達,程式將接受連線,並啟動一個獨立的執行緒處理與該客戶端之間的訊息通訊。
訊息收發:透過建立的TCP連線,使用者可以實時傳送和接收聊天訊息,確保訊息在網路不穩定的情況下仍能可靠傳輸。
2.3 多執行緒處理的必要性
由於UDP廣播接收、TCP連線監聽和訊息收發等操作需要同時進行,程式採用了多執行緒的設計。每個功能模組都執行在獨立的執行緒中,確保它們可以並行處理,互不干擾。這樣不僅提高了程式的響應速度,還增強了使用者體驗,確保通訊的實時性。
2.4 總結
透過UDP廣播發現區域網內的線上使用者,然後利用TCP協議建立可靠的通訊連線,這是區域網聊天軟體的核心設計思路。UDP廣播的輕量和廣泛性使得線上使用者的探測變得高效,而TCP連線則保證了後續通訊的可靠性。多執行緒的引入進一步最佳化了程式的效能,使得該區域網聊天軟體在實際應用中表現出色。
三、程式碼實現
下面是完整的程式碼。在VS2022裡執行測試。
#define _WINSOCK_DEPRECATED_NO_WARNINGS #include <stdio.h> #include <stdlib.h> #include <string.h> #include <winsock2.h> #include <ws2tcpip.h> #include <process.h> #pragma comment(lib, "ws2_32.lib") #define UDP_PORT 8888 #define TCP_PORT 8889 #define BROADCAST_ADDR "255.255.255.255" #define BUFFER_SIZE 1024 typedef struct ClientInfo { SOCKET socket; struct sockaddr_in address; } ClientInfo; void udp_broadcast_listener(void* param); void tcp_connection_listener(void* param); void tcp_message_listener(void* param); int main() { WSADATA wsaData; SOCKET udp_socket, tcp_socket; struct sockaddr_in udp_addr, tcp_addr; char buffer[BUFFER_SIZE]; struct sockaddr_in client_addr; int addr_len = sizeof(client_addr); // 初始化 Winsock if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { printf("WSAStartup failed\n"); return 1; } // 建立 UDP 套接字 udp_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (udp_socket == INVALID_SOCKET) { printf("UDP socket creation failed\n"); WSACleanup(); return 1; } // 配置 UDP 廣播地址 memset(&udp_addr, 0, sizeof(udp_addr)); udp_addr.sin_family = AF_INET; udp_addr.sin_port = htons(UDP_PORT); udp_addr.sin_addr.s_addr = inet_addr(BROADCAST_ADDR); // 啟動 UDP 廣播監聽執行緒 _beginthread(udp_broadcast_listener, 0, NULL); // 啟動 TCP 連線監聽執行緒 _beginthread(tcp_connection_listener, 0, NULL); // 向區域網內廣播自己上線 strcpy(buffer, "HELLO, I'M ONLINE"); sendto(udp_socket, buffer, strlen(buffer), 0, (struct sockaddr*)&udp_addr, sizeof(udp_addr)); while (1) { fgets(buffer, BUFFER_SIZE, stdin); buffer[strcspn(buffer, "\n")] = 0; // 移除換行符 sendto(udp_socket, buffer, strlen(buffer), 0, (struct sockaddr*)&udp_addr, sizeof(udp_addr)); } closesocket(udp_socket); WSACleanup(); return 0; } void udp_broadcast_listener(void* param) { SOCKET udp_socket; struct sockaddr_in udp_addr, sender_addr; char buffer[BUFFER_SIZE]; int addr_len = sizeof(sender_addr); // 建立 UDP 套接字 udp_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (udp_socket == INVALID_SOCKET) { printf("UDP socket creation failed in listener\n"); return; } // 配置 UDP 地址 memset(&udp_addr, 0, sizeof(udp_addr)); udp_addr.sin_family = AF_INET; udp_addr.sin_port = htons(UDP_PORT); udp_addr.sin_addr.s_addr = INADDR_ANY; // 繫結套接字 if (bind(udp_socket, (struct sockaddr*)&udp_addr, sizeof(udp_addr)) == SOCKET_ERROR) { printf("UDP socket binding failed\n"); closesocket(udp_socket); return; } while (1) { int recv_len = recvfrom(udp_socket, buffer, BUFFER_SIZE, 0, (struct sockaddr*)&sender_addr, &addr_len); if (recv_len > 0) { buffer[recv_len] = '\0'; printf("Received UDP broadcast from %s: %s\n", inet_ntoa(sender_addr.sin_addr), buffer); // 如果接收到"HELLO, I'M ONLINE",嘗試建立TCP連線 if (strcmp(buffer, "HELLO, I'M ONLINE") == 0) { SOCKET tcp_socket; struct sockaddr_in tcp_addr; tcp_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (tcp_socket == INVALID_SOCKET) { printf("TCP socket creation failed\n"); continue; } // 配置 TCP 地址 memset(&tcp_addr, 0, sizeof(tcp_addr)); tcp_addr.sin_family = AF_INET; tcp_addr.sin_port = htons(TCP_PORT); tcp_addr.sin_addr.s_addr = sender_addr.sin_addr.s_addr; if (connect(tcp_socket, (struct sockaddr*)&tcp_addr, sizeof(tcp_addr)) == SOCKET_ERROR) { printf("TCP connection failed to %s\n", inet_ntoa(tcp_addr.sin_addr)); closesocket(tcp_socket); } else { printf("Connected to %s\n", inet_ntoa(tcp_addr.sin_addr)); // 啟動 TCP 訊息監聽執行緒 ClientInfo* client = (ClientInfo*)malloc(sizeof(ClientInfo)); client->socket = tcp_socket; client->address = tcp_addr; _beginthread(tcp_message_listener, 0, client); } } } } closesocket(udp_socket); } void tcp_connection_listener(void* param) { SOCKET tcp_socket, client_socket; struct sockaddr_in tcp_addr, client_addr; int addr_len = sizeof(client_addr); // 建立 TCP 套接字 tcp_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (tcp_socket == INVALID_SOCKET) { printf("TCP socket creation failed\n"); return; } // 配置 TCP 地址 memset(&tcp_addr, 0, sizeof(tcp_addr)); tcp_addr.sin_family = AF_INET; tcp_addr.sin_port = htons(TCP_PORT); tcp_addr.sin_addr.s_addr = INADDR_ANY; // 繫結套接字 if (bind(tcp_socket, (struct sockaddr*)&tcp_addr, sizeof(tcp_addr)) == SOCKET_ERROR) { printf("TCP socket binding failed\n"); closesocket(tcp_socket); return; } // 開始監聽 if (listen(tcp_socket, 5) == SOCKET_ERROR) { printf("TCP socket listen failed\n"); closesocket(tcp_socket); return; } printf("TCP connection listener started...\n"); while (1) { client_socket = accept(tcp_socket, (struct sockaddr*)&client_addr, &addr_len); if (client_socket == INVALID_SOCKET) { printf("TCP accept failed\n"); continue; } printf("Accepted connection from %s\n", inet_ntoa(client_addr.sin_addr)); // 啟動 TCP 訊息監聽執行緒 ClientInfo* client = (ClientInfo*)malloc(sizeof(ClientInfo)); client->socket = client_socket; client->address = client_addr; _beginthread(tcp_message_listener, 0, client); } closesocket(tcp_socket); } void tcp_message_listener(void* param) { ClientInfo* client = (ClientInfo*)param; char buffer[BUFFER_SIZE]; int recv_len; while ((recv_len = recv(client->socket, buffer, BUFFER_SIZE, 0)) > 0) { buffer[recv_len] = '\0'; printf("Message from %s: %s\n", inet_ntoa(client->address.sin_addr), buffer); } printf("Connection closed by %s\n", inet_ntoa(client->address.sin_addr)); closesocket(client->socket); free(client); }
程式在主函式裡透過 WSAStartup
函式初始化Winsock庫,這是一種Windows平臺上的網路程式設計庫,提供了網路通訊所需的API。初始化成功後,程式可以使用Winsock提供的各種網路功能。
建立了兩個主要的套接字:
UDP套接字:用於廣播訊息和接收其他裝置的廣播。
TCP套接字:用於建立點對點的通訊連線。
在程式啟動時,透過UDP廣播向區域網內所有裝置傳送一個“HELLO, I'M ONLINE”的訊息。這一訊息用來告知區域網內的其他使用者自己的存在,從而實現線上使用者的探測。
爲了接收其他使用者的廣播訊息,程式建立了一個UDP套接字並繫結到特定的埠上(UDP_PORT
)。程式透過這個套接字監聽區域網內的所有廣播訊息,提取傳送者的IP地址,並處理接收到的訊息。
一旦接收到來自其他線上使用者的UDP廣播訊息,程式會嘗試透過TCP建立連線。步驟包括:
從UDP訊息中提取傳送者的IP地址。
使用提取的IP地址和預定義的TCP埠號發起TCP連線請求。
如果連線成功,程式將建立一個可靠的點對點通訊通道,用於後續的聊天訊息傳遞。
程式建立一個TCP套接字,並在特定埠上進行監聽,等待其他使用者的連線請求。當有新的連線請求到達時,程式接受該連線,併爲每個連線建立一個新的執行緒,以處理與該連線相關的訊息通訊。
使用者在鍵盤上輸入訊息後,程式透過UDP套接字廣播該訊息到區域網內的所有線上使用者。此功能確保所有線上的使用者都能看到傳送的訊息。
程式透過TCP連線接收來自其他使用者的訊息。接收到的訊息將被顯示在終端上,提供實時的聊天功能。每個TCP連線使用一個獨立的執行緒進行處理,確保訊息的及時傳遞和處理。
程式採用了多執行緒技術來並行處理不同的任務,確保系統的響應性和效率。主要執行緒包括:
UDP廣播監聽執行緒:處理UDP廣播訊息的接收和處理。
TCP連線監聽執行緒:接受來自其他使用者的TCP連線請求。
TCP訊息處理執行緒:處理與每個已連線使用者之間的訊息交換。
在程式退出時,所有開啟的套接字都會被關閉,資源得到釋放。程式透過呼叫 closesocket
函式關閉套接字,並呼叫 WSACleanup
進行Winsock庫的清理,確保程式在退出時不會洩漏資源。