目录
一、epoll基于ET模式如何进行服务器设计?
规则:报文和报文之间用X做分隔符,写一个epoll版本的计算器。
首先我们先分析下之前epoll版本服务器的不完善的地方
- 1.当你在读取的时候,你不能保证你能把这个数据读完;你也不能保证,如果你第一次没读完,下一次读的时候,还能继续去读。所以我们需要给每一个fd,都要有自己专属的输入输出缓冲区。实际上在进行服务器编写,尤其是epoll,当他用套接字读取数据时,必须有着自己的输入和输出缓冲区,将来有10个fd,就得有10对输入输出;有100个fd,就得有100个输入输出。如果没有,是不可能编写出一个好的服务器的。
- 2.虽然已经对等和拷贝在接口层面已经进行分离了,但是在代码逻辑上依旧是耦合在一起的。换句话说,如果将来想对读的数据同时做业务处理,实际上它的代码将来会黏在一起,代码会变得非常大。我们可以通过回调的方式进行解耦
- 3.epoll最大的优势在于就绪事件通知机制。
二、Reactor框架
epoll最大的优势在于就绪事件通知机制,所以我们可以这么干
epoll可以帮我们去检测哪些fd上的哪些事件就绪了,现在根据这仅有的54行代码,我们可以推测出将epoll设计成一个叫做就绪事件派发逻辑,说白了就是epoll可以告诉我们哪些fd就绪,哪些fd就绪了,我们就可以调用fd上的读写回调,所以我们最终就可以在epoll中进行事件派发,事件派发的时候,我们此时就可以通过epoll检测到哪些fd上的哪些事件就绪了,然后直接调用fd上的回调函数,就叫做把任务派发出去了,所以我们基于epoll写的这个事件派发的函数就可以称之为派发器。
至此,我们把这种可以用一个派发器将就绪事件派发给sock,让他执行对应的读写,并将数据读写到自己的输入输出缓冲区,这种策略我们称之为Reactor模式(反应堆模式)。
反应堆模式:派发器中一旦有sock就绪了,就会直接回调sock注册的回调方法。如果派发器中有多个sock,哪一个就绪后就会调用对应sock注册的回调方法。这就叫做反应堆模式。
接下来,我们Event进行完善:
- 1.在Event中设置回指Reactor的指针,所以将来Reactor:Event = 1:n;也就是说,我们可以把很多的Event放到一个Reactor里,每一个Event都能回指唯一的Reactor对象。
- 2.在Event中给三个方法注册对应的回调机制
编写Reactor的相关接口
编写InitReactor接口
说白了就是创建一个epoll模型
编写InsertEvent接口
将来我们向Reactor中插入1个事件,首先要保证有这个事件,其次要明白你所插入的事件它关心的事件是什么。
这个events就是对应sock所关心的事件,就是epoll提供的一系列宏,eg:EPOLLIN...
所以这里的插入要做两件事情:
- 1.将Event中的sock插入到epoll中。
- 2.将Event本身插入到unordered_map中。
DeleteEvent接口的编写
- 1.将sock从epoll中删除
- 2.将sock从unordered_map中删除
关于修改的接口,我们待会再说。
目前我们所做的工作就是对 Reactor做了一个封装,就是在多加了一个unordered_map将fd和Event的映射关系进行管理起来。
Dispather接口的编写
将来的用我这个服务器的人,一定是向我这个epoll模型中插入Event结构,Event结构插入以后一定会在epoll模型中管理起来,然后在unordered_map里将映射关系管理起来,一旦哪个套接字就绪了,我就能根据这个套接字找到它对应的Event结构(缓冲区,读写方法全有),所以就可以做到1个链接1个Event结构,这个Event结构上的所有信息我也都能找到。
写到这,我们也不用像select和poll一样写一个switch了,因为走到这里,所有的事件都是就绪的,就绪的个数就是n。再者对于epoll_wait等于0超时,小于0反回,循环体永远不执行,同样我们对超时或者出错也不进行处理,只关心就绪的。
接下来我们就可以做事件判断了:
revents代表的就是就绪fd关心的事件。以前我们只关心了读和写,今天我们在补充上EPOLLERR和EPOLLHUP
- EPOLLERR : 表示对应的文件描述符发生错误
- EPOLLHUP : 表示对应的文件描述符被挂断
如果fd上有出错事件,我们就把全部的出错事件归类成读事件或者写事件就绪。EPOLLHUP : 表示对应的文件描述符被挂断,说明就是对端断开连接了,之前我们解决对端链接关闭是通过recv读取,读取到对方的返回值为0,说明对端链接关闭了,实际上在recv读到0之前,epoll是能够识别到对端链接关闭的事件的。
我们这两个if代表差错处理,将所有的错误问题全部转化为让IO函数去解决,也就是说比如对方关闭链接或者读取失败,全部交给recv,send等接口统一处理。
接下来就是读事件或者写事件就绪,但是这个接口既然叫做事件派发,而且刚开始我们就说过我们以前的代码耦合度太高,也就是说我们之前是一旦检测到有事件就绪,我们接下来就直接开读。而我们今天就不是了。
读事件就绪的时候,我是不需要自己去读的,我只需要调用这个fd对应的recver回调就可以了。
events[sock]代表的就是fd对应的Event指针(当然你怎么保证这个events[sock]一定存在呢?稍后解决),然后找到recver,recver默认是空,只要他被设置过,就直接调用它的回调方法。这里调用它的回调方法就需要把Event*这个参数传进去。
换句话说,这个事件派发器最核心的工作就是根据特定的你曾经注册过的所有fd,然后就执行对应的回调方法,这就叫做将就绪事件和IO真正读取进行了解耦。
三、上层逻辑的编写(epoll_server.cc)
接下来,我们可以写一下上层逻辑
我们现在服务器中,listen套接字所对应的Event结构被托管给了Reactor当中,然后就开始派发。这个派发器就会走先epoll_wait,因为我们只有1个listen套接字且只关心读,然后因为listen套接字的recver方法被注册了(用Accepter注册了),所以这个listen是有读取的,就直接调用listen的回调,所以就完成了对listen的基本调用逻辑。
当我们的服务器一旦启动,创建套接字,创建epoll对象,将listen套接字封装成1个对应的Event结构,然后给Event结果里注册一个Accepter方法,然后插入到Reactor当中。然后就开始事件派发。一旦底层有链接到来,这个事件派发器获取新连接自己不做处理,它检测到有事件发生,识别到是in事件,直接就去调用回调,而这个回调方法就是刚刚注册的Accepter方法,所以就直接跳转到Accepter函数,直接去执行这个方法。
这是目前我们的Accepter方法,按照我们当前的逻辑,有了链接到来就会打印这句话。(一直循环打印是因为我们当前是LT模式)
所以,以后我们的服务器就是当有新链接到来时,就是获取到新的链接,封装成Event结构,然后扔给Reactor当中,Reactor就会自动进行派发,当你封装Event结构的时候,同时是注册了回调方法的,所以之后一旦有事件就绪,Reactor就会自动调用曾经注册过的回调。
目前我们的代码是LT模式,但是我们今天想要的是ET模式,所以在插入事件的时候就需要设置成ET模式。
这个时候确实变成了ET模式。
四、Accepter的编写
接下来就开始Accepter的编写:
如果你是ET模式,你接下来就是调用一次accept获取新链接,但底层可能会到来多个链接,所以你就要对到来的多个链接循环读取。如果你不循环读取,底层有5个链接,你只accept了1次,那么未来剩下的4个链接就再也不通知你了,因为是ET模式。所以我们必须得将fd设置成非阻塞。
所以在创建listen套接字之后,还要设置非阻塞
当获取连接成功后,你不能读,也不能写,你只需要把你这个对应的链接封装成Event结构,然后添加到Reactor里,然后Reactor里的事件派发器派发的时候,就会自动执行你注册的回调方法。因为这次获取到的链接可是真正要进行IO的,所以我们注册回调方法的时候,要把Recver, Sender, Errorer三个方法都注册上。同样我们待会要实现这三个方法。
为什么要让所有的Event指向自己所属的Reactor呢?
1.为了能够直接调用Reactor方法。比如说:我们这里通过listen套接字的Event里的Reactor,把新的套接字添加到同一个Reactor里。
所以不断经过这样的死循环,就会把所有的链接全部获取上来,向Reactor里面不断添加新的事件。
五、Server.hpp的编写
这个时候,我们的服务器注册好Accepter方法,一旦有新链接到了就把事件派发给listen套接字,listen套接字就执行Accepter里的获取方法,把所有获取上来的链接构建成Event结构,然后添加到Reactor里,之后就不管了。但是服务器一直在进行循环派发,所以正常的fd只要有读事件,写事件就绪就会调用自己的回调方法,也就是调用到了Recver, Sender, Errorer三个方法中。
我们暂时先让Recver打印下。
至此,我们的代码逻辑就是,一旦把初始化工作准备好(创建listen套接字...)然后就开始事件派发,因为注册过对listen套接字的Accepter,一旦有新连接来就直接执行Accepter,Accepter获取新链接就构建对应的Event结构,然后把Event结构添加到Reactor中。Reactor一直在做事件派发,又因为我们在Accepter中给正常fd设置的是Recver, Sender, Errorer这三个回调,所以再有事件就绪,就会进入对应的这三个回调中。
我们可以暂时测试一下当前代码逻辑:服务器启动以后,就可以派发事件,然后当有新的连接到来首先进入Accepter,然后不断去获取新链接,获取后构建新的Event添加到Reactor中,然后我们通过建立的连接发一条数据,Reactor事件派发的时候,就会发现有一个fd上读事件就绪了,然后就会调用Recver回调方法,打印出Recver函数中的话。所以只要看到Recver函数中的话就证明我们的逻辑就是通的。
所以我们现在已经做到了让每个套接字都有了一个Event结构,那么就有了自己的读写缓冲区,回调方法;我们已经完成了软件解耦(将等待和IO操作进行了解耦)
目前我们的代码逻辑分为了三层,最底层的是Reactor,对所有的Event做管理,底层里最重要的叫做事件派发器;再上层有一个Accepter链接管理器,一旦有对应连接到来就派发到了链接管理器中;再上层还有一个基本IO模块,一方面要将自己相关的IO方法注册进Accepter中,一旦注册好,事件派发器就可以将事件直接派发给基本IO模块。
IO板块的编写
如何进行Recver呢?
无论是Recver还是Sender传进来的都是1个Event参数,因为Reactor里的派发器直接就把就绪的Event直接传给了回调方法,就相当于此处在Recver这就拿到了Event的所有内容,包括Event的sock,inbuffer,outbuffer。所以我们这次读取时就可以将读到的数据全部先放在inbuffer中。实际上对于数据读取的时候,缓冲区是有专门的策略的,我们这用string就够了,也就是目前将底层的数据按字节流的方式全部读到了inbuffer里,TCP是面向字节流的,我今天就无脑把数据拷贝到我自己的inbuffer中,inbuffer是我自己的,所以我想怎么改就怎么改。所以我把数据全部拷贝到inbuffer中,接下来再做协议分析,即使一次没读完,也没关系,下次读的时候继续进行插入。
这个Recver做的事情有如下几个:
- 真正的读取数据
- 进行分包,所谓的分包就是你读到的数据有可能是半个报文,有可能恰好读到一个完整的报文,有可能读上四,五个报文...总之你对数据的读取是有很多情况的,而我们只想读取到1个或者多个报文。如果你读到了半个报文,那么这个分包就什么也不做还得等,如果此时读到了2个半报文,它只会把前两个报文拿出来,半个报文先放到缓冲区里,下一次分包的时候在拼接好,总之在向下交付的时候,只会交付给我一个个完整的报文,所以所谓的分包就是解决粘包问题。
- TCP是面向字节流的,TCP的缓冲区不是你的,你没办法对数据进行自由切割,所以我就把数据全读到我的缓冲区里,这样我就可以进行自由切割了。然后报文就变成了一个个的报文,但是这一个个的报文内部也有它自己的格式,所以此时我们就需要进行反序列化。针对一个报文提取有效参与计算或者存储的信息。
- 业务逻辑处理,然后得到结果
- 构建响应
- 尝试直接,间接进行发送
这就是我们代码的逻辑。
ReverCore接口的编写
因为我们今天一旦有新连接到来,链接已经被设置成非阻塞,而且工作模式是ET,所以你想进行读取,必须是无脑读取。如果读取失败就可能有两种情况:
- 读完,底层没数据了。
- 真正出错了。
判断这两种情况我们就可以借助errno。errno除了之前说过的EAGAIN和EWOULDBLOCK,还有一种情况叫做EINTR,它表示在进行IO读取的时候是可能被信号中断的,不管是阻塞还是非阻塞,进程有S和D两种状态,S叫做浅度睡眠,D叫做磁盘休眠也叫深度睡眠。其中S状态也叫做可中断睡眠,大部分情况下调用read的时候如果数据没就绪,叫做阻塞状态,而阻塞状态,真正在OS对应的进程的状态就是S,如果是S状态,我们可以给S状态发送信号,让S状态的进程立马被唤醒。所以万一引起休眠的是read或者write,此时当收到信号也可能把进程唤醒,此时read就不会再次回到阻塞的地方继续运行,而是从read之后在运行,可是你有可能被中断唤醒之后有部分数据没被读到。
所以读取出错了就进行差错处理,因为你曾经已经注册过Errorer,所以出错了就进行回调我们自己的Errorer方法。也就是说只要出错了,所有的错误我都扔给你这个errorer。也就是整个这一份代码把错误都放在errorer这个函数里。Reactor里的差错也不处理,把所有的差错转化成IO,然后IO进一步的只要出错了就去调用errorer,这就是Reactor里没有调用errorer,而是在IO服务里进行回调errorer。所以这整个系统里的错误都集中在errorer这一个函数里,所以你的代码出现问题,只要找这个函数就可以了(但是我们就不进行设计这个函数了)。
分包接口
我们目前想写的就是一个加法器,我们用X分离报文,比如: 客户端请求可能就是1+2X3+4X5+6X这样的;1+2X这样的;1+2X3这样的;不管收到什么样的,我们都只想把合法的报文处理掉。还没有完全的报文就给你留下来,后续进行处理。所以此处我们就需要把一个字符串以合法分隔符结尾的全部提取出来,放到一个vector里,把非法的先留下来。
反序列化
现在这个tokens里放的就是1+2,2+3...这样的东西
业务逻辑及构建响应
构建完响应以后,这个时候绝对不能直接send,你不能保证你send的时候这个fd是就绪的,所以完全没必要send,只需要将报文放到outbuffer里即可。所以所谓的发送数据并不是把数据真正的发送出去,而是把数据拷贝到缓冲区里。今天我们自己维护了一个outbuffer缓冲区,所以之前C/C++语言是自带缓冲区的,我今天就相当于自己设计了一个用户层缓冲区。这个数据什么时候发生也不是有你决定的。
尝试直接或间接发送数据
所谓的尝试发生是不是必须条件成熟了,你才能发送呢?
这里的条件成熟了指的是写事件就绪,我们曾经谈论的都是读事件就绪,你现在想发必须是写事件就绪才可发,写缓冲区虽然是一直就绪的(因为我就没发过数据)但是一般只要将报文处理完毕,才需要发送,没有发送的话,TCP的写缓冲区一直是空的,空间足以容纳大量数据,所以写事件一定是处于高频的就绪状态,所以写事件随时随地可以就绪。可是你只有读到报文,把报文处理完了才需要发送。所以写事件一般都是就绪的(但有时候架不住IO逻辑复杂,IO量过大可能写事件不就绪),但是用户不一定是就绪的。所以大部分情况下慢的不是发送缓冲区,慢的是用户。
对于写事件,我们通常是按需(按照用户的需要)设置。你有没有数据发送只有用户最清楚。
这也就是为什么套接字在Accpet中不能既关心读,又关心写, 因为你如果把EPOLLOUT设置了,EPOLLOUT缓冲区一直都存在,所以此时会导致你的事件派发当中一直派发写事件,可是你的用户压根什么都没有,你也没办法写,而你的写事件一直在触发,所以它会导致你的服务器几乎不会等待,大部分情况下一直在进行死循环,这样对我们服务器的压力是很大的,我们也不建议这么做,所以绝对不能写一个服务器是EPOLLOUT和EPOLLIN同时关心。所以以后一个人写服务器的时候,直接就把EPOLLOUT和EPOLLIN同时设置了,要么他就是在做测试,要么他的水平很低。
发送缓冲区不为空说明当前发送缓冲区有数据,可是默认你的EPOLLOUT是关闭的,所以我们需要提供一个是当前套接字能读写的接口
然后把写打开以后就能发送数据了。
因为写打开的时候,默认就是就绪的,因为从来没有人发送过数据,即便是发送缓冲区已经满了,但epoll的策略规定,只要用户重新设置了OUT事件,EPOLLOUT至少会触发一次。
为什么打开写就发送数据了?
一旦我们打开写,数据已经被写在了evp对应的outbuffer,outbuffer里面也有了数据,并且事件派发器就开始帮你去派发特定fd上面的写事件,写事件一旦帮你派发,此时会自动调用发送函数(sender方法)。换言之,当进行Recver读的时候把数据处理完,你只需要把fd关心读事件打开就行了,打开之后,我们的事件派发器当底层缓冲区有空间或者从无到有有空间,EPOLLOUT事件会自动被触发,就会自动调用sender方法,把outbuffer里的数据发送出去。
Sender方法
很简单,只需要无脑向缓冲区里去写入,写不成功就继续写;如果写完了就可以把该fd的写事件关闭。所以打开写入是Recver打开的因为只有它最懂需求,关闭对应的写由Sender来关闭,因为它最懂发送完成与否。
一切的一切在于Reactor会自动的给我们进行事件的派发,底层的事件就绪是由OS自动就绪的,底层OS一旦有事件就绪就会驱动Reactor里的Dispather派发事件,派发事件就会回调读写方法,读写方法之间的交互就是通过使能读写开关完成双方交付的。
发送的时候有可能你把发送缓冲区打满了,但是数据你还没发完,就需要你再次进行发送。就需要循环发送。
关于total的设置,因为这个outbuffer是我们自己定义,不是TCP的,所以我们要自己控制发送的区间(因为我们自己定义的数据发送完以后还在outbuffer里面,并不像TCP的发送缓冲区一样,发完数据就没了),所以通过这个total进行发送控制。
如果TCP的发送缓冲区满了,也不能在发了,而且还需要清理自己的outbuffer。比如说你自己的发送缓冲区大小是2048,TCP的发送缓冲区大小是1024,你发送1024以后,TCP的缓冲区满了,你就只能下次再发,同时还需要把outbuffer里已经发出去的数据直接清掉,以便下一次进行接收数据。
Errorer接口
出错了就去调用DeleteEvent
DeleteEvent的编写
删除的逻辑比较复杂,读写都可以进来的条件下,fd被删一定是合法的情况下,找到了才进行清理。
但是目前代码还存在一个小问题,当我们在做差错处理的时候,我们直接调用的就是Reactor的DeleteEvent方法做移除。就有可能存在这样一个问题,当你在进行事件派发的时候,把所有事件合并在读写,有可能调用读回调的时候已经错误了,我们就已经调用了DeleteEvent方法,也就意味着套接字什么的全部被删除了,此时你再去写的时候,在进行events索引的时候就可能报错了。所以我们必须保证你在进行后序处理的时候,这个套接字必须是有效套接字,所以我们要进行一个检查。
检查套接字是否存在
至此这份代码全部写完。
测试
退出的时候首先是Recv被调用,因为我们把所有的事件都转化成了读写,我们肯定先掉的是读方法(因为读方法在前,写在后)。调用Recver方法后去调用RecverCore,一定是返回0的,在Recver里就是失败了,进入差错处理,然后就删除所有的信息。我们再次连接fd还是5,说明成功删除。
我们可以看看inbuffer里面的数据
总结:这个Reactor我们称之为反应堆模式,通过多路转接方案,被动的采用事件派发的方式去反向的调用对应的回调函数。反应堆其实就类似于打地鼠,当哪个fd就绪了我就打他。Event就相当于地鼠的洞,套接字就相当于地鼠,当地鼠就绪时我们就可以执行对应回调。Reactor里会把所有的fd和Event全部管理起来,本质就是Event中存在一个Reactor的回指指针。
我们处理一件事情步骤:
- 1.检测到事件-epoll
- 2.派发事件--Dispatcher
- 3.链接--Accepter
- 4.IO--recver,sender。
六、完整代码
Reactor.hpp
#pragma once
#include<iostream>
#include<string>
#include<vector>
#include<unistd.h>
#include<cstdlib>
#include<unordered_map>
#include<sys/epoll.h>
#define SIZE 128
#define NUM 64
//一般处理IO的时候,我们只有三种接口需要处理
//处理读取
//处理写入
//处理异常
class Event;
class Reactor;
//返回值设为int,参数是Event*,这个就叫做函数指针类型
typedef int (*callback_t)(Event *ev);
//需要让epoll管理的基本节点
//事件结构体,将来accept上一个链接,不仅仅是管理这个连接,我要把sock放进这个event,整体管理这个event
class Event
{
public:
int sock; //对应的fd
std::string inbuffer; // sock对应的输入缓冲区
std::string outbuffer; // sock对应的输出缓冲区
//将来从套接字读到的所有数据放到inbuffer里面,因为每一个sock都有inbuffer,所以我就不担心
//一次没有读完,下次继续读就可以。以后我做数据分析,解决粘包问题就从inbuffer里解决,解决好
//后,所有的response我都放在outbuffer里面,最后再由epoll检测到事件,通过send发出去。
//给sock设置回调
callback_t recver;
callback_t sender;
callback_t errorer;
//设置Event回指Reactor的指针
Reactor *R;
public:
Event()
{
sock = -1;
recver = nullptr;
sender = nullptr;
errorer = nullptr;
R = nullptr;
}
void RegisterCallback(callback_t _recver, callback_t _sender, callback_t _errorer) //注册回调
{
recver = _recver;
sender = _sender;
errorer = _errorer;
}
~Event()
{
//do nothing
}
};
// 不需要关心任何sock的类型(无论listen,读,写)
// 只关心如何进行使用该类,对上面的Event进行管理
// 将来Reactor:Event = 1:n;也就是说,我们可以把很多的Event放到一个Reactor里,每一个Event都能回指唯一的Reactor
class Reactor
{
private:
int epfd; //epoll模型
//建立了一个fd和该fd对应的Event的映射结构,一旦有fd就绪了,我就可以拿着fd到这个map里找到对应的Event
//然后调用对应的回调读/写数据,放到对应的缓冲区里。当前次没读完也没关系,继续等,事件就绪了继续读/写。
std::unordered_map<int, Event *> events; //我的Epoll类管理的所有Event的集合
public:
Reactor()
:epfd(-1)
{}
void InitReactor()
{
epfd = epoll_create(SIZE); //SIZE是个宏:128
if(epfd < 0)
{
std::cerr << "epoll_create error" << std::endl;
exit(2);
}
std::cout << "InitReactor success" << std::endl;
}
// //第一个参数是sock,第二个参数是这个sock所关心的事件(evs的值就是epoll提供的一系列宏,eg:EPOLLIN...)
// bool InsertEvent(int sock, uint32_t evs)
// {
// //1,将sock插入到epoll中
// struct epoll_event ev;
// ev.events = evs;
// ev.data.fd = sock;
// //epoll_ctl成功返回0,失败返回-1
// if (epoll_ctl(epfd, EPOLL_CTL_ADD, sock, &ev) < 0) //插入失败
// {
// std::cerr << "epoll_ctl add event failed" << std::endl;
// return false;
// }
// // 2,将通过sock构建一个Event,然后将Event插入到unordered_map中
// Event *evp = new Event; // 因为在内部构造的时候需要设置RegisterCallback,所以这个Event必须作为参数传进来
// evp->sock = sock;
// }
// 第一个参数是Event,第二个参数是这个evp中的sock所关心的事件(evs的值就是epoll提供的一系列宏,eg:EPOLLIN...)
bool InsertEvent(Event* evp, uint32_t evs)
{
// 1,将sock插入到epoll中
struct epoll_event ev;
ev.events = evs;
ev.data.fd = evp->sock;
// epoll_ctl成功返回0,失败返回-1
if (epoll_ctl(epfd, EPOLL_CTL_ADD, evp->sock, &ev) < 0) // 插入失败
{
std::cerr << "epoll_ctl add event failed" << std::endl;
return false;
}
// 2,将通过sock构建一个Event,然后将Event插入到unordered_map中
events.insert({evp->sock, evp});
}
void DeleteEvent(Event *evp)
{
int sock = evp->sock;
auto iter = events.find(sock);
if(iter != events.end())
{
// 1.将sock从epoll中删除
epoll_ctl(epfd, EPOLL_CTL_DEL, sock, nullptr);
// 2.将sock从map中删除
events.erase(iter);
//3.close
close(evp->sock);
//4.删除event节点
delete evp;
}
}
//修改,使能读写
bool EnableRW(int sock, bool enbread, bool enbwrite)
{
struct epoll_event ev;
ev.events = EPOLLET | (enbread ? EPOLLIN : 0) | (enbwrite ? EPOLLOUT : 0);
ev.data.fd = sock;
if(epoll_ctl(epfd,EPOLL_CTL_MOD,sock,&ev) < 0)
{
std::cerr << "epoll_ctl mod event failed" << std::endl;
return false;
}
}
bool IsSockOk(int sock)
{
auto iter = events.find(sock);
return iter != events.end();
}
void Dispather(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函数去解决,也就是说比如对方关闭链接或者读取失败,全部交给recv,send等接口统一处理
//如果fd上有出错事件,我就把全部的出错事件归类成读事件或者写事件就绪
if(revents & EPOLLERR)
{
revents |= (EPOLLIN | EPOLLOUT);
}
//如果epoll发现对端链接关闭了
if(revents & EPOLLHUP)
{
revents |= (EPOLLIN | EPOLLOUT);
}
//读事件就绪,可能有bug,之后解决
if(revents & EPOLLIN)
{
if((IsSockOk(sock)) && events[sock]->recver)
{
//调用回调方法,执行对应的读取
//只要他的recver不为空,就说明被设置过,就调用它的回调方法
events[sock]->recver(events[sock]);
}
}
if (revents & EPOLLOUT)
{
if ((IsSockOk(sock)) && events[sock]->sender)
{
events[sock]->sender(events[sock]);
}
}
}
}
//所以,这个派发器最核心的工作就是根据特定的fd,就可以执行对应的回调方法,这就叫做将就绪事件和IO的真正读取进行了解耦。
~Reactor()
{
//do nothing
}
};
Accepter.hpp
#pragma once
#include"Reactor.hpp"
#include"Sock.hpp"
#include"Service.hpp"
#include"Util.hpp"
int Accepter(Event* evp)
{
std::cout << "有新的链接到来了,就绪的sock是: " << evp->sock << std::endl;
int sock = evp->sock;
while(true)
{
int sock = Sock::Accept(evp->sock);
if(sock < 0)
{
std::cout << "Accept Done!" << std::endl;
break;
}
std::cout << "Accept Success: " << sock << std::endl;
SetNonBlock(sock);
//构建Event*
Event *other_ev = new Event;
other_ev->sock = sock;
other_ev->R = evp->R; //为什么要让所有的Event指向自己所属的Reactor呢?
// Recver, Sender, Errorer,就是我们代码中的较顶层,只负责真正的读取。
other_ev->RegisterCallback(Recver, Sender, Errorer);
//将新的Event添加到Reactor中
evp->R->InsertEvent(other_ev, EPOLLIN | EPOLLET);
}
}
Sock.hpp
#pragma once
#include<iostream>
#include<string>
#include<cstring>
#include<cstdlib>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
using namespace std;
class Sock
{
public:
static int Socket()
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
cerr << "socket error" << endl;
exit(2); //直接终止进程
}
int opt = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
return sock;
}
static void Bind(int sock,uint16_t port)
{
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = INADDR_ANY;
if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
cerr<<"bind error!"<<endl;
exit(3);
}
}
static void Listen(int sock)
{
if (listen(sock, 5) < 0)
{
cerr << "listen error !" << endl;
exit(4);
}
}
static int Accept(int sock)
{
struct sockaddr_in peer; //对端的信息
socklen_t len = sizeof(peer);
int fd = accept(sock, (struct sockaddr *)&peer, &len);
if (fd >= 0)
{
return fd;
}
return -1;
}
static void Connect(int sock, std::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());
if (connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0)
{
cout << "Connect Success!" << endl;
}
else
{
cout << "Connect Failed!" << endl;
exit(5);
}
}
};
Service.hpp
#pragma once
#include"Reactor.hpp"
#include<cerrno>
#include"Util.hpp"
#include"Sock.hpp"
#define ONCE_SIZE 128
// 返回值1,本轮读取完毕
// 返回值-1,真正读取出错
// 返回值0,对端关闭连接
static int ReverCore(int sock, std::string &inbuffer)
{
while(true)
{
char buffer[ONCE_SIZE]; //一次读取128字节
ssize_t s = recv(sock, buffer, ONCE_SIZE, 0);
if(s > 0)
{
//读取成功
buffer[s] = '\0';
inbuffer += buffer;
//这里有些小问题,因为有可能对方发过来一种图片,文件内容就会包含\0,\0可不是字符串,
//就相当于buffer内部本身因为接收数据而有了\0,但是今天不考虑这个问题,只把对方发过来的内容当字符串
}
else if(s < 0)
{
//在读取的时候可能被信号中断
if(errno == EINTR)
{
//IO的信号被打断,只不过概率很低
continue;
}
//1.读完,底层没数据了
if (errno == EAGAIN || errno == EWOULDBLOCK)
{
return 1; //success
}
//2.真正出错了
return -1;
}
else
{
//s==0
return 0;
}
}
}
int Recver(Event *evp)
{
std::cout << "Recv CallBack has been called!" << std::endl;
//1.真正的读取
int result = ReverCore(evp->sock, evp->inbuffer);
if(result <= 0)
{
//差错处理
if(evp->errorer)
{
//我们已经注册过Errorer,回调我们自己的Errorer方法
evp->errorer(evp);
}
return -1;
}
// 2.分包--一个或者多个报文--解决粘包问题
std::vector<std::string> tokens;
std::string sep = "X";
SplitSegment(evp->inbuffer, &tokens, sep);
// 3.反序列化--针对一个报文--提取有效参与计算或者存储信息
for(auto &seg : tokens)
{
std::string data1, data2;
if(Deserialize(seg, &data1, &data2))
{
// 4.业务逻辑--得到结果
int x = atoi(data1.c_str());
int y = atoi(data2.c_str());
int z = x + y;
// 5.构建响应--添加到evp->outbuffer
// 2+3X -> 2+3=5X
std::string res = data1;
res += "+";
res += data2;
res += "=";
res += std::to_string(z);
res += sep;
//这个时候绝对不能直接send,你不能保证你send的时候这个fd是就绪的,
//所以完全没必要send,只需要放到outbuffer里即可。
evp->outbuffer += res; //发送数据
}
}
// 6.尝试直接间接发送
// TODO
if(!(evp->outbuffer).empty()) //说明当前发送缓冲区有数据
{
//写打开的时候,默认就是就绪的,即便是发送缓冲区已经满了
//epoll只要用户重新设置了out事件,EPOLLOUT至少会在触发一次
evp->R->EnableRW(evp->sock, true, true);
}
return 0;
}
// 返回值是1,将数据全部发送完成
// 返回值是0,数据没有发完,但是不能再发了
// 返回值是-1,发送失败
int SenderCore(int sock,std::string &outbuffer)
{
while(true)
{
int total = 0; //本轮累计发送的数据量
const char *start = outbuffer.c_str(); //因为send的参数是C式的
int size = outbuffer.size();
//start+total比如你刚开始发了10个字节,total为0,下次发送的时候就需要把这10个字节跳过去发送
//size-total比如你刚开始期望把buffer里的数据全发送出去,size初始值为50,当我发了10个,我期望发送的就剩20个了
ssize_t curr = send(sock, start + total, size - total, 0);
if(curr > 0)
{
total += curr;
if(total == size)
{
//全部将数据发送完成
outbuffer.clear(); //清理缓冲区,因为是我自己定义的
return 1;
}
}
else
{
if (errno == EINTR)
{
// IO的信号被打断,只不过概率很低
continue;
}
// 数据没发完,但是不能再发了!说明发送缓冲区满了
// 比如说你自己的发送缓冲区大小是2048,TCP的发送缓冲区大小是1024,你发送1024以后,TCP的缓冲区满了
// 你就只能下次再发,就需要把outbuffer里已经发出去的数据直接清掉
if (errno == EAGAIN || errno == EWOULDBLOCK)
{
outbuffer.erase(0, total);
return 0;
}
return -1;
}
}
}
int Sender(Event *evp)
{
std::cout << "Sender been called" << std::endl;
int result = SenderCore(evp->sock, evp->outbuffer);
if(result == 1)
{
//全部发完了,就把读取保留,把写关闭
evp->R->EnableRW(evp->sock, true, false);
}
else if(result == 0)
{
//目前数据还没发完
//1.你可以什么也不做
//2.继续打开写,继续让他发
evp->R->EnableRW(evp->sock, true, true);
}
else
{
if(evp->errorer)
{
evp->errorer(evp);
}
}
}
int Errorer(Event *evp)
{
std::cout << "Errorer been called" << std::endl;
evp->R->DeleteEvent(evp);
}
Util.hpp
#pragma once
#include<iostream>
#include<unistd.h>
#include<fcntl.h>
//工具类
//设置一个sock成为非阻塞
void SetNonBlock(int sock)
{
int fl = fcntl(sock, F_GETFL);
if(fl < 0)
{
std::cerr << "fcntl failed" << std::endl;
return;
}
fcntl(sock, F_SETFL, fl | O_NONBLOCK);
}
//1+2X3+4X5+6X
void SplitSegment(std::string &inbuffer, std::vector<std::string> *tokens, std::string sep)
{
while(true)
{
std::cout << "inbuffer: " << inbuffer << std::endl;
auto pos = inbuffer.find(sep);
//因为我们规定每一个完整的报文必须携带X,哪怕你是最后一个报文也得有X
if(pos == std::string::npos)
{
break; //不完整就不处理
}
std::string sub = inbuffer.substr(0, pos); //因为[)前闭后开 所以pos的数字就代表了截取的长度
(*tokens).push_back(sub);
inbuffer.erase(0, pos + sep.size()); //因为是读到我自己的缓冲区里,所以我们得自己手动处理,TCP的缓冲区就不用
//最后找不到X,你这个inbuffer里面要么就是没数据,要么就是剩下一个残缺的报文
}
}
bool Deserialize(const std::string &seg, std::string *out1, std::string *out2)
{
//1+2
std::string op = "+";
auto pos = seg.find(op);
if(pos == std::string::npos)
{
return false;
}
*out1 = seg.substr(0, pos);
*out2 = seg.substr(pos + op.size());
return true;
}
epoll_server.cc
#include"Sock.hpp"
#include"Reactor.hpp"
#include"Accepter.hpp"
#include"Util.hpp"
static void Usage(std::string proc)
{
std::cout << "Usage: " << proc << " port" << std::endl;
}
int main(int argc, char* argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(1);
}
//1.创建套接字,监听
int listen_sock = Sock::Socket();
SetNonBlock(listen_sock);//设为非阻塞
Sock::Bind(listen_sock, (uint16_t)atoi(argv[1]));
Sock::Listen(listen_sock);
//2.创建Reactor对象
Reactor *R = new Reactor;
R->InitReactor(); //构造好了epoll模型
//3.给Reactor反应堆中加柴火(我要让Reactor帮我管理所有的套接字)
//3.1 造柴火
Event *evp = new Event;
evp->sock = listen_sock;
evp->R = R;
//Accepter:链接管理器
evp->RegisterCallback(Accepter, nullptr, nullptr); //注册回调
//3.2 将准备好的柴火放进反应堆
R->InsertEvent(evp, EPOLLIN | EPOLLET); // 对于这个listen套接字只关心一种事件EPOLLIN
//4.开始进行事件派发
int timeout = 1000;
for (;;)
{
R->Dispather(timeout);
}
}