c++高并发tcp网络服务器实例渐进式教程-06

本文介绍了C++中使用select实现的TCP服务器,讲解了服务端代码编写、编译运行过程,以及select系统API的详细说明。通过IO复用,服务器在一个线程内处理新链接和读写事件,提高了效率,但存在最大连接数限制和查找效率低的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

近代TCP服务器-IO复用之select

所谓IO复用,复用的是线程,无论是accept的新链接事件,还是recv的可读事件,都可以在同一个线程中完成,不用引入新的独立线程。

IO复用随着历史发展经历了3个主要阶段,即select(),poll(),epoll().

这节课内容先介绍select方式的IO复用模型

一 服务端代码编写

我们的TCP服务器业务比较简单,代码架构简化模型如下:

服务器架构中只有一个主线程,主线程中有一个while死循环,while循环里面嵌套了一个有限的for循环。

//server5.cpp
#include<unistd.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<iostream>
#include<vector>
#include<cstring>


//有数据到来,触发此socket事件,才会进行recv,所以recv一般情况下不会死等在那里,recv必有数据来到。
//所以这个io模型相比多线程模型,已经很高效了。
const uint16_t port = 8880;
const uint16_t buf_len = 1024;

int main(){
    int max_fd;
    int rc,on=1;
    int fd = socket(AF_INET,SOCK_STREAM,0);
    if(-1 == fd ){
        std::cout << "create socket error!" <<std::endl;
        return -1;
    }

    struct sockaddr_in s_addr;
    s_addr.sin_family = AF_INET;
    s_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    s_addr.sin_port = htons(port);

    int ret = bind(fd,(struct sockaddr*)&s_addr,sizeof(s_addr));
    if(ret == -1){
        std::cout << "bind addr error!" <<std::endl;
        return -1;
    }

    max_fd = fd;
    ret = listen(fd,1024);
    if(-1 == ret){
        std::cout << "listen error!" << std::endl;
        return -1;
    }

    //声明两个描述符集合。
    fd_set main_fds,read_fds;

    //初始化集合。
    FD_ZERO(&main_fds);
    FD_ZERO(&read_fds);

    //添加fd到main_fds集合
    FD_SET(fd,&main_fds);

    while (1){
        //每次调用select后,被监听的集合会被破坏,所以需要复制未被破坏的集合(main_fds)到read_fds
        memcpy(&read_fds, &main_fds, sizeof(main_fds));

        //只监听可读事件的集合,其他无关参数设置为NULL.
        int result = select(max_fd+1, &read_fds,NULL,NULL,NULL);
        if(result < 1){
            std::cout << "select error!" <<std::endl;
            return -1;
        }

        std::cout << "select sockets = " << result << std::endl;

        std::string send_buf = R"({"radio": {"proto": "11axg","country": "Britain","bandwidth": "HT20","usrlimit": "32","chanid": "auto","bswitch": "1"}})";
        std::string recv_buff = "";
        recv_buff.resize(buf_len);
        //所有的描述符索引值一定在0~max_fd之间。
        for (int peer_fd = 0; peer_fd <= max_fd; peer_fd++){
            if(FD_ISSET(peer_fd,&read_fds)){
                //如果当前的描述符为主描述符,表示当前是一个新链接的事件。
                if(peer_fd == fd){
                    struct sockaddr_in c_addr;
                    int client_fd = accept(fd,(struct sockaddr*)NULL,NULL);
                    //接收新链接描述符
                    if(client_fd > 0){
                        //新链接的描述符添加到原始集合中。
                        FD_SET(client_fd,&main_fds);
                        //更新max_fd,保持为最大值。
                        if(client_fd > max_fd){
                            max_fd = client_fd;
                        }
                        std::cout << "accept a new client connection!" <<std::endl;
                    }else{
                        std::cout << "error client connection!" <<std::endl;
                        continue;
                    }
                }else{
                    //不是主描述符,就是一个可读事件,或者可写事件
                    memset(&recv_buff.at(0),0,recv_buff.size());
                    int c = recv(peer_fd,&recv_buff.at(0),recv_buff.size(),0);
                    if(c < 0){
                        std::cout << "conn " << peer_fd << " recv error!" << std::endl;
                        close(peer_fd);
                        //如果读写的连接发生致命错误,把当前描述符从集合去除。
                        FD_CLR(peer_fd,&main_fds);
                        std::cout << "close peer " << peer_fd << std::endl;
                        continue;
                    }else if(c == 0){
                        std::cout <<"peer " << peer_fd << " connection closed" << std::endl;
                        close(peer_fd);
                        FD_CLR(peer_fd,&main_fds);
                        std::cout << "close peer " << peer_fd << std::endl;
                        continue;
                    }else{
                        std::cout << "recv client peer " << peer_fd << " data: " << recv_buff << std::endl;
                    }

                    int ret = send(peer_fd,&send_buf.at(0),send_buf.size(),0);
                    if(ret < 0){
                        std::cout << "send data error!" << std::endl;
                    }
                }
            }else{
                // std::cout << "find fd!" << std::endl;
            }
        }
    }   

    //释放连接资源
    for (size_t i = 0; i < max_fd; i++){
        if(FD_ISSET(i,&main_fds)){
            close(i);
        }
    }
    
}

二 编译运行

2.1 服务端

编译
g++ -std=c++11 server5.cpp -lpthread -o server5
运行
./server5

2.2 客户端

编译客户端代码:
g++ client1.cpp -o client1

启动客户端:
./client1 192.168.199.160
client1 的第一个参数是服务端的地址(192.168.199.160)

服务端输出

可以同时启动多个client,验证一下服务器此时的并发数。
./client1 192.168.199.160
./client1 192.168.199.160
./client1 192.168.199.160
./client1 192.168.199.160
./client1 192.168.199.160
./client1 192.168.199.160

三 系统API 详解

3.1 select

#include <sys/select.h>
int select(int nfds, fd_set *restrict readfds,
            fd_set *restrict writefds, fd_set *restrict exceptfds,
            struct timeval *restrict timeout);
  • nfds
    socket文件描述符,动态保持为三个集合中最大值得描述符+1,表示一个遍历搜索的边界值。
  • readfds
    需要监听可读事件的描述符集合首地址。select() 返回后,readfds 将被清除所有文件描述符,除了那些有可读事件触发的描述符。
  • writefds
    需要监听可写事件的描述符集合首地址。select() 返回后,writefds 将被清除所有文件描述符,除了那些有可写事件触发的描述符。
  • exceptfds
    需要监听异常事件的描述符集合首地址。select() 返回后,exceptfds 将被清除所有文件描述符,除了那些有异常事件触发的描述符。
  • timeout
    等待可读可写事件的超时时间。
  • 返回值
    如果执行成功,返回有事件发生的描述符总数,这些描述符散落在readfds、writefds,exceptfds这三个集合中。如果超时过期,返回值可能为零。
    如果执行出错,返回-1,设置errno表示错误;

[!NOTE]
fd_set 是一个固定大小的缓冲区。表示一个有限集合。

3.2 辅助

void FD_ZERO(int fd, fd_set *set);//清除集合宏(删除集合中所有文件描述符)。被用作初始化一个文件描述符集。
void FD_SET(int fd, fd_set *set);//添加一个描述符到集合中。
void FD_CLR(fd_set *set);//从集合中删除一个描述符


int  FD_ISSET(int fd, fd_set *set);
//调用 select() 后,FD_ISSET() 宏可用于测试文件描述符是否仍然存在于集合中。
//如果 FD_ISSET() 返回非零值,则文件描述符 fd 存在于集合中。如果返回0,则不在集合中。

四 总结

可以看到select可以既监听新连接事件,也可以监听读写事件,用一个主线程就完成了多种事件的监听,这就是IO复用,复用了线程。linux下线程和进程是同等的,也可以说是复用了进程。

select比起之前的多线程模型效率大大提升,但是也存在致命缺陷,不能实现百万并发的目标。

缺点以下:

  • 最大连接数为1024
  • 查找集合中的描述符为线性的。查找效率不高。

下一节介绍更高效的IO复用接口poll

三种IO复用的优缺点总结:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值