服务器类型与TCP并发服务器构建(SELECT)

一、服务器核心类型区分

服务器类型 核心特点
单循环服务器 同一时刻仅能处理一个客户端的任务
并发服务器 同一时刻可处理多个客户端的任务

二、TCP并发服务器构建基础

1. TCP连接特性

TCP协议下,服务端与客户端需建立一对一连接,每个客户端(如cli1、cli2、cli3、cli4)会对应服务端的一个连接文件描述符(如connfd1、connfd2、connfd3、connfd4),通过该描述符实现数据交互。

三、TCP服务端并发模型(4种核心方案)

1. 多进程模型

  • 资源开销:进程资源开销大(进程间地址空间独立,切换成本高)。
  • 安全性:安全性高(进程间数据隔离,一个进程异常不影响其他进程)。
  • 代码
cs 复制代码
#include "head.h"

#define SER_PORT 50000
#define SER_ID "192.168.0.164"

int init_tcp_ser()
{
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (sockfd < 0)
	{
		perror("socket error");
		return -1;
	}
	
	struct sockaddr_in seraddr;
	seraddr.sin_family = AF_INET;
	seraddr.sin_port = htons(50000);
	seraddr.sin_addr.s_addr = inet_addr("192.168.0.164");

    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;
}

void wait_handler(int signo)
{
    wait(NULL);
}

int main(int argc, char const *argv[])
{
    signal(SIGCHLD_wait_handler);

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

    int sockfd = init_tcp_ser();
    if(sockfd < 0)
    {
        return -1;
    }

    while(1)
    {
        int connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &clilen);
        if(connfd < 0)
        {
            perror("accept error");
            close (sockfd);
            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 cnt = recv(connfd, buff, sizeof(buff), 0);
                if(cnt < 0)
                {
                    perror("recv error");
                    break;
                }
                else if(cnt == 0)
                {
                    printf("[ %s : %d ] : cli offline\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
                    break;
                }
                printf("[ %s : %d ] : buff = %s\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port), buff);
                
                strcat(buff, "------> ok");
                cnt = send(connfd, buff, sizeof(buff), 0);
                if(cnt < 0)
                {
                    perror("send error");
                    break;
                }
            }
            close(connfd);
        }
    }
    close(sockfd);
    return 0;
}

2. 多线程模型

  • 资源开销:相对进程开销小(线程共享进程地址空间,切换成本低)。
  • 并发能力:相同资源环境下,并发处理客户端的数量比多进程模型更优。
  • 代码
cs 复制代码
#include "head.h"


#define SER_PORT 50000
#define SER_ID "192.168.0.164"


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

	struct sockaddr_in seraddr;
	seraddr.sin_family = AF_INET;
	seraddr.sin_port = htons(50000);
	seraddr.sin_addr.s_addr = inet_addr("192.168.0.164");

    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;
}

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

int main(int argc, char const *argv[])
{
    struct sockaddr_in cliaddr;
    socklen_t clilen = sizeof(cliaddr);

    int sockfd = init_tcp_ser();
    if(sockfd < 0)
    {
        return -1;
    }

    while(1)
    {
        int connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &clilen);
        if(connfd < 0)
        {
            perror("accept error");
            close (sockfd);
            return -1;
        }

        pthread_t tid;
        int ret = pthread_create(&tid, NULL, task, &connfd);
        if(ret != 0)
        {
            perror("pthread_create error");
            return -1;
        }
        pthread_detach(tid);
    }
    close(sockfd);
    return 0;
}

3. 线程池模型

  • 设计目的:解决多线程/多进程模型中"频繁创建、销毁线程(或进程)"带来的时间消耗问题。
  • 核心原理 :基于生产者-消费者编程模型 ,结合任务队列实现的多线程框架(提前创建固定数量线程,循环处理队列中的任务,避免频繁启停线程)。

4. IO多路复用模型

  • 核心逻辑 :将"IO操作"与"文件描述符(fd)"关联,复用一个进程即可实现对"多个文件描述符读写状态"的同时监测。
  • 关键优势:无需创建新进程或线程,仅用单个进程完成多客户端IO事件处理。
  • 关联概念:阻塞IO模式下,多个任务会呈现"同步执行"效果(一个任务阻塞时,其他任务需等待)。
  • 实现方式 :支持3种主流方案,selectpollepoll

四、select实现IO多路复用(详细流程与函数)

1. 核心实现步骤(5步)

  1. 创建文件描述符集合 :定义fd_set类型的集合,用于存放需监测的文件描述符。
  2. 添加关注的文件描述符 :通过FD_SET()宏,将待监测的fd加入集合。
  3. 内核监测事件 :调用select()函数,将fd集合传递给内核,由内核监测fd的读写/异常事件。
  4. 解除阻塞并获取结果 :当内核监测到目标事件(如fd可读),select()解除阻塞,应用层获取事件结果。
  5. 处理任务 :根据select()返回的结果,对触发事件的fd进行针对性处理(如recv()读取数据)。

2. 关键宏函数(操作fd集合)

宏函数原型 功能描述
void FD_CLR(int fd, fd_set *set) 将指定fd从fd集合中移除
int FD_ISSET(int fd, fd_set *set) 判断指定fd是否在fd集合中(存在则返回非0)
void FD_SET(int fd, fd_set *set) 将指定fd添加到fd集合中
void FD_ZERO(fd_set *set) 清空fd集合(初始化集合时必须调用)

3. select函数详情

(1)函数原型
复制代码
int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);
(2)功能

将fd集合传递给内核,等待内核返回"触发事件的fd结果",实现对多fd的事件监测。

(3)参数说明
参数名 含义
nfds 需关注的"最大文件描述符 + 1"(内核通过该值确定监测范围)
readfds 监测"读事件"的fd集合(如客户端发送数据,fd变为可读)
writefds 监测"写事件"的fd集合(如fd可写入数据,无缓冲区满问题)
exceptfds 监测"异常事件"的fd集合(如fd发生错误)
timeout 超时时间设置: <br>- NULL:无超时,select()一直阻塞等待事件; <br>- 非NULL:超时后若无事件,select()解除阻塞
(4)返回值
  • 成功:返回内核监测到的"触发事件的fd个数"。
  • 失败 :返回-1(如参数错误、系统调用异常)。
  • 超时 :返回0(超时时间到达,且无任何事件触发)。
(5)代码
cs 复制代码
#include "head.h"

#define SER_PORT 50000
#define SER_ID "192.168.0.164"

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

    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[])
{
    char buff[1024] = {0};
    struct sockaddr_in cliaddr;
    socklen_t clilen = sizeof(cliaddr);

    int sockfd = init_tcp_ser();
    if(sockfd < 0)
    {
        return -1;
    }

    // 创建文件描述符集合

    fd_set rdfds;
    fd_set rdfdstmp;

    // 清零
    FD_ZERO(&rdfds);

    // 添加关注的文件描述符到集合
    FD_SET(sockfd, &rdfds);
    int maxfd = sockfd;

    while(1)
    {
        rdfdstmp = rdfds;

        // 传递集合到内核,并等待返回监测结果
        int cnt = select(maxfd + 1, &rdfdstmp, NULL, NULL, NULL);
        if(cnt < 0)
        {
            perror("select error");
            return -1;
        }

        // 是否有监听套接字事件到达 ----》三次握手已完成,可以accept
        if(FD_ISSET(sockfd, &rdfdstmp))
        {
            int connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &clilen);
            if(connfd < 0)
            {
                perror("accept error");
                close (sockfd);
                return -1;
            }
            FD_SET(connfd, &rdfds);    // 加到原始的集合
            maxfd = maxfd > connfd ? maxfd : connfd;
            
        }

        // 是否有通信套接字事件到达
        for(int i = sockfd + 1; i <= maxfd; i++)
        {
            if(FD_ISSET(i, &rdfdstmp))  // 判断i
            {
                memset(buff, 0, sizeof(buff));
                // 接收 
                ssize_t cnt = recv(i, buff, sizeof(buff), 0);
                if(cnt < 0)
                {
                    perror("recv error");
                    FD_CLR(i, &rdfds);      // i 错误 ,从集合中删除i
                    close(i);              // 先删除再关闭
                    continue;               // 不能使用 break   return
                }
                else if(cnt == 0)
                {
                    FD_CLR(i, &rdfds);
                    close(i);
                    continue;
                }
                printf("%s\n", buff);

                strcat(buff , "-----> ok");
                cnt = send(i, buff, sizeof(buff), 0);
                if(cnt < 0)
                {
                    perror("send error");
                    FD_CLR(i, &rdfds);
                    close(i);
                    continue;
                }
            }
        }
    }
    close(sockfd);
    return 0;
}
相关推荐
朱包林13 分钟前
数据库服务-日志管理-备份与恢复-主从同步
linux·运维·服务器·数据库·mysql·云计算
神色自若2 小时前
AbpVnext 阿里云ssl证书多个生产环境自动更新
服务器·阿里云·ssl
万花丛中一抹绿4 小时前
服务器硬件电路设计之 SPI 问答(六):如何提升服务器硬件电路中的性能?如何强化稳定性?
服务器·spi·服务器硬件电路设计
2401_888423094 小时前
网络编程-TCP的并发服务器构建
服务器·网络·tcp/ip
王火火(DDoS CC防护)9 小时前
服务器网络带宽不足要怎么处理?
运维·服务器
潇凝子潇9 小时前
获取服务器指标的信息
linux·运维·服务器
FreeBuf_9 小时前
Chrome高危零日漏洞PoC公开,已被用于野外攻击
linux·运维·服务器·安全·web安全
小白银子12 小时前
零基础从头教学Linux(Day 20)
linux·运维·服务器·php·国安工程师
古月-一个C++方向的小白13 小时前
Linux初始——基础指令篇
linux·运维·服务器