You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Linux下C语言双进程TCP服务器:跨进程send操作实现咨询

解决多进程TCP服务器定时发送+多客户端接收的问题

首先,你用fork()拆分父子进程的思路方向是对的,但核心卡点在于父子进程的内存空间是相互独立的——你没法直接在父进程里访问子进程维护的客户端套接字列表,反过来也一样。另外要明确:每个客户端连接最好对应一个独立的处理进程,而父进程专门负责监听新连接、维护活跃客户端列表,以及定时发送数据。

下面给你两种可行的实现方案,按需选择:

方案一:父进程管理客户端+定时发送,子进程处理单客户端接收

这个方案的核心逻辑是:父进程掌控全局的客户端套接字列表,每3秒遍历列表发送数据;每当有新客户端连接时,fork()出子进程专门处理该客户端的recv()操作,子进程退出时通知父进程清理对应套接字。

关键实现细节

  1. 客户端套接字的维护与同步

    • 父进程用动态数组/链表保存所有活跃客户端的套接字,同时维护「子进程PID-套接字」的映射表(比如结构体数组),这样子进程退出时能精准找到要移除的套接字。
    • 父进程通过SIGCHLD信号捕获子进程的退出事件,避免僵尸进程,同时从列表中删除对应的无效套接字。
    • 父子进程共享套接字文件描述符,但内存不共享,所以客户端列表只能由父进程单独维护,子进程只专注接收数据。
  2. 定时发送的实现
    推荐用select()实现3秒超时(比alarm()的信号机制更稳定,不会打断其他系统调用),超时触发时遍历客户端列表执行send()。

  3. 核心代码示例

#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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.20 09:04:08