Linux网络编程总目录(点击下面链接即可到达对应章节)
Linux网络编程_01_网络基础
Linux网络编程_02_socket套接字
Linux网络编程_03_应用层HTTP协议
Linux网络编程_04_传输层UDP和TCP协议
Linux网络编程_05_网络层IP协议
Linux网络编程_06_数据链路层MAC帧协议
Linux网络编程_07_多路转接
一. 高级IO概念
1.1 IO的简述
IO操作我们可以理解为两部分,一个是等,另一个是拷贝数据。 等就是等IO事件就绪,分别有读事件就绪和写事件就绪这两种。一般我们想让IO变得高效,就要让等变得高效,即单位时间内减少等的比重。
1.2 异步通信和同步通信
- 同步通信: 调用发出后,由调用者主动获取这个调用的结果
- 异步通信: 调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用
1.3 阻塞非阻塞
1.3.1 两者对比
阻塞: 指调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果之后才会返回
非阻塞: 在不能立刻得到结果之前,该调用不会阻塞当前线程
1.3.2 fcntl接口
一个文件描述符, 默认都是阻塞IO,我们可以通过fcntl使其变成非阻塞IO
函数介绍
// 1. 头文件
#include <unistd.h>
#include <fcntl.h>
// 2. 函数声明
int fcntl(int fd, int cmd, ... /* arg */ );
// 3.参数解释
// fd:需要操作的文件描述符
// cmd:
// 复制一个现有的描述符(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)
// cmd后面的是一个可变参数,根据cmd值的不同,后面的参数也各有不同
// 4. 调用成功返回值跟参数有关,比如下面例子我们需要传的cmd参数为GETFL,返回的就是文件状态标记,调用失败返回-1
函数使用案例
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
void SetNonBlock(int fd)
{
int fl = fcntl(fd, F_GETFL); // 获取文件状态标记
if (fl < 0)
{
perror("fcntl\n");
return;
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK); // 设置文件状态标记,设置为非阻塞
}
int main()
{
SetNonBlock(0);
while (1)
{
char buffer[10];
ssize_t rlen = read(0, buffer, sizeof(buffer) - 1);
if (rlen > 0)
{
buffer[rlen] = '\0';
write(1, buffer, strlen(buffer));
printf("read success! rlen: %d, errno: %d", rlen, errno);
}
else
{
if (errno == EAGAIN || errno == EWOULDBLOCK)
{
printf("数据还没有准备好!\n");
printf("read failed! rlen: %d, errno: %d", rlen, errno);
sleep(1);
}
}
}
return 0;
}
二. 五种IO模型
阻塞IO: 在内核将数据准备好之前,当前系统调用会一直等待,不会干其他事情。所有的套接字,默认都是阻塞方式
非阻塞IO: 如果内核还未将数据准备好,系统调用仍然会直接返回,并且返回EWOULDBLOCK错误码(所以返回错误码不一定是出错)。没有就绪和出错的错误码是一样的,我们不能区分它们,真正能够区分是没有就绪还是真正的错误可以通过errno来判定。非阻塞IO往往需要循环的方式反复尝试读写文件描述符,这个过程称为轮询。这对CPU来说是较大的浪费,一般只有特定场景下才使用
信号驱动IO: 内核将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作
IO多路转接: IO多路转接能够同时等待多个文件描述符的就绪状态
异步IO: 由内核在数据拷贝完成时, 通知应用程序
三. IO多路转接
3.1 select
3.1.1 简述
上面我们说过IO可以分为两大部分,等和拷贝数据。select系统调用就是用来等的,select让我们的程序监视多个文件描述符的状态变化的,程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变。
3.1.2 接口说明
// 1. 头文件
/* According to POSIX.1-2001 2001标准之后的,现在用这个就好了*/
#include <sys/select.h>
/* According to earlier standards 更早的标准*/
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
// 2. 函数声明
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
// 3. fd_set类型
// fd_set是一个位图结构,fd_set的大小是128字节,一共1024个比特位,所以fd_set可以标记0~1023一共1024个文件描述符的状态。
// 下面几个接口是提供使用fd_set类型的
void FD_CLR(int fd, fd_set *set); // 在set中将fd对应的比特位置为0
int FD_ISSET(int fd, fd_set *set); // 判断set中fd对应的比特位是否为1
void FD_SET(int fd, fd_set *set); // 将set中fd对应的比特位置为1
void FD_ZERO(fd_set *set); // 将set中所有比特位置为0
// 4. 参数说明
// nfds:监视的最大文件描述符+1
// readfds writefds exceptfds:分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集 合及异常文件描述符的集合
// timeout:用来设置等待时间,下面是对应的传参方式
// NULL:则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件
// 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生
// 特定的时间值:如果在特定时间没有等待到就绪,就结束等待;如果在等待的期间有就绪就会立即结束等待,并且把没有等待完的时间设置进timeout里面,所以此时的timeout是一个输入输出型参数。
struct timeval {
long tv_sec; /* seconds秒*/
long tv_usec; /* microseconds微妙*/
};
// 4. 返回值:执行成功则返回readfds writefds exceptfds这三个监测集合文件描述符状态已改变的个数,如果返回0代表在描述词状态改变前已超过timeout时间,没有返回。当有错误发生时则返回-1,错误原因存于errno,此时参数readfds,writefds, exceptfds和timeout的值变成不可预测。下面是可能错误的原因。
// EBADF 文件描述词为无效的或该文件已关闭
// EINTR 此调用被信号所中断
// EINVAL 参数n 为负值。
// ENOMEM 核心内存不足
3.1.3 select就绪条件
读就绪
- socket内核中,接收缓冲区中的字节数,大于等于低水位标记SO_RCVLOWAT,此时可以无阻塞的读该文件描述符,并且返回值大于0
- socket TCP通信中,对端关闭连接,此时对该socket读,则返回0
- 监听的socket上有新的连接请求
- socket上有未处理的错误
写就绪
- socket内核中,发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小),大于等于低水位标记SO_SNDLOWAT,此时可以无阻塞的写,并且返回值大于0
- socket的写操作被关闭(close或者shutdown),对一个被关闭的socket进行写操作,会触发SIGPIPE信号
- socket使用非阻塞connect连接成功或失败之后
- socket上有未读取的错误
3.1.4select使用案例
#include <iostream>
#include <sys/select.h>
#include "sock.hpp"
#include <string>
short fd_array[1024]; // 每个位置取值-1为没有,取值为0~1023表示对应的文件描述符
short fd_array_size = 0; // 统计fd_array的有效元素个数,以减少遍历次数
void Usage(const char* proc)
{
std::cout << "Usage:" << proc << " port" << std::endl;
}
int main(int argc, char* argv[])
{
if (argc != 2)
{
Usage(argv[0]);
return 1;
}
// 1. 创建套接字
int sockfd = sock::Socket();
// 2. 绑定端口
uint16_t port = (uint16_t)stoi(argv[1]);
sock::Bind(sockfd, port);
// 3. listen
sock::Listen(sockfd);
// 初始化fd_array
for (int i = 0; i < 1024; i++)
{
fd_array[i] = -1;
}
fd_array[0] = sockfd; // 让listen的那个文件描述符在第一个位置
fd_array_size++;
int maxfd = sockfd; // 先默认为sockfd为最大的文件描述符
fd_set readfds; // 用来设置监听的文件描述符和接收监听结果
struct timeval timeout; // 设置等待的时间,如果等待中有就绪文件,也可以接收还没有等待完的时间
while (true)
{
FD_ZERO(&readfds); // 初始化,全部比特位置为0
for (int i = 0; i < fd_array_size; i++)
{
std::cout << "fd_array[" << i << "]: " << fd_array[i] << std::endl;
if (fd_array[i] != -1)
{
FD_SET(fd_array[i], &readfds); // 设置监听的文件
if (maxfd < fd_array[i])
{
maxfd = fd_array[i];
}
std::cout << "监测" << fd_array[i] << "号文件" << std::endl;
}
}
timeout = {5, 0}; // 设置等待时间为5秒
int ret = select(maxfd + 1, &readfds, nullptr, nullptr, &timeout);
std::cout << "timeout:" << timeout.tv_sec << "秒" << timeout.tv_usec << "微秒" << std::endl; // 打印没有等待完的时间
switch(ret)
{
case -1: // 出错
std::cerr << "select error" << std::endl;
break;
case 0: // 等待时间过了,还没有等到就绪的文件
std::cout << "timeout" << std::endl;
break;
default: // 有就绪的文件
for (int i = 0; i < fd_array_size; i++)
{
if (-1 == fd_array[i]) // 过滤掉没有设置监听的文件
continue;
if (FD_ISSET(fd_array[i], &readfds)) // 判断是哪个文件就绪了
{
if (fd_array[i] == sockfd) // 说明有新的连接来了
{
std::cout << sockfd << "号文件:有新的连接来了!" << std::endl;
if (1024 == fd_array_size)
{
std::cout << "服务器满载,拒绝连接!" << std::endl;
continue;
}
int sock = sock::Accept(sockfd);
if (sock >= 0)
{
int pos = 0;
for (; pos < fd_array_size; pos++)
{
if (-1 == fd_array[pos]) // 查找一个空位置放新的文件描述符
{
fd_array[pos] = sock;
break;
}
}
if (pos == fd_array_size)
{
fd_array[fd_array_size] = sock;
fd_array_size++;
}
std::cout << sock << "号:连接成功!" << std::endl;
}
}
else // 说明是普通的读事件
{
std::cout << "有普通文件读取了!" << std::endl;
char buffer[1024] = {'\0'};
ssize_t readlen = read(fd_array[i], buffer, sizeof(buffer) - 1);
if (-1 == readlen)
{
close(fd_array[i]);
std::cerr << fd_array[i] << "号文件:读取错误,关闭该文件并且将其从fd_array中去掉!" << std::endl;
fd_array[i] = -1;
if (i == fd_array_size - 1)
{
fd_array_size--;
}
}
else if (0 == readlen)
{
close(fd_array[i]);
std::cerr << fd_array[i] << "号文件:读取失败,对方已关闭链接,关闭该文件并且将其从fd_array中去掉!" << std::endl;
fd_array[i] = -1;
if (i == fd_array_size - 1)
{
fd_array_size--;
}
}
else
{
buffer[readlen] = '\0';
std::cout << fd_array[i] << "号文件:" << buffer << std::endl;
}
}
}
}
break;
}
}
//std::cout << "fd_set的大小是" << sizeof(fd_set) << "字节" << std::endl;
return 0;
}
3.1.5 select总结
select的优点
- 一次可以等待多个fd,在一定程度上提高了IO效率
select的缺点
- 每次调用select,都需要手动设置fd集合,非常不便
- 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
- 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
- select支持的文件描述符数量太小
3.2 poll
3.2.1 接口说明
// 1. 头文件
#include <poll.h>
// 2. 函数声明
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
// 3. struct pollfd类型
struct pollfd {
int fd; /* file descriptor 文件描述符 */
short events; /* requested events 需要监听的文件事件 */
short revents; /* returned events 监听成功的文件事件 */
};
// 一般常用的事件有POLLIN(读就绪),POLLOUT(写就绪)
// 4. 参数说明
// fds:是一个指针,也可以认为是一个数组的首地址,传参时这个数组装的是我们需要监测的文件描述符,和对应的事件,调用结束后这个数组装的是监听到就绪的文件和对应的事件
// nfds:就是上面说的fds数组的长度
// timeout:设置阻塞的时间,单位为毫秒,-1表示一直阻塞,0表示轮询
// 5. 返回值
// 返回值小于0, 表示出错
// 返回值等于0, 表示poll函数等待超时
// 返回值大于0, 表示poll由于监听的文件描述符就绪而返回
3.2.2 poll使用案例
#include <iostream>
#include <poll.h>
#include <unistd.h>
int main()
{
struct pollfd pfd;
pfd.fd = 0; // 监听标准输入
pfd.events |= POLLIN; // 监听读就绪
while (true)
{
size_t n = poll(&pfd, 1, 1000); // 设置阻塞等待
switch (n)
{
case -1: // 监听出错
std::cerr << "poll error" << std::endl;
break;
case 0: // 监听一次结束,没有就绪的文件
std::cout << "timeout" << std::endl;
break;
default: // 监听到就绪的文件
char buffer[16];
int rsize = read(pfd.fd, buffer, sizeof(buffer) - 1);
if (rsize > 0)
{
buffer[rsize] = '\0';
std::cout << pfd.fd << "号说:" << buffer << std::endl;
}
break;
}
}
return 0;
}
3.2.3 poll总结
优点
- pollfd结构包含了要监视的event和发生的revent,使得监听和返回的结果解耦,不再用添加重复的监听对象。
缺点
- 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符
- 每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中
- 同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降
3.3 epoll
3.3.1 认识接口
epoll_create接口
// 1. 头文件
#include <sys/epoll.h>
//2. 函数声明
int epoll_create(int size);
// 3. 参数说明
// size:表示最大监听的数目,从Linux 2.6.8开始,size参数将被忽略,但必须大于零
// 4. 返回值:调用成功返回一个句柄的文件描述符,这个句柄可以像普通文件一样调用close关闭
epoll_ctl
// 1. 头文件
#include <sys/epoll.h>
// 2. 函数声明
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 3. 参数说明
// epfd:我们需要使用的句柄,即epoll_create生成的一个句柄的文件描述符
// op:第二个参数表示动作,用三个宏来表示
// EPOLL_CTL_ADD :注册新的fd到epfd中
// EPOLL_CTL_MOD :修改已经注册的fd的监听事件
// EPOLL_CTL_DEL :从epfd中删除一个fd
// fd:我们需要监听的文件描述符
// event:是告诉内核需要监听什么事
// truct epoll_event类型
struct epoll_event
{
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
} __EPOLL_PACKED;
// events常见的设置参数(用的时候或上就好了)
// EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
// EPOLLOUT : 表示对应的文件描述符可以写
typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
// 返回值:成功返回0,失败返回-1
epoll_wait
// 1. 头文件
#include <sys/epoll.h>
// 2. 函数声明
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
// 3. 参数说明
// epfd:我们需要使用的句柄,即epoll_create生成的一个句柄的文件描述符
// events:epoll将会把发生的事件赋值到events数组中 (events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存)
// maxevents:告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size
// timeout:设置阻塞的时间,单位为毫秒,-1表示一直阻塞,0表示轮询
// 4. 返回值:成功返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时, 返回小于0表示函数调用失败
3.3.2 epoll使用案例
#include <iostream>
#include <cstdlib>
#include <string>
#include <unistd.h>
#include <sys/epoll.h>
#include "sock.hpp"
void Usage(const char *proc)
{
std::cout << proc << "port" << std::endl;
}
int main(int argc, char *argv[])
{
if (2 != argc)
{
Usage(argv[0]);
exit(1);
}
// 1. 创建套接字
const int sockfd = sock::Socket();
// 2. 绑定ip和port
uint16_t port = (uint16_t)std::stoi(argv[1]);
sock::Bind(sockfd, port);
// 3. 设置listen状态
sock::Listen(sockfd);
// 创建句柄
int epfd = epoll_create(128);
// 添加需要等待的事件
struct epoll_event ev;
ev.events = EPOLLIN; // 添加读事件
ev.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
// 等待事件的数组
struct epoll_event revs[128];
int tmp_fd; // 用来接收就绪事件的fd
while (true)
{
int n = epoll_wait(epfd, revs, 128, -1);
switch (n)
{
case -1:
std::cerr << "epool_wait error!" << std::endl;
break;
case 0:
std::cout << "timeout!" << std::endl;
break;
default:
// std::cout << "有事件就绪" << std::endl;
for (int i = 0; i < n; i++)
{
if (revs[i].events & EPOLLIN) // 筛选出读事件
{
int tmp_fd = revs[i].data.fd;
if (sockfd == tmp_fd) // 有新链接没有处理
{
int cfd = sock::Accept(sockfd);
if (cfd > 0)
{
std::cout << cfd << "号链接成功!" << std::endl;
struct epoll_event ev;
ev.data.fd = cfd;
ev.events = EPOLLIN;
if (!epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev))
{
std::cout << "已经将" << cfd << "号托管给epoll" << std::endl;
}
}
}
else // 有普通的读事件就绪
{
char buffer[16];
ssize_t read_size = read(tmp_fd, buffer, sizeof(buffer) - 1);
if (read_size > 0)
{
buffer[read_size] = '\0';
std::cout << tmp_fd << "号:" << buffer << std::endl;
}
else if (0 == read_size) // 对方关闭连接
{
close(tmp_fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, tmp_fd, nullptr);
std::cout << tmp_fd << "号链接已经断开,已经将其从epoll托管中删去" << std::endl;
}
else if (-1 == read_size) // 读取出错
{
close(tmp_fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, tmp_fd, nullptr);
std::cout << tmp_fd << "号链读取异常,已经将其从epoll托管中删去" << std::endl;
}
}
}
}
break;
}
}
close(sockfd);
close(epfd);
return 0;
}
3.3.3 epoll原理
epoll_create: 创建一个epoll对象,这个对象里面有一个eventpoll结构体,结构体有两个主要成员分别为rbr红黑树和rdllist双链表。
epoll_ctl: epoll_ctl方法向epoll对象中添加进来的事件。这些事件都会挂载在红黑树中,重复添加的事件就可以通过红黑树而高效的识别出来,红黑树的查找、插入和删除时间复杂度都为O (log2N)。添加到epoll中的事件都会与设备驱动程序建立回调关系,当响应的事件发生时会调用这个回调方法。这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中。 简单地说就是,添加或者删去监听对象,本质就是添加或者删去红黑树中的节点,建立或者撤销对应fd的回调机制。
epool_wait: 调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户,这个操作的时间复杂度是O(1)。简单地说就是以O(1)的时间复杂度检测是否有事件就绪,有就返回给用户。
3.3.4 epoll总结
- 接口使用方便:虽然拆分成了三个函数,但是反而使用起来更方便高效,不需要每次循环都设置关注的文件描述符,也做到了输入输出参数分离开
- 数据拷贝轻量:只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中,这个操作并不频繁,而select/poll每次循环都要进行拷贝
- 事件回调机制: 避免使用遍历, 而是使用回调函数的方式,将就绪的文件描述符结构加入到就绪队列中,epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪, 这个操作时间复杂度O(1),即使文件描述符数目很多, 效率也不会受到影响
- 没有数量限制:文件描述符数目无上限
3.3.5 epoll的方式
水平触发(Level Triggered)工作模式
epoll默认状态下就是LT工作模式,当epoll_wait检测到事件就绪时,会通知用户,如果没有及时处理的话,后面调用epoll_wait时还会继续通知,直到就绪被处理完。
边缘触发Edge Triggered工作模式
当epoll_wait检测到事件就绪时,必须立刻处理,在后面调用epoll_wait 的时候,epoll_wait 不会再返回了。也就是说, ET模式下,文件描述符上的事件就绪后,只有一次处理机会。ET的性能比LT性能更高, epoll_wait 返回的次数少了很多。使用 ET 模式的 epoll,需要将文件描述设置为非阻塞。
3.3.5 epoll的使用场景
多连接,且多连接中只有一部分连接比较活跃时,比较适合使用epoll。例如,一个需要处理上万个客户端的服务器,或者是各种互联网APP的入口服务器,这样的服务器就很适合epoll。如果只是系统内部,服务器和服务器之间进行通信,只有少数的几个连接,这种情况下用epoll就并不合适
四. Reactor反应堆模式
通过多路转接方案,采用事件派发的方式,去调用对应的回调函数
4.1 common.h
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <unordered_map>
#include <sys/epoll.h>
#include <cstdlib> // for exit
#include <unistd.h>
#include <fcntl.h>
#include <cerrno>
#include "Reactor.hpp"
#include "sock.hpp"
#include "Util.hpp"
#include "Service.hpp"
4.2 server.cpp
#include "common.h"
void Usage(const char* proc)
{
std::cerr << "Usage:" << proc << " port" << std::endl;
}
int main(int argc, char* argv[])
{
if (2 != argc)
{
Usage(argv[0]);
exit(1);
}
// 创建套接字
int sockfd = sock::Socket();
// 设置为非阻塞
SetNonBlock(sockfd);
// bind端口号
uint16_t port = (uint16_t)(std::stoi(argv[1]));
sock::Bind(sockfd, port);
// 设置为listen状态
sock::Listen(sockfd);
// 创建Reactor对象
Reactor* R = new Reactor();
R->InitReactor();
// 将listen事件添加到epoll的红黑树中和_events这个hash表里面
Event* listen_pev = new Event();
listen_pev->_sock = sockfd;
listen_pev->_R = R;
listen_pev->RegisterCallback(accepter, nullptr, nullptr);
R->InsertEvent(listen_pev, EPOLLIN | EPOLLET);
// 开始处理业务(业务派发)
while (true)
{
R->Dispatcher(1000);
}
return 0;
}
4.3 Reactor.hpp
#pragma once
#include "common.h"
#define SIZE 128
#define NUM 64
class Reactor;
class Event;
// 定义函数指针(用来回调函数)
typedef int (*callback_t)(Event* pev);
class Event
{
public:
int _sock; // 文件描述符
std::string _inbuffer; // 我们自己定义的输入缓冲区
std::string _outbuffer; // 我们自己定义的输出缓冲区
callback_t _recver; // 读就绪回调
callback_t _sender; // 写就绪回调
callback_t _errorer; // 错误就绪回调
Reactor* _R;
public:
Event()
: _sock(-1), _recver(nullptr), _sender(nullptr), _errorer(nullptr)
{
}
~Event()
{
}
// 注册回调
void RegisterCallback(callback_t recver, callback_t sender, callback_t errorer)
{
_recver = recver;
_sender = sender;
_errorer = errorer;
}
};
class Reactor
{
private:
int _epfd; // epoll的fd
std::unordered_map<int, Event *> _events; // 用来装Event对象的Hash表
public:
Reactor()
: _epfd(-1)
{}
// 初始化--> 创建一个epoll对象
void InitReactor()
{
_epfd = epoll_create(SIZE);
if (_epfd < 0)
{
std::cerr << "epoll_create error!" << std::endl;
exit(1);
}
std::cout << "InitReactor success!" << std::endl;
}
// 插入一个事件(插入分两步,一是添加到epoll对象,二是添加到_event这个Hash表里面)
bool InsertEvent(Event* pev, uint32_t evs)
{
// 1. 将事件添加到epoll的红黑树里面
struct epoll_event ev;
ev.events = evs;
ev.data.fd = pev->_sock;
if (epoll_ctl(_epfd, EPOLL_CTL_ADD, ev.data.fd, &ev) < 0)
{
std::cerr << "epoll_ctl add error" << std::endl;
return false;
}
// 2. 将事件添加到我们自己定义的_event这个hash表里面
_events.insert(std::make_pair(pev->_sock, pev));
}
// 删除一个事件(删除也大致分为两步,一是从epoll对象里删除,二是从_event这个Hash表里删除)
void DeleteEvent(Event* pev)
{
auto iter = _events.find(pev->_sock);
if (iter != _events.end())
{
// 1. 将事件从epoll的红黑树中删去
epoll_ctl(_epfd, EPOLL_CTL_DEL, pev->_sock, nullptr);
// 2. 将事件从我们自己定义的_event这个hash表里面删去
_events.erase(iter);
// 关闭文件描述符
close(pev->_sock);
// 回收这个节点
delete pev;
}
}
// 修改一个事件 --> 使能读写,即控制这个事件读写就绪的开和关
bool EnableReadWrite(int sock, bool read, bool write)
{
struct epoll_event ev;
ev.events = EPOLLET | (read ? EPOLLIN : 0) | (write ? EPOLLOUT : 0);
ev.data.fd = sock;
if (!epoll_ctl(_epfd, EPOLL_CTL_MOD, sock, &ev))
{
return true;
}
else
{
std::cerr << "epoll_ctl_mod error!" << std::endl;
return false;
}
}
// 就绪事件事件派发器(获取就绪的事件,看看是什么就绪就交付给对应的回调处理)
void Dispatcher(int timeout)
{
struct epoll_event revs[NUM];
int n = epoll_wait(_epfd, revs, NUM, timeout);
for (int i = 0; i < n; i++)
{
int sock = revs[i].data.fd;
uint32_t revents = revs[i].events;
// 出错或者对应文件描述符被挂掉,将他们交给IO函数去处理
if (revents & EPOLLERR || revents & EPOLLHUP) // EPOLLHUP表示对应的文件描述符被挂断
{
revents |= (EPOLLIN | EPOLLOUT);
}
if (revents & EPOLLIN)
{
_events[sock]->_recver(_events[sock]); // 先拿到event事件对象的地址,然后调用回调函数
}
if (revents & EPOLLOUT)
{
_events[sock]->_sender(_events[sock]);
}
}
}
~Reactor(){}
};
4.4 Service.hpp
#pragma once
#include "common.h"
#define ONCE_SIZE 128
int recver(Event* pev);
int sender(Event* pev);
int errorer(Event* pev);
int accepter(Event* pev)
{
// 可能会有多个链接就绪,所以设置死循环把所有就绪的链接都处理好了
while (true)
{
// 接受链接
int sock = sock::Accept(pev->_sock);
if (sock < 0)
{
std::cout << "Accepter done!" << std::endl;
break;
}
std::cout << "Accept success,sock is " << sock << std::endl;
// 设置非阻塞
SetNonBlock(sock);
Event *new_event = new Event();
new_event->_sock = sock;
new_event->_R = pev->_R;
// 注册回调函数
new_event->RegisterCallback(recver, sender, errorer);
// 将新的事件添加的hash表里面,并且关心读就绪和设置为ET模式(边缘触发)
pev->_R->InsertEvent(new_event, EPOLLIN | EPOLLET);
}
}
// 将接收缓冲区的所有数据读到_inbuffer
static int RecverCore(const int& sock, string& inbuffer)
{
char buffer[ONCE_SIZE];
while (true)
{
ssize_t recv_size = recv(sock, buffer, sizeof(buffer) - 1, 0);
if (recv_size > 0)
{
buffer[recv_size] = '\0';
inbuffer += buffer;
}
else if (recv_size < 0)
{
// 信号被打断
if (errno == EINTR)
{
continue;
}
// 底层没有数据了,全部读完了
if (errno == EAGAIN || errno == EWOULDBLOCK)
{
return 0;
}
// 真的出错了
return -1;
}
else // 对端关闭链接
{
return 1;
}
}
}
int recver(Event* pev)
{
int ret = RecverCore(pev->_sock, pev->_inbuffer);
if (0 != ret)
{
if (pev->_errorer)
{
pev->_errorer(pev);
}
return -1;
}
// 处理粘包问题
// 规定以x为包与包之间的分隔符
std::vector<std::string> tokens;
std::string sep = "x";
SplitSegment(pev->_inbuffer, &tokens, sep);
// 依次处理请求,并且构建响应返回
for (auto& token : tokens)
{
int x;
int y;
if (Deserialize(&x, &y, token))
{
int z = x + y;
std::string send_str = std::to_string(x);
send_str += '+';
send_str += std::to_string(y);
send_str += '=';
send_str += std::to_string(z);
send_str += sep;
pev->_outbuffer += send_str; // 将构建好的一个相应相应回去
}
}
if (!pev->_outbuffer.empty())
{
pev->_R->EnableReadWrite(pev->_sock, true, true);
}
return 0;
}
int SenderCore(const int& sock, std::string& outbuffer)
{
int total = 0;
while (true)
{
ssize_t send_size = send(sock, outbuffer.c_str() + total, outbuffer.size() - total, 0);
if (send_size > 0)
{
total += send_size;
if (outbuffer.size() == total) // 证明已经将数据全部拷贝到发送缓冲区了
{
outbuffer.clear();
return 0;
}
}
else
{
if (errno == EINTR) // 由于信号中断,没写成功数据
{
continue;
}
else if (errno == EAGAIN || errno == EWOULDBLOCK) // 发送缓冲区满了
{
outbuffer.erase(0, total);
return 1;
}
else // 出错了
{
return -1;
}
}
}
}
int sender(Event* pev)
{
const int result = SenderCore(pev->_sock, pev->_outbuffer);
if (result == 0)
{
pev->_R->EnableReadWrite(pev->_sock, true, false);
return 0;
}
else if (result == 1)
{
// 不做任何操作,继续监测write
}
else
{
if (pev->_errorer)
{
pev->_errorer(pev);
}
}
}
int errorer(Event* pev)
{
pev->_R->DeleteEvent(pev);
}
4.5 sock.hpp
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<iostream>
#include<string>
#include<cstdlib>
using namespace std;
class sock
{
public:
static int Socket()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
cerr << "socket error" << endl;
exit(-1);
}
return sockfd;
}
static void Bind(const int& sockfd, const uint16_t& port)
{
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(port);
server.sin_addr.s_addr= INADDR_ANY;
int ret = bind(sockfd, (struct sockaddr*)&server, sizeof(server));
if (ret < 0)
{
cerr << "bind error" << endl;
exit(-1);
}
}
static void Listen(const int& sockfd)
{
if (listen(sockfd, 5) < 0)
{
cerr << "listen error" << endl;
exit(-1);
}
}
static int Accept(const int& sockfd)
{
sockaddr_in client;
socklen_t len;
int accfd = accept(sockfd, (struct sockaddr*)&client, &len);
if (accfd < 0)
{
cerr << "accept error" << endl;
return -1;
}
else
{
cout << "get a client... IP --> " << inet_ntoa(client.sin_addr) << " Port --> " << client.sin_port << endl;
return accfd;
}
}
static int Connect(const int& sockfd, const char* serverIP, const uint16_t& serverPort)
{
sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(serverPort);
server.sin_addr.s_addr = inet_addr(serverIP);
int confd = connect(sockfd, (struct sockaddr*)&server, sizeof(server));
if (confd < 0)
{
cerr << "connect error" << endl;
exit(-1);
}
else
{
return confd;
}
}
};
4.6 Util.hpp
#pragma once
#include "common.h"
// 设置非阻塞
void SetNonBlock(int sock)
{
int fl = fcntl(sock, F_GETFL); // 传参数是F_GETFL,如果成功就返回文件的状态flags信息,出错就返回-1
if (fl < 0)
{
std::cerr << "fcntl failed" << std::endl;
return;
}
fcntl(sock, F_SETFL, fl | O_NONBLOCK);
}
// 解决粘包问题(跟据我们自己制定的协议,以x为分隔符进行数据分离)
void SplitSegment(std::string &inbuffer, std::vector<std::string> *tokens, const std::string &sep)
{
size_t start = 0;
size_t end = 0;
while (true)
{
end = inbuffer.find(sep, start);
if (std::string::npos == end)
{
break;
}
tokens->push_back(inbuffer.substr(start, end - start));
start = end + sep.size();
}
inbuffer.clear();
}
// 反序列化
bool Deserialize(int* data1, int* data2, const std::string& token)
{
const char op = '+';
size_t pos = token.find(op);
if (std::string::npos == pos)
{
return false;
}
*data1 = std::stoi(token.substr(0, pos));
*data2 = std::stoi(token.substr(pos + 1, token.size()));
return true;
}
4.7 Makefile
server:server.cpp
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -rf server