随着局域网(LAN)应用的广泛使用,网络通信已经成为软件设计中不可或缺的一部分。局域网聊天软件作为一种常见的网络应用,可以实现多个用户之间的实时通信,广泛应用于企业内部沟通和小型网络环境中。本项目设计并实现一个基于C语言的局域网群聊程序,通过UDP广播搜索在线用户,并在发现其他在线应用程序后,自动建立TCP连接,实现消息的收发。本程序展示了如何在Windows环境下使用Winsock API进行网络编程,提供了对UDP和TCP协议的实际应用,体现了网络通信中的多线程处理、广播通信和实时消息传递的关键技术点。
在局域网内探测并发现其他在线用户是局域网聊天软件最主要的核心功能。该过程涉及到局域网广播(UDP广播)和TCP连接两个关键步骤。
下面将详细介绍实现这一功能的方法和设计思路。
UDP(用户数据报协议)是一种无连接的、轻量级的传输协议,适用于发送小数据包。UDP广播允许将数据包发送到局域网内的所有设备,所有在监听特定端口的设备都能够接收到广播消息。这种特性使得UDP广播非常适合用于探测和发现局域网内的在线设备。
在程序启动时,客户端会通过UDP广播发送一个上线通知消息,表示自己已在线。其他监听同一端口的客户端接收到这一消息后,可以获知该客户端的IP地址,并识别出它在线。具体的实现步骤如下:
UDP广播虽然可以有效地发现在线用户,但由于其无连接的特点,不适合用于长时间的可靠通信。因此,在发现其他在线用户后,程序需要通过TCP(传输控制协议)建立可靠的点对点连接。TCP是一种面向连接的协议,能够确保数据的完整性和顺序传输,非常适合用于聊天消息的传递。
由于UDP广播接收、TCP连接监听和消息收发等操作需要同时进行,程序采用了多线程的设计。每个功能模块都运行在独立的线程中,确保它们可以并行处理,互不干扰。这样不仅提高了程序的响应速度,还增强了用户体验,确保通信的实时性。
通过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广播向局域网内所有设备发送一个“HELLO, I’M ONLINE”的消息。这一消息用来告知局域网内的其他用户自己的存在,从而实现在线用户的探测。
为了接收其他用户的广播消息,程序创建了一个UDP套接字并绑定到特定的端口上(UDP_PORT)。程序通过这个套接字监听局域网内的所有广播消息,提取发送者的IP地址,并处理接收到的消息。
一旦接收到来自其他在线用户的UDP广播消息,程序会尝试通过TCP建立连接。步骤包括:
程序创建一个TCP套接字,并在特定端口上进行监听,等待其他用户的连接请求。当有新的连接请求到达时,程序接受该连接,并为每个连接创建一个新的线程,以处理与该连接相关的消息通信。
用户在键盘上输入消息后,程序通过UDP套接字广播该消息到局域网内的所有在线用户。此功能确保所有在线的用户都能看到发送的消息。
程序通过TCP连接接收来自其他用户的消息。接收到的消息将被显示在终端上,提供实时的聊天功能。每个TCP连接使用一个独立的线程进行处理,确保消息的及时传递和处理。
程序采用了多线程技术来并行处理不同的任务,确保系统的响应性和效率。主要线程包括:
在程序退出时,所有打开的套接字都会被关闭,资源得到释放。程序通过调用 closesocket 函数关闭套接字,并调用 WSACleanup 进行Winsock库的清理,确保程序在退出时不会泄漏资源。