目录
一.五种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 name
from 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 0x00000000
socklen_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和accept
if (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);
//检测发送缓冲区是否有数据,如果有就直接发送
//读取一般都是常设置,写入一般按需设置,只有有数据的时候,直接发送以后,如果发送完了关闭对写的关心
//如果没有发送完,继续启动对写的关心n
if (!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 MYSELF
string 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 -> string
Json::StyledWriter writer;
string request = writer.write(root);
return request;
#endif
}
// 反序列化"123+321"
void deserialize(const std::string &str)
{
#ifdef MYSELF
string 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 MYSELF
string 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 MYSELF
string 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;
}
测试结果: