欢迎光临
我们一直在努力

IO、进线程、socket编程(详细解读)

引言

摘要:本文是一份面向已掌握 C 语言基础、希望入门 Linux 系统编程的开发者的完整学习笔记,覆盖四大核心主题:文件 I/O(open/read/write/close 等系统调用与「一切皆文件」哲学)、进程与线程(fork/exec/wait 创建回收进程,pthread 创建线程,并用互斥锁、条件变量解决并发同步)、Socket 网络编程(TCP 服务器与客户端的完整编程流程,以及用 fork/pthread 实现多客户端并发处理)、Makefile 编写(变量、模式规则、自动依赖生成与伪目标 clean)。全文配有大量可直接编译运行的 C 示例代码、TCP 编程流程 ASCII 图、线程池实现与并发服务器对比,并给出学习路径、调试工具(man/strace/gdb)与经典参考书籍,帮助读者按「文件 I/O → 进程 → 线程 → Socket → Makefile」的顺序循序渐进地跑通每个示例。

本文面向已经掌握 C 语言基础、希望入门 Linux 系统编程的开发者。如果你熟悉指针、结构体、函数指针等 C 语言核心概念,但对「程序如何与操作系统打交道」还比较陌生,那么这份笔记正是为你准备的。

通过本文,你将掌握 Linux 系统编程的四大核心技能:

  • 文件 I/O:理解「一切皆文件」的设计哲学,熟练使用 open/read/write/close 等系统调用读写文件。
  • 进程与线程:掌握 fork/exec/wait 创建和回收进程,用 pthread 创建线程,并用互斥锁、条件变量解决并发同步问题。
  • Socket 网络编程:掌握 TCP 服务器与客户端的完整编程流程,理解 socket -> bind -> listen -> accept -> recv/send -> close 与 socket -> connect -> send/recv -> close 的每一步,并能用 fork 或 pthread 实现多客户端并发处理。
  • Makefile 编写:学会用 Makefile 描述编译规则,避免重复输入 gcc 命令,掌握变量、模式规则、自动依赖生成等进阶用法。

建议的学习路径:按章节顺序依次阅读——先打好文件 I/O 的基础,再理解进程与线程的并发模型,接着进入 Socket 网络编程,最后用 Makefile 把前面所有示例统一管理起来。每读完一个章节,请务必动手编译并运行其中的每个示例,在终端里亲眼看到输出结果,再进入下一节。编程是实践的艺术,只有亲手跑通代码,才能真正内化这些系统调用的用法与边界条件。

下面是一份 Linux 系统编程入门笔记,覆盖四个核心主题:文件 I/O、进程/线程创建、Socket 网络编程、Makefile 编写。默认环境为 Linux + gcc + make。


目录

  • 一、Linux 文件 I/O
  • 二、进程与线程创建
  • 三、网络 Socket 编程
  • 四、Makefile 编写

一、Linux 文件 IO
Linux 中“一切皆文件”,文件 I/O 主要使用系统调用::
函数作用
open打开/创建文件
read读文件
write写文件
close关闭文件
lseek移动文件偏移
stat/fstat获取文件信息
文件描述符:0 标准输入,1 标准输出,2 标准错误。
示例:复制文件 copy.c
c
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main(int argc, char *argv[]) {
if (argc != 3) {
fprintf(stderr, “Usage: %s \\n”, argv[0]);
return 1;
}

int in = open(argv[1], O_RDONLY);
if (in < 0) {
perror("open src");
return 1;
}

int out = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (out < 0) {
perror("open dst");
close(in);
return 1;
}

char buf[4096];
ssize_t n;

while ((n = read(in, buf, sizeof(buf))) > 0) {
ssize_t off = 0;
while (off < n) {
ssize_t w = write(out, buf + off, n – off);
if (w < 0) {
perror("write");
close(in);
close(out);
return 1;
}
off += w;
}
}

if (n < 0) {
perror("read");
}

close(in);
close(out);
return 0;

}
编译运行:
bash
gcc -Wall -g -o copy copy.c
./copy a.txt b.txt
注意:read/write 可能返回少于请求的字节数,所以写操作通常要循环写。


二、进程与线程创建

  • 进程:fork + exec + wait
    •fork():创建子进程,父进程返回子进程 PID,子进程返回 0。
    •exec*():用新程序替换当前进程映像。
    •wait/waitpid:回收子进程,避免僵尸进程。
    示例:fork_demo.c
    c
    #include <stdio.h>
    #include <unistd.h>
    #include <sys/wait.h>
    #include <stdlib.h>
  • int main() {
    pid_t pid = fork();

    if (pid < 0) {
    perror("fork");
    return 1;
    }

    if (pid == 0) {
    // 子进程
    printf("child pid=%d, parent=%d\\n", getpid(), getppid());
    execlp("ls", "ls", "-l", NULL);
    perror("execlp");
    _exit(127);
    }

    // 父进程
    int status;
    waitpid(pid, &status, 0);

    if (WIFEXITED(status)) {
    printf("child exited with %d\\n", WEXITSTATUS(status));
    }

    return 0;

    }
    编译:
    bash
    gcc -Wall -g -o fork_demo fork_demo.c
    2. 线程:pthread
    线程共享同一进程地址空间,创建开销小于进程。
    函数作用
    pthread_create创建线程
    pthread_join等待线程结束
    pthread_mutex_lock/unlock互斥锁
    pthread_exit退出线程
    示例:thread_demo.c
    c
    #include <pthread.h>
    #include <stdio.h>
    #include <unistd.h>

    void *worker(void *arg) {
    int id = *(int *)arg;
    printf(“thread %d, tid=%lu\\n”, id, (unsigned long)pthread_self());
    return NULL;
    }

    int main() {
    pthread_t t[3];
    int ids[3];

    for (int i = 0; i < 3; i++) {
    ids[i] = i;
    if (pthread_create(&t[i], NULL, worker, &ids[i]) != 0) {
    perror("pthread_create");
    return 1;
    }
    }

    for (int i = 0; i < 3; i++) {
    pthread_join(t[i], NULL);
    }

    return 0;

    }
    编译时必须加 -pthread:
    bash

    示例:使用互斥锁保护共享计数器 mutex_demo.c
    c
    #include <pthread.h>
    #include <stdio.h>
    #include <stdlib.h>

    #define THREAD_NUM 5
    #define LOOP_NUM 100000

    static int counter = 0;
    static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

    void *worker(void *arg) {
    (void)arg;
    for (int i = 0; i < LOOP_NUM; i++) {
    pthread_mutex_lock(&lock); // 加锁,进入临界区
    counter++; // 共享变量,必须互斥访问
    pthread_mutex_unlock(&lock); // 解锁,离开临界区
    }
    return NULL;
    }

    int main() {
    pthread_t t[THREAD_NUM];

    for (int i = 0; i < THREAD_NUM; i++) {
    if (pthread_create(&t[i], NULL, worker, NULL) != 0) {
    perror("pthread_create");
    return 1;
    }
    }

    for (int i = 0; i < THREAD_NUM; i++) {
    pthread_join(t[i], NULL);
    }

    printf("counter = %d (expected %d)\\n", counter, THREAD_NUM * LOOP_NUM);
    return 0;

    }
    编译运行:
    bash
    gcc -Wall -g -pthread -o mutex_demo mutex_demo.c
    ./mutex_demo
    输出:
    text
    counter = 500000 (expected 500000)
    说明:若去掉加锁/解锁,counter 最终结果往往小于 500000,因为多个线程同时执行 counter++ 会产生竞态条件(read-modify-write 不是原子操作)。

    互斥锁使用场景:
    •多个线程同时读写同一个共享变量(如计数器、缓存、队列)。
    •多个线程同时操作同一个文件描述符或全局数据结构。
    •需要保证“检查-修改-写入”这类复合操作不被并发打断。

    注意事项:
    •加锁后必须解锁:每个 pthread_mutex_lock 都要有对应的 pthread_mutex_unlock,否则其他线程会永久阻塞。
    •避免死锁:不要在一个线程里同时持有多个锁时以相反顺序加锁;可用 pthread_mutex_trylock 或统一加锁顺序来规避。
    •锁粒度控制:临界区尽量短,只保护真正需要互斥的代码,不要在锁内做耗时的 IO 或 sleep,否则会降低并发度。
    •初始化与销毁:可用 PTHREAD_MUTEX_INITIALIZER 静态初始化,或用 pthread_mutex_init 动态初始化;不再使用时调用 pthread_mutex_destroy 释放资源。

    示例:基于 pthread 的线程池 thread_pool.c
    线程池预先创建一组工作线程,通过任务队列接收任务,配合条件变量实现「有任务就唤醒、无任务就休眠」,避免频繁创建/销毁线程的开销。

    c
    #include <pthread.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>

    #define THREAD_NUM 4
    #define TASK_NUM 10

    // 任务节点
    typedef struct Task {
    void (*func)(void *arg);
    void *arg;
    struct Task *next;
    } Task;

    // 线程池
    typedef struct {
    Task *head; // 任务队列头
    Task *tail; // 任务队列尾
    int shutdown; // 是否关闭线程池
    pthread_mutex_t lock; // 保护任务队列的互斥锁
    pthread_cond_t cond; // 条件变量:队列非空时唤醒工作线程
    pthread_t threads[THREAD_NUM];
    } ThreadPool;

    // 简单任务:打印线程 id 和任务编号
    void do_work(void *arg) {
    int id = *(int *)arg;
    printf(“task %d handled by thread %lu\\n”, id, (unsigned long)pthread_self());
    usleep(100000); // 模拟耗时操作
    }

    // 工作线程主函数
    void *worker(void *arg) {
    ThreadPool *pool = (ThreadPool *)arg;

    while (1) {
    pthread_mutex_lock(&pool->lock);

    // 队列为空且未关闭时,等待条件变量
    while (pool->head == NULL && !pool->shutdown) {
    pthread_cond_wait(&pool->cond, &pool->lock);
    }

    // 关闭且队列为空,退出线程
    if (pool->shutdown && pool->head == NULL) {
    pthread_mutex_unlock(&pool->lock);
    break;
    }

    // 取出队首任务
    Task *task = pool->head;
    pool->head = task->next;
    if (pool->head == NULL) {
    pool->tail = NULL;
    }

    pthread_mutex_unlock(&pool->lock);

    // 执行任务(在锁外执行,避免长时间持锁)
    task->func(task->arg);
    free(task);
    }

    return NULL;

    }

    // 初始化线程池:创建 THREAD_NUM 个工作线程
    void pool_init(ThreadPool *pool) {
    pool->head = NULL;
    pool->tail = NULL;
    pool->shutdown = 0;

    pthread_mutex_init(&pool->lock, NULL);
    pthread_cond_init(&pool->cond, NULL);

    for (int i = 0; i < THREAD_NUM; i++) {
    pthread_create(&pool->threads[i], NULL, worker, pool);
    }

    }

    // 提交任务:追加到队列尾部并唤醒一个工作线程
    void pool_submit(ThreadPool *pool, void (*func)(void *), void *arg) {
    Task *task = (Task *)malloc(sizeof(Task));
    task->func = func;
    task->arg = arg;
    task->next = NULL;

    pthread_mutex_lock(&pool->lock);

    if (pool->tail == NULL) {
    pool->head = task;
    pool->tail = task;
    } else {
    pool->tail->next = task;
    pool->tail = task;
    }

    pthread_cond_signal(&pool->cond); // 唤醒一个等待中的工作线程
    pthread_mutex_unlock(&pool->lock);

    }

    // 销毁线程池:置 shutdown 标志,广播唤醒所有线程并回收
    void pool_destroy(ThreadPool *pool) {
    pthread_mutex_lock(&pool->lock);
    pool->shutdown = 1;
    pthread_cond_broadcast(&pool->cond); // 唤醒所有等待线程
    pthread_mutex_unlock(&pool->lock);

    for (int i = 0; i < THREAD_NUM; i++) {
    pthread_join(pool->threads[i], NULL);
    }

    pthread_mutex_destroy(&pool->lock);
    pthread_cond_destroy(&pool->cond);

    }

    int main() {
    ThreadPool pool;
    pool_init(&pool);

    int ids[TASK_NUM];
    for (int i = 0; i < TASK_NUM; i++) {
    ids[i] = i;
    pool_submit(&pool, do_work, &ids[i]);
    }

    pool_destroy(&pool);
    printf("all tasks done\\n");
    return 0;

    }
    编译运行:
    bash
    gcc -Wall -g -pthread -o thread_pool thread_pool.c
    ./thread_pool
    输出示例:
    text
    task 0 handled by thread 139912345678912
    task 1 handled by thread 139912345678913
    task 2 handled by thread 139912345678914
    task 3 handled by thread 139912345678915
    task 4 handled by thread 139912345678912
    task 5 handled by thread 139912345678913
    …
    all tasks done
    说明:tid 具体数值因系统而异,关键是 4 个工作线程并发消费 10 个任务,每个任务只被处理一次。

    线程池要点:
    •任务队列 + 互斥锁:所有对队列的读写(提交、取出)都要在 lock 保护下进行,避免竞态条件。
    •条件变量:队列为空时工作线程 pthread_cond_wait 休眠并释放锁;提交任务后 pthread_cond_signal 唤醒一个线程。用 while 而非 if 检查条件,防止「虚假唤醒」。
    •任务在锁外执行:取出任务后先解锁再执行,避免长时间持锁阻塞其他线程提交任务。
    •关闭流程:置 shutdown 标志后用 pthread_cond_broadcast 唤醒所有线程,线程发现队列为空且已关闭即退出,最后 join 回收。

    示例:使用条件变量实现线程池 thread_pool_cond.c
    下面是一个更完整的线程池实现,重点展示条件变量在「任务队列为空时让工作线程休眠、有新任务时唤醒」这一协作机制中的核心作用。代码包含任务队列、互斥锁、条件变量、线程创建与销毁,并对关键步骤加了注释。

    c
    #include <pthread.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>

    #define THREAD_NUM 4
    #define TASK_NUM 10

    // 任务节点:保存要执行的函数及其参数
    typedef struct Task {
    void (*func)(void *arg);
    void *arg;
    struct Task *next;
    } Task;

    // 线程池结构
    typedef struct {
    Task *head; // 任务队列头指针
    Task *tail; // 任务队列尾指针
    int shutdown; // 关闭标志:1 表示线程池正在关闭
    pthread_mutex_t lock; // 互斥锁:保护任务队列的并发访问
    pthread_cond_t cond; // 条件变量:队列非空时唤醒工作线程
    pthread_t threads[THREAD_NUM]; // 工作线程数组
    } ThreadPool;

    // 模拟任务:打印线程 id 和任务编号
    void do_work(void *arg) {
    int id = *(int *)arg;
    printf(“task %d handled by thread %lu\\n”, id, (unsigned long)pthread_self());
    usleep(100000); // 模拟耗时操作
    }

    // 工作线程主函数:循环从队列取任务执行
    void *worker(void *arg) {
    ThreadPool *pool = (ThreadPool *)arg;

    while (1) {
    // 1. 加锁:访问任务队列前必须先获取互斥锁
    pthread_mutex_lock(&pool->lock);

    // 2. 用 while 而非 if 检查条件,防止「虚假唤醒」:
    // 队列为空且未关闭时,释放锁并休眠,等待条件变量被 signal/broadcast
    while (pool->head == NULL && !pool->shutdown) {
    pthread_cond_wait(&pool->cond, &pool->lock);
    }

    // 3. 线程池已关闭且队列为空,退出线程
    if (pool->shutdown && pool->head == NULL) {
    pthread_mutex_unlock(&pool->lock);
    break;
    }

    // 4. 取出队首任务
    Task *task = pool->head;
    pool->head = task->next;
    if (pool->head == NULL) {
    pool->tail = NULL;
    }

    // 5. 解锁:取出任务后立即释放锁,避免长时间持锁阻塞其他线程
    pthread_mutex_unlock(&pool->lock);

    // 6. 在锁外执行任务,提高并发度
    task->func(task->arg);
    free(task); // 任务执行完毕,释放内存
    }

    return NULL;

    }

    // 初始化线程池:初始化锁和条件变量,创建 THREAD_NUM 个工作线程
    void pool_init(ThreadPool *pool) {
    pool->head = NULL;
    pool->tail = NULL;
    pool->shutdown = 0;

    // 初始化互斥锁和条件变量
    pthread_mutex_init(&pool->lock, NULL);
    pthread_cond_init(&pool->cond, NULL);

    // 创建固定数量的工作线程
    for (int i = 0; i < THREAD_NUM; i++) {
    pthread_create(&pool->threads[i], NULL, worker, pool);
    }

    }

    // 提交任务:追加到队列尾部,并唤醒一个等待中的工作线程
    void pool_submit(ThreadPool *pool, void (*func)(void *), void *arg) {
    // 创建任务节点
    Task *task = (Task *)malloc(sizeof(Task));
    task->func = func;
    task->arg = arg;
    task->next = NULL;

    // 加锁后修改队列
    pthread_mutex_lock(&pool->lock);

    if (pool->tail == NULL) {
    pool->head = task;
    pool->tail = task;
    } else {
    pool->tail->next = task;
    pool->tail = task;
    }

    // 唤醒一个正在 pthread_cond_wait 休眠的工作线程
    pthread_cond_signal(&pool->cond);
    pthread_mutex_unlock(&pool->lock);

    }

    // 销毁线程池:置关闭标志,广播唤醒所有线程,回收资源
    void pool_destroy(ThreadPool *pool) {
    // 加锁并置 shutdown 标志
    pthread_mutex_lock(&pool->lock);
    pool->shutdown = 1;
    // 广播唤醒所有等待中的工作线程,让它们检查 shutdown 后退出
    pthread_cond_broadcast(&pool->cond);
    pthread_mutex_unlock(&pool->lock);

    // 等待所有工作线程退出并回收
    for (int i = 0; i < THREAD_NUM; i++) {
    pthread_join(pool->threads[i], NULL);
    }

    // 销毁互斥锁和条件变量
    pthread_mutex_destroy(&pool->lock);
    pthread_cond_destroy(&pool->cond);

    }

    int main() {
    ThreadPool pool;
    pool_init(&pool);

    int ids[TASK_NUM];
    for (int i = 0; i < TASK_NUM; i++) {
    ids[i] = i;
    pool_submit(&pool, do_work, &ids[i]);
    }

    pool_destroy(&pool);
    printf("all tasks done\\n");
    return 0;

    }
    编译运行:
    bash
    gcc -Wall -g -pthread -o thread_pool_cond thread_pool_cond.c
    ./thread_pool_cond
    输出示例:
    text
    task 0 handled by thread 139912345678912
    task 1 handled by thread 139912345678913
    task 2 handled by thread 139912345678914
    task 3 handled by thread 139912345678915
    task 4 handled by thread 139912345678912
    task 5 handled by thread 139912345678913
    …
    all tasks done
    说明:tid 具体数值因系统而异,关键是 4 个工作线程并发消费 10 个任务,每个任务只被处理一次。

    条件变量协作机制说明:
    •pthread_cond_wait 会原子地释放锁并休眠,被唤醒后重新获取锁,因此必须配合互斥锁使用。
    •用 while 循环检查条件而非 if,是为了应对「虚假唤醒」:即使没有 signal,线程也可能被唤醒,需要重新检查条件。
    •pthread_cond_signal 唤醒一个线程,适合「来一个任务唤醒一个线程」;pthread_cond_broadcast 唤醒所有线程,适合关闭线程池时让所有线程退出。
    •条件变量必须在持有锁的情况下调用 signal/broadcast,否则可能丢失唤醒信号。
    •适用场景:任务数量多、单个任务执行时间短、需要限制并发数的场景(如网络请求处理、日志写入);相比每任务一线程,线程池复用线程,减少创建/销毁开销。
    •锁不是万能的:锁能保证正确性,但过度加锁会拖慢性能;读多写少场景可考虑读写锁 pthread_rwlock_t。
    gcc -Wall -g -pthread -o thread_demo thread_demo.c


    三、网络 Socket 编程
    TCP 编程流程:
    服务器:
    socket -> bind -> listen -> accept -> recv/send -> close

    服务器端流程(含错误处理):

    ┌─────────────────────────────────────────────────────────────────────┐
    │ TCP 服务器端编程流程 │
    └─────────────────────────────────────────────────────────────────────┘

    ┌──────────────┐
    │ socket() │
    │ 创建套接字 │
    └──────┬───────┘
    │
    ▼
    ┌──────────────┐ 失败(如协议不支持、内存不足)
    │ 返回值 < 0 │──────────────────────────────► perror("socket"); return 1;
    └──────┬───────┘
    │ 成功
    ▼
    ┌──────────────┐
    │ bind() │
    │ 绑定地址端口 │
    └──────┬───────┘
    │
    ▼
    ┌──────────────┐ 失败(如端口被占用、无权限绑定<1024)
    │ 返回值 < 0 │──────────────────────────────► perror("bind"); return 1;
    └──────┬───────┘
    │ 成功
    ▼
    ┌──────────────┐
    │ listen() │
    │ 进入监听状态 │
    └──────┬───────┘
    │
    ▼
    ┌──────────────┐ 失败(如套接字未绑定、fd 非法)
    │ 返回值 < 0 │──────────────────────────────► perror("listen"); return 1;
    └──────┬───────┘
    │ 成功
    ▼
    ┌──────────────┐
    │ accept() │
    │ 接受客户端连接│
    └──────┬───────┘
    │
    ▼
    ┌──────────────┐ 失败(如连接被中断、fd 非法)
    │ 返回值 < 0 │──────────────────────────────► perror("accept"); continue;
    └──────┬───────┘
    │ 成功(获得新连接 fd)
    ▼
    ┌──────────────────────────────────────────────┐
    │ recv() / send() │
    │ 循环收发数据 │
    │ recv 返回 0 → 客户端关闭连接,退出循环 │
    │ recv 返回 <0 → 出错,perror("recv") 后退出 │
    │ send 返回 <0 → 出错,perror("send") 后退出 │
    └──────────────────────┬───────────────────────┘
    │ 数据收发完毕
    ▼
    ┌──────────────┐
    │ close(fd) │
    │ 关闭连接套接字│
    └──────────────┘

    客户端流程(含错误处理):

    ┌─────────────────────────────────────────────────────────────────────┐
    │ TCP 客户端编程流程 │
    └─────────────────────────────────────────────────────────────────────┘

    ┌──────────────┐
    │ socket() │
    │ 创建套接字 │
    └──────┬───────┘
    │
    ▼
    ┌──────────────┐ 失败(如协议不支持、内存不足)
    │ 返回值 < 0 │──────────────────────────────► perror("socket"); return 1;
    └──────┬───────┘
    │ 成功
    ▼
    ┌──────────────┐
    │ connect() │
    │ 连接服务器 │
    └──────┬───────┘
    │
    ▼
    ┌──────────────┐ 失败(如服务器未启动、端口错误、网络不通)
    │ 返回值 < 0 │──────────────────────────────► perror("connect"); return 1;
    └──────┬───────┘
    │ 成功
    ▼
    ┌──────────────────────────────────────────────┐
    │ send() / recv() │
    │ 循环收发数据 │
    │ send 返回 <0 → 出错,perror("send") 后退出 │
    │ recv 返回 0 → 服务器关闭连接,退出循环 │
    │ recv 返回 <0 → 出错,perror("recv") 后退出 │
    └──────────────────────┬───────────────────────┘
    │ 数据收发完毕
    ▼
    ┌──────────────┐
    │ close(fd) │
    │ 关闭套接字 │
    └──────────────┘

    客户端:
    socket -> connect -> send/recv -> close

  • TCP 客户端 tcp_client.c
  • 客户端流程相对简单:socket 创建套接字 → connect 连接服务器 → send/recv 循环收发数据 → close 关闭连接。下面是与 tcp_server.c 配套的客户端示例。

    #include <stdio.h>
    #include <string.h>
    #include <unistd.h>
    #include <arpa/inet.h>
    #include <sys/socket.h>

    #define PORT 8080
    #define BUF_SIZE 1024

    int main(int argc, char *argv[]) {
    const char *host = argc > 1 ? argv[1] : "127.0.0.1";

    // 1. socket:创建 TCP 套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd < 0) {
    perror("socket");
    return 1;
    }

    // 2. 构造服务器地址结构
    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(PORT);

    // 把点分十进制 IP 转换为网络字节序
    if (inet_pton(AF_INET, host, &addr.sin_addr) <= 0) {
    perror("inet_pton");
    close(fd);
    return 1;
    }

    // 3. connect:连接服务器
    if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
    perror("connect");
    close(fd);
    return 1;
    }

    printf("connected to %s:%d, type 'quit' to exit\\n", host, PORT);

    // 4. send/recv:循环收发数据
    char buf[BUF_SIZE];

    while (fgets(buf, sizeof(buf), stdin)) {
    // 去掉末尾换行,便于判断 quit
    buf[strcspn(buf, "\\n")] = '\\0';

    if (strcmp(buf, "quit") == 0) {
    break;
    }

    // 发送数据(简化版,生产代码需循环发送)
    if (send(fd, buf, strlen(buf), 0) < 0) {
    perror("send");
    break;
    }

    // 接收服务器回显
    ssize_t n = recv(fd, buf, sizeof(buf), 0);
    if (n <= 0) {
    perror("recv");
    break;
    }

    buf[n] = '\\0';
    printf("echo: %s\\n", buf);
    }

    // 5. close:关闭连接
    close(fd);
    return 0;
    }

    编译运行:

    gcc -Wall -g -o tcp_client tcp_client.c
    ./tcp_client 127.0.0.1

    输出示例:

    connected to 127.0.0.1:8080, type 'quit' to exit
    hello
    echo: hello
    world
    echo: world
    quit

    流程说明:

    • socket:创建 TCP 套接字,返回文件描述符 fd。
    • connect:向服务器发起连接,失败时返回 -1(如服务器未启动、端口错误、网络不通)。
    • send/recv:send 发送用户输入,recv 接收服务器回显;recv 返回 0 表示服务器关闭连接,返回 -1 表示出错。
    • close:关闭套接字,释放文件描述符。

    注意事项:

    • 字节序:htons 把端口号转换为网络字节序,inet_pton 把 IP 字符串转换为二进制地址。
    • 循环收发:send/recv 可能只发送/接收部分数据,生产代码需要循环处理,这里为演示做了简化。
    • 与服务器联调:先启动 ./tcp_server,再运行 ./tcp_client 127.0.0.1,输入任意文本即可看到服务器回显。
  • TCP 回显服务器 tcp_server.c
    c
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <arpa/inet.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
  • #define PORT 8080
    #define BUF_SIZE 1024

    int main() {
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
    perror(“socket”);
    return 1;
    }

    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
    perror("bind");
    return 1;
    }

    if (listen(server_fd, 10) < 0) {
    perror("listen");
    return 1;
    }

    printf("listening on port %d…\\n", PORT);

    while (1) {
    struct sockaddr_in cli;
    socklen_t len = sizeof(cli);

    int fd = accept(server_fd, (struct sockaddr *)&cli, &len);
    if (fd < 0) {
    perror("accept");
    continue;
    }

    char ip[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &cli.sin_addr, ip, sizeof(ip));
    printf("client %s:%d\\n", ip, ntohs(cli.sin_port));

    // 回显循环:recv 收到数据后原样 send 回去
    char buf[BUF_SIZE];
    ssize_t n;
    while ((n = recv(fd, buf, sizeof(buf), 0)) > 0) {
    // send 可能只发送部分数据,需循环发送
    ssize_t sent = 0;
    while (sent < n) {
    ssize_t w = send(fd, buf + sent, n – sent, 0);
    if (w < 0) {
    perror("send");
    break;
    }
    sent += w;
    }
    }

    if (n < 0) {
    perror("recv");
    }

    // 客户端关闭连接或出错,关闭连接套接字
    close(fd);
    printf("client %s:%d disconnected\\n", ip, ntohs(cli.sin_port));
    }

    close(server_fd);
    return 0;

    }

    **编译运行:**

    ```bash
    gcc -Wall -g -o tcp_server tcp_server.c
    ./tcp_server

    输出示例:

    listening on port 8080…
    client 127.0.0.1:52341
    client 127.0.0.1:52342
    client 127.0.0.1:52341 disconnected

    说明:

    • accept 循环:服务器在 while (1) 中反复调用 accept,每接受一个客户端连接就进入回显循环,处理完该连接后关闭 fd,再回到 accept 等待下一个客户端。
    • recv/send 回显:recv 返回 0 表示客户端正常关闭连接,返回 -1 表示出错;收到数据后 send 原样返回,且 send 也可能只发送部分数据,因此用内层循环确保全部发完。
    • close 关闭连接:回显循环结束后调用 close(fd) 关闭连接套接字,释放该连接占用的资源;监听套接字 server_fd 在整个程序生命周期内保持打开。
    • 单线程模型:这个版本同一时刻只能服务一个客户端,处理完当前连接才会 accept 下一个;要支持多客户端并发,请参考后面的 tcp_server_fork.c 和 tcp_server_thread.c。

    cp_server tcp_server.c
    ./tcp_server

    **输出示例:**

    ```text
    listening on port 8080…
    client 127.0.0.1:52341

    流程说明:

    • accept:接受客户端连接,返回新的连接套接字 fd;失败时 perror("accept") 后 continue 继续等待下一个连接。
    • recv/send 回显:recv 循环读取客户端数据,send 原样回显;recv 返回 0 表示客户端正常关闭连接,返回 -1 表示出错(如对端异常断开收到 RST)。
    • close:数据收发完毕后 close(fd) 关闭连接套接字,释放文件描述符;主循环继续 accept 下一个客户端。

    注意事项:

    • send 也要循环:send 和 recv 一样可能只发送部分数据,生产代码应循环发送直到全部发完,或处理 EINTR 中断错误。

    • 单线程模型:该版本 accept 后立即处理当前连接的收发,处理完才 accept 下一个,同一时刻只能服务一个客户端;若某个客户端长时间不发送数据,后续客户端会被阻塞在 accept 队列里。要支持多客户端并发,可改用 fork 或 pthread 版本(见后文)。

      close(server_fd);
      return 0;
      }

    多线程并发回显服务器 tcp_server_thread.c

    上面的 tcp_server.c 是单线程模型:accept 之后在同一个循环里处理当前连接的收发,处理完一个客户端才会 accept 下一个,因此同一时刻只能服务一个客户端。下面使用 pthread 为每个客户端连接创建一个独立线程处理 recv/send,主线程继续 accept 新连接,从而支持多客户端并发。

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

    #define PORT 8080
    #define BUF_SIZE 1024

    // 线程函数:处理单个客户端的收发(回显)
    void *handle_client(void *arg) {
    int fd = *(int *)arg;
    free(arg); // 释放传入的 fd 副本

    char buf[BUF_SIZE];
    ssize_t n;

    while ((n = recv(fd, buf, sizeof(buf), 0)) > 0) {
    send(fd, buf, n, 0); // 回显
    }

    close(fd);
    return NULL;
    }

    int main() {
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
    perror("socket");
    return 1;
    }

    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
    perror("bind");
    return 1;
    }

    if (listen(server_fd, 10) < 0) {
    perror("listen");
    return 1;
    }

    printf("listening on port %d…\\n", PORT);

    while (1) {
    struct sockaddr_in cli;
    socklen_t len = sizeof(cli);

    int fd = accept(server_fd, (struct sockaddr *)&cli, &len);
    if (fd < 0) {
    perror("accept");
    continue;
    }

    char ip[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &cli.sin_addr, ip, sizeof(ip));
    printf("client %s:%d\\n", ip, ntohs(cli.sin_port));

    // 为每个连接创建一个独立线程处理收发
    pthread_t tid;
    int *pfd = (int *)malloc(sizeof(int));
    *pfd = fd;

    if (pthread_create(&tid, NULL, handle_client, pfd) != 0) {
    perror("pthread_create");
    close(fd);
    free(pfd);
    continue;
    }

    // 分离线程:线程结束后自动回收资源,无需 join
    pthread_detach(tid);
    }

    close(server_fd);
    return 0;
    }

    编译运行:

    gcc -Wall -g -pthread -o tcp_server_thread tcp_server_thread.c
    ./tcp_server_thread

    输出示例:

    listening on port 8080…
    client 127.0.0.1:52341
    client 127.0.0.1:52342

    与单线程版本的对比:

    • 并发能力提升:单线程版本 accept 后立即处理该连接的收发,处理完才 accept 下一个。若某个客户端长时间不发送数据,后续客户端会被阻塞在 accept 队列里,无法及时服务。多线程版本主线程只负责 accept,每来一个连接就创建一个线程去处理,主线程立刻回到 accept,因此多个客户端可以同时被服务,互不阻塞。
    • 需注意线程安全:线程共享同一进程的地址空间,若多个线程同时读写同一全局变量(如计数器、日志缓冲区)必须加锁保护,否则会产生竞态条件。
    • 需注意资源限制:每来一个连接就创建一个线程,线程数会随并发量增长。线程创建虽比 fork 开销小,但仍有栈空间(默认约 8MB 虚拟内存)等资源消耗,连接数极大时可能耗尽系统资源,高并发场景建议改用线程池或 I/O 多路复用(select/poll/epoll)。

    关键点说明:

    • 线程参数传递:这里把 fd 拷贝到堆上再传给线程,避免多个线程共享同一个栈变量导致数据竞争;线程函数内用完即 free。
    • 线程分离:用 pthread_detach 让线程结束后自动回收资源,避免线程资源泄漏;若用 pthread_join 则需保存每个线程 id 并逐一回收。
  • TCP 客户端 tcp_client.c
    c
    #include <stdio.h>
    #include <string.h>
    #include <unistd.h>
    #include <arpa/inet.h>
    #include <sys/socket.h>
  • #define PORT 8080
    #define BUF_SIZE 1024

    int main(int argc, char *argv[]) {
    const char *host = argc > 1 ? argv[1] : “127.0.0.1”;

    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd < 0) {
    perror("socket");
    return 1;
    }

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(PORT);

    if (inet_pton(AF_INET, host, &addr.sin_addr) <= 0) {
    perror("inet_pton");
    return 1;
    }

    if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
    perror("connect");
    return 1;
    }

    char buf[BUF_SIZE];

    while (fgets(buf, sizeof(buf), stdin)) {
    send(fd, buf, strlen(buf), 0);

    ssize_t n = recv(fd, buf, sizeof(buf), 0);
    if (n <= 0) break;

    fwrite(buf, 1, n, stdout);
    }

    close(fd);
    return 0;

    }
    编译:
    bash
    gcc -Wall -g -o tcp_server tcp_server.c
    gcc -Wall -g -o tcp_client tcp_client.c
    运行:
    bash

    Socket 编程常见错误与排查

    网络编程中,很多问题并非代码逻辑错误,而是环境、端口、连接状态等外部因素导致。下面总结最常见的几类错误及其排查方法。

    bind 失败

    bind 返回 -1 时,常见原因有两个:

    • 端口被占用:另一个进程已经绑定了同一个端口。此时 perror("bind") 会输出 Address already in use。可用 netstat -tlnp 查看端口占用情况,找到占用进程后 kill 掉,或换一个端口。
    • 无权限绑定 1024 以下端口:Linux 规定只有 root 用户才能绑定 1~1023 的端口。若用普通用户绑定 80 端口,会报 Permission denied。解决方法是改用 1024 以上的端口(如 8080),或用 sudo 运行。

    排查命令:

    netstat -tlnp | grep 8080 # 查看 8080 端口是否被占用
    lsof -i :8080 # 查看占用 8080 端口的进程

    connect 失败

    connect 返回 -1 时,常见原因:

    • 服务器未启动:客户端连接时服务器进程还没运行,会报 Connection refused。先确认服务器已启动并处于监听状态。
    • 端口错误:客户端连接的端口与服务器绑定的端口不一致,同样会 Connection refused。
    • 网络不通:跨主机连接时,IP 地址错误、防火墙拦截、路由不通都会导致 connect 超时或失败。先 ping 目标主机确认网络连通性。

    排查命令:

    ping 192.168.1.100 # 确认网络连通
    telnet 192.168.1.100 8080 # 测试端口是否可连接
    netstat -tlnp | grep 8080 # 确认服务器在监听

    recv/send 返回 0 或 -1 的含义
    • recv 返回 0:表示对端已正常关闭连接(发送了 FIN)。此时应退出收发循环并 close(fd),不要再继续 recv。
    • recv 返回 -1:表示出错。常见原因是对端异常断开(收到 RST),或 fd 已关闭。此时应 perror("recv") 查看具体错误,并关闭 fd。
    • send 返回 -1:表示发送失败,常见原因是连接已断开(对端关闭或网络异常)。同样用 perror("send") 定位。
    • send 返回小于请求长度:TCP 是字节流协议,send 可能只发送部分数据,必须循环发送直到全部发完,否则数据会丢失。
    使用 strace 和 netstat 定位问题

    strace 可以跟踪程序的所有系统调用,直接看到 socket、bind、connect、recv、send 的返回值,是定位网络问题最有力的工具:

    strace -f -e trace=network ./tcp_server
    strace -f -e trace=network ./tcp_client 127.0.0.1

    输出会显示每一步系统调用的参数和返回值,例如:

    socket(AF_INET, SOCK_STREAM, 0) = 3
    bind(3, {sa_family=AF_INET, sin_port=htons(8080), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
    listen(3, 10) = 0
    accept(3, …) = 4
    recvfrom(4, "hello", 1024, 0, …) = 5

    如果某一步返回 -1,后面会跟着 errno 和错误描述,能精确定位失败原因。

    netstat 用于查看端口监听和连接状态:

    netstat -tlnp # 查看所有监听端口及对应进程
    netstat -tnp # 查看所有 TCP 连接状态

    常见的连接状态:

    • LISTEN:服务器正在监听端口,等待连接。
    • ESTABLISHED:连接已建立,正在通信。
    • TIME_WAIT:连接已关闭,但端口仍被占用一段时间(约 2 分钟),此时若立即重启服务器可能导致 bind 失败,可用 SO_REUSEADDR 解决。

    进阶示例:支持多客户端并发处理的 TCP 回显服务器

    上面的 tcp_server.c 是单线程模型:accept 之后在同一个循环里处理当前连接的收发,处理完一个客户端才会 accept 下一个,因此同一时刻只能服务一个客户端。要支持多客户端并发,常用两种方式:fork 出子进程,或为每个连接创建 pthread 线程。下面分别给出两种实现,并对比其优缺点。

    方式一:使用 fork 实现并发服务器 tcp_server_fork.c

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

    #define PORT 8080
    #define BUF_SIZE 1024

    // 子进程处理函数:循环收发数据(回显)
    void handle_client(int fd) {
    char buf[BUF_SIZE];
    ssize_t n;

    while ((n = recv(fd, buf, sizeof(buf), 0)) > 0) {
    // 注意:send 也可能只发送部分数据,生产代码应循环发送
    ssize_t sent = 0;
    while (sent < n) {
    ssize_t w = send(fd, buf + sent, n – sent, 0);
    if (w < 0) {
    perror("send");
    break;
    }
    sent += w;
    }
    }

    if (n < 0) {
    perror("recv");
    }

    close(fd);
    _exit(0); // 子进程处理完毕,直接退出,不执行父进程的清理代码
    }

    int main() {
    // 忽略 SIGCHLD,让子进程退出后由内核自动回收,避免僵尸进程
    signal(SIGCHLD, SIG_IGN);

    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
    perror("socket");
    return 1;
    }

    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
    perror("bind");
    return 1;
    }

    if (listen(server_fd, 10) < 0) {
    perror("listen");
    return 1;
    }

    printf("listening on port %d…\\n", PORT);

    while (1) {
    struct sockaddr_in cli;
    socklen_t len = sizeof(cli);

    int fd = accept(server_fd, (struct sockaddr *)&cli, &len);
    if (fd < 0) {
    perror("accept");
    continue;
    }

    char ip[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &cli.sin_addr, ip, sizeof(ip));
    printf("client %s:%d\\n", ip, ntohs(cli.sin_port));

    pid_t pid = fork();
    if (pid < 0) {
    perror("fork");
    close(fd);
    continue;
    }

    if (pid == 0) {
    // 子进程:处理该连接,处理完退出
    close(server_fd); // 子进程不再需要监听套接字
    handle_client(fd);
    }

    // 父进程:关闭已交给子进程的连接 fd,继续 accept 下一个客户端
    close(fd);
    }

    close(server_fd);
    return 0;
    }

    编译运行:

    gcc -Wall -g -o tcp_server_fork tcp_server_fork.c
    ./tcp_server_fork

    方式二:使用 pthread 实现并发服务器 tcp_server_thread.c

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

    #define PORT 8080
    #define BUF_SIZE 1024

    // 线程函数:处理单个客户端的收发(回显)
    void *handle_client(void *arg) {
    int fd = *(int *)arg;
    free(arg); // 释放传入的 fd 副本

    char buf[BUF_SIZE];
    ssize_t n;

    while ((n = recv(fd, buf, sizeof(buf), 0)) > 0) {
    // 注意:send 也可能只发送部分数据,生产代码应循环发送
    ssize_t sent = 0;
    while (sent < n) {
    ssize_t w = send(fd, buf + sent, n – sent, 0);
    if (w < 0) {
    perror("send");
    break;
    }
    sent += w;
    }
    }

    if (n < 0) {
    perror("recv");
    }

    close(fd);
    return NULL;
    }

    int main() {
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
    perror("socket");
    return 1;
    }

    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
    perror("bind");
    return 1;
    }

    if (listen(server_fd, 10) < 0) {
    perror("listen");
    return 1;
    }

    printf("listening on port %d…\\n", PORT);

    while (1) {
    struct sockaddr_in cli;
    socklen_t len = sizeof(cli);

    int fd = accept(server_fd, (struct sockaddr *)&cli, &len);
    if (fd < 0) {
    perror("accept");
    continue;
    }

    char ip[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &cli.sin_addr, ip, sizeof(ip));
    printf("client %s:%d\\n", ip, ntohs(cli.sin_port));

    // 为每个连接创建一个独立线程处理收发
    pthread_t tid;
    int *pfd = (int *)malloc(sizeof(int));
    *pfd = fd;

    if (pthread_create(&tid, NULL, handle_client, pfd) != 0) {
    perror("pthread_create");
    close(fd);
    free(pfd);
    continue;
    }

    // 分离线程:线程结束后自动回收资源,无需 join
    pthread_detach(tid);
    }

    close(server_fd);
    return 0;
    }

    编译运行:

    gcc -Wall -g -pthread -o tcp_server_thread tcp_server_thread.c
    ./tcp_server_thread

    两种方式的优缺点对比
    对比维度fork 方式pthread 方式
    地址空间 子进程拥有独立的地址空间,数据隔离性好,互不干扰 线程共享同一进程地址空间,全局变量、堆内存、文件描述符表共享
    创建开销 fork 需要复制整个进程地址空间(写时复制),开销较大 pthread_create 只创建线程控制块并分配栈,开销远小于 fork
    数据共享 子进程间共享数据需要 mmap、管道、共享内存等 IPC 机制,较繁琐 天然共享全局变量和堆内存,通信方便,但需加锁保护避免竞态
    健壮性 某个子进程崩溃(如段错误)不影响其他子进程和父进程 某个线程崩溃可能导致整个进程崩溃,影响所有连接
    资源回收 需 wait/waitpid 回收,或 signal(SIGCHLD, SIG_IGN) 让内核自动回收 用 pthread_detach 分离线程,结束后自动回收;或 pthread_join 手动回收
    适用场景 连接数不多、需要强隔离、或子进程需要独立环境的场景 连接数较多、需要频繁创建/销毁、共享数据较多的场景

    关键注意事项:

    • 子进程要关闭 server_fd:fork 后子进程只处理自己的连接 fd,若不关闭监听套接字,会导致父进程无法正常关闭端口。
    • 父进程要关闭 fd:父进程把连接 fd 交给子进程/线程后应立即 close(fd),否则 fd 引用计数不为 0,连接不会真正关闭。
    • 线程参数要小心:把 fd 拷贝到堆上再传给线程,避免多个线程共享同一个栈变量导致数据竞争;线程函数内用完即 free。
    • 高并发场景:每连接一进程/一线程的开销会随并发量线性增长,连接数很大时建议改用线程池或 I/O 多路复用(select/poll/epoll)。

    实战示例:支持多客户端并发处理的 TCP 回显服务器

    上面的 tcp_server.c 是单线程模型:accept 之后在同一个循环里处理当前连接的收发,处理完一个客户端才会 accept 下一个,因此同一时刻只能服务一个客户端。要支持多客户端并发,常用两种方式:fork 出子进程,或为每个连接创建 pthread 线程。

    并发模型的选择依据
    • fork 模型:每个客户端连接由独立的子进程处理。子进程拥有独立的地址空间,天然隔离,互不干扰;但 fork 需要复制整个进程地址空间,创建开销较大,且进程间共享数据需要额外的 IPC 机制(如管道、共享内存)。
    • pthread 模型:每个客户端连接由独立的线程处理。线程共享同一进程的地址空间,创建开销远小于 fork,适合连接数较多的场景;但线程间共享全局变量,需要加锁保护,否则会产生竞态条件。

    选择建议:连接数不多、追求简单隔离时选 fork;连接数较多、追求性能时选 pthread。高并发场景建议进一步使用线程池或 I/O 多路复用(select/poll/epoll)。

    示例一:使用 fork 实现并发服务器 tcp_server_fork.c

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

    #define PORT 8080
    #define BUF_SIZE 1024

    // 子进程处理函数:对单个客户端连接进行回显
    void handle_client(int fd) {
    char buf[BUF_SIZE];
    ssize_t n;

    while ((n = recv(fd, buf, sizeof(buf), 0)) > 0) {
    send(fd, buf, n, 0); // 回显
    }

    close(fd);
    _exit(0); // 子进程处理完毕,立即退出
    }

    int main() {
    // 忽略 SIGCHLD,让子进程退出后由内核自动回收,避免僵尸进程
    signal(SIGCHLD, SIG_IGN);

    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
    perror("socket");
    return 1;
    }

    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
    perror("bind");
    return 1;
    }

    if (listen(server_fd, 10) < 0) {
    perror("listen");
    return 1;
    }

    printf("listening on port %d…\\n", PORT);

    while (1) {
    struct sockaddr_in cli;
    socklen_t len = sizeof(cli);

    int fd = accept(server_fd, (struct sockaddr *)&cli, &len);
    if (fd < 0) {
    perror("accept");
    continue;
    }

    pid_t pid = fork();
    if (pid < 0) {
    perror("fork");
    close(fd);
    continue;
    }

    if (pid == 0) {
    // 子进程:处理该连接,处理完退出
    close(server_fd); // 子进程不再需要监听套接字
    handle_client(fd);
    }

    // 父进程:关闭已交给子进程的连接 fd,继续 accept 下一个客户端
    close(fd);
    }

    close(server_fd);
    return 0;
    }

    编译运行:

    gcc -Wall -g -o tcp_server_fork tcp_server_fork.c
    ./tcp_server_fork

    与单线程 accept 循环的差异:

    • 单线程模型:accept 后立即处理该连接的收发,处理完才 accept 下一个。若某个客户端长时间不发送数据,后续客户端会被阻塞在 accept 队列里,无法及时服务。
    • fork 模型:父进程只负责 accept,每来一个连接就 fork 一个子进程去处理,父进程立刻回到 accept,因此多个客户端可以同时被服务,互不阻塞。

    注意事项:

    • 子进程要关闭 server_fd:子进程只处理自己的连接 fd,若不关闭监听套接字,会导致父进程无法正常关闭端口。
    • 父进程要关闭 fd:父进程把连接 fd 交给子进程后应立即 close(fd),否则 fd 引用计数不为 0,连接不会真正关闭。
    • 避免僵尸进程:子进程退出后父进程要 wait/waitpid 回收,或用 signal(SIGCHLD, SIG_IGN) 让内核自动回收。
    • fork 开销较大:每来一个连接就 fork 一次,进程数会随并发量增长,适合连接数不多的场景;高并发建议用线程池或 I/O 多路复用。
    示例二:使用 pthread 实现并发服务器 tcp_server_thread.c

    与 fork 版本不同,这里为每个 accept 返回的连接 fd 创建一个独立线程来处理收发,主循环继续 accept 新连接。线程共享同一进程地址空间,创建开销比 fork 更小。

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

    #define PORT 8080
    #define BUF_SIZE 1024

    // 线程函数:处理单个客户端的收发(回显)
    void *handle_client(void *arg) {
    int fd = *(int *)arg;
    free(arg); // 释放传入的 fd 副本

    char buf[BUF_SIZE];
    ssize_t n;

    while ((n = recv(fd, buf, sizeof(buf), 0)) > 0) {
    // 注意:send 也可能只发送部分数据,生产代码应循环发送
    ssize_t sent = 0;
    while (sent < n) {
    ssize_t w = send(fd, buf + sent, n – sent, 0);
    if (w < 0) {
    perror("send");
    break;
    }
    sent += w;
    }
    }

    if (n < 0) {
    perror("recv");
    }

    close(fd);
    return NULL;
    }

    int main() {
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
    perror("socket");
    return 1;
    }

    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
    perror("bind");
    return 1;
    }

    if (listen(server_fd, 10) < 0) {
    perror("listen");
    return 1;
    }

    printf("listening on port %d…\\n", PORT);

    while (1) {
    struct sockaddr_in cli;
    socklen_t len = sizeof(cli);

    int fd = accept(server_fd, (struct sockaddr *)&cli, &len);
    if (fd < 0) {
    perror("accept");
    continue;
    }

    char ip[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &cli.sin_addr, ip, sizeof(ip));
    printf("client %s:%d\\n", ip, ntohs(cli.sin_port));

    // 为每个连接创建一个独立线程处理收发
    pthread_t tid;
    int *pfd = (int *)malloc(sizeof(int));
    if (pfd == NULL) {
    perror("malloc");
    close(fd);
    continue;
    }
    *pfd = fd;

    if (pthread_create(&tid, NULL, handle_client, pfd) != 0) {
    perror("pthread_create");
    close(fd);
    free(pfd);
    continue;
    }

    // 分离线程:线程结束后自动回收资源,无需 join
    pthread_detach(tid);
    }

    close(server_fd);
    return 0;
    }

    编译运行:

    gcc -Wall -g -pthread -o tcp_server_thread tcp_server_thread.c
    ./tcp_server_thread

    与 fork 版本的对比:

    • 线程共享地址空间:所有线程属于同一个进程,共享全局变量、堆内存和文件描述符表,通信无需进程间通信(IPC)机制;而 fork 出的子进程拥有独立的地址空间,共享数据需要 mmap、管道等额外手段。
    • 创建开销更小:pthread_create 只创建线程控制块并分配栈,比 fork 复制整个进程地址空间快得多,适合连接数较多的场景。
    • 需注意线程安全:线程共享全局变量,若多个线程同时读写同一数据(如计数器、日志缓冲区)必须加锁保护,否则会产生竞态条件;fork 版本因地址空间隔离,天然不存在这类共享冲突。
    • 线程函数要 detach 或 join:这里用 pthread_detach 让线程结束后自动回收资源,避免线程资源泄漏;若用 join 则需保存每个线程 id 并逐一回收。
    • 传入参数要小心:这里把 fd 拷贝到堆上再传给线程,避免多个线程共享同一个栈变量导致数据竞争;线程函数内用完即 free。
    • send 也要循环:send 和 recv 一样可能只发送部分数据,生产代码应循环发送直到全部发完,或处理 EINTR 中断错误。
    与 tcp_client.c 的联调测试步骤
  • 编译服务器和客户端:
  • gcc -Wall -g -o tcp_server_fork tcp_server_fork.c
    gcc -Wall -g -o tcp_client tcp_client.c

  • 启动服务器(终端 1):
  • ./tcp_server_fork

  • 启动多个客户端(终端 2、终端 3、终端 4…):
  • ./tcp_client 127.0.0.1

  • 验证并发效果:在每个客户端中输入文本并回车,服务器会立即回显。由于每个连接由独立的子进程/线程处理,多个客户端可以同时收发数据,互不阻塞。例如在终端 2 输入 hello from client 1,在终端 3 输入 hello from client 2,两个客户端都能同时收到各自的回显。

  • 观察服务器输出:服务器会打印每个客户端的 IP 和端口,例如:

  • listening on port 8080…
    client 127.0.0.1:52341
    client 127.0.0.1:52342

  • 验证资源回收:关闭某个客户端(按 Ctrl+D 或 Ctrl+C),服务器端对应的子进程/线程会自动退出并被回收,不会产生僵尸进程或线程泄漏。可用 ps 或 top 观察进程/线程数量变化。
  • ./tcp_server

    另一个终端

    示例:基于 fork 的多客户端并发服务器 tcp_server_concurrent.c

    上面的 tcp_server.c 是单线程模型:accept 之后在同一个循环里处理当前连接的收发,处理完一个客户端才会 accept 下一个,因此同一时刻只能服务一个客户端。要支持多客户端并发,常用两种方式:fork 出子进程,或为每个连接创建 pthread 线程。下面先给出基于 fork 的版本。

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

    #define PORT 8080
    #define BUF_SIZE 1024

    // 子进程处理函数:循环收发数据(回显)
    void handle_client(int fd) {
    char buf[BUF_SIZE];
    ssize_t n;

    while ((n = recv(fd, buf, sizeof(buf), 0)) > 0) {
    // 注意:send 也可能只发送部分数据,生产代码应循环发送
    ssize_t sent = 0;
    while (sent < n) {
    ssize_t w = send(fd, buf + sent, n – sent, 0);
    if (w < 0) {
    perror("send");
    break;
    }
    sent += w;
    }
    }

    if (n < 0) {
    perror("recv");
    }

    close(fd);
    _exit(0); // 子进程处理完毕,直接退出,不执行父进程的清理代码
    }

    int main() {
    // 忽略 SIGCHLD,让子进程退出后由内核自动回收,避免僵尸进程
    signal(SIGCHLD, SIG_IGN);

    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
    perror("socket");
    return 1;
    }

    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
    perror("bind");
    return 1;
    }

    if (listen(server_fd, 10) < 0) {
    perror("listen");
    return 1;
    }

    printf("listening on port %d…\\n", PORT);

    while (1) {
    struct sockaddr_in cli;
    socklen_t len = sizeof(cli);

    int fd = accept(server_fd, (struct sockaddr *)&cli, &len);
    if (fd < 0) {
    perror("accept");
    continue;
    }

    char ip[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &cli.sin_addr, ip, sizeof(ip));
    printf("client %s:%d\\n", ip, ntohs(cli.sin_port));

    pid_t pid = fork();
    if (pid < 0) {
    perror("fork");
    close(fd);
    continue;
    }

    if (pid == 0) {
    // 子进程:处理该连接,处理完退出
    close(server_fd); // 子进程不再需要监听套接字
    handle_client(fd);
    }

    // 父进程:关闭已交给子进程的连接 fd,继续 accept 下一个客户端
    close(fd);
    }

    close(server_fd);
    return 0;
    }

    编译运行:

    gcc -Wall -g -o tcp_server_concurrent tcp_server_concurrent.c
    ./tcp_server_concurrent

    与单线程 accept 循环的差异:

    • 单线程模型:accept 后立即处理该连接的收发,处理完才 accept 下一个。若某个客户端长时间不发送数据,后续客户端会被阻塞在 accept 队列里,无法及时服务。
    • fork 模型:父进程只负责 accept,每来一个连接就 fork 一个子进程去处理,父进程立刻回到 accept,因此多个客户端可以同时被服务,互不阻塞。
    • 线程模型:与 fork 类似,但用 pthread_create 为每个连接创建线程,共享进程地址空间,创建开销更小;缺点是线程间共享全局变量,需要加锁保护。

    注意事项:

    • 子进程要关闭 server_fd:子进程只处理自己的连接 fd,若不关闭监听套接字,会导致父进程无法正常关闭端口。
    • 父进程要关闭 fd:父进程把连接 fd 交给子进程后应立即 close(fd),否则 fd 引用计数不为 0,连接不会真正关闭。
    • 避免僵尸进程:子进程退出后父进程要 wait/waitpid 回收,或用 signal(SIGCHLD, SIG_IGN) 让内核自动回收。
    • fork 开销较大:每来一个连接就 fork 一次,进程数会随并发量增长,适合连接数不多的场景;高并发建议用线程池或 I/O 多路复用(select/poll/epoll)。
  • 支持多客户端并发连接的 TCP 服务器 tcp_server_fork.c
    上面的 tcp_server.c 是单线程模型:accept 之后在同一个循环里处理当前连接的收发,处理完一个客户端才会 accept 下一个,因此同一时刻只能服务一个客户端。要支持多客户端并发,常用两种方式:fork 出子进程,或为每个连接创建 pthread 线程。
  • 示例:使用 fork 实现并发服务器
    c
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <signal.h>
    #include <sys/wait.h>
    #include <arpa/inet.h>
    #include <sys/socket.h>
    #include <netinet/in.h>

    #define PORT 8080
    #define BUF_SIZE 1024

    void handle_client(int fd) {
    char buf[BUF_SIZE];
    ssize_t n;

    while ((n = recv(fd, buf, sizeof(buf), 0)) > 0) {
    send(fd, buf, n, 0); // 回显
    }

    close(fd);
    _exit(0);

    }

    int main() {
    // 忽略 SIGCHLD,让子进程退出后由内核自动回收,避免僵尸进程
    signal(SIGCHLD, SIG_IGN);

    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
    perror("socket");
    return 1;
    }

    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
    perror("bind");
    return 1;
    }

    if (listen(server_fd, 10) < 0) {
    perror("listen");
    return 1;
    }

    printf("listening on port %d…\\n", PORT);

    while (1) {
    struct sockaddr_in cli;
    socklen_t len = sizeof(cli);

    int fd = accept(server_fd, (struct sockaddr *)&cli, &len);
    if (fd < 0) {
    perror("accept");
    continue;
    }

    pid_t pid = fork();
    if (pid < 0) {
    perror("fork");
    close(fd);
    continue;
    }

    if (pid == 0) {
    // 子进程:处理该连接,处理完退出
    close(server_fd); // 子进程不再需要监听套接字
    handle_client(fd);
    }

    // 父进程:关闭已交给子进程的连接 fd,继续 accept 下一个客户端
    close(fd);
    }

    close(server_fd);
    return 0;

    }
    编译运行:
    bash
    gcc -Wall -g -o tcp_server_fork tcp_server_fork.c
    ./tcp_server_fork

    多个终端分别运行

    ./tcp_client 127.0.0.1
    ./tcp_client 127.0.0.1

    与单线程 accept 循环的差异:
    • 单线程模型:accept 后立即处理该连接的收发,处理完才 accept 下一个。若某个客户端长时间不发送数据,后续客户端会被阻塞在 accept 队列里,无法及时服务。
    • fork 模型:父进程只负责 accept,每来一个连接就 fork 一个子进程去处理,父进程立刻回到 accept,因此多个客户端可以同时被服务,互不阻塞。
    • 线程模型:与 fork 类似,但用 pthread_create 为每个连接创建线程,共享进程地址空间,创建开销更小;缺点是线程间共享全局变量,需要加锁保护。

    注意事项:
    • 子进程要关闭 server_fd:子进程只处理自己的连接 fd,若不关闭监听套接字,会导致父进程无法正常关闭端口。
    • 父进程要关闭 fd:父进程把连接 fd 交给子进程后应立即 close(fd),否则 fd 引用计数不为 0,连接不会真正关闭。
    •避免僵尸进程:子进程退出后父进程要 wait/waitpid 回收,或用 signal(SIGCHLD, SIG_IGN) 让内核自动回收。
    •fork 开销较大:每来一个连接就 fork 一次,进程数会随并发量增长,适合连接数不多的场景;高并发建议用线程池或 I/O 多路复用(select/poll/epoll)。

    示例:使用 pthread 实现并发服务器 tcp_server_thread.c
    与 fork 版本不同,这里为每个 accept 返回的连接 fd 创建一个独立线程来处理收发,主循环继续 accept 新连接。线程共享同一进程地址空间,创建开销比 fork 更小。

    c
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <pthread.h>
    #include <arpa/inet.h>
    #include <sys/socket.h>
    #include <netinet/in.h>

    #define PORT 8080
    #define BUF_SIZE 1024

    // 线程函数:处理单个客户端的收发(回显)
    void *handle_client(void *arg) {
    int fd = *(int *)arg;
    free(arg); // 释放传入的 fd 副本

    char buf[BUF_SIZE];
    ssize_t n;

    while ((n = recv(fd, buf, sizeof(buf), 0)) > 0) {
    send(fd, buf, n, 0); // 回显
    }

    close(fd);
    return NULL;

    }

    int main() {
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
    perror(“socket”);
    return 1;
    }

    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
    perror("bind");
    return 1;
    }

    if (listen(server_fd, 10) < 0) {
    perror("listen");
    return 1;
    }

    printf("listening on port %d…\\n", PORT);

    while (1) {
    struct sockaddr_in cli;
    socklen_t len = sizeof(cli);

    int fd = accept(server_fd, (struct sockaddr *)&cli, &len);
    if (fd < 0) {
    perror("accept");
    continue;
    }

    char ip[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &cli.sin_addr, ip, sizeof(ip));
    printf("client %s:%d\\n", ip, ntohs(cli.sin_port));

    // 为每个连接创建一个独立线程处理收发
    pthread_t tid;
    int *pfd = (int *)malloc(sizeof(int));
    *pfd = fd;

    if (pthread_create(&tid, NULL, handle_client, pfd) != 0) {
    perror("pthread_create");
    close(fd);
    free(pfd);
    continue;
    }

    // 分离线程:线程结束后自动回收资源,无需 join
    pthread_detach(tid);
    }

    close(server_fd);
    return 0;

    }
    编译运行:
    bash
    gcc -Wall -g -pthread -o tcp_server_thread tcp_server_thread.c
    ./tcp_server_thread

    多个终端分别运行

    ./tcp_client 127.0.0.1
    ./tcp_client 127.0.0.1

    与 fork 版本的对比:
    •线程共享地址空间:所有线程属于同一个进程,共享全局变量、堆内存和文件描述符表,通信无需进程间通信(IPC)机制;而 fork 出的子进程拥有独立的地址空间,共享数据需要 mmap、管道等额外手段。
    •创建开销更小:pthread_create 只创建线程控制块并分配栈,比 fork 复制整个进程地址空间快得多,适合连接数较多的场景。
    •需注意线程安全:线程共享全局变量,若多个线程同时读写同一数据(如计数器、日志缓冲区)必须加锁保护,否则会产生竞态条件;fork 版本因地址空间隔离,天然不存在这类共享冲突。
    •线程函数要 detach 或 join:这里用 pthread_detach 让线程结束后自动回收资源,避免线程资源泄漏;若用 join 则需保存每个线程 ID 并逐一回收。
    •传入参数要小心:这里把 fd 拷贝到堆上再传给线程,避免多个线程共享同一个栈变量导致数据竞争;线程函数内用完即 free。

    ./tcp_client 127.0.0.1
    注意:网络编程中要处理字节序:htons/htonl/ntohs/ntohl。send/recv 也可能只发送/接收部分数据,生产代码需要循环处理。
    注意:网络编程中要处理字节序:htons/htonl/ntohs/ntohl。send/recv 也可能只发送/接收部分数据,生产代码需要循环处理。

    进阶示例:支持多客户端并发处理的 TCP 回显服务器

    上面的 tcp_server.c 是单线程模型:accept 之后在同一个循环里处理当前连接的收发,处理完一个客户端才会 accept 下一个,因此同一时刻只能服务一个客户端。要支持多客户端并发,常用两种方式:fork 出子进程,或为每个连接创建 pthread 线程。下面分别给出两种实现,并对比其优缺点。

    方式一:使用 fork 实现并发服务器 tcp_server_fork.c

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

    #define PORT 8080
    #define BUF_SIZE 1024

    // 子进程处理函数:循环收发数据(回显)
    void handle_client(int fd) {
    char buf[BUF_SIZE];
    ssize_t n;

    while ((n = recv(fd, buf, sizeof(buf), 0)) > 0) {
    // 注意:send 也可能只发送部分数据,生产代码应循环发送
    ssize_t sent = 0;
    while (sent < n) {
    ssize_t w = send(fd, buf + sent, n – sent, 0);
    if (w < 0) {
    perror("send");
    break;
    }
    sent += w;
    }
    }

    if (n < 0) {
    perror("recv");
    }

    close(fd);
    _exit(0); // 子进程处理完毕,直接退出,不执行父进程的清理代码
    }

    int main() {
    // 忽略 SIGCHLD,让子进程退出后由内核自动回收,避免僵尸进程
    signal(SIGCHLD, SIG_IGN);

    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
    perror("socket");
    return 1;
    }

    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
    perror("bind");
    return 1;
    }

    if (listen(server_fd, 10) < 0) {
    perror("listen");
    return 1;
    }

    printf("listening on port %d…\\n", PORT);

    while (1) {
    struct sockaddr_in cli;
    socklen_t len = sizeof(cli);

    int fd = accept(server_fd, (struct sockaddr *)&cli, &len);
    if (fd < 0) {
    perror("accept");
    continue;
    }

    char ip[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &cli.sin_addr, ip, sizeof(ip));
    printf("client %s:%d\\n", ip, ntohs(cli.sin_port));

    pid_t pid = fork();
    if (pid < 0) {
    perror("fork");
    close(fd);
    continue;
    }

    if (pid == 0) {
    // 子进程:处理该连接,处理完退出
    close(server_fd); // 子进程不再需要监听套接字
    handle_client(fd);
    }

    // 父进程:关闭已交给子进程的连接 fd,继续 accept 下一个客户端
    close(fd);
    }

    close(server_fd);
    return 0;
    }

    编译运行:

    gcc -Wall -g -o tcp_server_fork tcp_server_fork.c
    ./tcp_server_fork

    方式二:使用 pthread 实现并发服务器 tcp_server_thread.c

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

    #define PORT 8080
    #define BUF_SIZE 1024

    // 线程函数:处理单个客户端的收发(回显)
    void *handle_client(void *arg) {
    int fd = *(int *)arg;
    free(arg); // 释放传入的 fd 副本

    char buf[BUF_SIZE];
    ssize_t n;

    while ((n = recv(fd, buf, sizeof(buf), 0)) > 0) {
    // 注意:send 也可能只发送部分数据,生产代码应循环发送
    ssize_t sent = 0;
    while (sent < n) {
    ssize_t w = send(fd, buf + sent, n – sent, 0);
    if (w < 0) {
    perror("send");
    break;
    }
    sent += w;
    }
    }

    if (n < 0) {
    perror("recv");
    }

    close(fd);
    return NULL;
    }

    int main() {
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
    perror("socket");
    return 1;
    }

    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
    perror("bind");
    return 1;
    }

    if (listen(server_fd, 10) < 0) {
    perror("listen");
    return 1;
    }

    printf("listening on port %d…\\n", PORT);

    while (1) {
    struct sockaddr_in cli;
    socklen_t len = sizeof(cli);

    int fd = accept(server_fd, (struct sockaddr *)&cli, &len);
    if (fd < 0) {
    perror("accept");
    continue;
    }

    char ip[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &cli.sin_addr, ip, sizeof(ip));
    printf("client %s:%d\\n", ip, ntohs(cli.sin_port));

    // 为每个连接创建一个独立线程处理收发
    pthread_t tid;
    int *pfd = (int *)malloc(sizeof(int));
    *pfd = fd;

    if (pthread_create(&tid, NULL, handle_client, pfd) != 0) {
    perror("pthread_create");
    close(fd);
    free(pfd);
    continue;
    }

    // 分离线程:线程结束后自动回收资源,无需 join
    pthread_detach(tid);
    }

    close(server_fd);
    return 0;
    }

    编译运行:

    gcc -Wall -g -pthread -o tcp_server_thread tcp_server_thread.c
    ./tcp_server_thread

    两种方式的优缺点对比
    对比维度fork 方式pthread 方式
    地址空间 子进程拥有独立的地址空间,数据隔离性好,互不干扰 线程共享同一进程地址空间,全局变量、堆内存、文件描述符表共享
    创建开销 fork 需要复制整个进程地址空间(写时复制),开销较大 pthread_create 只创建线程控制块并分配栈,开销远小于 fork
    数据共享 子进程间共享数据需要 mmap、管道、共享内存等 IPC 机制,较繁琐 天然共享全局变量和堆内存,通信方便,但需加锁保护避免竞态
    健壮性 某个子进程崩溃(如段错误)不影响其他子进程和父进程 某个线程崩溃可能导致整个进程崩溃,影响所有连接
    资源回收 需 wait/waitpid 回收,或 signal(SIGCHLD, SIG_IGN) 让内核自动回收 用 pthread_detach 分离线程,结束后自动回收;或 pthread_join 手动回收
    适用场景 连接数不多、需要强隔离、或子进程需要独立环境的场景 连接数较多、需要频繁创建/销毁、共享数据较多的场景

    关键注意事项:

    • 子进程要关闭 server_fd:fork 后子进程只处理自己的连接 fd,若不关闭监听套接字,会导致父进程无法正常关闭端口。
    • 父进程要关闭 fd:父进程把连接 fd 交给子进程/线程后应立即 close(fd),否则 fd 引用计数不为 0,连接不会真正关闭。
    • 线程参数要小心:把 fd 拷贝到堆上再传给线程,避免多个线程共享同一个栈变量导致数据竞争;线程函数内用完即 free。
    • 高并发场景:每连接一进程/一线程的开销会随并发量线性增长,连接数很大时建议改用线程池或 I/O 多路复用(select/poll/epoll)。

    四、Makefile 编写
    Makefile 用来描述编译规则,避免重复输入 gcc 命令。
    示例 Makefile
    makefile
    CC := gcc
    CFLAGS := -Wall -Wextra -g
    TARGETS := copy fork_demo thread_demo tcp_server tcp_client

    all: $(TARGETS)

    四、Makefile 编写

    Makefile 用来描述编译规则,避免重复输入 gcc 命令。

    示例:支持多文件编译的 Makefile

    大型项目通常由多个源文件组成,需要分别编译成目标文件再链接。下面是一个支持多文件编译的 Makefile 示例,包含变量定义、自动依赖生成和伪目标 clean。

    # 变量定义
    CC := gcc
    CFLAGS := -Wall -Wextra -g
    LDFLAGS :=
    TARGET := app
    SRCS := main.c util.c net.c
    OBJS := $(SRCS:.c=.o) # main.o util.o net.o
    DEPS := $(OBJS:.o=.d) # 自动生成的依赖文件

    all: $(TARGET)

    # 链接:由所有目标文件生成可执行文件
    $(TARGET): $(OBJS)
    $(CC) $(LDFLAGS) $^ -o $@

    # 编译:由 .c 生成 .o,并生成依赖文件
    %.o: %.c
    $(CC) $(CFLAGS) -MMD -MP -c $< -o $@

    # 包含自动生成的依赖文件(若存在)
    -include $(DEPS)

    clean:
    rm -f $(TARGET) $(OBJS) $(DEPS)

    .PHONY: all clean

    语法要点:

    • 变量:CC、CFLAGS 等用 := 定义,$(CC) 引用变量值。SRCS 列出所有源文件,OBJS := $(SRCS:.c=.o) 用替换引用把 .c 后缀换成 .o,避免逐个手写目标文件。
    • 自动依赖生成 -MMD -MP:-MMD 让 gcc 在编译时自动生成 .d 依赖文件,记录该 .c 包含的头文件;-MP 为每个头文件生成一个空规则,避免头文件被删除后 make 报错。-include $(DEPS) 把这些依赖文件包含进来,头文件一旦变化,make 会自动重编相关目标。
    • 伪目标 clean:clean 不是真实文件,用 .PHONY 声明后,即使目录下存在同名文件,make clean 也会执行删除命令。
    • 自动变量:$@ 表示目标名,$< 表示第一个依赖,$^ 表示所有依赖。链接时用 $^ 把全部 .o 传给 gcc。
    • 模式规则:%.o: %.c 是一条模式规则,匹配任意 .c 到 .o 的编译,无需为每个文件单独写规则。

    示例:使用 pthread 的 Makefile

    线程程序编译时必须加 -pthread 标志,否则链接阶段会报 undefined reference to pthread_create。下面是一个同时编译普通程序和线程程序的 Makefile:

    CC := gcc
    CFLAGS := -Wall -Wextra -g
    TARGETS := copy tcp_server tcp_client mutex_demo thread_pool

    all: $(TARGETS)

    # 通用规则:由 .c 生成同名可执行文件
    %: %.c
    $(CC) $(CFLAGS) $< -o $@

    # 线程程序需要额外链接 pthread
    mutex_demo: mutex_demo.c
    $(CC) $(CFLAGS) -pthread $< -o $@

    thread_pool: thread_pool.c
    $(CC) $(CFLAGS) -pthread $< -o $@

    clean:
    rm -f $(TARGETS)

    .PHONY: all clean

    要点:

    • 普通程序(如 copy、tcp_server)走通用规则 %: %.c,不需要 -pthread。
    • 线程程序(如 mutex_demo、thread_pool)单独写规则,在 $(CFLAGS) 后追加 -pthread。-pthread 同时影响预处理和链接:编译时定义 _REENTRANT 等宏,链接时自动链接 libpthread。
    • 若漏加 -pthread,编译可能通过,但链接时报错:undefined reference to 'pthread_create'。

    使用:

    make # 编译所有目标
    make mutex_demo # 只编译 mutex_demo
    make clean # 清理

    示例:统一管理所有示例的 Makefile

    下面是一个完整的 Makefile,用于统一编译管理本文中出现的所有示例程序:copy、fork_demo、thread_demo、mutex_demo、thread_pool、tcp_server、tcp_client。它综合运用了变量定义、模式规则、自动依赖生成(-MMD -MP)和 clean 伪目标。

    # ============ 变量定义 ============
    CC := gcc
    CFLAGS := -Wall -Wextra -g
    LDFLAGS :=

    # 普通程序(无需额外链接库)
    COMMON_TARGETS := copy fork_demo tcp_server tcp_client

    # 线程程序(需要链接 pthread)
    THREAD_TARGETS := thread_demo mutex_demo thread_pool

    # 全部目标
    TARGETS := $(COMMON_TARGETS) $(THREAD_TARGETS)

    # 源文件与目标文件、依赖文件
    SRCS := $(addsuffix .c, $(TARGETS))
    OBJS := $(SRCS:.c=.o)
    DEPS := $(OBJS:.o=.d)

    # ============ 默认目标 ============
    all: $(TARGETS)

    # ============ 通用规则:由 .c 生成同名可执行文件 ============
    %: %.c
    $(CC) $(CFLAGS) $< -o $@

    # ============ 线程程序需要额外链接 pthread ============
    thread_demo: thread_demo.c
    $(CC) $(CFLAGS) -pthread $< -o $@

    mutex_demo: mutex_demo.c
    $(CC) $(CFLAGS) -pthread $< -o $@

    thread_pool: thread_pool.c
    $(CC) $(CFLAGS) -pthread $< -o $@

    # ============ 自动依赖生成(-MMD -MP) ============
    # 编译时生成 .d 依赖文件,记录头文件依赖;头文件变化时自动重编
    %.o: %.c
    $(CC) $(CFLAGS) -MMD -MP -c $< -o $@

    # 包含自动生成的依赖文件(若存在)
    -include $(DEPS)

    # ============ 清理 ============
    clean:
    rm -f $(TARGETS) $(OBJS) $(DEPS)

    .PHONY: all clean

    说明:

    • 变量定义:CC、CFLAGS 等用 := 定义,$(CC) 引用变量值。COMMON_TARGETS 和 THREAD_TARGETS 分别列出普通程序和线程程序,TARGETS 合并两者,避免重复书写。
    • 模式规则:%: %.c 是一条模式规则,匹配任意 .c 到同名可执行文件的编译,普通程序(如 copy、tcp_server)自动走这条规则,无需为每个文件单独写规则。
    • 线程程序单独规则:thread_demo、mutex_demo、thread_pool 需要 -pthread 标志,因此单独写规则,在 $(CFLAGS) 后追加 -pthread。-pthread 同时影响预处理和链接:编译时定义 _REENTRANT 等宏,链接时自动链接 libpthread。
    • 自动依赖生成 -MMD -MP:-MMD 让 gcc 在编译时自动生成 .d 依赖文件,记录该 .c 包含的头文件;-MP 为每个头文件生成一个空规则,避免头文件被删除后 make 报错。-include $(DEPS) 把这些依赖文件包含进来,头文件一旦变化,make 会自动重编相关目标。
    • 伪目标 clean:clean 不是真实文件,用 .PHONY 声明后,即使目录下存在同名文件,make clean 也会执行删除命令。
    • 自动变量:$@ 表示目标名,$< 表示第一个依赖。链接时用 $^ 把全部 .o 传给 gcc。

    使用方式:

    make # 编译所有目标(copy fork_demo thread_demo mutex_demo thread_pool tcp_server tcp_client)
    make copy # 只编译 copy
    make tcp_server # 只编译 tcp_server
    make clean # 清理所有可执行文件、目标文件和依赖文件

    验证示例:

    # 编译全部
    make

    # 运行普通程序
    ./copy a.txt b.txt
    ./fork_demo

    # 运行线程程序
    ./thread_demo
    ./mutex_demo
    ./thread_pool

    # 运行网络程序(先启动服务器,再启动客户端)
    ./tcp_server
    ./tcp_client 127.0.0.1

    # 清理
    make clean

    要点:

    • Makefile 命令行前必须是 Tab,不是空格。
    • $@ 表示目标名,$< 表示第一个依赖。
    • .PHONY 声明伪目标,避免和同名文件冲突。
    • 变量用 := 或 = 定义。
    • 大型项目可以用 wildcard、patsubst、自动依赖生成 -MMD -MP。

    通用规则:由 .c 生成同名可执行文件

    %: %.c
    $(CC) $(CFLAGS) $< -o $@

    线程程序需要额外链接 pthread

    thread_demo: thread_demo.c
    $(CC) $(CFLAGS) -pthread $< -o $@

    clean:
    rm -f $(TARGETS)

    .PHONY: all clean
    使用:

    make # 编译所有目标
    make copy # 只编译 copy
    make clean # 清理

    要点:
    •Makefile 命令行前必须是 Tab,不是空格。
    •@表示目标名,@ 表示目标名,@表示目标名,< 表示第一个依赖。
    •.PHONY 声明伪目标,避免和同名文件冲突。
    •变量用 := 或 = 定义。
    •大型项目可以用 wildcard、patsubst、自动依赖生成 -MMD -MP。


    学习建议

  • 多用 man:
  • man 2 open
    man 2 fork
    man 3 pthread_create
    man 2 socket

  • 用 strace 观察系统调用:
  • strace ./copy a.txt b.txt

  • 用 gdb 调试:
  • gcc -g -o demo demo.c
    gdb ./demo

  • 经典书籍:
    • 《UNIX 环境高级编程》APUE
    • 《UNIX 网络编程》UNP
    • 《Linux 程序设计》
      掌握顺序建议:文件 I/O → 进程 → 线程 → Socket → Makefile/调试工具。先把每个示例跑通通,再逐步加入错误处理、循环读写、并发连接、线程池、I/O 多路复用等进阶内容。

    总结

    本文覆盖了 Linux 系统编程的四个核心主题,要点如下:

    • 文件描述符与系统调用:Linux 中“一切皆文件”,文件 I/O 通过 open/read/write/close 等系统调用完成,文件描述符 0/1/2 分别对应标准输入、标准输出和标准错误;read/write 可能返回少于请求的字节数,写操作需循环处理。
    • fork/exec/wait 与 pthread 对比:fork 创建独立地址空间的子进程,配合 exec* 替换进程映像、wait 回收避免僵尸进程;pthread_create 创建共享地址空间的线程,创建开销更小,但共享全局变量需用互斥锁保护,避免竞态条件。
    • TCP 编程流程:服务器遵循 socket -> bind -> listen -> accept -> recv/send -> close,客户端遵循 socket -> connect -> send/recv -> close;每一步都要检查返回值并处理错误,多客户端并发可用 fork 或 pthread 实现。
    • Makefile 作用:Makefile 用规则描述编译过程,避免重复输入 gcc 命令;通过变量、模式规则、自动依赖生成(-MMD -MP)和伪目标 clean 实现高效、可维护的构建;线程程序需额外加 -pthread 标志。

    进一步学习建议

    • I/O 多路复用:当并发连接数很大时,fork/pthread 每连接一线程的开销过高,可学习 select/poll/epoll,用单线程同时监听大量 fd,是高性能网络服务器的常用方案。
    • 线程池优化:本文的线程池示例已展示任务队列 + 条件变量的基本模型,可进一步优化:动态调整线程数、任务优先级、无锁队列(如 CAS)等,以提升吞吐量。
    • gdb 调试:用 gcc -g 编译后,可用 gdb 设置断点、单步跟踪、查看变量,定位段错误和逻辑错误;配合 strace 观察系统调用,能更直观地理解程序行为。

    经典参考书籍与文档

    • 《UNIX 环境高级编程》(APUE):系统编程的“圣经”,系统调用、进程/线程、文件 I/O 的权威参考。
    • 《UNIX 网络编程》(UNP):Socket 编程的经典之作,深入讲解 TCP/IP 编程模型与并发服务器设计。
    • 《Linux 程序设计》:面向 Linux 应用开发的入门经典,覆盖文件、进程、线程、网络与 Makefile 等主题。
    • man 手册:Linux 自带的最权威参考,man 2 open、man 2 fork、man 3 pthread_create、man 2 socket 等随时查阅。
    • GNU Make 手册:官方文档,系统讲解 Makefile 语法、变量、模式规则与自动依赖生成,是编写复杂构建脚本的权威指南。

    掌握顺序建议:文件 I/O → 进程 → 线程 → Socket → Makefile/调试工具。先把每个示例跑通,再逐步加入错误处理、循环读写、并发连接、线程池、I/O 多路复用等进阶内容。

    赞(0)
    未经允许不得转载:171主机测评 » IO、进线程、socket编程(详细解读)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址