1.核心目标

让服务器能够同时处理多个客户端的连接请求


2. 方案一:多进程模型 (Process-based)

利用 fork() 系统调用,为每一个新连接的客户端创建一个独立的子进程来处理。 关键知识点

  1. 僵尸进程的回收:
    • 子进程结束后,如果父进程没有读取其退出状态,子进程会变成“僵尸进程”,占用系统资源。
    • 解决方法:使用信号机制。注册 SIGCHLD 信号处理函数,在函数中调用 wait(NULL) 自动回收子进程。
  2. 文件描述符的回收与继承:
    • fork() 之后,子进程会复制父进程的所有文件描述符(包括监听套接字 listfd 和通信套接字 conn)。
    • 原则:
      • 父进程:只负责监听 (listfd),不需要通信 (conn)。所以父进程收到 conn 后应立刻 close(conn)。
      • 子进程:只负责通信 (conn),不需要监听 (listfd)。所以子进程进入循环前应 close(listfd)。
    • 后果:如果不关闭不需要的描述符,会导致文件描述符泄露,且可能导致端口无法释放。

💻 代码逻辑梳理

A. 信号处理 (防止僵尸进程)
// 定义信号处理函数
void handle(int num) 
{
    // wait(NULL) 会阻塞等待任意一个子进程结束,并回收其资源
    // 放在信号处理函数中,当子进程结束时,内核发送 SIGCHLD 信号触发此函数
    wait(NULL); 
}

// 在 main 函数中注册
signal(SIGCHLD, handle);
B. 主流程 (Main Loop)
int main()
 {
    // 1. 创建监听套接字 (socket, bind, listen) ... [省略]

    while (1) 
{
        // 2. 接受连接 (阻塞)
        int conn = accept(listfd, ...);
        if (conn == -1) { perror("accept"); continue; }

        // 3. 【核心】创建子进程
        pid_t pid = fork();

        if (pid > 0) 
{ 
            // --- 父进程区域 ---
            // 父进程不需要和这个特定的客户端通信,关闭 conn
            close(conn); 
            // 父进程继续循环,去 accept 下一个客户
            // 注意:这里不需要 wait(NULL),因为已经用信号处理了
        } 

        else if (pid == 0) 
{ 
            // --- 子进程区域 ---
            // 子进程不需要监听新连接,关闭 listfd (非常重要!)
            close(listfd); 

            // 4. 业务处理循环 (Echo Server 示例)
            while (1) 
{
                char buf[512] = {0};
                int rd_ret = recv(conn, buf, sizeof(buf), 0);
                
                if (rd_ret <= 0) 
{
                    printf("cli offline\n");
                    exit(0); // 子进程处理完毕或出错,直接退出
                }

                // 处理数据 (例如加上时间戳返回)
                time_t tm; time(&tm);
                sprintf(buf, "%s %s", buf, ctime(&tm));
                send(conn, buf, strlen(buf), 0);
            }
        } 
        else 
{
            // fork 失败
            perror("fork");
            close(conn);
        }
    }
    close(listfd);
    return 0;
}

3. 方案二:多线程模型 (Thread-based)

利用 pthread_create,为每一个新连接的客户端创建一个独立的线程来处理。相比进程,线程更轻量级,切换开销小,共享内存方便。

🔑 关键知识点

  1. 线程属性的设置 (分离属性):
    • 默认情况下,线程结束后需要其他线程调用 pthread_join 来回收资源,否则会变成“僵尸线程”。
    • 解决方法:设置线程属性为 PTHREAD_CREATE_DETACHED (分离状态)。这样线程结束后,系统会自动回收资源,无需 join。
  2. 参数传递的安全性:
    • pthread_create 的第 4 个参数 void *arg 传递给线程函数。
    • 陷阱:如果在主线程中直接传 &conn (局部变量的地址),由于主线程循环很快,conn 的值可能在子线程还没用到时就被下一次 accept 修改了。
    • 解决:
      • 方法 A:malloc 一块内存存 conn,传给线程,线程用完 free。
      • 方法 B (代码中使用):使用信号量 (sem_t) 进行同步,确保主线程在子线程保存好 conn 之前,不进行下一次循环修改 conn。
  3. 互斥与同步 (信号量):
    • 代码中使用 sem_wait 和 sem_post 来保证 conn 变量在多线程间传递时的安全性。

💻 代码逻辑梳理

A. 线程函数 (Worker Thread)
// 全局信号量,用于同步 conn 的传递
sem_t sem_arg; 

void *th(void *arg)
 {
    // 1. 获取传入的 conn 指针,并解引用拿到值
    // 注意:这里强转为 int* 是因为传入的是 &conn
    int conn = *(int*)arg; 
    
    // 2. 通知主线程:我已经拿到 conn 了,你可以继续循环了
    sem_post(&sem_arg); 

    // 3. 设置为分离线程 (可选,也可以在 create 时设置属性)
    pthread_detach(pthread_self());

    // 4. 业务处理循环
    while (1) 
{
        char buf[512] = {0};
        int rd_ret = recv(conn, buf, sizeof(buf), 0);
        
        if (rd_ret <= 0) 
{
            printf("cli offline\n");
            close(conn);
            break; // 跳出循环,线程函数结束,线程自动销毁
        }

        // 处理数据...
        time_t tm; time(&tm);
        sprintf(buf, "%s %s", buf, ctime(&tm));
        send(conn, buf, strlen(buf), 0);
    }
    return NULL;
}
B. 主流程 (Main Loop)
int main() 
{
    // 1. 初始化信号量 (初始值为 0)
    // 意味着 sem_wait 会阻塞,直到 sem_post 被调用
    sem_init(&sem_arg, 0, 0);

    // 2. 创建监听套接字 (socket, bind, listen) ... [省略]

    while (1)
 {
        int conn = accept(listfd, ...);
        if (conn == -1) { perror("accept"); continue; }

        pthread_t tid;
        
        // 3. 【核心】创建线程
        // 第 4 个参数传入 &conn (conn 变量的地址)
        pthread_create(&tid, NULL, th, &conn);

        // 4. 【同步】等待线程拿走 conn
        // 如果线程还没执行到 sem_post,这里主线程会阻塞
        // 防止主线程立刻进入下一次循环,修改了 conn 的值,导致线程拿到错误的 fd
        sem_wait(&sem_arg); 
        
        // 此时线程已经安全保存了 conn 的值,主线程可以继续 accept 下一个
    }
    
    sem_destroy(&sem_arg);
    close(listfd);
    return 0;
}

4. 两种模型对比总结

特性多进程模型 (Process)多线程模型 (Thread)
资源开销大。每个进程有独立的内存空间、页表等。小。线程共享进程内存,只有栈和寄存器独立。
切换速度慢。涉及上下文切换,TLB 刷新等。快。只需切换栈和寄存器。
数据共享难。需要使用 IPC (管道、共享内存、消息队列)。易。直接读写全局变量即可 (但需加锁)。
稳定性高。一个进程崩溃不影响其他进程。低。一个线程崩溃 (如段错误) 可能导致整个进程挂掉。
适用场景对稳定性要求极高,或需要利用多核 CPU 隔离任务。高并发、IO 密集型任务 (如 Web 服务器、聊天室)。
代码关键点fork(), wait(), SIGCHLD, 关闭多余 fdpthread_create, sem_t, 线程分离, 参数传递安全

常见坑点

  1. 多进程中:为什么子进程要 close(listfd)?
    • 答:如果不关闭,所有子进程都持有一份监听套接字的副本。当主进程想关闭服务器时,端口可能因为还有子进程占用而无法释放。此外,这也浪费了文件描述符资源。
  2. 多线程中:为什么不能直接传 conn 而要传 &conn 还要加信号量?
    • 答:pthread_create 是异步的。如果直接传 conn 的值(通过 malloc),没问题。但如果传主线程栈上的 &conn,主线程循环极快,可能在子线程还没来得及读取 *arg 时,conn 的值就已经变成下一个客户的 fd 了。加信号量是为了强制主线程“等一等”,确保数据同步。
  3. 僵尸进程/线程:
    • 进程靠 signal(SIGCHLD, handler) + wait() 解决。
    • 线程靠 pthread_detach() 解决。
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐