Linux 软件编程(十二)网络编程:TCP 并发服务器构建与 IO 多路复用

在网络编程中,服务器需要应对多个客户端的连接与请求,单循环服务器因同一时刻仅能处理一个客户端任务,无法满足高并发场景。而 TCP 并发服务器可实现多客户端同时处理 。

一、TCP 并发服务器

(一)单循环与并发服务器区别

  • 单循环服务器:同一时刻仅能处理一个客户端任务,处理完当前客户端才能响应下一个,效率低。
  • 并发服务器:可同时处理多个客户端任务,提升服务端资源利用率与响应能力。

(二)TCP 连接特性

TCP 基于 “三次握手” 建立 一对一连接 ,可靠传输数据,但需高效并发模型支撑多客户端交互。

二、TCP 服务端并发模型

(一)多进程模型

  1. 原理:服务端通过 fork() 创建子进程,父进程负责监听新连接,子进程处理客户端交互。
  2. 特点
    • 资源开销大:进程有独立地址空间,创建、切换成本高。
    • 安全性高:进程间资源隔离,一个进程异常一般不影响其他进程。
  3. 代码示例
#include <stdio.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
 #include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <pthread.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <signal.h>

#define SER_PORT 50000
#define SER_IP "192.168.0.149"

struct sockaddr_in seraddr;

void hander(int handnum)
{
    wait(NULL);
}

int init_tcp_recv()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd < 0)
    {
        perror("sockfd error");
        return -1;
    }
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(SER_PORT);
    seraddr.sin_addr.s_addr = inet_addr(SER_IP);

    int ret = bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
    if(ret < 0)
    {
        perror("bind error");
        return -1;
    }

    ret =listen(sockfd, 10);
    if(ret < 0)
    {
        perror("listen error");
        return -1;
    }
    return sockfd;
}


int main(int argc, char const *argv[])
{
    signal(SIGCHLD, hander);
    int sockfd = init_tcp_recv();

    struct sockaddr_in cliaddr;
    socklen_t lenaddr = sizeof(cliaddr);

    while(1)
    {
        int connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &lenaddr);
        if(connfd < 0)
        {
            perror("accept error");
            return -1;
        }
        pid_t pid = fork();
        if(pid > 0)
        {

        }
        else if(pid == 0)
        {
            char buff[1024] = {0};
            while(1)
            {
                memset(buff, 0, sizeof(buff));
                int ret = recv(connfd, buff, sizeof(buff), 0);
                if(ret < 0)
                {
                    perror("error recv");
                    break;
                }
                else if(0 == ret)
                {
                    printf("[%s:%d]: client off\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
                    close(connfd);
                    break;
                }
                printf("[%s:%d]:%s\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port), buff);
                strcat(buff, "---->ok");
                int cnt = send(connfd, buff, strlen(buff), 0);
                if(cnt < 0)
                {
                    perror("send error");
                    close(connfd);
                    break;
                }
                
            }
            
        }
    }
    close(sockfd);

    return 0;

}

(二)多线程模型

  1. 原理:借助 pthread_create() 创建线程,主线程负责 accept 新连接,子线程处理客户端数据收发。
  2. 特点
    • 资源开销小:线程共享进程地址空间,创建、切换成本低于进程,相同资源环境下并发量更高。
    • 需注意同步:线程共享资源,如需客户端地址信息等,需用锁机制(如互斥锁)避免竞争,否则易引发数据混乱,或者创建结构体将客户端信息和通信套接字放在里面,然后线程传参时第四个参数传入结构体的地址,然后进行访问内容。
  3. 伪代码示例
#include <stdio.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
 #include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <pthread.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include<pthread.h>

#define SER_PORT 50000
#define SER_IP "192.168.0.149"

struct sockaddr_in seraddr;

int init_tcp_recv()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd < 0)
    {
        perror("sockfd error");
        return -1;
    } 
    //允许绑定处于TIME_WAIT状态的地址,避免端口占用问题:
	int optval = 1;
	setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(SER_PORT);
    seraddr.sin_addr.s_addr = inet_addr(SER_IP);

    int ret = bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
    if(ret < 0)
    {
        perror("bind error");
        return -1;
    }

    ret =listen(sockfd, 10);
    if(ret < 0)
    {
        perror("listen error");
        return -1;
    }
    return sockfd;
}

// struct sockaddr_in cliaddr;
// socklen_t lenaddr = sizeof(cliaddr);

void *recv_senf(void *arg)
{
    int connfd = *((int *)arg);
    char buff[1024] = {0};
    while(1)
    {
        memset(buff, 0, sizeof(buff));
        int ret = recv(connfd, buff, sizeof(buff), 0);
        if(ret < 0)
        {
            perror("error recv");
            break;
        }
        else if(0 == ret)
        {
            printf("client off\n");
            close(connfd);
            break;
        }
        printf("%s\n", buff);
        strcat(buff, "---->ok");
        int cnt = send(connfd, buff, strlen(buff), 0);
        if(cnt < 0)
        {
            perror("send error");
            close(connfd);
            break;
        } 
    }
    return NULL;
}

int main(int argc, char const *argv[])
{
    int sockfd = init_tcp_recv();
    if(sockfd < 0)
    {
        return -1;
    }
    pthread_t tid;
    while(1)
    {
        int connfd = accept(sockfd, NULL, NULL);
        if(connfd < 0)
        {
            perror("accept error");
            return -1;
        }
        pthread_create(&tid, NULL, recv_senf, &connfd);
        pthread_detach(tid);
    }

    close(sockfd);

    return 0;

}

(三)线程池模型

  1. 背景:多线程 / 多进程模型中,频繁创建、销毁线程 / 进程会产生大量时间消耗。线程池基于 生产者 - 消费者模型 与 任务队列 ,预先创建一定数量线程,复用线程处理任务,减少资源开销。
  2. 原理
    • 生产者(主线程):负责 accept 客户端连接,将任务(如处理客户端请求)放入任务队列。
    • 消费者(线程池中的次线程):从任务队列取出任务并执行,执行完可继续获取新任务,无需频繁创建销毁。
  3. 模型示意图
    主线程作为生产者生产任务(如客户端连接处理任务),放入任务队列;多个次线程作为消费者,从队列取任务执行,实现任务高效复用与并发处理。

(四)IO 多路复用模型

  1. 核心思想:让一个进程 / 线程 复用多个文件描述符(fd)的读写操作 ,不创建新进程 / 线程,通过一个进程监测、处理多个文件(如 socket 连接)的读写事件。
  2. 应用场景:需同时监听多个 fd (如 stdinconnfd 等),避免阻塞 IO 导致程序 “卡主”,典型函数有 selectpollepoll 。
(1)select 函数
  • 函数与操作步骤
    • 创建文件描述符集合:用 fd_set 类型,通过 FD_ZERO 清空集合,FD_SET 添加关注的 fd 。
    • 传递集合给内核监测:调用 select 函数,内核监测集合中 fd 的事件(读、写、异常等),监测期间进程阻塞。
    • 处理事件:内核监测到事件后,select 解除阻塞,通过 FD_ISSET 判断具体 fd 事件,执行对应读写操作。
  • 关键函数与 select 原型
  • void FD_CLR(int fd, fd_set *set); 
    int  FD_ISSET(int fd, fd_set *set); 
    void FD_SET(int fd, fd_set *set); 
    void FD_ZERO(fd_set *set); 

    int select(int nfds, fd_set *readfds, fd_set *writefds,
               fd_set *exceptfds, struct timeval *timeout);
    功能:传递文件描述符结合表给内核并等待获取事件结果
           参数:
                   nfds : 关注的最大文件描述符+1
                   readfds:读事件的文件描述符集合
                  writefds:写事件的文件描述符集合
                  exceptfds:其他事件的文件描述符集合
                  timeout:设置select监测时的超时时间
                                   NULL : 不设置超时时间(select一直阻塞等待)

            返回值:
                成功:返回内核监测的到达事件的个数
                失败:-1
                0 : 超时时间到达,但没有事件发生,则返回

  • 伪代码示例(结合 TCP 服务端)



#include <sys/select.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/time.h>
#include<string.h>
#include <stdio.h>
#include <sys/types.h>          /* See NOTES */
 #include <sys/socket.h>
#include <netinet/ip.h> /* superset of previous */
#include <unistd.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include<pthread.h>

#define SER_PORT 50000
#define SER_IP "192.168.0.149"

struct sockaddr_in seraddr;

int init_tcp_recv()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd < 0)
    {
        perror("sockfd error");
        return -1;
    } 
    //允许绑定处于TIME_WAIT状态的地址,避免端口占用问题:
	int optval = 1;
	setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(SER_PORT);
    seraddr.sin_addr.s_addr = inet_addr(SER_IP);

    int ret = bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
    if(ret < 0)
    {
        perror("bind error");
        return -1;
    }

    ret =listen(sockfd, 10);
    if(ret < 0)
    {
        perror("listen error");
        return -1;
    }
    return sockfd;
}

int main(int argc, char const *argv[])
{
    int sockfd = init_tcp_recv();
    if(sockfd < 0)
    {
        return -1;
    }

    fd_set rdfds;
    fd_set rdfdstem;

    FD_ZERO(&rdfds);
    FD_ZERO(&rdfdstem);

    FD_SET(sockfd, &rdfds);
    int maxfd = sockfd; 
    char buff[1024] = {0};
    while(1)
    {
        rdfdstem = rdfds;
        int ret = select(maxfd + 1, &rdfds, NULL, NULL, NULL);
        if(ret < 0)
        {
            perror("select error");
            return -1;
        }
  
        if(FD_ISSET(sockfd, &rdfdstem))
        {
            int connfd = accept(sockfd, NULL, NULL);
            if(connfd < 0)
            {
                perror("accept error");
                return -1;
            }
            FD_SET(connfd, &rdfds);
            maxfd = maxfd > connfd ? maxfd : connfd;
        }

        for(int i = sockfd + 1;i < maxfd + 1;++i)
        {
            if(FD_ISSET(i, &rdfdstem))
            {
                memset(buff, 0, sizeof(buff));
                ssize_t cnt  = recv(i, buff, sizeof(buff), 0);
                if(cnt < 0)
                {
                    perror("recv error");
                    FD_CLR(i, &rdfds);
                    close(i);
                    continue;
                }
                else if(cnt == 0)
                {
                    FD_CLR(i, &rdfds);
                    close(i);
                    continue;
                }
                printf("%s\n", buff);
                strcat(buff, "---->ok");
                cnt = send(i, buff, strlen(buff), 0);
                if(cnt < 0)
                {
                    perror("send error");
                    FD_CLR(i, &rdfds);
                    close(i);
                    continue;
                }
            }
        }         
    }
    close(sockfd);
    return 0;
}

  • 特点
    • 需维护 fd 集合,每次 select 后需重新添加 fd(因内核会修改集合)。
    • fd 数量受限(受系统默认限制,如 1024 ),高并发场景可能不够用。
(2)poll/epoll 
  • poll:与 select 类似,通过 struct pollfd 数组传递监测的 fd 及事件,解决 select 中 fd 数量受限问题,但本质仍需遍历 fd 判断事件,高并发下效率一般。
  • epoll:Linux 下高效的 IO 多路复用机制,通过 红黑树 维护监测的 fd回调机制 通知事件,无需遍历所有 fd ,高并发场景(如大量客户端连接)效率远高于 select/poll 。

三、总结

TCP 并发服务器构建有多种模式:

  • 多进程模型资源开销大但安全;
  • 多线程模型轻量但需注意同步;
  • 线程池模型优化线程资源管理,适配高频任务场景;
  • IO 多路复用(select/poll/epoll )让单进程实现多 fd 监测,高效处理并发连接。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值