Linux下C语言双进程TCP服务器:跨进程send操作实现咨询
解决多进程TCP服务器定时发送+多客户端接收的问题
首先,你用fork()拆分父子进程的思路方向是对的,但核心卡点在于父子进程的内存空间是相互独立的——你没法直接在父进程里访问子进程维护的客户端套接字列表,反过来也一样。另外要明确:每个客户端连接最好对应一个独立的处理进程,而父进程专门负责监听新连接、维护活跃客户端列表,以及定时发送数据。
下面给你两种可行的实现方案,按需选择:
方案一:父进程管理客户端+定时发送,子进程处理单客户端接收
这个方案的核心逻辑是:父进程掌控全局的客户端套接字列表,每3秒遍历列表发送数据;每当有新客户端连接时,fork()出子进程专门处理该客户端的recv()操作,子进程退出时通知父进程清理对应套接字。
关键实现细节
客户端套接字的维护与同步
- 父进程用动态数组/链表保存所有活跃客户端的套接字,同时维护「子进程PID-套接字」的映射表(比如结构体数组),这样子进程退出时能精准找到要移除的套接字。
- 父进程通过
SIGCHLD信号捕获子进程的退出事件,避免僵尸进程,同时从列表中删除对应的无效套接字。 - 父子进程共享套接字文件描述符,但内存不共享,所以客户端列表只能由父进程单独维护,子进程只专注接收数据。
定时发送的实现
推荐用select()实现3秒超时(比alarm()的信号机制更稳定,不会打断其他系统调用),超时触发时遍历客户端列表执行send()。核心代码示例
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <sys/wait.h> #include <signal.h> #include <string.h> #define MAX_CLIENTS 100 // 维护子进程PID与客户端套接字的映射 typedef struct { pid_t pid; int sock_fd; } ClientInfo; ClientInfo clients[MAX_CLIENTS]; int client_count = 0; // 处理子进程退出的信号回调 void handle_child_exit(int sig) { pid_t pid; int status; // 循环处理所有退出的子进程,避免僵尸进程 while ((pid = waitpid(-1, &status, WNOHANG)) > 0) { // 找到对应PID的客户端套接字并清理 for (int i = 0; i < client_count; i++) { if (clients[i].pid == pid) { close(clients[i].sock_fd); // 数组元素前移填补空位 clients[i] = clients[--client_count]; break; } } } } // 向所有客户端发送定时消息 void broadcast_heartbeat() { char heartbeat[] = "Server heartbeat: 3 seconds elapsed\n"; for (int i = 0; i < client_count; i++) { ssize_t sent = send(clients[i].sock_fd, heartbeat, strlen(heartbeat), 0); if (sent == -1) { perror("send failed"); // 发送失败说明客户端已断开,直接清理 close(clients[i].sock_fd); clients[i] = clients[--client_count]; i--; // 数组前移后,当前位置需要重新检查 } } } // 子进程专属的客户端接收逻辑 void handle_client(int sock_fd) { char buffer[1024]; while (1) { ssize_t recv_len = recv(sock_fd, buffer, sizeof(buffer)-1, 0); if (recv_len <= 0) { // 客户端断开或接收出错,子进程退出 break; } buffer[recv_len] = '\0'; printf("Received from client: %s\n", buffer); // 这里可以添加自定义的数据处理逻辑 } } int main() { // 注册子进程退出信号处理 signal(SIGCHLD, handle_child_exit); // 创建监听套接字并完成绑定、监听 int listen_fd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = INADDR_ANY; server_addr.sin_port = htons(8080); bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)); listen(listen_fd, 5); fd_set read_fds; struct timeval timeout; while (1) { FD_ZERO(&read_fds); FD_SET(listen_fd, &read_fds); int max_fd = listen_fd; // 设置3秒超时,触发定时发送 timeout.tv_sec = 3; timeout.tv_usec = 0; int ret = select(max_fd + 1, &read_fds, NULL, NULL, &timeout); if (ret == -1) { perror("select error"); continue; } else if (ret == 0) { // 超时,执行广播心跳 broadcast_heartbeat(); continue; } // 有新客户端连接请求 if (FD_ISSET(listen_fd, &read_fds)) { struct sockaddr_in client_addr; socklen_t client_len = sizeof(client_addr); int client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len); if (client_fd == -1) { perror("accept error"); continue; } // 检查客户端数量是否超限 if (client_count >= MAX_CLIENTS) { char reject[] = "Server is full, try later\n"; send(client_fd, reject, strlen(reject), 0); close(client_fd); continue; } // fork子进程处理该客户端 pid_t pid = fork(); if (pid == -1) { perror("fork error"); close(client_fd); continue; } else if (pid == 0) { // 子进程:关闭监听套接字,专注处理当前客户端 close(listen_fd); handle_client(client_fd); close(client_fd); exit(0); } else { // 父进程:保存客户端信息,关闭套接字副本(子进程已持有) clients[client_count++] = (ClientInfo){pid, client_fd}; close(client_fd); } } } close(listen_fd); return 0; }
注意事项
- 父进程
accept后必须关闭客户端套接字的副本,否则套接字引用计数不为0,客户端断开后无法正确释放资源。 - 一定要维护「PID-套接字」的映射,否则子进程退出时无法精准清理对应套接字。
方案二:用线程替代进程(更简单的选择)
如果不需要严格用进程拆分任务,用线程会大幅降低实现复杂度——线程共享同一内存空间,不需要IPC机制就能直接访问客户端列表。核心逻辑:
- 主线程监听新连接,维护客户端列表;
- 每个客户端连接创建一个子线程处理
recv(); - 单独启动一个定时线程,每3秒遍历列表发送数据。
核心代码示例
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <pthread.h> #include <string.h> #define MAX_CLIENTS 100 int client_sockets[MAX_CLIENTS]; int client_count = 0; pthread_mutex_t client_mutex = PTHREAD_MUTEX_INITIALIZER; // 互斥锁保护列表 // 定时发送线程函数 void* heartbeat_thread(void* arg) { while (1) { sleep(3); char msg[] = "Server heartbeat: 3 seconds elapsed\n"; // 加锁保护客户端列表的读写 pthread_mutex_lock(&client_mutex); for (int i = 0; i < client_count; i++) { ssize_t sent = send(client_sockets[i], msg, strlen(msg), 0); if (sent == -1) { perror("send failed"); close(client_sockets[i]); client_sockets[i] = client_sockets[--client_count]; i--; } } pthread_mutex_unlock(&client_mutex); } return NULL; } // 客户端接收线程函数 void* client_handler(void* arg) { int sock_fd = *(int*)arg; free(arg); char buffer[1024]; while (1) { ssize_t recv_len = recv(sock_fd, buffer, sizeof(buffer)-1, 0); if (recv_len <= 0) { break; } buffer[recv_len] = '\0'; printf("Received from client: %s\n", buffer); } // 客户端断开,清理套接字 pthread_mutex_lock(&client_mutex); for (int i = 0; i < client_count; i++) { if (client_sockets[i] == sock_fd) { close(client_sockets[i]); client_sockets[i] = client_sockets[--client_count]; break; } } pthread_mutex_unlock(&client_mutex); close(sock_fd); return NULL; } int main() { // 创建并分离定时线程 pthread_t hb_thread; pthread_create(&hb_thread, NULL, heartbeat_thread, NULL); pthread_detach(hb_thread); // 创建监听套接字并完成绑定、监听 int listen_fd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = INADDR_ANY; server_addr.sin_port = htons(8080); bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)); listen(listen_fd, 5); while (1) { struct sockaddr_in client_addr; socklen_t client_len = sizeof(client_addr); int client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len); if (client_fd == -1) { perror("accept error"); continue; } pthread_mutex_lock(&client_mutex); if (client_count >= MAX_CLIENTS) { char reject[] = "Server is full, try later\n"; send(client_fd, reject, strlen(reject), 0); close(client_fd); pthread_mutex_unlock(&client_mutex); continue; } client_sockets[client_count++] = client_fd; pthread_mutex_unlock(&client_mutex); // 创建并分离客户端处理线程 int* fd_ptr = malloc(sizeof(int)); *fd_ptr = client_fd; pthread_t client_thread; pthread_create(&client_thread, NULL, client_handler, fd_ptr); pthread_detach(client_thread); } close(listen_fd); pthread_mutex_destroy(&client_mutex); return 0; }
注意事项
- 必须用互斥锁保护客户端列表的访问,避免多线程同时读写导致的竞态条件;
- 线程分离后无需主线程等待,简化资源管理。
总结
如果你坚持用进程方案,核心是解决进程间的客户端套接字同步问题,要么让父进程完全掌控套接字列表,要么用共享内存+信号量实现列表共享;如果可以用线程,实现会简单很多,因为内存共享无需额外IPC。
不管用哪种方案,都要注意:
- 及时关闭无效套接字,避免资源泄漏;
- 处理
send()/recv()的错误返回,比如客户端断开时的异常; - 避免竞态条件(进程用IPC同步,线程用互斥锁)。
内容的提问来源于stack exchange,提问作者lyfeng
相关产品推荐
相关产品推荐

