Linux网络编程_07_多路转接(网络编程完美撒花)

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模型

  1. 阻塞IO: 在内核将数据准备好之前,当前系统调用会一直等待,不会干其他事情。所有的套接字,默认都是阻塞方式

  2. 非阻塞IO: 如果内核还未将数据准备好,系统调用仍然会直接返回,并且返回EWOULDBLOCK错误码(所以返回错误码不一定是出错)。没有就绪和出错的错误码是一样的,我们不能区分它们,真正能够区分是没有就绪还是真正的错误可以通过errno来判定。非阻塞IO往往需要循环的方式反复尝试读写文件描述符,这个过程称为轮询。这对CPU来说是较大的浪费,一般只有特定场景下才使用

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

  4. IO多路转接: IO多路转接能够同时等待多个文件描述符的就绪状态

  5. 异步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

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柿子__

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值