TCP服务器epoll的多种实现
TCP服务器epoll
的多种实现
对于网络IO会涉及到两个系统对象
- 用户空间中进程或者线程
- 操作系统内核
比如发生read
操作时就会经历两个阶段
- 等待数据就绪
- 将数据从内核缓冲区拷贝到用户缓冲区
由于各个阶段多有不同的情况,一组合么就产生了多种网络 IO 模型
阻塞IO
在Linux中默认所有socket
都是blocking
的,一个典型的读流程
当应用进程调用
read
这个系统调用,如果数据没有到达,或者收到的数据包还不完整就会阻塞read
调用,等待足够的数据到达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
中是否有事件就绪.这里的事件为以下三类:
- 读事件就绪
- 在
socket
内核中,接收缓冲区中的字节数大于或者等于低水位标记SO_RCVLOWAT
,此时调用rec
或read
函数可以无阻塞的读取该文件描述符,并且返回值大于零 - TCP连接的对端关闭连接,此时本端调用r
recv
或read
函数对socket
进行读操作,recv
或read
函数返回0 - 在监听的
socket
上有新的连接请求 - 在
socket
尚有未处理的错误
- 在
- 写事件就绪
- 在
socket
内核中,发送缓冲区中的可用字节数大于等于低水位标记时,可以无阻塞的写,并且返回值大于0 socket
的写操作被关闭时,对一个写操作被关闭的socket
进行写操作,会触发SIGPIPE信号socket
使用非阻塞connect
连接成功或失败时
- 在
- 异常事件就绪
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
来模拟客户端,打开三个终端
关于以上代码,需要注意以下几点:
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]; } }
select
函数也会修改timeval
结构体的值,如果想复用这些变量,需要重新设置timeval tm{}; tm.tv_sec = 1; tm.tv_usec =0;
如果将
select
的timeval
参数设置为NULL
,则select
函数会一直阻塞下去
TCP服务器epoll的多种实现相关推荐
- 一文掌握tcp服务器epoll的多种实现
tcp服务器epoll的多种实现 总结 我们在读写文件的时候,这是一款服务器,CS,这是一个服务器,这个客户端去连接服务器的时候,中间大家知道从连接的这个过程中间产生通过三次握手连接,服务器先进行监听 ...
- 单进程epoll版-TCP服务器(python 版)
epoll版-TCP服务器 1. epoll的优点: 没有最大并发连接的限制,能打开的FD(指的是文件描述符,通俗的理解就是套接字对应的数字编号)的上限远大于1024 效率提升,不是轮询的方式,不会随 ...
- [C/C++后端开发学习] 7 tcp服务器的epoll实现以及Reactor模型
tcp服务器的epoll实现以及Reactor模型 1 IO多路复用 select poll epoll 2 epoll详解 2.1 基本使用方法 2.2 LT水平触发和ET边沿触发 2.3 实现服务 ...
- libevent实现TCP服务器通信
libevent实现TCP服务器通信 1.libevent库安装 1.1 libevent库优点 1.2源码包安装步骤 2.libevent框架 2.1创建事件 2.2添加事件到 event_base ...
- TCP服务器和客户端的链接例子(侧重点在注意关闭套接子,减少套接子的描述子)
TCP服务器和客户端的链接例子(侧重点在注意关闭套接子,减少套接子的描述子) 每个文件或套接口都有一个访问计数,该访问计数在文件表项中维护,它表示当前指向该文件或套接口的打开的描述字个数. 每个文件, ...
- reactor线程模型_从TCP服务器到I/O模型,带你学习Netty
学习Netty就不得不从TCP服务器和I/O模型说起,了解TCP服务器架构和I/O模型的演进有助于深入了解Netty. TCP服务器的架构 一般地,TCP服务器有两种套接字,监听套接字和已连接套接字. ...
- 通讯接口应用笔记3:使用W5500实现Modbus TCP服务器
前面我们设计实现了W5500的驱动程序,也讲解了驱动的使用方式.在最近一次的项目应用中,正好有一个使用W5500实现TCP通讯的需求,所以我们就使用该驱动程序轻松实现.这一篇中我们就来说一说基于我 ...
- 单进程select版-TCP服务器(python 版)
select版-TCP服务器 1. select 原理 在多路复用的模型中,比较常用的有select模型和epoll模型.这两个都是系统接口,由操作系统提供.当然,Python的select模块进行了 ...
- 单进程服务器-epoll版
epoll版-TCP服务器 1. epoll的优点: 没有最大并发连接的限制,能打开的FD(指的是文件描述符,通俗的理解就是套接字对应的数字编号)的上限远大于1024 效率提升,不是轮询的方式,不会随 ...
最新文章
- Druid 连接池 JDBCUtils 工具类的使用
- free malloc
- 程序员怎样才能写出一篇好的技术文章
- 1.12 四类向量组
- IntelliJ idea 中使用Git
- 在Linux上编译dotnet cli的源代码生成.NET Core SDK的安装包
- P1099 树网的核
- 命令行请求网站地址带token_利用gitlab或gitee作为网站免费图床的C#实现
- MySQL · 性能优化 · SQL错误用法详解
- win7删除桌面快捷方式图片的小箭头
- 100行JS代码实现❤坦克大战js小游戏源码 HTML5坦克大战游戏代码(HTML+CSS+JavaScript )...
- 【rmzt】阳光美女win7主题
- aide制作软件教程_AIDE开发教程合集
- SDK数据采集抓取精准主要
- 函数连续性的无穷小定义
- MySQL的关键技术及主要特征_生物特征识别十大关键技术解析
- Spring5春天还是配置地狱
- Git 学习之团队协作(Gitee实操)
- c语言变量按作用域范围分两种,第02天C语言(10):变量-作用域
- 使用winrar压缩分卷(csdn上传大资源使用)
热门文章
- mysql创建的数据库都在哪里看_mysql 怎么查看创建的数据库和表
- 控制usb扫码枪_无线也可以很牢靠-世达SATA热熔胶枪评测
- swift int转string_Swift集合类型协议浅析(下)
- c++归并排序_合并排序法
- 每日一题——leetcode237 删除链表中的结点
- 小程序 WXS响应事件(滚动菜单栏tab吸顶)
- 显示当前行号、文件名和函数名(二)
- 将二进制文件bold转化为文件file
- [react] react的mixins有什么作用?适用于什么场景?
- React开发(125):ant design学习指南之form中的hasFeedback