Linux学习-TCP并发服务器构建

发布于:2025-08-28 ⋅ 阅读:(4) ⋅ 点赞:(0)

TCP 并发服务器与 IO 多路复用详解

一、服务器基础架构

(一)单循环服务器深度解析

单循环服务器采用最简单的工作模式,其核心流程为:

  1. 创建监听套接字并绑定端口
  2. 进入无限循环:
    • 调用accept()阻塞等待客户端连接
    • 连接建立后,在同一循环中处理该客户端的所有请求
    • 完成后关闭连接,再回到循环等待下一个连接

局限性

  • 同一时间只能处理一个客户端,其他客户端必须排队等待
  • 对于长时间连接或处理耗时的请求,会导致服务响应严重延迟
  • 仅适用于测试环境或非常简单的应用场景

(二)并发服务器设计理念

并发服务器的核心目标是打破单循环的限制,实现多客户端的并行处理。其设计需考虑:

  • 连接管理:如何高效接收和管理多个客户端连接
  • 资源分配:如何为每个连接分配适当的系统资源
  • 同步机制:当多个处理单元共享资源时的同步问题
  • 性能优化:减少上下文切换和资源消耗

二、TCP 并发服务器实现模型

(一)多进程并发模型

工作原理

主进程负责监听和接收新连接,每建立一个新连接就创建一个子进程专门处理该连接的所有交互。

完整实现流程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <signal.h>
#include <sys/wait.h>

// 信号处理函数,回收僵尸进程
void handle_zombie(int sig) {
    while (waitpid(-1, NULL, WNOHANG) > 0);
}

// 子进程处理客户端请求
void handle_client(int connfd) {
    char buffer[1024];
    ssize_t n;
    
    while ((n = read(connfd, buffer, sizeof(buffer)-1)) > 0) {
        buffer[n] = '\0';
        printf("Received: %s\n", buffer);
        
        // 简单回显服务
        write(connfd, buffer, n);
    }
    
    if (n < 0) {
        perror("read error");
    }
    
    close(connfd);
    exit(0);
}

int main() {
    int listenfd, connfd;
    struct sockaddr_in servaddr, cliaddr;
    socklen_t clilen;
    pid_t childpid;
    
    // 注册信号处理函数,防止僵尸进程
    signal(SIGCHLD, handle_zombie);
    
    // 创建TCP套接字
    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("socket error");
        exit(1);
    }
    
    // 初始化服务器地址结构
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(8080);
    
    // 绑定套接字到端口
    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("bind error");
        exit(1);
    }
    
    // 开始监听,最大等待队列长度为10
    if (listen(listenfd, 10) < 0) {
        perror("listen error");
        exit(1);
    }
    
    printf("Server listening on port 8080...\n");
    
    while (1) {
        clilen = sizeof(cliaddr);
        
        // 接受客户端连接
        if ((connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen)) < 0) {
            perror("accept error");
            continue;
        }
        
        // 创建子进程处理客户端
        if ((childpid = fork()) == 0) {
            // 子进程关闭监听套接字
            close(listenfd);
            // 处理客户端请求
            handle_client(connfd);
        }
        // 父进程关闭连接套接字
        close(connfd);
    }
    
    return 0;
}
优缺点分析

优点

  • 进程间完全隔离,一个客户端的处理崩溃不会影响其他客户端和主进程
  • 可以充分利用多CPU系统的并行处理能力

缺点

  • 进程创建和销毁的开销大,消耗系统资源多
  • 进程间通信(IPC)复杂,需要使用管道、共享内存等机制
  • 并发量受限,系统能创建的进程数量有限
适用场景
  • 客户端连接数不多,但每个连接的处理逻辑复杂
  • 对安全性和隔离性要求高的场景
  • 需要利用多个CPU核心进行计算的服务

(二)多线程并发模型

工作原理

主线程负责监听和接收新连接,每建立一个新连接就创建一个新线程专门处理该连接的交互。与多进程模型相比,线程共享进程的地址空间,资源消耗更少。

关键实现代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <pthread.h>

// 线程处理函数的参数结构体
typedef struct {
    int connfd;
    struct sockaddr_in cliaddr;
} ThreadArgs;

// 线程处理函数,处理客户端请求
void *handle_client(void *arg) {
    ThreadArgs *args = (ThreadArgs *)arg;
    int connfd = args->connfd;
    char buffer[1024];
    ssize_t n;
    
    // 分离线程,自动回收资源
    pthread_detach(pthread_self());
    free(args);  // 释放动态分配的参数
    
    while ((n = read(connfd, buffer, sizeof(buffer)-1)) > 0) {
        buffer[n] = '\0';
        printf("Received: %s\n", buffer);
        
        // 简单回显服务
        write(connfd, buffer, n);
    }
    
    if (n < 0) {
        perror("read error");
    }
    
    close(connfd);
    return NULL;
}

int main() {
    int listenfd, connfd;
    struct sockaddr_in servaddr, cliaddr;
    socklen_t clilen;
    pthread_t tid;
    ThreadArgs *args;
    
    // 创建TCP套接字
    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("socket error");
        exit(1);
    }
    
    // 初始化服务器地址结构
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(8080);
    
    // 绑定套接字到端口
    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("bind error");
        exit(1);
    }
    
    // 开始监听
    if (listen(listenfd, 10) < 0) {
        perror("listen error");
        exit(1);
    }
    
    printf("Server listening on port 8080...\n");
    
    while (1) {
        clilen = sizeof(cliaddr);
        
        // 接受客户端连接
        if ((connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen)) < 0) {
            perror("accept error");
            continue;
        }
        
        // 为线程分配参数
        args = malloc(sizeof(ThreadArgs));
        args->connfd = connfd;
        args->cliaddr = cliaddr;
        
        // 创建线程处理客户端
        if (pthread_create(&tid, NULL, handle_client, args) != 0) {
            perror("pthread_create error");
            close(connfd);
            free(args);
        }
    }
    
    close(listenfd);
    return 0;
}
优缺点分析

优点

  • 线程创建和切换的开销比进程小很多
  • 线程间共享地址空间,数据共享方便
  • 可以支持比多进程模型更高的并发量

缺点

  • 线程安全问题:多个线程共享资源时需要同步机制(互斥锁、条件变量等)
  • 一个线程崩溃可能导致整个进程崩溃
  • 受限于进程的资源限制
适用场景
  • 中等并发量的网络服务
  • 线程间需要频繁数据交换的场景
  • 对响应速度要求较高的服务

(三)线程池模型

工作原理

线程池是一种池化技术,预先创建一定数量的线程,放在"池"中等待任务。当新的客户端连接到来时,主线程将连接作为任务放入任务队列,线程池中的空闲线程会从队列中取出任务进行处理。

实现关键点
  1. 固定数量的工作线程
  2. 线程安全的任务队列
  3. 任务添加和取出的同步机制
  4. 线程池的创建、销毁和管理接口
优缺点分析

优点

  • 避免了频繁创建和销毁线程的开销
  • 控制并发线程数量,防止资源耗尽
  • 任务处理响应更快,因为线程已经预先创建

缺点

  • 实现相对复杂
  • 线程池大小需要根据应用场景调优
  • 长时间运行的任务可能导致线程池阻塞
适用场景
  • 高并发、短任务的网络服务
  • Web服务器、数据库连接池等
  • 需要限制并发线程数量的场景

三、IO多路复用模型

(一)基本概念

IO多路复用允许一个进程同时监视多个文件描述符,一旦某个描述符就绪(可读、可写或异常),能够通知程序进行相应的处理。这种模型不需要创建大量进程或线程,而是通过一个进程处理多个IO操作。

(二)select模型

工作原理

select函数允许程序监视多个文件描述符,等待其中一个或多个变为就绪状态。它使用三个文件描述符集合分别表示可读、可写和异常事件。

完整实现代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/select.h>

#define MAX_CLIENTS 10
#define BUFFER_SIZE 1024

int main() {
    int listenfd, connfd, maxfd;
    struct sockaddr_in servaddr, cliaddr;
    socklen_t clilen;
    char buffer[BUFFER_SIZE];
    fd_set readfds, allfds;
    int clientfds[MAX_CLIENTS];
    int i, nready, n;
    
    // 初始化客户端文件描述符数组
    for (i = 0; i < MAX_CLIENTS; i++) {
        clientfds[i] = -1;
    }
    
    // 创建TCP套接字
    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("socket error");
        exit(1);
    }
    
    // 初始化服务器地址结构
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(8080);
    
    // 绑定套接字到端口
    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("bind error");
        exit(1);
    }
    
    // 开始监听
    if (listen(listenfd, 10) < 0) {
        perror("listen error");
        exit(1);
    }
    
    printf("Server listening on port 8080...\n");
    
    // 初始化文件描述符集合
    FD_ZERO(&allfds);
    FD_SET(listenfd, &allfds);
    maxfd = listenfd;
    
    while (1) {
        // 每次调用select前都需要重置readfds
        readfds = allfds;
        
        // 调用select等待就绪事件
        nready = select(maxfd + 1, &readfds, NULL, NULL, NULL);
        if (nready < 0) {
            perror("select error");
            continue;
        }
        
        // 检查监听套接字是否有新连接
        if (FD_ISSET(listenfd, &readfds)) {
            clilen = sizeof(cliaddr);
            if ((connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen)) < 0) {
                perror("accept error");
                continue;
            }
            
            printf("New client connected\n");
            
            // 将新连接添加到客户端数组和文件描述符集合
            for (i = 0; i < MAX_CLIENTS; i++) {
                if (clientfds[i] == -1) {
                    clientfds[i] = connfd;
                    FD_SET(connfd, &allfds);
                    if (connfd > maxfd) {
                        maxfd = connfd;
                    }
                    break;
                }
            }
            
            // 如果客户端数量达到上限
            if (i == MAX_CLIENTS) {
                printf("Too many clients\n");
                close(connfd);
            }
            
            // 如果没有其他就绪事件,继续循环
            if (--nready <= 0) {
                continue;
            }
        }
        
        // 检查客户端套接字的读写事件
        for (i = 0; i < MAX_CLIENTS; i++) {
            if ((connfd = clientfds[i]) == -1) {
                continue;
            }
            
            if (FD_ISSET(connfd, &readfds)) {
                // 读取客户端数据
                if ((n = read(connfd, buffer, BUFFER_SIZE - 1)) <= 0) {
                    // 客户端关闭连接
                    if (n < 0) {
                        perror("read error");
                    } else {
                        printf("Client disconnected\n");
                    }
                    
                    close(connfd);
                    FD_CLR(connfd, &allfds);
                    clientfds[i] = -1;
                } else {
                    // 处理数据,这里简单回显
                    buffer[n] = '\0';
                    printf("Received: %s\n", buffer);
                    write(connfd, buffer, n);
                }
                
                if (--nready <= 0) {
                    break;
                }
            }
        }
    }
    
    close(listenfd);
    return 0;
}
优缺点分析

优点

  • 跨平台支持好,在Linux、Windows等系统都有实现
  • 实现相对简单,容易理解和使用

缺点

  • 每次调用select都需要将文件描述符集合从用户空间复制到内核空间
  • 文件描述符数量有限制(通常是1024)
  • 需要遍历所有文件描述符来检查就绪状态,效率随描述符数量增加而下降

网站公告

今日签到

点亮在社区的每一天
去签到