云计算百科
云计算领域专业知识百科平台

Linux网络编程:TCP多进程/多线程并发服务器详解

Linux网络编程:TCP多进程/多线程并发服务器详解

  • TCP并发服务器概述
  • 在Linux网络编程中,TCP服务器主要有三种并发模型:

  • 多进程模型:为每个客户端连接创建新进程
  • 多线程模型:为每个客户端连接创建新线程
  • I/O多路复用:使用select/poll/epoll管理多个连接
  • 本文将重点讲解多进程和多线程实现方式,并分析关键技术和常见问题。

  • 多进程并发服务器实现
  • 2.1 核心代码解析

    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <string.h>
    #include <signal.h>
    #include <sys/wait.h>

    #define BACKLOG 5

    void client_handler(int client_fd) {
    char buf[BUFSIZ];
    while(1) {
    bzero(buf, BUFSIZ);
    int ret = read(client_fd, buf, BUFSIZ);
    if(ret <= 0) break;
    printf("Received: %s\\n", buf);
    }
    close(client_fd);
    }

    int main(int argc, char *argv[]) {
    // 创建socket
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);

    // 设置地址重用
    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    // 绑定地址
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(atoi(argv[2]));
    inet_aton(argv[1], &addr.sin_addr);
    bind(server_fd, (struct sockaddr*)&addr, sizeof(addr));

    // 监听
    listen(server_fd, BACKLOG);

    // 处理僵尸进程
    signal(SIGCHLD, SIG_IGN);

    while(1) {
    // 接受连接
    struct sockaddr_in client_addr;
    socklen_t len = sizeof(client_addr);
    int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &len);

    // 创建子进程
    if(fork() == 0) {
    close(server_fd); // 子进程关闭监听socket
    client_handler(client_fd);
    exit(0);
    }
    close(client_fd); // 父进程关闭客户端socket
    }
    }

    2.2 关键技术点

  • 地址重用(SO_REUSEADDR)
  • int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    • 允许服务器快速重启而不需要等待TIME_WAIT状态结束

    • 避免"Address already in use"错误

  • 僵尸进程处理
  • signal(SIGCHLD, SIG_IGN); // 最简单的方式
    // 或者
    signal(SIGCHLD, [](int sig) { while(waitpid(1, NULL, WNOHANG) > 0); });

    • 子进程退出后会变成僵尸进程

    • 通过信号处理或显式wait/waitpid回收资源

  • 文件描述符关闭
  • 重要原则:只有当所有进程都关闭了文件描述符,内核才会真正释放资源。

    • 父进程需要关闭客户端socket

    • 子进程需要关闭监听socket

  • 多线程并发服务器实现
  • 3.1 核心代码解析

    #include <pthread.h>

    void* client_handler(void* arg) {
    int client_fd = *(int*)arg;
    char buf[BUFSIZ];

    while(1) {
    bzero(buf, BUFSIZ);
    int ret = read(client_fd, buf, BUFSIZ);
    if(ret <= 0) break;
    printf("Received: %s\\n", buf);
    }

    close(client_fd);
    free(arg); // 释放动态分配的参数
    return NULL;
    }

    int main(int argc, char *argv[]) {
    // … (初始化部分与多进程相同)

    while(1) {
    struct sockaddr_in client_addr;
    socklen_t len = sizeof(client_addr);
    int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &len);

    pthread_t tid;
    int *arg = malloc(sizeof(int));
    *arg = client_fd;
    pthread_create(&tid, NULL, client_handler, arg);
    pthread_detach(tid); // 分离线程,自动回收资源
    }
    }

    3.2 关键技术点

  • 线程参数传递
  • • 必须确保每个线程获得独立的client_fd

    • 动态分配内存传递参数,避免竞争条件

  • 线程分离
  • pthread_detach(tid);

    • 使线程成为"分离状态",退出时自动回收资源

    • 替代方案:在主线程中调用pthread_join

  • 线程安全函数
  • • inet_ntoa是非线程安全的,考虑使用inet_ntop

    • 避免在多线程中使用全局变量和静态变量

  • 关键工具函数详解
  • 4.1 bzero vs memset

    void bzero(void *s, size_t n); // 清零内存
    void *memset(void *s, int c, size_t n); // 设置内存值

    • bzero是BSD遗留函数,POSIX标准推荐使用memset

    • 现代代码中建议使用:memset(buf, 0, BUFSIZ)

    4.2 地址转换函数

    // 字符串转二进制地址
    int inet_aton(const char *cp, struct in_addr *inp);

    // 二进制地址转字符串(非线程安全)
    char *inet_ntoa(struct in_addr in);

    // 推荐使用线程安全版本
    const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
    int inet_pton(int af, const char *src, void *dst);

  • 性能对比与选择建议
  • 特性多进程模型多线程模型
    资源开销 高(每个连接独立进程) 较低(共享地址空间)
    稳定性 高(进程隔离) 较低(一个线程崩溃可能影响整个服务)
    编程复杂度 中等 较高(需处理线程同步)
    适用场景 CPU密集型任务 I/O密集型任务
    跨平台一致性 好(所有Unix-like系统支持) 一般(实现细节有差异)

    选择建议: • 需要高稳定性:选择多进程

    • 需要高并发性能:选择多线程或I/O多路复用

    • 现代服务器通常使用线程池+epoll的组合方案

  • 完整示例代码
  • [完整的多进程和多线程示例代码已在文中提供,可直接编译测试]

  • 常见问题解答
  • Q1: 为什么在多进程模型中父进程要关闭客户端socket?

    A: 因为子进程已经复制了父进程的文件描述符表,只有当父子进程都关闭了socket,内核才会真正释放连接资源。

    Q2: 如何避免大量TIME_WAIT状态?

    A: 可以设置SO_REUSEADDR选项,或者调整内核参数:

    sysctl -w net.ipv4.tcp_tw_reuse=1
    sysctl -w net.ipv4.tcp_tw_recycle=1

    Q3: 为什么多线程服务器中要动态分配参数?

    A: 如果直接传递栈上的变量地址,可能在线程读取前就被主线程修改了,导致数据竞争。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Linux网络编程:TCP多进程/多线程并发服务器详解
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!