如何基于TCP/IP实现支持多客户端接入的多线程C/S通信系统
TCP服务端多线程改造方案
原有单连接服务端的核心问题是listen backlog设置过小、仅接受一次连接就退出,无法处理多客户端并发。直接使用POSIX线程库改造即可,线程调度由操作系统内核自动完成,不需要手动实现调度逻辑,核心思路是主线程专职监听新连接,每建立一个客户端连接就创建独立线程处理消息收发。
核心改造逻辑
- 监听逻辑调整:将
listen的等待队列长度从1改为128(常规服务端配置),主线程进入死循环调用accept等待新连接,不再接受一个连接就退出 - 线程模型:每个新连接对应一个独立处理线程,将客户端套接字作为参数传入线程函数,线程内完成消息接收、打印后主动关闭客户端套接字
- 资源回收:设置线程为分离状态,线程退出后内核自动回收资源,不需要主线程调用
pthread_join等待 - 错误修复:删除原代码中TCP场景下无效的
unlink调用(该逻辑仅用于Unix域套接字清理,TCP下使用会误删本地文件)
改造后完整代码
服务端代码(server.c)
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <pthread.h> #define SIZE 1024 // 客户端消息处理线程函数 void *client_handler(void *arg) { int client_sock = *(int *)arg; free(arg); // 释放提前分配的套接字内存 char msg[SIZE] = {0}; // 接收客户端消息 ssize_t recv_len = recv(client_sock, msg, SIZE - 1, 0); if (recv_len > 0) { printf("收到客户端消息: %s", msg); } close(client_sock); return NULL; } int main(int argc, char *argv[]) { if (argc != 2) { printf("启动参数错误,用法: ./server 端口号\n"); return 1; } int psock; struct sockaddr_in addr; // 初始化监听套接字 addr.sin_family = AF_INET; addr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定所有网卡IP,不需要单独指定IP addr.sin_port = htons(atoi(argv[1])); psock = socket(AF_INET, SOCK_STREAM, 0); // 端口复用,避免重启时端口占用 int opt = 1; setsockopt(psock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); bind(psock, (struct sockaddr *)&addr, sizeof(addr)); listen(psock, 128); // 监听队列长度设为128 printf("等待客户端连接,监听端口: %s\n", argv[1]); // 循环接受新连接 while (1) { struct sockaddr_in cli_addr; socklen_t cli_addr_len = sizeof(cli_addr); // 单独分配内存存客户端套接字,避免多线程变量覆盖 int *client_sock = malloc(sizeof(int)); *client_sock = accept(psock, (struct sockaddr *)&cli_addr, &cli_addr_len); if (*client_sock < 0) { perror("接受连接失败"); free(client_sock); continue; } printf("新客户端连接建立\n"); pthread_t tid; pthread_create(&tid, NULL, client_handler, client_sock); pthread_detach(tid); // 设置线程分离,自动回收资源 } close(psock); return 0; }
客户端代码(client.c)
原客户端逻辑基本可用,仅删除不存在的头文件引用即可,支持多开客户端测试并发:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define SIZE 1024 int main(int argc, char *argv[]) { if (argc != 3) { printf("启动参数错误,用法: ./client 服务端IP 端口号\n"); return 1; } int sock; char msg[SIZE] = {0}; struct sockaddr_in addr; addr.sin_family = AF_INET; inet_aton(argv[1], &addr.sin_addr); addr.sin_port = htons(atoi(argv[2])); sock = socket(AF_INET, SOCK_STREAM, 0); if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) { perror("连接服务端失败"); close(sock); return 1; } printf("请输入要发送的消息: "); fgets(msg, sizeof(msg), stdin); send(sock, msg, strlen(msg) + 1, 0); close(sock); return 0; }
编译运行指令
注意服务端编译需要链接线程库,加-lpthread参数:
# 编译服务端 gcc server.c -o server -lpthread # 启动服务端,监听1234端口 ./server 1234 # 新开多个终端,分别编译启动客户端即可测试并发 gcc client.c -o client ./client 127.0.0.1 1234
注意事项
- 不需要手动实现线程调度逻辑,Linux内核会根据CPU负载自动完成线程的调度与资源分配
- 传递客户端套接字给线程时不要直接传局部变量地址,否则主线程快速accept时会覆盖内存值,导致线程拿到错误的套接字,用单独malloc的内存传递即可避免该问题
- 代码默认用
INADDR_ANY绑定所有网卡IP,属于无固定IP绑定的实现,不需要单独指定服务端监听IP,符合简易实现需求 - 如果需要支持客户端长连接发多条消息,只要把线程处理函数里的
recv改成循环接收即可,现有框架不用调整
内容的提问来源于stack exchange,提问作者Lilfio
相关产品推荐
相关产品推荐

