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

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

  1. TCP并发服务器概述

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

  1. 多进程模型:为每个客户端连接创建新进程
  2. 多线程模型:为每个客户端连接创建新线程
  3. I/O多路复用:使用select/poll/epoll管理多个连接

本文将重点讲解多进程和多线程实现方式,并分析关键技术和常见问题。

  1. 多进程并发服务器实现

2.1 核心代码解析

c 复制代码
#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 关键技术点

  1. 地址重用(SO_REUSEADDR)
c 复制代码
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

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

• 避免"Address already in use"错误

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

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

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

  1. 文件描述符关闭

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

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

• 子进程需要关闭监听socket

  1. 多线程并发服务器实现

3.1 核心代码解析

c 复制代码
#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 关键技术点

  1. 线程参数传递

• 必须确保每个线程获得独立的client_fd

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

  1. 线程分离
c 复制代码
pthread_detach(tid);

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

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

  1. 线程安全函数

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

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

  1. 关键工具函数详解

4.1 bzero vs memset

c 复制代码
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 地址转换函数

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

选择建议:

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

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

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

  1. 完整示例代码

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

  1. 常见问题解答

Q1: 为什么在多进程模型中父进程要关闭客户端socket?

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

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

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

bash 复制代码
sysctl -w net.ipv4.tcp_tw_reuse=1
sysctl -w net.ipv4.tcp_tw_recycle=1

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

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

相关推荐
xiaoye-duck3 分钟前
《Linux系统编程》Linux 进程间通信之管道基础解析:从匿名管道原理到基于管道的进程池实现
linux
z200509306 分钟前
【Linux学习】Linux中的进程程序替换
linux·服务器·学习
ytdbc19 分钟前
OSPF综合实验
网络
bush424 分钟前
嵌入式linux学习记录四
linux·运维·学习
kaisun641 小时前
Docker 构建网络问题排查
网络·docker·eureka
lihao lihao1 小时前
软硬链接
linux·运维·服务器
TOWE technology1 小时前
智能安防监控系统如何做好防雷?——视频信号SPD综合应用方案解析
运维·服务器·防雷产品·信号保护·信号防雷·spd
雪度娃娃2 小时前
存储器层次结构——磁盘硬盘存储
服务器·网络·数据库·计算机组成原理
YY&DS2 小时前
Qt 嵌入 CEF 在 Linux 下必须设置 `QT_XCB_GL_INTEGRATION=xcb_egl才能加载网页
linux·开发语言·qt