Linux网络——高级IO
目录
一.五种IO模型
1.阻塞式IO
2.非阻塞式IO
3.信号驱动IO
4.多路转接IO:
5.异步IO
二.同步通信 vs 异步通信
三.设置非阻塞IO
1.阻塞 vs 非阻塞
2.非阻塞IO
3.实现函数SetNoBlock
四.I/O多路转接之select
1.初识select
2.select函数原型
3.socket就绪条件
4.设置select服务器
5. select的特点
6.select缺点
五.I/O多路转接之poll
1.poll函数接口
2.poll的工作模式
3.poll 的特点
4.poll的缺点
5.poll示例,使用poll实现多路转接服务器
六.I/O多路转接之epoll
1.初识epoll
2.epoll的相关系统调用
3.epoll工作原理
4.epoll工作模式
5.epoll的使用场景
6.epoll示例,使用poll实现多路转接服务器

一.五种IO模型
1.阻塞式IO
阻塞IO: 在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方式.
阻塞IO是最常见的IO模型.

例如:
代码:
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cstring>using namespace std;int main()
{char buff[1024];int n = read(0, buff, sizeof(buff));cout << buff << endl;
}

没有数据来时就是阻塞在这里,当有数据时才会继续往后执行。

2.非阻塞式IO
非阻塞IO: 如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码.


3.信号驱动IO
信号驱动IO: 内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作.

4.多路转接IO:
IO多路转接: 虽然从流程图上看起来和阻塞IO类似. 实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态.

select一次可以等待多个文件描述符,如果这些文件描述符中有就绪的文件描述符,就会通知应用层,让应用层进行读取。
5.异步IO
异步IO: 由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据).

小结:任何IO过程中, 都包含两个步骤. 第一是等待, 第二是拷贝. 而且在实际的应用场景中, 等待消耗的时间往往都远远高于拷贝的时间. 让IO更高效, 最核心的办法就是让等待的时间占比尽量小.
二.同步通信 vs 异步通信
同步和异步关注的是消息通信机制:
- 所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回. 但是一旦调用返回,就得到结果了; 换句话说,就是由调用者主动等待这个调用的结果;
- 异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果; 换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果; 而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用.
另外, 我们回忆在讲多进程多线程的时候, 也提到同步和互斥. 这里的同步通信和进程之间的同步是完全不想干的概念.
- 进程/线程同步也是进程/线程之间直接的制约关系。
- 是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待、传递信息所产生的制约关系. 尤其是在访问临界资源的时候。
三.设置非阻塞IO
1.阻塞 vs 非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.
- 阻塞调用是指调用结果返回之前,当前线程会被挂起. 调用线程只有在得到结果之后才会返回.
- 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程.
2.非阻塞IO
fcntl函数可以获取一个文件描述符的状态标志位,同时也可以设置一个文件描述符的状态标志位。
函数原型如下:
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
传入的cmd的值不同, 后面追加的参数也不相同.
fcntl函数有5种功能:
- 复制一个现有的描述符(cmd=F_DUPFD).
- 获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).
- 获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).
- 获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).
- 获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW).
我们此处只是用第三种功能, 获取/设置文件状态标记, 就可以将一个文件描述符设置为非阻塞.
3.实现函数SetNoBlock
基于fcntl, 我们实现一个SetNoBlock函数, 将文件描述符设置为非阻塞。
void SetNoBlock(int fd)
{//获取文件描述符的标记位int fl = fcntl(fd, F_GETFL);if (fl < 0){perror("fcntl");return;}//设置,添加O_NONBLOCK到文件描述符的标记位中fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
使用F_GETFL将当前的文件描述符的属性取出来(这是一个位图).
然后再使用F_SETFL将文件描述符设置回去. 设置回去的同时, 加上一个O_NONBLOCK参数.
测试:
轮询方式读取标准输入
#include <iostream>
#include <cstring>
#include <cstdio>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <fcntl.h>
using namespace std;void SetNoBlock(int fd)
{int fl = fcntl(fd, F_GETFL);if (fl < 0){perror("fcntl");return;}fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
int main()
{// 将标准输入设置成非阻塞SetNoBlock(0);char buff[1024];// 循环读取while (1){int n = read(0, buff, sizeof(buff));if (n < 0){if (errno == EAGAIN || errno == EWOULDBLOCK){printf("No Data,errno:%d,%s\n", errno, strerror(errno));}sleep(1);continue;}cout << "read Data:" << buff << endl;}
}
运行结果:

四.I/O多路转接之select
1.初识select
系统提供select函数来实现多路复用输入/输出模型.
- select系统调用是用来让我们的程序监视多个文件描述符的状态变化的;
- 程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变;
2.select函数原型
select的函数原型如下:
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
参数解释:
- 参数nfds是需要监视的最大的文件描述符值+1;
- rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集 合及异常文件描述符的集合;
- 参数timeout为结构timeval,用来设置select()的等待时间.
关于fd_set结构和参数:
/* The fd_set member is required to be an array of longs. */
typedef long int __fd_mask;
/* fd_set for select and pselect. */
#define __FD_SETSIZE 1024
#define __NFDBITS (8 * (int)sizeof(__fd_mask))
typedef struct
{/* XPG4.2 requires this member name. Otherwise avoid the namefrom the global namespace. */
#ifdef __USE_XOPEN// _fds_bits是一个1024/(8*8)=16个元素的__fd_mask(long int)数组,共1024比特位__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
#define __FDS_BITS(set) ((set)->fds_bits)
#else__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
#define __FDS_BITS(set) ((set)->__fds_bits)
#endif
} fd_set;
所以fd_set结构本质就是一个位图结构,能够存放1024个比特位的位图。每一个位置都对应了一个文件描述符是否有效。例如:作为输入型参数:readfds位图第5号位置为1,5号文件描述符上的读事件正在被select监视。
作为select参数,fd_set*类型,都是输入输出性参数,因为select系统调用是用来让我们的程序监视多个文件描述符的状态变化的,一旦select监视的文件描述符有就绪的,fd_set* readfds,就会作为结果返回给用户程序,作为输出型参数:如果readfds位图第5号位置为1,代表5号文件描述符上的读事件已经就绪。
同理writefds,exceptfds也是一样的。
注意:当select一旦返回之前所设置在内核的fd_set也会消失,我们需要重新设置。
系统中提供了一组操作fd_set的接口, 来比较方便的操作位图:
void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd 的位。
int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组set中相关fd 的位是否为真。
void FD_SET(int fd, fd_set *set); // 用来设置描述词组set中相关fd的位。
void FD_ZERO(fd_set *set); // 用来清除描述词组set的全部位。
参数timeout取值:
struct timeval{__time_t tv_sec; /* Seconds. 秒*/__suseconds_t tv_usec; /* Microseconds. 微秒 */};
- NULL:则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件;
- 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。
- 特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回。
3.socket就绪条件
读就绪:
- socket内核中, 接收缓冲区中的字节数, 大于等于低水位标记SO_RCVLOWAT. 此时可以无阻塞的读该文件描述符, 并且返回值大于0;
- socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;
- 监听的socket上有新的连接请求;
- socket上有未处理的错误;
写就绪:
- socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0;
- socket的写操作被关闭(close或者shutdown). 对一个写操作被关闭的socket进行写操作, 会触发SIGPIPE信号;
- socket使用非阻塞connect连接成功或失败之后;
- socket上有未读取的错误;
异常就绪:
- socket上收到带外数据,关于带外数据, 和TCP紧急模式相关。
4.设置select服务器
server.hpp:
#include <iostream>
#include <sys/select.h>
#include "Sock.hpp"
#include "Log.hpp"
using namespace std;#define READ_EVENT (1 << 0)
#define WRITE_EVENT (1 << 1)
#define EXCEP_EVENT (1 << 2)struct FdEvent
{int fd;uint16_t event;string clientip;uint16_t clientport;
};class Server
{typedef FdEvent type_t;static const int N = (sizeof(fd_set) * 8);static const int defaultfd = -1;public:Server(uint16_t port): _tcplisten(port), _port(port){_tcplisten.Bind();_tcplisten.Listen();// 初始化for (int i = 0; i < N; i++){_fdarr[i].fd = defaultfd;_fdarr[i].event = 0;_fdarr[i].clientport = -1;}}// 处理连接void Accept(){// 1.接受连接string clientip;uint16_t clientport;int fd = _tcplisten.Accept(&clientip, &clientport);// 2.将新的accept连接放入fdarr,后续让select监测int i;for (i = 1; i < N; i++){if (_fdarr[i].fd == defaultfd){_fdarr[i].fd = fd;_fdarr[i].clientip = clientip;_fdarr[i].clientport = clientport;_fdarr[i].event = READ_EVENT;break;}}if (i == N){close(fd);Logmessage(Error, "_fdarr[] is full!!!!!");}}void ServerIO(type_t fdevent, int index){if (fdevent.event & READ_EVENT){char buff[1024];int n = read(fdevent.fd, buff, sizeof(buff));if (n > 0) // 读取成功{buff[n] = 0;cout << "client#:" << buff;// 返回一句话string str = "server#: I have message, ";str += buff;send(fdevent.fd, str.c_str(), str.size(), 0);}else if (n == 0) // 对端关闭{Logmessage(Info, "client close....");close(fdevent.fd);_fdarr[index].fd = defaultfd;_fdarr[index].event = 0;_fdarr[index].clientport = 0;_fdarr[index].clientip = "";}else // 读取失败{Logmessage(Info, "Read Err,errno:%d,%s", errno, strerror(errno));close(fdevent.fd);_fdarr[index].fd = defaultfd;_fdarr[index].event = 0;_fdarr[index].clientport = 0;_fdarr[index].clientip = "";}}else if (fdevent.event & WRITE_EVENT){// TODO}else{ // fdevent.event & EXCEP_EVENT// TODO}}void HeadleEvent(fd_set fdset_read, fd_set fdset_write){// 这里说明已经有文件描述符就绪了,但是我们要区分到底是listen套接字,还是accept通信套接字// 已经就绪的文件描述符就在fdset里面// 遍历_fdarr,查看是否在fdset中for (int i = 0; i < N; i++){if (_fdarr[i].fd != defaultfd){// 就绪的文件描述符是listen套接字if (_fdarr[i].fd == _tcplisten.FD() && FD_ISSET(_fdarr[i].fd, &fdset_read)){Accept(); // 有新连接到来}else if (_fdarr[i].fd != _tcplisten.FD() && FD_ISSET(_fdarr[i].fd, &fdset_read) || FD_ISSET(_fdarr[i].fd, &fdset_write)) // 就绪的文件描述符是accept套接字{ServerIO(_fdarr[i], i); // 有读写事件就绪}}}}void start(){_fdarr[0].fd = _tcplisten.FD();_fdarr[0].event = READ_EVENT;while (1){// 让select对我们的listen套接字进行监测// 因为select,fdset是一个输入输出型参数,调用一次以后,// select就不能知道之前有哪些文件描述符需要监测了,// 所以需要对_fdarr数组记录都有哪些文件描述符需要select检测,每次循环都设置一次。// 关心读事件位图fd_set fdset_read;FD_ZERO(&fdset_read);// 关心写事件位图fd_set fdset_write;FD_ZERO(&fdset_write);int maxfd = _fdarr[0].fd;for (int i = 0; i < N; i++){if (_fdarr[i].fd != defaultfd){// 将需要关心的文件描述符写入fd_set结构if (_fdarr[i].event & READ_EVENT)FD_SET(_fdarr[i].fd, &fdset_read);else if (_fdarr[i].event & WRITE_EVENT)FD_SET(_fdarr[i].fd, &fdset_write);// 求出文件描述符最大值maxfd = max(maxfd, _fdarr[i].fd);}}struct timeval select_time = {2, 0}; // 超时间2秒0微秒int n = select(maxfd + 1, &fdset_read, &fdset_write, nullptr, &select_time);switch (n){case 0: // 阻塞超时Logmessage(Debug, "time out...,errno:%d,%s", errno, strerror(errno));break;case -1: // 出现错误Logmessage(Warning, "errno:%d,%s", errno, strerror(errno));break;default: // 检测到有事件就绪Logmessage(Debug, "have a event really...");HeadleEvent(fdset_read, fdset_write); // 处理就绪的事件debug_fdarr();sleep(1);break;}}}void debug_fdarr(){cout << "_fdarr[]:";for (int i = 0; i < N; i++){if (_fdarr[i].fd != defaultfd)cout << _fdarr[i].fd << " ";}cout << endl;}private:uint16_t _port; // 端口Tcp _tcplisten; // TCP网络套接字type_t _fdarr[N]; // 记录需要检测的文件描述符
};
main.cc:
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <fcntl.h>
#include "Log.hpp"
#include "server.hpp"using namespace std;int main()
{Server tcpserver(8081);tcpserver.start();return 0;
}
Log.hpp
#pragma once#include <iostream>
#include <unordered_map>
#include <string>
#include <cstdio>
#include <ctime>
#include <cstdarg>
#include <sys/types.h>
#include <unistd.h>using namespace std;
enum
{Debug = 0,Info,Warning,Error,Fatal,Uknown
};unordered_map<int, string> Level{{0, "Debug"}, {1, "Info"}, {2, "Warning"}, {3, "Error"}, {4, "Fatal"}, {5, "Uknown"}};string gettime()
{time_t cur = time(nullptr);struct tm *t = localtime(&cur);char buff[128];snprintf(buff, sizeof(buff), "%d-%d-%d %d:%d:%d", t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec);return buff;
}//[等级+日志时间+进程pid]+[描述信息]。
void Logmessage(int level, const char *format, ...)
{char logleft[1024];string mess_level = Level[level];string mess_time = gettime();sprintf(logleft, "[%s][%s][PID:%d]", mess_level.c_str(), mess_time.c_str(), getpid());char logright[1024];va_list p;va_start(p, format);vsnprintf(logright, sizeof(logright), format, p);va_end(p);printf("%s %s\n", logleft, logright);
}
Sock.hpp
#pragma once
#include <iostream>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Log.hpp"
#define TCP SOCK_STREAM
#define UDP SOCK_DGRAM
const static int backlog = 1;enum
{SOCK_ERR = 10,BING_ERR,LISTEN_ERR,CONNECT_ERR
};class Udp
{
public:Udp(int SOCK){_listensock = socket(AF_INET, SOCK, 0);if (_listensock == -1){Logmessage(Fatal, "socket err ,error code %d,%s", errno, strerror(errno));exit(SOCK_ERR);}}Udp(uint16_t port, int SOCK): _port(port){_listensock = socket(AF_INET, SOCK, 0);if (_listensock == -1){Logmessage(Fatal, "socket err ,error code %d,%s", errno, strerror(errno));exit(10);}}void Bind(){// 设置无需等待TIME_WAIT状态int opt = 1;setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));struct sockaddr_in host;host.sin_family = AF_INET;host.sin_port = htons(_port);host.sin_addr.s_addr = INADDR_ANY; // #define INADDR_ANY 0x00000000socklen_t hostlen = sizeof(host);int n = bind(_listensock, (struct sockaddr *)&host, hostlen);if (n == -1){Logmessage(Fatal, "bind err ,error code %d,%s", errno, strerror(errno));exit(BING_ERR);}}int FD(){return _listensock;}~Udp(){close(_listensock);}protected:int _listensock;uint16_t _port;
};class Tcp : public Udp
{
public:Tcp(uint16_t port): Udp(port, TCP){}Tcp(): Udp(TCP){}void Listen(){int n = listen(_listensock, backlog);if (n == -1){Logmessage(Fatal, "listen err ,error code %d,%s", errno, strerror(errno));exit(LISTEN_ERR);}}int Accept(string *clientip, uint16_t *clientport){struct sockaddr_in client;socklen_t clientlen = sizeof(client);int sock = accept(_listensock, (struct sockaddr *)&client, &clientlen);if (sock < 0){Logmessage(Warning, "bind err ,error code %d,%s", errno, strerror(errno));}else{*clientip = inet_ntoa(client.sin_addr);*clientport = ntohs(client.sin_port);}return sock;}void Connect(string ip, uint16_t port){struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(port);server.sin_addr.s_addr = inet_addr(ip.c_str());socklen_t hostlen = sizeof(server);int n = connect(_listensock, (struct sockaddr *)&server, hostlen);if (n == -1){Logmessage(Fatal, "Connect err ,error code %d,%s", errno, strerror(errno));exit(CONNECT_ERR);}}~Tcp(){}
};
测试结果:

5. select的特点
可监控的文件描述符个数取决与sizeof(fd_set)的值. 我这边服务器上sizeof(fd_set)=128,每bit表示一个文件描述符,则我服务器上支持的最大文件描述符是128*8=1024.
将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd:
- 一是用于再select 返回后,array作为源数据和fd_set进行FD_ISSET判断。
- 二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。
6.select缺点
- 每次调用select, 都需要手动设置fd集合, 从接口使用角度来说也非常不便。
- 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大。
- 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大。
- select支持的文件描述符数量有限。
五.I/O多路转接之poll
1.poll函数接口
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
/* Data structure describing a polling request. */
struct pollfd
{int fd; /* File descriptor to poll. */short int events; /* Types of events poller cares about. */short int revents; /* Types of events that actually occurred. */
};
参数说明:
- fds是一个poll函数监听的结构列表. 每一个元素中, 包含了三部分内容: 文件描述符, 监听的事件集合, 返回的事件集合。
- nfds表示fds数组的长度。
- timeout表示poll函数的超时时间, 单位是毫秒(ms)。
events和revents的取值:
| 事件 | 描述 | 是否可作为输入 | 是否可作为输出 |
| POLLIN | 数据可读(包括普通数据和优先数据) | 是 | 是 |
| POLLRDNORM | 普通数据可读 | 是 | 是 |
| POLLRDBAND | 优先级带数据可读(Linux不支持) | 是 | 是 |
| POLLPRI | 高优先级数据可读,比如TCP带外数据 | 是 | 是 |
| POLLOUT | 数据(包括普通数据和优先数据)可写 | 是 | 是 |
| POLLWRNORM | 普通数据可写 | 是 | 是 |
| POLLWRBAND | 优先级带数据可写 | 是 | 是 |
| POLLRDHUP | TCP连接被对方关闭,或者对方关闭了写操作 | 是 | 是 |
| POLLERR | 错误 | 否 | 是 |
| POLLHUP | 挂起 | 否 | 是 |
| POLLNVAL | 文件描述符没被打开 | 否 | 是 |
返回结果:
- 返回值小于0, 表示出错;
- 返回值等于0, 表示poll函数等待超时;
- 返回值大于0, 表示poll由于监听的文件描述符就绪而返回
2.poll的工作模式
首先仍需要每次循环都需要将pollfd数组,用户程序拷贝给内核,
第一个参数是一个输入输出型参数,当poll返回时,会设置就绪的事件的pollfd结构中revent。
检查数组中哪些被检测的文件pollfd结构中的revent不是默认值,就知道那个文件描述符的什么事件就绪了。
3.poll 的特点
不同于select使用三个位图来表示三个fdset的方式,poll使用一个pollfd的指针实现.
- pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式. 接口使用比select更方便.
- poll并没有最大数量限制,使用链表管理的。 (但是数量过大后性能也是会下降).
4.poll的缺点
poll中监听的文件描述符数目增多时
- 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符.
- 每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中.
- 同时连接的大量客户端在一时刻可能只有很少的处于就绪状态, 因此随着监视的描述符数量的增长, 其效率也会线性下降.
5.poll示例,使用poll实现多路转接服务器
server.hpp
#include <iostream>
#include <sys/select.h>
#include <poll.h>#include "Sock.hpp"
#include "Log.hpp"
using namespace std;#define defaultevent 0// struct pollfd
// {
// int fd; /* file descriptor */
// short events; /* requested events */
// short revents; /* returned events */
// };// POLLIN:There is data to read.
// POLLPRI:There is urgent data to read (e.g., out-of-band data on TCP socket; pseudoterminal master in packet mode has
// seen state change in slave).
// POLLOUT:Writing now will not block.
// POLLRDHUP:(since Linux 2.6.17)
// Stream socket peer closed connection, or shut down writing half of connection. The _GNU_SOURCE feature test
// macro must be defined (before including any header files) in order to obtain this definition.
// POLLERR:Error condition (output only).
// POLLHUP:Hang up (output only).
// POLLNVAL:Invalid request: fd not open (output only).class Poll_Server
{typedef pollfd type_t;static const int N = 4096;static const int defaultfd = -1;public:Poll_Server(uint16_t port): _tcplisten(port), _port(port){_tcplisten.Bind();_tcplisten.Listen();// 初始化for (int i = 0; i < N; i++){_fdarr[i].fd = defaultfd;_fdarr[i].events = defaultevent;_fdarr[i].revents = defaultevent;}}// 处理连接void Accept(){// 1.接受连接string clientip;uint16_t clientport;int fd = _tcplisten.Accept(&clientip, &clientport);// 2.将新的accept连接放入fdarr,后续让poll监测int i;for (i = 1; i < N; i++){if (_fdarr[i].fd == defaultfd){_fdarr[i].fd = fd;_fdarr[i].events = POLLIN;break;}}if (i == N){close(fd);Logmessage(Error, "_fdarr[] is full!!!!!");}}void ServerIO(type_t fdevent, int index){if (fdevent.revents & POLLIN){char buff[1024];int n = read(fdevent.fd, buff, sizeof(buff));if (n > 0) // 读取成功{buff[n] = 0;cout << "client#:" << buff;// 返回一句话string str = "server#: I have message, ";str += buff;send(fdevent.fd, str.c_str(), str.size(), 0);}else if (n == 0) // 对端关闭{Logmessage(Info, "对端关闭");close(fdevent.fd);_fdarr[index].fd = defaultfd;_fdarr[index].events = defaultevent;}else // 读取失败{Logmessage(Info, "读取失败,errno:%d,%s", errno, strerror(errno));close(fdevent.fd);_fdarr[index].fd = defaultfd;_fdarr[index].events = defaultevent;}}else if (fdevent.revents & POLLOUT){// TODO}else{ // fdevent.event & EXCEP_EVENT// TODO}}void HeadleEvent(){for (int i = 0; i < N; i++){if (_fdarr[i].fd != defaultfd){if (_fdarr[i].fd == _tcplisten.FD() && _fdarr[i].revents & POLLIN) // 就绪的文件描述符是listen套接字{Accept(); // 有新连接到来}else if (_fdarr[i].fd != _tcplisten.FD() && _fdarr[i].revents & POLLIN || _fdarr[i].revents & POLLOUT) // 就绪的文件描述符是accept套接字{ServerIO(_fdarr[i], i); // 有读写事件就绪}}}}void start(){_fdarr[0].fd = _tcplisten.FD();_fdarr[0].events = POLLIN;while (1){int timeout = 1000;int n = poll(_fdarr, N, timeout);switch (n){case 0: // 阻塞超时Logmessage(Debug, "time out...,errno:%d,%s", errno, strerror(errno));break;case -1: // 出现错误Logmessage(Warning, "errno:%d,%s", errno, strerror(errno));break;default: // 检测到有事件就绪Logmessage(Debug, "have a event really...");HeadleEvent();debug_fdarr();sleep(1);break;}}}void debug_fdarr(){cout << "_fdarr[]:";for (int i = 0; i < N; i++){if (_fdarr[i].fd != defaultfd)cout << _fdarr[i].fd << " ";}cout << endl;}~Poll_Server(){close(_tcplisten.FD());}private:uint16_t _port;Tcp _tcplisten;type_t _fdarr[N]; // 记录需要检测的文件描述符
};
测试:

六.I/O多路转接之epoll
1.初识epoll
按照man手册的说法: 是为处理大批量句柄而作了改进的poll.
它是在2.5.44内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44)
它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法.
2.epoll的相关系统调用
epoll 有3个相关的系统调用:
(1) epoll_create
int epoll_create(int size);
创建一个epoll的句柄,返回一个epoll文件描述符
- 自从linux2.6.8之后,size参数是被忽略的.
- 用完之后, 必须调用close()关闭.

epoll_create()返回一个引用新epoll实例的文件描述符。此文件描述符用于所有对epoll接口的后续调用。当不再需要时,epoll_create()返回的文件描述符应为使用close关闭。当所有引用epoll实例的文件描述符都已关闭时,内核将销毁实例,并释放相关联的资源以供重用。
(2)epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注册函数:
它不同于select()是在监听事件时告诉内核要监听什么类型的事件, 而是在这里先注册要监听的事件类型.
- 第一个参数是epoll_create()的返回值(epoll实例的文件描述符).
- 第二个参数表示动作,用三个宏来表示.
- 第三个参数是需要监听的fd.
- 第四个参数是告诉内核需要监听什么事件.
第二个参数的取值:
- EPOLL_CTL_ADD :注册新的fd到epfd中;
- EPOLL_CTL_MOD :修改已经注册的fd的监听事件;
- EPOLL_CTL_DEL :从epfd中删除一个fd;
struct epoll_event结构如下:
typedef union epoll_data{void *ptr;int fd;//文件描述符uint32_t u32;uint64_t u64;} epoll_data_t;struct epoll_event{uint32_t events; /* Epoll events */epoll_data_t data; /* User data variable */};
events可以是以下几个宏的集合:
- EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
- EPOLLOUT : 表示对应的文件描述符可以写;
- EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
- EPOLLERR : 表示对应的文件描述符发生错误;
- EPOLLHUP : 表示对应的文件描述符被挂断;
- EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
- EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加入到EPOLL队列里.
(3) epoll_wait
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
收集在epoll监控的事件中已经发送的事件:
- 参数events是分配好的epoll_event结构体数组.
- epoll将会把发生的事件赋值到events数组中 (events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存),这个数组需要我们自己维护。
- maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size.
- 参数timeout是超时时间 (毫秒,0会立即返回,-1是永久阻塞).
- 如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时, 返回小于0表示函数失败.
3.epoll工作原理
当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关.
struct eventpoll{..../*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/struct rb_root rbr;/*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/struct list_head rdlist;....
};
- 每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件.
- 这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度).
- 而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当响应的事件发生时会调用这个回调方法.
- 这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中.
- 在epoll中,对于每一个事件,都会建立一个epitem结构体。
struct epitem{struct rb_node rbn;//红黑树节点struct list_head rdllink;//双向链表节点struct epoll_filefd ffd; //事件句柄信息struct eventpoll *ep; //指向其所属的eventpoll对象struct epoll_event event; //期待发生的事件类型
}
- 当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可.
- 如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户. 这个操作的时间复杂度是O(1).
总结一下, epoll的使用过程就是三部曲:
- 调用epoll_create创建一个epoll句柄;
- 调用epoll_ctl, 将要监控的文件描述符进行注册;
- 调用epoll_wait, 等待文件描述符就绪;
epoll的优点(和 select 的缺点对应) :
- 接口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文件描述符, 也做到了输入输出参数分离开
- 数据拷贝轻量: 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)
- 事件回调机制: 避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中,epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度O(1). 即使文件描述符数目很多, 效率也不会受到影响.
- 没有数量限制: 文件描述符数目无上限.
4.epoll工作模式
epoll有2种工作方式-水平触发(LT)和边缘触发(ET):
(1)水平触发Level Triggered 工作模式
举一个栗子:
假如有这样一个例子:
- 我们已经把一个tcp socket添加到epoll描述符
- 这个时候socket的另一端被写入了2KB的数据
- 调用epoll_wait,并且它会返回. 说明它已经准备好读取操作
- 然后调用read, 只读取了1KB的数据
- 继续调用epoll_wait......
epoll默认状态下就是LT工作模式:
- 当epoll检测到socket上事件就绪的时候, 可以不立刻进行处理. 或者只处理一部分.
- 如上面的例子, 由于只读了1K数据, 缓冲区中还剩1K数据, 在第二次调用 epoll_wait 时, epoll_wait仍然会立刻返回并通知socket读事件就绪.
- 直到缓冲区上所有的数据都被处理完, epoll_wait 才不会立刻返回.
- 支持阻塞读写和非阻塞读写
边缘触发Edge Triggered工作模式:
如果我们在第1步将socket添加到epoll描述符的时候使用了EPOLLET标志, epoll进入ET工作模式.
- 当epoll检测到socket上事件就绪时, 必须立刻处理.
- 如上面的例子, 虽然只读了1K的数据, 缓冲区还剩1K的数据, 在第二次调用 epoll_wait 的时候,
- epoll_wait 不会再返回了.
- 也就是说, ET模式下, 文件描述符上的事件就绪后, 只有一次处理机会.
- ET的性能比LT性能更高( epoll_wait 返回的次数少了很多). Nginx默认采用ET模式使用epoll.
- 只支持非阻塞的读写
select和poll其实也是工作在LT模式下. epoll既可以支持LT, 也可以支持ET.
对比LT和ET:
- LT是 epoll 的默认行为. 使用 ET 能够减少 epoll 触发的次数. 但是代价就是强逼着程序猿一次响应就绪过程中就把所有的数据都处理完.
- 相当于一个文件描述符就绪之后, 不会反复被提示就绪, 看起来就比 LT 更高效一些. 但是在 LT 情况下如果也能做到每次就绪的文件描述符都立刻处理, 不让这个就绪被重复提示的话, 其实性能也是一样的.
- 另一方面, ET 的代码复杂程度更高了.
理解ET模式和非阻塞文件描述符:
使用 ET 模式的 epoll, 需要将文件描述设置为非阻塞. 这个不是接口上的要求, 而是 "工程实践" 上的要求.假设这样的场景: 服务器接受到一个10k的请求, 会向客户端返回一个应答数据. 如果客户端收不到应答, 不会发送第二个10k请求.
如果服务端写的代码是阻塞式的read, 并且一次只 read 1k 数据的话(read不能保证一次就把所有的数据都读出来,参考 man 手册的说明, 可能被信号打断), 剩下的9k数据就会待在缓冲区中.
此时由于 epoll 是ET模式, 并不会认为文件描述符读就绪. epoll_wait 就不会再次返回. 剩下的 9k 数据会一直在缓冲区中. 直到下一次客户端再给服务器写数据. epoll_wait 才能返回。
所以, 为了解决上述问题(阻塞read不一定能一下把完整的请求读完), 于是就可以使用非阻塞轮训的方式来读缓冲区,保证一定能把完整的请求都读出来.
而如果是LT没这个问题. 只要缓冲区中的数据没读完, 就能够让 epoll_wait 返回文件描述符读就绪.
5.epoll的使用场景
epoll的高性能, 是有一定的特定场景的. 如果场景选择的不适宜, epoll的性能可能适得其反.
- 对于多连接, 且多连接中只有一部分连接比较活跃时, 比较适合使用epoll.
例如, 典型的一个需要处理上万个客户端的服务器, 例如各种互联网APP的入口服务器, 这样的服务器就很适合epoll.
如果只是系统内部, 服务器和服务器之间进行通信, 只有少数的几个连接, 这种情况下用epoll就并不合适. 具体要根据需求和场景特点来决定使用哪种IO模型
6.epoll示例,使用poll实现多路转接服务器
server.hpp
#pragma once
#include <iostream>
#include <string>
#include <unordered_map>
#include <functional>
#include <cassert>
#include <sys/types.h>
#include <sys/socket.h>
#include "Sock.hpp"
#include "Log.hpp"
#include "Epoll.hpp"
#include "Util.hpp"
#include "Protocol.hpp"
struct connection;
class EpollServer;
const static int timeout = -1;
unordered_map<uint32_t, string> EventMap = {{EPOLLIN, "EPOLLIN"}, {EPOLLOUT, "EPOLLOUT"}};
using CallBack = std::function<void(connection *)>;
using Func = std::function<Responce(Request)>;struct connection
{connection(int fd, uint32_t events): _fd(fd), _event(events){}void SetCallBack(CallBack recvfunc, CallBack sendfunc, CallBack excepfunc){_recvfunc = recvfunc;_sendfunc = sendfunc;_excepfunc = excepfunc;}int _fd; // 文件描述符uint32_t _event; // 关心的事件string _recvstr; // 接受缓冲区string _sendstr; // 发送缓冲区CallBack _recvfunc; // 处理读取CallBack _sendfunc; // 处理发送CallBack _excepfunc; // 处理异常// 客户端的信息string _clientip;uint16_t _port;
};class EpollServer
{
public:EpollServer(uint16_t port, Func func): _port(port), _sock(port), _func(func){_sock.Bind();_sock.Listen();_epoll.Create();AddConnection(_sock.FD(), EPOLLIN | EPOLLET);Logmessage(Debug, "init server success");}void Distribution(){while (1){LoopOnce();}}void LoopOnce(){// 1.提取就绪链接int n = _epoll.Wait(_eventarr, gsize, timeout);for (int i = 0; i < n; i++){// 提取就绪链接信息int fd = _eventarr[i].data.fd;uint32_t event = _eventarr[i].events;// 2.处理就绪事件// 2.1将就绪的异常事件,转移到读写事件就绪if ((event & EPOLLERR) || (event & EPOLLHUP))event |= (EPOLLIN | EPOLLOUT | EPOLLET);// 2.2 处理就绪读写事件if ((event & EPOLLIN) && ConnIsEXist(fd)) // 读事件就绪,处理{_Connection[fd]->_recvfunc(_Connection[fd]);// Logmessage(Debug, "处理 fd:%d,events:%s,的读事件", fd, EventMap[event].c_str());}if ((event & EPOLLOUT) && ConnIsEXist(fd)) // 写事件就绪,处理{_Connection[fd]->_sendfunc(_Connection[fd]);// Logmessage(Debug, "处理 fd:%d,events:%s,的写事件", fd, EventMap[event].c_str());}}}bool ConnIsEXist(int fd){return _Connection.find(fd) != _Connection.end();}void AddConnection(int fd, uint32_t events, string clienip = "127.0.0.1", uint16_t port = 8081){// 如果当前文件描述符是ET,文件描述符需要是非阻塞if (events & EPOLLET)Util::Noblock(fd);// 1.构建connection对象,添加到connection对象中connection *conn = new connection(fd, events);conn->_clientip = clienip;conn->_port = port;// 1.1设置回调,由于文件描述符的类型不同,设置的回调也会不同————listen和acceptif (fd == _sock.FD()) // listen{conn->SetCallBack(std::bind(&EpollServer::Accept, this, placeholders::_1), nullptr, nullptr);}else // accept{conn->SetCallBack(std::bind(&EpollServer::Recv, this, placeholders::_1),std::bind(&EpollServer::Send, this, placeholders::_1),std::bind(&EpollServer::Execp, this, placeholders::_1));}// 1.1添加到connection对象中_Connection.insert({fd, conn});// 2.添加到内核Epoll对象_epoll.Addevent(fd, events);}void Accept(connection *conn){do{int err = 0;string clientip;uint16_t clientport;int fd = _sock.Accept(&clientip, &clientport, &err);if (fd > 0){Logmessage(Debug, "有一个client:IP->%s,client:port->%d连接上服务器", clientip.c_str(), clientport);AddConnection(fd, EPOLLIN | EPOLLET, clientip, clientport);}else{if (err == EAGAIN || err == EWOULDBLOCK) // 在非阻塞的时候,可接收的链接已经处理完,没有可接受的链接了,导致出错break;else if (err == EINTR) // 因信号中断导致链接接受错误continue;else{Logmessage(Warning, "errno:%d,%s", err, strerror(err));continue;}}} while (conn->_event & EPOLLET);}void ProtocolHandle(connection *conn){bool quit = true;string request_str;while (1){// 判断是否读取到一个完整的报文,返回有效载荷长度int len = ReadFormat(conn->_recvstr, &request_str);if (len == 0)continue;// 读取到一个完整的报文————request,开始处理报文// 1.去报头request_str = Rehead(request_str, len);// 2.构建请求Request request;// 3.反序列化request.deserialize(request_str);// 4.处理业务Responce responce = _func(request);// 5.序列化string responce_str = responce.serialize();// 6.添加报头responce_str = Addhead(responce_str);// 7.添加到发送缓冲区conn->_sendstr += responce_str;break;}}bool RecvHandle(connection *conn){bool stat;// 读取一个完整的报文,根据自己协议定制的char buff[1024];int fd = conn->_fd;do{// 尝试读取int n = recv(conn->_fd, buff, sizeof(buff) - 1, 0);if (n > 0) // 正确读取到数据{// 添加到独立缓冲区buff[n] = 0;conn->_recvstr += buff;}else if (n == 0){ // 对端关闭conn->_excepfunc(conn);stat = false;break;}else{// 读取出错if (errno == EAGAIN || errno == EWOULDBLOCK) // 1.在非阻塞时候,没有数据时读取break;else if (errno == EINTR) // 读取时信号中断continue;else // 读取异常{conn->_excepfunc(conn);stat = false;break;}}} while (conn->_event & EPOLLET);}void Recv(connection *conn){Logmessage(Debug, "处理 fd:%d,events:%s,的读事件", conn->_fd, "EPOLLIN");// 报文读取int stat = RecvHandle(conn);if (!stat)return;// 报文协议解析ProtocolHandle(conn);//检测发送缓冲区是否有数据,如果有就直接发送//读取一般都是常设置,写入一般按需设置,只有有数据的时候,直接发送以后,如果发送完了关闭对写的关心//如果没有发送完,继续启动对写的关心nif (!conn->_sendstr.empty())Send(conn);}bool EnableReadWrite(connection *conn, bool readable, bool writeable){conn->_event = (readable ? EPOLLIN : 0) | (writeable ? EPOLLOUT : 0) | EPOLLET;return AddModEvent(conn->_fd, conn->_event, EPOLL_CTL_MOD);}bool AddModEvent(int fd, uint32_t event, int op){struct epoll_event ev;ev.data.fd = fd;ev.events = event;int n = epoll_ctl(_epoll.Fd(), op, fd, &ev);if (n < 0){Logmessage(Warning, "epoll_ctl error, code: %d, errstring: %s", errno, strerror(errno));return false;}return true;}void Send(connection *conn){Logmessage(Debug, "处理 fd:%d,events:%s,的写事件", conn->_fd, "EPOLLOUT");do{int n = send(conn->_fd, conn->_sendstr.c_str(), conn->_sendstr.size(), 0);if (n > 0){conn->_sendstr.erase(0, n);if (conn->_sendstr.empty()){cout << "send end..." << endl;EnableReadWrite(conn, true, false);break;}else{cout << "send no end..." << endl;EnableReadWrite(conn, true, true);}}else{if (errno == EAGAIN || errno == EWOULDBLOCK){Logmessage(Warning, "Send:errno:%s,%s", "EAGAIN & EWOULDBLOCK", strerror(errno));break;}else if (errno == EINTR){Logmessage(Warning, "Send:errno:%s,%s", "EINTR", strerror(errno));continue;}else{conn->_excepfunc(conn);Logmessage(Warning, "Send:errno:%s,%s", errno, strerror(errno));break;}}} while (conn->_event & EPOLLET);}void Execp(connection *conn){Logmessage(Debug, "Excepter..., fd: %d, clientinfo: [%s:%d]", conn->_fd, conn->_clientip.c_str(), conn->_port);// 1.从epoll中去除_epoll.Delevent(conn->_fd);// 2.关闭文件描述符close(conn->_fd);// 3.从_Connection中去除_Connection.erase(conn->_fd);// 4.释放connection对象delete conn;}private:uint16_t _port; // 端口号Tcp _sock; // 套接字对象Epoll _epoll; // epoll对象struct epoll_event _eventarr[gsize]; // 就绪事件管理unordered_map<int, connection *> _Connection; // 当前事件的处理和缓冲区管理Func _func; // 逻辑处理函数
};
Protocol.hpp
#pragma once
#include <cstring>
#include <cstdio>
#include <string>
#include <vector>
#include <sys/types.h>
#include <sys/socket.h>
#include <jsoncpp/json/json.h>
using namespace std;// #define MYSELF 1#define SEP "\r\n"
#define SEPLEN strlen(SEP)// str:报文;len:有效载荷的长度
std::string Rehead(std::string str, int len)
{return str.substr(str.length() - 2 - len, len);
}// 报文=报头+有效载荷——————"有效载荷长度"\n\r"有效载荷"\n\r
std::string Addhead(std::string str)
{std::string len = to_string(str.length());//"7\n\r123+123\n\r"return len + SEP + str + SEP;
}int ReadFormat(std::string &inputstr, std::string *target)
{// 从流中读取—————— "7"+\n\r+"123+321"+\n\r// 解析判断读取的字符串是否完整// 尝试读取报头int pos = inputstr.find(SEP, 0);if (pos == std::string::npos) // 没有找到分割"\n\r"return 0;// 找到报头分隔符,提取报头——————有效载荷的长度string headstr = inputstr.substr(0, pos);int len = atoi(headstr.c_str());// 计算出整个报文应该有的长度——————,报头+分割符+有效载荷int formatlen = headstr.length() + len + 2 * SEPLEN;if (inputstr.length() < formatlen) // 读取的报文长度小于报文应该有的长度,没有读取完整return 0;// 读取到一个完整的报文了*target = inputstr.substr(0, formatlen);inputstr.erase(0, formatlen);// cout << *target << endl;return len;
}class Request
{
public:Request(){}Request(int x, int y, char op): _x(x), _y(y), _op(op){}// 序列化std::string serialize(){
#ifdef MYSELFstring strx = to_string(_x);string stry = to_string(_y);string strop;strop += _op;string request = strx + strop + stry;return request;
#else// 使用json序列化Json::Value root; // Value: 一种万能对象, 接受任意的kv类型root["x"] = _x;root["y"] = _y;root["op"] = _op;// Json::FastWriter writer; // Writer:是用来进行序列化的 struct -> stringJson::StyledWriter writer;string request = writer.write(root);return request;#endif}// 反序列化"123+321"void deserialize(const std::string &str){
#ifdef MYSELFstring strx;string stry;string strop;bool isleft = 1;for (auto e : str){if (e >= '0' && e <= '9' && isleft){strx += e;}else if (e < '0' || e > '9'){strop += e;isleft = 0;}else if (e >= '0' && e <= '9' && !isleft){stry += e;}}_x = atoi(strx.c_str());_y = atoi(stry.c_str());_op = strop[0];
#else// 使用json反序列化Json::Value root;Json::Reader reader; // Reader: 用来进行反序列化的reader.parse(str, root);_x = root["x"].asInt();_y = root["y"].asInt();_op = root["op"].asInt();
#endif}public:int _x;int _y;char _op;
};class Responce
{
public:Responce(int result, int code): _result(result), _code(code){}Responce(){}// 序列化std::string serialize(){
#ifdef MYSELFstring strresult = to_string(_result);string strcode = to_string(_code);return strresult + SEP + strcode;#else// 使用json序列化Json::Value root;root["result"] = _result;root["code"] = _code;Json::StyledWriter writer;return writer.write(root);#endif}// 反序列化void deserialize(const std::string &str){
#ifdef MYSELFstring strresult;string strcode;bool isleft = 1;for (auto e : str){if (e >= '0' && e <= '9' && isleft){strresult += e;}else if (e <= '0' || e >= '9'){isleft = 0;}else if (e >= '0' && e <= '9' && !isleft){strcode += e;}}_result = atoi(strresult.c_str());_code = atoi(strcode.c_str());
#else// 使用json反序列化Json::Value root;Json::Reader reader; // Reader: 用来进行反序列化的reader.parse(str, root);_result = root["result"].asInt();_code = root["code"].asInt();
#endif}public:int _result;int _code;
};
epoll.hpp
#pragma once
#include <iostream>
#include <vector>
#include <string.h>
#include <sys/epoll.h>
#include "Log.hpp"using namespace std;
const static int gsize = 128;
const static int defaultepfd = -1;class Epoll
{
public:Epoll(){}void Create(int size = gsize){_epfd = epoll_create(size);if (_epfd < 0){Logmessage(Error, "errno:%d,%s", errno, strerror(errno));exit(Error);}}// typedef union epoll_data// {// void *ptr;// int fd;// uint32_t u32;// uint64_t u64;// } epoll_data_t;// struct epoll_event// {// uint32_t events; /* Epoll events */// epoll_data_t data; /* User data variable */// };bool Addevent(int fd, uint32_t events){epoll_event event;event.events = events;event.data.fd = fd;int ret = epoll_ctl(_epfd, EPOLL_CTL_ADD, fd, &event);if (ret < 0){Logmessage(Warning, "errno:%d,%s", errno, strerror(errno));return false;}return true;}bool Delevent(int fd){int ret = epoll_ctl(_epfd, EPOLL_CTL_DEL, fd, nullptr);if (ret < 0){Logmessage(Warning, "errno:%d,%s", errno, strerror(errno));return false;}return true;}bool Modevent(int fd, uint32_t events){epoll_event event;event.data.fd = fd;event.events = events;int ret = epoll_ctl(_epfd, EPOLL_CTL_MOD, fd, &event);if (ret < 0){Logmessage(Warning, "errno:%d,%s", errno, strerror(errno));return false;}return true;}int Wait(struct epoll_event *eventarr, int maxevent, int timeout){// int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);int n = epoll_wait(_epfd, eventarr, maxevent, timeout);return n;}int Fd(){return _epfd;}void Close(){if (_epfd != defaultepfd)close(_epfd);}~Epoll(){}private:int _epfd = defaultepfd;
};
Util.hpp
#pragma once#include <iostream>
#include <string>
#include <vector>
#include <cstdlib>
#include <unistd.h>
#include <fcntl.h>
#include "Log.hpp"
using namespace std;struct Util
{static bool Noblock(int fd){// 获得文件状态int fl = fcntl(fd, F_GETFD);if (fl < 0){Logmessage(Warning, "Set Noblock Fail...");return false;}// 文件状态添加非阻塞fcntl(fd, F_SETFL, fl | O_NONBLOCK);return true;}
};
client.cc
#include <iostream>
#include "Protocol.hpp"
#include "Sock.hpp"static void usage(std::string proc)
{std::cout << "Usage:\n\t" << proc << " serverip serverport\n"<< std::endl;
}int main(int argc, char *argv[])
{if (argc != 3){usage(argv[0]);exit(0);}std::string serverip = argv[1];uint16_t serverport = atoi(argv[2]);Tcp tcp;tcp.Connect(serverip, serverport);while (1){// 构建一个请求int x;int y;char op;cout << "Input operand 1: ";cin >> x;cout << "Input operand 2: ";cin >> y;cout << "Input operand op: ";cin >> op;// 1.构建请求Request request(x, y, op);// 2.有效载荷序列化string message = request.serialize();// 3.添加报头message = Addhead(message);// 4.发送给服务器send(tcp.FD(), message.c_str(), message.length(), 0);int formatlen = 0;string input;string target;// 1.构建响应Responce respon;char buff[1024];while (1){// 2.读取响应cout << "读取中......." << endl;int n = recv(tcp.FD(), buff, sizeof(buff), 0);if (n == 0){Logmessage(Debug, "对端关闭");break;}if (n < 0){Logmessage(Warning, "errno:%d,%s", errno, strerror(errno));break;}if (n > 0){input += buff;int formatlen = ReadFormat(input, &target);if (formatlen == 0)continue;// 读取到一个完整的报文cout << "读取到一个完整的报文:" << target << endl;// 3.去报头string format = Rehead(target, formatlen);cout << "报文去报头后:" << format << endl;// 4.有效载荷反序列化respon.deserialize(format);cout << "反序列化:" << respon._result << ":" << respon._result << endl;break;}}cout << "Result :" << respon._result << ",Exit code:" << respon._code << endl;}return 0;
}
server.cc
#include <iostream>
#include "EpollServer.hpp"// 请求处理函数,返回响应
Responce calculate(Request request)
{int result;int exitcode;switch (request._op){case '+':result = request._x + request._y;exitcode = 0;break;case '-':result = request._x - request._y;exitcode = 0;break;case '*':result = request._x * request._y;exitcode = 0;break;case '/':if (request._y == 0)exitcode = 1;else{result = request._x / request._y;exitcode = 0;}break;case '%':if (request._y == 0)exitcode = 2;else{result = request._x % request._y;exitcode = 0;}break;default:break;}return Responce(result, exitcode);
}
int main()
{EpollServer epollserver(8080, calculate);epollserver.Distribution();return 0;
}
测试结果:

相关文章:
Linux网络——高级IO
目录 一.五种IO模型 1.阻塞式IO 2.非阻塞式IO 3.信号驱动IO 4.多路转接IO: 5.异步IO 二.同步通信 vs 异步通信 三.设置非阻塞IO 1.阻塞 vs 非阻塞 2.非阻塞IO 3.实现函数SetNoBlock 四.I/O多路转接之select 1.初识select 2.select函数原型 3.socket就绪…...
Java注解详解
概述 注解是对程序代码进行标注和解释的一种方式。在Java中,注解提供了一种元数据形式,能够在程序中嵌入有关程序的信息,以便进行进一步的处理。注解通过使用符号来声明,如Override、Deprecated等。 注解和注释的区别 注释&…...
Android wifi 框架以及Enable流程
Android P相比于Android O的变化 多了WifiStateMachinePrime(状态机的前处理机制),wifiService的相关cmd 不再是直接send 给WifiStateMachine,而是被送到WifiStateMachinePrime先进行处理后,再送往WifiStateMachine也…...
十五、机器学习进阶知识:K-Means聚类算法
文章目录 1、聚类概述2、K-Means聚类算法原理3、K-Means聚类实现3.1 基于SKlearn实现K-Means聚类3.2 自编写方式实现K-Means聚类 4、算法不足与解决思路4.1 存在的问题4.2 常见K值确定方法4.3 算法评估优化思路 1、聚类概述 聚类(Clustering)是指将不同…...
软件崩溃时Visual Studio中看不到有效的调用堆栈,使用Windbg动态调试去分析定位
目录 1、问题说明 2、使用Windbg查看崩溃时详细的函数调用堆栈...
搭乘“低代码”快车,引领食品行业数字化转型全新升级
数字化技术作为重塑传统行业重要的力量,正以不可逆转的趋势改变着企业经营与客户消费的方式。 在近些年的企业数字化服务与交流过程中,织信团队切实感受到大多数企业经营者们从怀疑到犹豫再到焦虑最终转为坚定的态度转变。 在这场数字化转型的竞赛中&a…...
Axure->Axure安装,Axure菜单栏和工具栏功能介绍,页面及概要区
Axure安装Axure菜单栏和工具栏功能介绍,页面及概要区 1.Axure安装 即时设计 - 可实时协作的专业 UI 设计工具 (js.design) 点击上方下载安装⬆ 打开软件点击帮助->管理授权-> 被授权人 Axure 授权密钥:gjqpIxSSUUqFwPoZPi8XwBBhRE2VNmOQsrord0JqShk4QCXxrw6…...
【BUG】微信小程序image不会随着url动态变化
问题描述: 第一次打开界面,显示的是默认头像而不是用户头像,似乎image里面的src只要第一次有值就不会再更新了 解决 不要给src里面的变量设置初始值,而是直接赋空值...
供应链管理痛点大解析!内附解决方案
供应链是指涉及产品或服务生产、运输、分销和最终交付给客户的过程。 用一个汽车制造的例子来帮助大家理解: 原材料采购: 汽车制造商需要从供应商处采购制造汽车所需的原材料,例如金属、橡胶、塑料和玻璃。生产制造:获得原材料&…...
【Python深度学习第二版】学习笔记之——神经网络
首先来说对于神经网络这几章看的很懵,虽然作者已经去掉了数学公式相关内容,讲得已经很想让读者容易理解了,奈何读完还是一知半解,下面就以我目前的理解简单记录一下吧,往后了解的多了再回头看一看。 一、张量运算 作…...
计算机视觉之手势、面部、姿势捕捉以Python Mediapipe为工具
计算机视觉之手势、面部、姿势捕捉以 Python Mediapipe为工具 文章目录 1.Mediapipe库概述2.手势捕捉(hands)3.面部捕捉(face)4.姿势捕捉(pose) 1.Mediapipe库概述 Mediapipe是一个开源且强大的Python库,由Google开发和维护。它提供了丰富的工具和功能,…...
基于AWS Serverless的Glue服务进行ETL(提取、转换和加载)数据分析(一)——创建Glue
1 通过Athena查询s3中的数据 此实验使用s3作为数据源 ETL: E extract 输入 T transform 转换 L load 输出 大纲 1 通过Athena查询s3中的数据1.1 架构图1.2 创建Glue数据库1.3 创建爬网程序1.4 创建表1.4.1 爬网程序创建表1.4.2 手动创建表 1…...
Vue学习计划-Vue2--VueCLi(二)vuecli脚手架创建的项目内部主要文件分析
1. 文件分析 1. 补充: 什么叫单文件组件? 一个文件中只有一个组件 vue-cli创建的项目中,.vue的文件都是单文件组件,例如App.vue 2. 进入分析 1. package.json: 项目依赖配置文件: 如图,我们说主要的属性…...
spring boot项目如何自定义参数校验规则
spring boot项目对参数进行校验时,比如非空校验,可以直接用validation包里面自带的注解。但是对于一些复杂的参数校验,自带的校验规则无法满足要求,此时需要我们自定义参数校验规则。自定义校验规则和自带的规则实现方式一样&…...
springboot整合xxl-job,通过代码进行调度中心注册开启任务等
背景:由于工作需要,当用户在登录时自动触发定时任务。而不需要我们手动到调度中心管理页面去创建任务。 工程介绍:分为两个项目,第一个是调度中心的项目(xxl-job-admin)。第二个是我们自己的项目࿰…...
k8s集群部分使用gpu资源的pod出现UnexpectedAdmissionError问题
记录一次排查UnexpectedAdmissionError问题的过程 1. 问题 环境 3master节点N个GPU节点 kubelet版本:v1.19.4 kubernetes版本:v1.19.4 生产环境K8S集群,莫名其妙的出现大量UnexpectedAdmissionError状态的Pod,导致部分任务执…...
自定义 el-select 和 el-input 样式
文章目录 需求分析el-select 样式el-input 样式el-table 样式 需求 自定义 选择框的下拉框的样式和输入框 分析 el-select 样式 .select_box{// 默认placeholder:deep .el-input__inner::placeholder {font-size: 14px;font-weight: 500;color: #3E534F;}// 默认框状态样式更…...
解决本地centos虚拟机重启,自动变换 ip 地址的问题
修改网卡配置文件 vi /etc/sysconfig/network-scripts/ifcfg-ens33 原配置: TYPE"Ethernet" PROXY_METHOD"none" BROWSER_ONLY"no" BOOTPROTO"dhcp" DEFROUTE"yes" IPV4_FAILURE_FATAL"no" IPV6INI…...
pt36项目短信OAth2.0
5、短信验证码 1、注册容联云账号,登录并查看开发文档(以下分析来自接口文档) 2、开发文档【准备1】:请求URL地址1.示例:https://app.cloopen.com:8883/2013-12-26/Accounts/{}/SMS/TemplateSMS?sig{}ACCOUNT SID# s…...
教师们如何一对一私发成绩?
在传统的教育中,老师通常会通过班级群或家长会等方式发布学生的成绩信息。然而,这种公开的方式可能会让一些学生感到尴尬和不安,因为他们可能不愿意让其他人知道他们的成绩情况。为了解决这个问题,今天我就给老师们推荐一款免费的…...
【Python】 -- 趣味代码 - 小恐龙游戏
文章目录 文章目录 00 小恐龙游戏程序设计框架代码结构和功能游戏流程总结01 小恐龙游戏程序设计02 百度网盘地址00 小恐龙游戏程序设计框架 这段代码是一个基于 Pygame 的简易跑酷游戏的完整实现,玩家控制一个角色(龙)躲避障碍物(仙人掌和乌鸦)。以下是代码的详细介绍:…...
Ubuntu系统下交叉编译openssl
一、参考资料 OpenSSL&&libcurl库的交叉编译 - hesetone - 博客园 二、准备工作 1. 编译环境 宿主机:Ubuntu 20.04.6 LTSHost:ARM32位交叉编译器:arm-linux-gnueabihf-gcc-11.1.0 2. 设置交叉编译工具链 在交叉编译之前&#x…...
安宝特案例丨Vuzix AR智能眼镜集成专业软件,助力卢森堡医院药房转型,赢得辉瑞创新奖
在Vuzix M400 AR智能眼镜的助力下,卢森堡罗伯特舒曼医院(the Robert Schuman Hospitals, HRS)凭借在无菌制剂生产流程中引入增强现实技术(AR)创新项目,荣获了2024年6月7日由卢森堡医院药剂师协会࿰…...
华为OD机试-最短木板长度-二分法(A卷,100分)
此题是一个最大化最小值的典型例题, 因为搜索范围是有界的,上界最大木板长度补充的全部木料长度,下界最小木板长度; 即left0,right10^6; 我们可以设置一个候选值x(mid),将木板的长度全部都补充到x,如果成功…...
零知开源——STM32F103RBT6驱动 ICM20948 九轴传感器及 vofa + 上位机可视化教程
STM32F1 本教程使用零知标准板(STM32F103RBT6)通过I2C驱动ICM20948九轴传感器,实现姿态解算,并通过串口将数据实时发送至VOFA上位机进行3D可视化。代码基于开源库修改优化,适合嵌入式及物联网开发者。在基础驱动上新增…...
【Linux】Linux安装并配置RabbitMQ
目录 1. 安装 Erlang 2. 安装 RabbitMQ 2.1.添加 RabbitMQ 仓库 2.2.安装 RabbitMQ 3.配置 3.1.启动和管理服务 4. 访问管理界面 5.安装问题 6.修改密码 7.修改端口 7.1.找到文件 7.2.修改文件 1. 安装 Erlang 由于 RabbitMQ 是用 Erlang 编写的,需要先安…...
针对药品仓库的效期管理问题,如何利用WMS系统“破局”
案例: 某医药分销企业,主要经营各类药品的批发与零售。由于药品的特殊性,效期管理至关重要,但该企业一直面临效期问题的困扰。在未使用WMS系统之前,其药品入库、存储、出库等环节的效期管理主要依赖人工记录与检查。库…...
深度解析:etcd 在 Milvus 向量数据库中的关键作用
目录 🚀 深度解析:etcd 在 Milvus 向量数据库中的关键作用 💡 什么是 etcd? 🧠 Milvus 架构简介 📦 etcd 在 Milvus 中的核心作用 🔧 实际工作流程示意 ⚠️ 如果 etcd 出现问题会怎样&am…...
【QT控件】显示类控件
目录 一、Label 二、LCD Number 三、ProgressBar 四、Calendar Widget QT专栏:QT_uyeonashi的博客-CSDN博客 一、Label QLabel 可以用来显示文本和图片. 核心属性如下 代码示例: 显示不同格式的文本 1) 在界面上创建三个 QLabel 尺寸放大一些. objectName 分别…...
PostgreSQL 与 SQL 基础:为 Fast API 打下数据基础
在构建任何动态、数据驱动的Web API时,一个稳定高效的数据存储方案是不可或缺的。对于使用Python FastAPI的开发者来说,深入理解关系型数据库的工作原理、掌握SQL这门与数据库“对话”的语言,以及学会如何在Python中操作数据库,是…...

