UDP服务器接收和区分多个客户端连接的实现

发布于:2025-08-31 ⋅ 阅读:(15) ⋅ 点赞:(0)

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;
}

代码关键特性解析

  1. 客户端识别机制

    • 使用struct sockaddr_storage存储客户端地址,兼容IPv4和IPv6
    • 通过get_client_id函数将客户端IP和端口转换为唯一字符串标识
    • 实现find_client函数查找客户端是否已存在
  2. 客户端管理

    • 维护一个客户端列表,记录每个客户端的地址、最后活动时间和状态
    • 提供add_client函数添加新客户端
    • 实现超时清理机制,自动移除长时间不活动的客户端
  3. 并发处理

    • 使用互斥锁pthread_mutex_t保证客户端列表的线程安全
    • 单独的清理线程定期检查并移除超时客户端
  4. 双栈支持

    • 配置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服务器能够有效地接收和区分多个客户端,并进行基本的客户端管理。


网站公告

今日签到

点亮在社区的每一天
去签到