UDP服务器接收和区分多个客户端连接的实现
与TCP不同,UDP是无连接协议,没有"连接"的概念,但UDP服务器仍然可以通过客户端的IP地址和端口号来区分不同的客户端。下面将详细介绍UDP服务器如何接收和区分多个客户端,并提供完整的实现代码。
UDP服务器区分客户端的原理
UDP协议虽然不维护连接连接状态,但每个UDP数据包都包含发送方的IP地址和端口号,服务器可以通过以下信息唯一标识一个客户端:
- 客户端的IP地址(IPv4或IPv6)
- 客户端的端口号
服务器通过维护一个"客户端信息表",记录已通信的客户端标识及其相关状态,从而实现对多个客户端的区分和管理。
实现代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <time.h>
#define PORT 8080
#define BUFFER_SIZE 1024
#define MAX_CLIENTS 100 // 最大客户端数量
// 客户端信息结构体
typedef struct {
struct sockaddr_storage addr; // 客户端地址
socklen_t addr_len; // 地址长度
time_t last_active; // 最后活动时间
int is_active; // 是否活跃
} Client;
// 全局客户端列表及同步机制
Client clients[MAX_CLIENTS];
int client_count = 0;
pthread_mutex_t clients_mutex = PTHREAD_MUTEX_INITIALIZER;
// 将客户端地址转换为字符串标识
void get_client_id(struct sockaddr_storage *addr, char *id, size_t id_len) {
char ip[INET6_ADDRSTRLEN];
int port;
if (addr->ss_family == AF_INET) {
struct sockaddr_in *ipv4 = (struct sockaddr_in *)addr;
inet_ntop(AF_INET, &(ipv4->sin_addr), ip, sizeof(ip));
port = ntohs(ipv4->sin_port);
} else {
struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)addr;
inet_ntop(AF_INET6, &(ipv6->sin6_addr), ip, sizeof(ip));
port = ntohs(ipv6->sin6_port);
}
snprintf(id, id_len, "%s:%d", ip, port);
}
// 查找客户端在列表中的索引
int find_client(struct sockaddr_storage *addr, socklen_t addr_len) {
pthread_mutex_lock(&clients_mutex);
for (int i = 0; i < client_count; i++) {
if (clients[i].is_active &&
addr->ss_family == clients[i].addr.ss_family &&
memcmp(&addr->ss_addr, &clients[i].addr.ss_addr,
addr->ss_family == AF_INET ? sizeof(struct in_addr) : sizeof(struct in6_addr)) == 0) {
// 检查端口是否匹配
if (addr->ss_family == AF_INET) {
struct sockaddr_in *client_addr = (struct sockaddr_in *)&clients[i].addr;
struct sockaddr_in *new_addr = (struct sockaddr_in *)addr;
if (client_addr->sin_port == new_addr->sin_port) {
pthread_mutex_unlock(&clients_mutex);
return i;
}
} else {
struct sockaddr_in6 *client_addr = (struct sockaddr_in6 *)&clients[i].addr;
struct sockaddr_in6 *new_addr = (struct sockaddr_in6 *)addr;
if (client_addr->sin6_port == new_addr->sin6_port) {
pthread_mutex_unlock(&clients_mutex);
return i;
}
}
}
}
pthread_mutex_unlock(&clients_mutex);
return -1;
}
// 添加新客户端到列表
int add_client(struct sockaddr_storage *addr, socklen_t addr_len) {
pthread_mutex_lock(&clients_mutex);
// 检查是否已达到最大客户端数量
if (client_count >= MAX_CLIENTS) {
pthread_mutex_unlock(&clients_mutex);
return -1;
}
// 查找空位置
int i;
for (i = 0; i < MAX_CLIENTS; i++) {
if (!clients[i].is_active) break;
}
// 初始化客户端信息
memcpy(&clients[i].addr, addr, addr_len);
clients[i].addr_len = addr_len;
clients[i].last_active = time(NULL);
clients[i].is_active = 1;
if (i >= client_count) {
client_count = i + 1;
}
char client_id[INET6_ADDRSTRLEN + 6]; // IP + : + 端口(最多5位)
get_client_id(addr, client_id, sizeof(client_id));
printf("New client: %s (total: %d)\n", client_id, client_count);
pthread_mutex_unlock(&clients_mutex);
return i;
}
// 更新客户端活动时间
void update_client_activity(int index) {
pthread_mutex_lock(&clients_mutex);
if (index >= 0 && index < MAX_CLIENTS && clients[index].is_active) {
clients[index].last_active = time(NULL);
}
pthread_mutex_unlock(&clients_mutex);
}
// 清理超时客户端
void *cleanup_clients(void *arg) {
(void)arg;
time_t timeout = 300; // 5分钟超时
while (1) {
sleep(60); // 每分钟检查一次
time_t now = time(NULL);
pthread_mutex_lock(&clients_mutex);
int cleaned = 0;
for (int i = 0; i < MAX_CLIENTS; i++) {
if (clients[i].is_active && (now - clients[i].last_active) > timeout) {
char client_id[INET6_ADDRSTRLEN + 6];
get_client_id(&clients[i].addr, client_id, sizeof(client_id));
printf("Client %s timed out\n", client_id);
clients[i].is_active = 0;
cleaned++;
client_count--;
}
}
if (cleaned > 0) {
printf("Cleaned %d inactive clients, remaining: %d\n", cleaned, client_count);
}
pthread_mutex_unlock(&clients_mutex);
}
return NULL;
}
int main() {
int server_fd;
char buffer[BUFFER_SIZE];
struct sockaddr_storage client_addr;
socklen_t client_addr_len = sizeof(client_addr);
// 初始化客户端列表
memset(clients, 0, sizeof(clients));
// 创建UDP socket
if ((server_fd = socket(AF_INET6, SOCK_DGRAM, 0)) < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 配置socket同时支持IPv4和IPv6
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 禁用IPv6-only模式,允许接收IPv4数据包
int v6only = 0;
setsockopt(server_fd, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof(v6only));
// 绑定到所有接口和指定端口
struct sockaddr_in6 server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin6_family = AF_INET6;
server_addr.sin6_addr = in6addr_any;
server_addr.sin6_port = htons(PORT);
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("bind failed");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("UDP server listening on port %d...\n", PORT);
// 启动客户端清理线程
pthread_t cleanup_thread;
pthread_create(&cleanup_thread, NULL, cleanup_clients, NULL);
pthread_detach(cleanup_thread);
// 主循环:接收并处理客户端数据
while (1) {
// 接收客户端数据
ssize_t recv_len = recvfrom(server_fd, buffer, BUFFER_SIZE, 0,
(struct sockaddr *)&client_addr, &client_addr_len);
if (recv_len < 0) {
perror("recvfrom failed");
continue;
}
buffer[recv_len] = '\0';
// 识别客户端
char client_id[INET6_ADDRSTRLEN + 6];
get_client_id(&client_addr, client_id, sizeof(client_id));
printf("\nReceived from %s: %s\n", client_id, buffer);
// 查找或添加客户端
int client_idx = find_client(&client_addr, client_addr_len);
if (client_idx == -1) {
client_idx = add_client(&client_addr, client_addr_len);
if (client_idx == -1) {
const char *msg = "Server is full, try again later";
sendto(server_fd, msg, strlen(msg), 0,
(struct sockaddr *)&client_addr, client_addr_len);
continue;
}
} else {
update_client_activity(client_idx);
}
// 处理请求并回复
char response[BUFFER_SIZE];
snprintf(response, BUFFER_SIZE, "Server received: %s (from %s)", buffer, client_id);
sendto(server_fd, response, strlen(response), 0,
(struct sockaddr *)&client_addr, client_addr_len);
// 显示当前客户端数量
pthread_mutex_lock(&clients_mutex);
printf("Current clients: %d\n", client_count);
pthread_mutex_unlock(&clients_mutex);
}
// 清理资源
close(server_fd);
return 0;
}
代码关键特性解析
客户端识别机制:
- 使用
struct sockaddr_storage
存储客户端地址,兼容IPv4和IPv6 - 通过
get_client_id
函数将客户端IP和端口转换为唯一字符串标识 - 实现
find_client
函数查找客户端是否已存在
- 使用
客户端管理:
- 维护一个客户端列表,记录每个客户端的地址、最后活动时间和状态
- 提供
add_client
函数添加新客户端 - 实现超时清理机制,自动移除长时间不活动的客户端
并发处理:
- 使用互斥锁
pthread_mutex_t
保证客户端列表的线程安全 - 单独的清理线程定期检查并移除超时客户端
- 使用互斥锁
双栈支持:
- 配置IPv6 socket同时支持IPv4和IPv6客户端
- 通过
IPV6_V6ONLY
选项禁用纯IPv6模式
编译与测试
编译代码:
gcc -o udp_server udp_server.c -lpthread
运行服务器:
./udp_server
测试客户端(可在多个终端同时运行):
# IPv4测试
echo "Hello from IPv4" | nc -u 127.0.0.1 8080
# IPv6测试
echo "Hello from IPv6" | nc -u ::1 8080
UDP与TCP处理客户端的主要区别
特性 | TCP服务器 | UDP服务器 |
---|---|---|
连接管理 | 有明确的连接建立和关闭过程 | 无连接概念,基于数据包交互 |
客户端标识 | 通过socket文件描述符 | 通过客户端IP地址和端口号 |
数据传输 | 流式传输,保证顺序和可靠性 | 数据报传输,不保证顺序和可靠性 |
服务器实现 | 每个连接通常对应一个线程/进程 | 单线程即可处理多个客户端 |
资源占用 | 较高(每个连接维护状态) | 较低(仅记录必要客户端信息) |
UDP服务器适合对实时性要求高、可以容忍少量数据丢失的场景,如语音通话、视频流、游戏等。通过上述实现,UDP服务器能够有效地接收和区分多个客户端,并进行基本的客户端管理。