7、多线程并发服务器:使用pthread_create()创建线程处理多客户端连接

说实话,多客户端并发处理这块,是很多C语言网络编程新手的第一道坎。我记得自己刚入行那会儿,写了个单线程的echo服务器,结果一上线就被三个客户端同时连接给搞崩了。嗯,从那以后我就明白了——单线程服务器,只配活在教科书里

今天咱们聊聊怎么用pthread_create()搞一个真正能扛得住多客户端连接的并发服务器。说白了,就是来一个客户端,我们就创建一个线程去伺候它。

7.1 为什么需要多线程?

你想想看,一个单线程的服务器,accept()完一个连接后,如果去处理recv()、send(),那其他客户端就只能排队等着。这在生产环境里根本没法用。

我个人的习惯是:主线程只负责accept(),业务逻辑全扔给子线程。这样主线程永远不阻塞,新连接来了就能立刻响应。

核心思想: 主线程 = 接待员,子线程 = 服务员。接待员只管领位,服务员负责点菜上菜。

7.2 多线程服务器的基本架构

咱们先画个图,把整体逻辑理清楚。我每次写这种代码前,都会先在脑子里过一遍这个流程。

多线程并发服务器架构图 主线程(接待员) 职责: 1. socket() → bind() → listen() 2. 循环 accept() 等待新连接 3. 收到连接 → 创建子线程 子线程 1(服务员) 职责: 1. recv() 接收客户端数据 2. 处理业务逻辑 3. send() 返回结果 子线程 2(服务员) 职责: 1. recv() 接收客户端数据 2. 处理业务逻辑 3. send() 返回结果 子线程 3(服务员) 职责: 1. recv() 接收客户端数据 2. 处理业务逻辑 3. send() 返回结果 客户端A连接 客户端B连接 客户端C连接

7.3 核心代码实现

好了,图看完了,咱们直接上代码。这是我在项目中反复打磨过的一个模板,你直接拿去改改就能用。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <pthread.h>

#define PORT 8888
#define BUFFER_SIZE 1024

// 客户端信息结构体
typedef struct {
    int client_fd;
    struct sockaddr_in client_addr;
} client_info_t;

// 子线程处理函数
void* handle_client(void* arg) {
    client_info_t* info = (client_info_t*)arg;
    char buffer[BUFFER_SIZE];
    char client_ip[INET_ADDRSTRLEN];

    // 获取客户端IP和端口
    inet_ntop(AF_INET, &info->client_addr.sin_addr, client_ip, sizeof(client_ip));
    printf("[线程 %lu] 新客户端连接: %s:%d\n",
           pthread_self(), client_ip, ntohs(info->client_addr.sin_port));

    // 循环接收数据
    while (1) {
        memset(buffer, 0, BUFFER_SIZE);
        int recv_len = recv(info->client_fd, buffer, BUFFER_SIZE - 1, 0);

        if (recv_len <= 0) {
            // 客户端断开连接
            printf("[线程 %lu] 客户端 %s 已断开\n", pthread_self(), client_ip);
            break;
        }

        printf("[线程 %lu] 收到: %s", pthread_self(), buffer);

        // 回显数据(实际项目这里替换成你的业务逻辑)
        send(info->client_fd, buffer, strlen(buffer), 0);
    }

    // 关闭连接,释放资源
    close(info->client_fd);
    free(info);
    printf("[线程 %lu] 线程退出\n", pthread_self());
    return NULL;
}

int main() {
    int server_fd;
    struct sockaddr_in server_addr;

    // 1. 创建socket
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
        perror("socket 创建失败");
        exit(EXIT_FAILURE);
    }

    // 2. 设置地址复用(防止"Address already in use")
    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    // 3. 绑定地址
    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(PORT);

    if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind 失败");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 4. 监听
    if (listen(server_fd, 10) < 0) {
        perror("listen 失败");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    printf("服务器启动,监听端口: %d\n", PORT);

    // 5. 主循环:accept + 创建线程
    while (1) {
        struct sockaddr_in client_addr;
        socklen_t addr_len = sizeof(client_addr);

        // 接受客户端连接
        int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &addr_len);
        if (client_fd < 0) {
            perror("accept 失败");
            continue;
        }

        // 封装客户端信息
        client_info_t* info = (client_info_t*)malloc(sizeof(client_info_t));
        if (!info) {
            perror("malloc 失败");
            close(client_fd);
            continue;
        }
        info->client_fd = client_fd;
        info->client_addr = client_addr;

        // 创建子线程处理
        pthread_t tid;
        if (pthread_create(&tid, NULL, handle_client, info) != 0) {
            perror("pthread_create 失败");
            free(info);
            close(client_fd);
            continue;
        }

        // 分离线程(线程结束后自动回收资源)
        pthread_detach(tid);

        printf("主线程: 已创建线程 %lu 处理新客户端\n", tid);
    }

    close(server_fd);
    return 0;
}

7.4 关键点解析

代码看完了,咱们聊聊几个容易踩坑的地方。我曾经在这些问题上浪费过整整一个下午。

7.4.1 为什么用 malloc 传递参数?

你想想看,如果我在主线程里定义一个局部变量 client_info_t info,然后传给子线程。主线程继续循环,下一次accept()就会覆盖这个变量。子线程拿到的是脏数据。

所以我的做法是:每次accept()后,malloc一块新内存,把数据拷贝进去。子线程用完后再free掉。这样主线程和子线程之间就没有数据竞争了。

小技巧: 如果你不想用malloc,也可以用pthread_create的第四个参数传一个整型(比如文件描述符),但要注意类型转换。我个人还是推荐malloc,结构清晰,不容易出错。

7.4.2 pthread_detach() 的作用

这里有个细节:我调用了 pthread_detach(tid)。为什么要这么做?

默认情况下,线程退出后它的资源不会被自动回收,需要其他线程调用 pthread_join() 来回收。但我们的服务器场景里,主线程不可能去等每个子线程结束——那不就又变成串行了吗?

所以,detach的意思就是:告诉系统,这个线程结束后你自动帮我清理。这样主线程就彻底解放了。

注意: 一旦detach,就不能再pthread_join了。如果你需要获取线程的返回值,就不要detach,而是把线程ID存起来,在合适的时机join。

7.4.3 线程安全与资源管理

多线程编程最头疼的就是资源竞争。在这个例子里,每个子线程操作的是自己独有的client_fd,所以不存在竞争。但如果你有全局变量(比如统计在线人数),那就得加锁了。

我曾经在一个项目里,多个线程同时写日志文件,结果日志全混在一起了。后来加了个互斥锁才解决。

资源类型 是否需要同步 建议方案
client_fd(每个线程独有) 不需要 直接使用
全局计数器(如在线人数) 需要 pthread_mutex_t 加锁
共享数据结构(如任务队列) 需要 互斥锁 + 条件变量
日志文件 需要 建议用独立日志线程

7.5 编译与运行

代码写好了,怎么编译?记住要链接pthread库:

gcc -o multi_thread_server multi_thread_server.c -lpthread

然后启动服务器:

./multi_thread_server

用telnet或者nc测试:

# 开三个终端分别执行
telnet 127.0.0.1 8888
nc 127.0.0.1 8888

你会看到每个客户端连接都被分配了一个独立的线程ID,互不干扰。

7.6 性能与局限性

说实话,这种"来一个客户端创建一个线程"的模式,在小规模场景下非常好用。但如果你要处理成千上万的并发连接,就得考虑线程创建的开销了。

我个人的经验是:几百个并发以内,这种模式完全够用。超过这个量级,建议用线程池或者epoll + 非阻塞IO。

一句话总结: pthread_create() 多线程服务器,是理解并发编程的绝佳起点。它简单、直观、够用。等你真正遇到性能瓶颈了,再去研究更高级的方案也不迟。

好了,这一章的内容就到这里。代码你拿去跑一跑,有问题随时调试。记住,多线程编程的核心就四个字:资源隔离。每个线程管好自己的那一亩三分地,世界就太平了。