TCP服务器epoll的多种实现

对于网络IO会涉及到两个系统对象

  1. 用户空间中进程或者线程
  2. 操作系统内核

比如发生read操作时就会经历两个阶段

  1. 等待数据就绪
  2. 将数据从内核缓冲区拷贝到用户缓冲区

由于各个阶段多有不同的情况,一组合么就产生了多种网络 IO 模型

阻塞IO

在Linux中默认所有socket都是blocking的,一个典型的读流程

  1. 当应用进程调用read这个系统调用,如果数据没有到达,或者收到的数据包还不完整就会阻塞read调用,等待足够的数据到达

  2. Kernel准备好数据,他就会将数据从Kernel中拷贝到用户内存,Kernel返回结果,解除block状态,重新运行起来

    于是就有了下面这种服务结构

代码实现一个简单的反射服务器:

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#pragma clang diagnostic push
#pragma ide diagnostic ignored "EndlessLoop"
using std::cout;
using std::endl;
int main(int argc,char * argv[])
{//1.create socketint listenfd = socket(AF_INET,SOCK_STREAM,0);if(listenfd == -1){cout<<"create listenfd failed"<<endl;return -1;}//2.Initialize server addressstruct sockaddr_in bindaddr{};bindaddr.sin_family =AF_INET;bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);bindaddr.sin_port= htons(3000);if (bind(listenfd,(struct  sockaddr*) &bindaddr, sizeof(bindaddr)) == -1){cout<<"bind listen socket failed!"<<endl;return -1;}//3.Start listeningif(listen(listenfd,SOMAXCONN) == -1){cout<<"listen error"<<endl;return -1;}while (true){sockaddr_in clientaddr{};socklen_t  clientaddrlen = sizeof(clientaddr);//4.accept client connectint clientfd = accept(listenfd,(struct sockaddr*)&clientaddr,&clientaddrlen);if (clientfd != -1){//5.Receive data from the clientchar recvBuf[32]={0};int ret = recv(clientfd,recvBuf,32,0);if (ret > 0){cout<<"Receive data from the client:"<<recvBuf<<endl;ret = send(clientfd,recvBuf, strlen(recvBuf),0);if(ret != strlen(recvBuf))cout<<"send failed"<<endl;elsecout<<"send successfully"<<endl;}else{cout<<"Receive data error"<<endl;}close(clientfd);}}//7.close listenclose(listenfd);return 0;
}
#pragma clang diagnostic pop

但这样的架构有巨大的缺陷:

  • 因为所有IO都是阻塞的,这就造成send 过程中线程将被阻塞,会浪费大量的CPU时间,效率极低

非阻塞IO

在Linux下,我们可以主动将socket设置为非阻塞,这时流程就会编程下面这样

返回值 含义
大于0 接收到的字节数
等于0 连接正常断开
等于-1,error等于EAGAIN 表示recv操作还没有完成
等于-1,error不等于EAGAIN 表示recv操作遇到系统错误

使用如下函数将socket设置为非阻塞状态

fcntl( fd, F_SETFL, O_NONBLOCK );

于是我们可以实现如下模型

可以看到服务器线程可以通过循环调用 recv()接口,可以在单个线程内实现对所有连接的数据接收工作。但是上述模型绝不被推荐。因为,循环调用 recv()将大幅度推高 CPU 占用率;此外,在这个方案中 recv()更多的是起到检测“操作是否完成”的作用,实际操作系统提供了更为高效的检测“操作是否完成“作用的接口,例如 select()多路复用模式, 可以一次检测多个连接是否活跃

多路复用IO (IO multiplexing)

采用Linux中的select或者poll

下面我们以select举例

select函数用于检测一组socket中是否有事件就绪.这里的事件为以下三类:

  1. 读事件就绪

    • socket内核中,接收缓冲区中的字节数大于或者等于低水位标记SO_RCVLOWAT,此时调用recread函数可以无阻塞的读取该文件描述符,并且返回值大于零
    • TCP连接的对端关闭连接,此时本端调用rrecvread函数对socket进行读操作,recvread函数返回0
    • 在监听的socket上有新的连接请求
    • socket尚有未处理的错误
  2. 写事件就绪
    • socket内核中,发送缓冲区中的可用字节数大于等于低水位标记时,可以无阻塞的写,并且返回值大于0
    • socket的写操作被关闭时,对一个写操作被关闭的socket进行写操作,会触发SIGPIPE信号
    • socket使用非阻塞connect连接成功或失败时
  3. 异常事件就绪

select()如下:

#include <sys/select.h>   int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);

参数说明

nfds: Linux上的socket也叫作fd,将这个参数的值设置为所有需要使用select函数检测事件的fd中的最大值加1即nfds=max(fd1,fd2,...,fdn)+1
readfds: 需要监听可读事件的fd集合
writefds: 需要监听可写事件fd的集合
exceptfds: 需要监听异常事件的fd集合
timeout: 超时时间,即在这个参数设定的时间内检测这些fd的事件,超过这个时间后,select函数立即返回,这是一个timeval结构体

其定义如下:

struct timeval{      long tv_sec;   /*秒 */long tv_usec;  /*微秒 */   }

参数readfds,writefds,exceptfds的类型都是fd_set,这是一个结构体信息

定义如下

//#define __FD_SETSIZE       1024
#define __NFDBITS   (8 * (int) sizeof (__fd_mask))
#define __FD_ELT(d) ((d) / __NFDBITS)
#define __FD_MASK(d)    ((__fd_mask) (1UL << ((d) % __NFDBITS)))/* fd_set for select and pselect.  */
typedef struct{/* XPG4.2 requires this member name.  Otherwise avoid the namefrom the global namespace.  */
#ifdef __USE_XOPEN//typedef long int __fd_mask;__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#endif} fd_set;/* 最大数量`fd_set'.  */
#define FD_SETSIZE      __FD_SETSIZE

假设未定义__USE_XOPEN整理一年

typedef struct{//typedef long int __fd_mask;long int fds_bits[__FD_SETSIZE / __NFDBITS];} fd_set;

将一个fd添加到fd_set这个集合中时需要使用FD_SET宏,其定义如下:

void FD_SET(fd, fdsetp)

实现如下:

#define  FD_SET(fd, fdsetp)  __FD_SET (fd, fdsetp)

__FD_SET (fd, fdsetp)实现如下:

/* We don't use `memset' because this would require a prototype and   the array isn't too big.  */# define __FD_ZERO(set)  \  do {                                         \    unsigned int __i;                                  \    fd_set *__arr = (set);                            \    for (__i = 0; __i < sizeof (fd_set) / sizeof (__fd_mask); ++__i)         \      __FDS_BITS (__arr)[__i] = 0;                        \  } while (0)#endif  /* GNU CC */#define __FD_SET(d, set) \  ((void) (__FDS_BITS (set)[__FD_ELT (d)] |= __FD_MASK (d)))

举个例子,假设现在fd的值为43,那么在数组下表为0的元素中第43个bit被置为1


再Linux上,向fd_set集合中添加新的fd时,采用位图法确定位置;在windows中添加fd至fd_set的实现规则依次从数组第0个位置开始向后递增


也就是说,FD_SET宏本质上是在一个有1024个连续bit的数组的第fd位置置1.

同理,FD_CLR删除一个fd的原理,也就是将数组的第fd位置置为0

实例;

#include <sys/types.h>#include <sys/socket.h>#include <arpa/inet.h>#include <unistd.h>#include <iostream>#include <cstring>#include <sys/time.h>#include <vector>#include <cerrno>//Customize the value representing invalid fd#pragma clang diagnostic push#pragma ide diagnostic ignored "EndlessLoop"#define INVALID_FD -1int main(int argc,char * argv[]){    //create a listen socket    int listenfd = socket(AF_INET,SOCK_STREAM,0);    if(listenfd == INVALID_FD)    {        printf("创建监听socket失败");        return -1;    }    //init server addr    sockaddr_in bindaddr{};    bindaddr.sin_family = AF_INET;    bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);    bindaddr.sin_port= htons(3000);    if(bind(listenfd,(struct sockaddr*) &bindaddr, sizeof(bindaddr)) == -1)    {        printf("绑定socket失败");        close(listenfd);        return -1;    }    //start listen    if(listen(listenfd,SOMAXCONN) == -1)    {        printf("监听失败!");        close(listenfd);        return -1;    }    //Store the client's socket data    std::vector<int> clientfds;    int maxfd;    while(true)    {        fd_set readset;        FD_ZERO(&readset);        FD_SET(listenfd,&readset);        maxfd = listenfd;        unsigned long clientfdslength = clientfds.size();        for (int i = 0; i < clientfdslength; ++i)        {            if(clientfds[i] != INVALID_FD)            {                FD_SET(clientfds[i],&readset);                if(maxfd<clientfds[i])                    maxfd = clientfds[i];            }        }        timeval tm{};        tm.tv_sec = 1;        tm.tv_usec =0;        int  ret = select(maxfd+1,&readset, nullptr, nullptr,&tm);        if(ret == -1)        {            if (errno != EINTR)                break;        }        //time out        else if (ret ==0 )        {            continue;        }        else        {            //event detected on a socket            if (FD_ISSET(listenfd,&readset))            {                sockaddr_in clientaddr{};                socklen_t  clientaddrlen = sizeof(clientaddr);                //accept client connection                int clientfd = accept(listenfd,(struct sockaddr *)&clientaddr,&clientaddrlen);                if (clientfd == INVALID_FD)                {                    break;                }                std::cout<<"接受到客户端连接,fd:"<<clientfd<<std::endl;                clientfds.push_back(clientfd);            }            else            {                //Assume that the data length sent by the client is not greater than 63                char recvbuf[64];                unsigned long clientfdslength = clientfds.size();                for (int i = 0; i < clientfdslength; ++i)                {                    if(clientfds[i] != INVALID_FD && FD_ISSET(clientfds[i],&readset))                    {                        memset(recvbuf,0, sizeof(recvbuf));                        //accept data                        int length = recv(clientfds[i],recvbuf,64,0);                        //recv的返回值等于0,表示客户端关闭了连接                        if (length <=0 )                        {                            //error                            std::cout<<"error"<<clientfds[i]<<std::endl;                            close(clientfds[i]);                            clientfds[i] == INVALID_FD;                            continue;                        }                        std::cout<<"clientfd: "<<clientfds[i]<<", recv data:"<<recvbuf<<std::endl;                    }                }            }        }    }    //close all client socket    int clientfdslength = clientfds.size();    for (int i = 0; i < clientfdslength; ++i)    {        if(clientfds[i] != INVALID_FD)        {            close(clientfds[i]);        }    }    //close socket    close(listenfd);    return 0;}#pragma clang diagnostic pop

使用nc -v 127.0.0.1 3000来模拟客户端,打开三个终端

关于以上代码,需要注意以下几点:

  1. select函数在调用前后可能会修改readfds,writefds,exceptfds所以想在下次调用select函数时服用这些fd_set变量需要重新清零,添加内容

    for (int i = 0; i < clientfdslength; ++i)        {            if(clientfds[i] != INVALID_FD)            {                FD_SET(clientfds[i],&readset);                if(maxfd<clientfds[i])                    maxfd = clientfds[i];            }        }
    
  2. select函数也会修改timeval结构体的值,如果想复用这些变量,需要重新设置

    timeval tm{};        tm.tv_sec = 1;        tm.tv_usec =0;
    
  3. 如果将selecttimeval参数设置为NULL,则select函数会一直阻塞下去

TCP服务器epoll的多种实现相关推荐

  1. 一文掌握tcp服务器epoll的多种实现

    tcp服务器epoll的多种实现 总结 我们在读写文件的时候,这是一款服务器,CS,这是一个服务器,这个客户端去连接服务器的时候,中间大家知道从连接的这个过程中间产生通过三次握手连接,服务器先进行监听 ...

  2. 单进程epoll版-TCP服务器(python 版)

    epoll版-TCP服务器 1. epoll的优点: 没有最大并发连接的限制,能打开的FD(指的是文件描述符,通俗的理解就是套接字对应的数字编号)的上限远大于1024 效率提升,不是轮询的方式,不会随 ...

  3. [C/C++后端开发学习] 7 tcp服务器的epoll实现以及Reactor模型

    tcp服务器的epoll实现以及Reactor模型 1 IO多路复用 select poll epoll 2 epoll详解 2.1 基本使用方法 2.2 LT水平触发和ET边沿触发 2.3 实现服务 ...

  4. libevent实现TCP服务器通信

    libevent实现TCP服务器通信 1.libevent库安装 1.1 libevent库优点 1.2源码包安装步骤 2.libevent框架 2.1创建事件 2.2添加事件到 event_base ...

  5. TCP服务器和客户端的链接例子(侧重点在注意关闭套接子,减少套接子的描述子)

    TCP服务器和客户端的链接例子(侧重点在注意关闭套接子,减少套接子的描述子) 每个文件或套接口都有一个访问计数,该访问计数在文件表项中维护,它表示当前指向该文件或套接口的打开的描述字个数. 每个文件, ...

  6. reactor线程模型_从TCP服务器到I/O模型,带你学习Netty

    学习Netty就不得不从TCP服务器和I/O模型说起,了解TCP服务器架构和I/O模型的演进有助于深入了解Netty. TCP服务器的架构 一般地,TCP服务器有两种套接字,监听套接字和已连接套接字. ...

  7. 通讯接口应用笔记3:使用W5500实现Modbus TCP服务器

      前面我们设计实现了W5500的驱动程序,也讲解了驱动的使用方式.在最近一次的项目应用中,正好有一个使用W5500实现TCP通讯的需求,所以我们就使用该驱动程序轻松实现.这一篇中我们就来说一说基于我 ...

  8. 单进程select版-TCP服务器(python 版)

    select版-TCP服务器 1. select 原理 在多路复用的模型中,比较常用的有select模型和epoll模型.这两个都是系统接口,由操作系统提供.当然,Python的select模块进行了 ...

  9. 单进程服务器-epoll版

    epoll版-TCP服务器 1. epoll的优点: 没有最大并发连接的限制,能打开的FD(指的是文件描述符,通俗的理解就是套接字对应的数字编号)的上限远大于1024 效率提升,不是轮询的方式,不会随 ...

最新文章

  1. Druid 连接池 JDBCUtils 工具类的使用
  2. free malloc
  3. 程序员怎样才能写出一篇好的技术文章
  4. 1.12 四类向量组
  5. IntelliJ idea 中使用Git
  6. 在Linux上编译dotnet cli的源代码生成.NET Core SDK的安装包
  7. P1099 树网的核
  8. 命令行请求网站地址带token_利用gitlab或gitee作为网站免费图床的C#实现
  9. MySQL · 性能优化 · SQL错误用法详解
  10. win7删除桌面快捷方式图片的小箭头
  11. 100行JS代码实现❤坦克大战js小游戏源码 HTML5坦克大战游戏代码(HTML+CSS+JavaScript )...
  12. 【rmzt】阳光美女win7主题
  13. aide制作软件教程_AIDE开发教程合集
  14. SDK数据采集抓取精准主要
  15. 函数连续性的无穷小定义
  16. MySQL的关键技术及主要特征_生物特征识别十大关键技术解析
  17. Spring5春天还是配置地狱
  18. Git 学习之团队协作(Gitee实操)
  19. c语言变量按作用域范围分两种,第02天C语言(10):变量-作用域
  20. 使用winrar压缩分卷(csdn上传大资源使用)

热门文章

  1. mysql创建的数据库都在哪里看_mysql 怎么查看创建的数据库和表
  2. 控制usb扫码枪_无线也可以很牢靠-世达SATA热熔胶枪评测
  3. swift int转string_Swift集合类型协议浅析(下)
  4. c++归并排序_合并排序法
  5. 每日一题——leetcode237 删除链表中的结点
  6. 小程序 WXS响应事件(滚动菜单栏tab吸顶)
  7. 显示当前行号、文件名和函数名(二)
  8. 将二进制文件bold转化为文件file
  9. [react] react的mixins有什么作用?适用于什么场景?
  10. React开发(125):ant design学习指南之form中的hasFeedback