从零开始写一个Redis-2

Reactor和Log

文章目录

  • Reactor和Log
    • 1. Logger
      • 1.1 EasyLogging++
      • 1.2 muduo::Logging
    • 2. Reactor
      • 2.1 我的文章参考
      • 2.2 简单说明
      • 2.3 第一个版本
        • 2.3.1 Event
        • 2.3.2 EventLoop
        • 2.3.3 Poller
        • 2.3.4 例子
      • 2.4 第二个版本
    • 3. Acceptor
      • 3.1 源码实现
      • 3.1 echo服务器测试
    • 4. 结语

1. Logger

1.1 EasyLogging++

在正式开始之前,我们先考虑下日志的问题,之前学习muduo是写过一个Log的,但是那个Log仅仅是为了学习使用,所以比较简陋,我这里又不想花费过多的时间再Log上,所以选用了一个轻量级别的日志框架。


选用EasyLogging++

如何使用


因此关于日志,我们就迅速跳过。

1.2 muduo::Logging

第二个使用选项是muduo中的Logging,拿过来改了改觉得比较好用。

2. Reactor

2.1 我的文章参考

https://blog.csdn.net/weixin_43468441/article/details/103336870

https://blog.csdn.net/weixin_43468441/article/details/103336897

https://blog.csdn.net/weixin_43468441/article/details/93775965

2.2 简单说明

在我看来,Reactor的优点主要就是利用IO复用,即epoll,select或者poll,即半同步半异步的方式。

实际上就是我们不用使用阻塞调用去傻傻地等待连接,或者客户端的数据到来。而是取而代之被IO复用来通知。

第一个版本的Reactor,是为了方便阅读,所以相当于直接模仿Redis做的。

第二个版本的Reactor,则是为了更加方便使用。手法更加类似muduo。

2.3 第一个版本

2.3.1 Event

#include 
#include 
#include 

#define AE_NONE       0 
#define AE_READABLE   1 
#define AE_WRITABLE   2

class EventLoop;

class FileEvent;
using FileEventPtr = std::shared_ptr<FileEvent>;

class FileEvent : boost::noncopyable
{
     
  friend class EventLoop;
  using Callback = std::function<void(int)>;
public:
  FileEvent(int fd);

  void RegisterReadHandler(Callback cb);
  void RegisterWriteHandler(Callback cb);


private:
  void HandleRead(int connfd)
  {
     
    if(readHandler_)
      readHandler_(connfd);
  }
  void HandleWrite(int connfd)
  {
     
    if(writeHandler_)
      writeHandler_(connfd);
  }

  int FD() const {
      return fd_; }
  // 返回就绪的事件mask
  int Mask() const {
      return mask_; }
  int RMask() const {
      return rmask_; }
private:
  const int fd_;
  int mask_;
  int rmask_;
  Callback readHandler_;
  Callback writeHandler_;
};

struct FiredEvent
{
     
  FiredEvent() = default;
  FiredEvent(int fd, int rmask)
    : fd_(fd)
    , rmask_(rmask)
  {
     }

  int fd_;
  int rmask_;
};

  1. 可以看到,FileEvent只暴露了来个接口,用来注册Handler即回调函数。当前对于回调函数的形式没有考虑太完全,因此,只用了void(int)这种类型。
  2. 俩个私有函数,主要是提供给FileEvent的友元类EventLoop使用的,当事件就绪的时候用来执行回调函数。
  3. mask和rmask实际上是学习libevent使用的。mask代表FileEvent关注的事件,而rmask则代表从epoll返回时就绪的事件。
  4. FiredEvent代表就绪的事件。

2.3.2 EventLoop

class FileEvent;
class TimeEvent;

class EventLoop : boost::noncopyable
{
     
public:
  EventLoop()
    : maxfd_(10)
    , setsize_(1024)
    , isStarting_(false)
    , fileEvents_(setsize_)
    , firedEvents_(setsize_)
    , poller_(new Poller())
  {
     

  }
  ~EventLoop() = default;

  void RegisterFileEvent(std::shared_ptr<FileEvent> fileEventPtr);
  void DeleteFileEvent(int fd);
  void RegisterTimeEvent();
  void DeleteTimeEvent();
  void Exit()
  {
     
    isStarting_ = false;
  }

  void Loop();
  void beforeWait();
private:
  int maxfd_;
  int setsize_;
  bool isStarting_;

  std::vector<std::shared_ptr<FileEvent>> fileEvents_;
  std::vector<FiredEvent> firedEvents_;
  std::list<TimeEvent> timeEvents_;
  std::unique_ptr<Poller> poller_;
};

void 
EventLoop::Loop()
{
     
  isStarting_ = true;
  while(isStarting_)
  {
     
    poller_->Poll(-1, maxfd_, &firedEvents_);
    for(auto &one : firedEvents_)
    {
     
      if(one.rmask_ & AE_READABLE)
      {
     
        fileEvents_[one.fd_]->HandleRead(one.fd_);
      }
      if(one.rmask_ & AE_WRITABLE)
      {
     
        fileEvents_[one.fd_]->HandleWrite(one.fd_);
      }
    }
  }
}

  1. 对于EventLoop来说,实际上也就是关注2个接口。一个是向Poller注册事件的注册函数,一个是Loop函数,实际上就是去调用epoll_wait。
  2. fileEvents_实际上是一个哈希表。其索引对应fd,因此实际上等同于的hashmap。但是这里采用了vector实现。
  3. firedEvents为就绪事件列表。
  4. beforeWait实际上准备写成可以注册回调函数的形式,这样可以在epoll_wait前面添加一些init化函数,当然更好地是做成TaskBuffer地形式,类似muduo。
  5. 查看Loop,可以看到,Poll会返回就绪的事件列表,然后依次对应地执行回调函数。

2.3.3 Poller

class Poller : boost::noncopyable
{
     
public:
  Poller();
  ~Poller();

  // 根据mask,来决定关注读或者写事件
  bool UpdateEvent(int fd, int mask);
  bool DeleteEvent(int fd);
  void Poll(int count, int setsize, vector<FiredEvent>* firedEvents);
private:
  int epfd_;
};

bool 
Poller::UpdateEvent(int fd, int rmask)
{
     
  struct epoll_event ee;
  bzero(&ee, sizeof(ee));

  if(mask & AE_READABLE)
  {
     
    ee.events |= EPOLLIN;
  }
  if(mask & AE_WRITABLE)
  {
     
    ee.events |= EPOLLOUT;
  }
  ee.data.fd = fd;
  
  //int op = rmask == AE_NONE ? EPOLL_CTL_ADD : EPOLL_CTL_MOD;
  int op = EPOLL_CTL_ADD; // for simple

  if(epoll_ctl(epfd_, op, fd, &ee) == -1) 
  {
     
    return -1;
  }
  return 0;
}


void 
Poller::Poll(int count, int setsize, vector<FiredEvent>* activeEvents)
{
     
  int retval = 0;
  struct epoll_event events[setsize];

  retval = epoll_wait(epfd_, events, setsize, count);

  if(retval > 0)
  {
     
    int j;
    int numevents = retval;
    activeEvents->resize(numevents);

    for(j = 0; j < numevents; j++)
    {
     
      int mask = 0;
      struct epoll_event * e = events + j;
      if(e->events & EPOLLIN) mask |= AE_READABLE;
      if(e->events & EPOLLOUT) mask |= AE_WRITABLE;
      if(e->events & EPOLLERR) mask |= AE_WRITABLE;
      if(e->events & EPOLLHUP) mask |= AE_WRITABLE;

      activeEvents->push_back(FiredEvent {
     e->data.fd, mask});
    }
  }
}

可以看到Poller主要的作用就是为了封装epoll。
而Poll函数中唯一的变化就是需要设置返回的就绪事件列表。

2.3.4 例子

EventLoop eventLoop;

void onReading(int fd)
{
     
  char buff[1024] = {
     0};
  ::read(fd, buff, 1024);
  ::write(fd, buff, strlen(buff));
}

void onAccept(int fd)
{
     
  cout << "accept" << endl;
  int connfd = Socket::Accept(fd);
  auto event = make_shared<FileEvent>(connfd);
  event->RegisterReadHandler(onReading);
  eventLoop.RegisterFileEvent(event);
}

int main()
{
     
  Socket s(Socket::CreateTCPSocket());
  SocketAddr addr("127.0.0.1:8888");
  Socket::Bind(s.FD(), addr);
  Socket::Listen(s.FD());
  auto event = make_shared<FileEvent>(s.FD());
  event->RegisterReadHandler(onAccept);

  eventLoop.RegisterFileEvent(event);
  eventLoop.Loop();
}

使用流程

  1. 做一个listenfd出来,然后做一个事件。
  2. 注册对应地回调函数onAccept。
  3. 然后EventLoop注册这个事件。可以发现,2,3这俩件事情可以一步完成,版本2将对此优化。
  4. 开启事件循环。
  5. 当有连接到来时,就回调onAccept。
  6. 然后获得connfd,然后继续注册事件,以此类推。

通过这么使用,发现我得需要一个全局的EventLoop才行。实际上我们可以发现一个EventLoop可以拥有多个FileEvent,而每个FileEvent只属于一个EventLoop。因此,没有EventLoop的FileEvent是没有意义的。因此FileEvent是依赖EventLoop的。

2.4 第二个版本

https://github.com/patientCat/Jedis

第一个和第二个版本分别放到了Reactor_0x中。可以自己对比,来发现代码中的简化。

3. Acceptor

或者说是ListenSocket,如果你经常写服务器的话,可以发现,我们总是要socket-bind-listen来做一个listenfd。实际上我们完全可以可以抽象出一个Acceptor来。

在服务器启动前,完成这一步骤,然后将自己的OnAcceptHandler()注册到EventLoop中去。

这里可能有点难度,主要在于要习惯利用bind/function来注册函数,而Acceptor实际上就是我们的EventLoop启动的钥匙。它是第一个发生事件。后面的陆续发生的事件实际上都是源于这里的。

因为看过muduo源码,我总是不自觉地去模仿muduo,没办法。

3.1 源码实现

class EventLoop;

class Acceptor
{
     
public:
  using AcceptHandler = std::function<void(int)>;
  explicit Acceptor(EventLoop* loop, const SocketAddr& addr)
    : loop_(loop)
    , addr_(addr)
    , socket_(Socket::CreateNonBlockTCPSocket())
  {
     
    Socket::SetReuseAddr(socket_.FD());
    Socket::Bind(socket_.FD(), addr);
    Socket::Listen(socket_.FD());
    FileEventPtr eventPtr = std::make_shared<FileEvent>(socket_.FD());
    eventPtr->RegisterReadHandler(std::bind(&Acceptor::OnAcceptHandler, this));
  }
  void OnAcceptHandler()
  {
     
    int cfd = Socket::Accept(socket_.FD());
    Socket::SetNonBlock(cfd);
    onAcceptHandler_(cfd);
  }
  void RegisterOnAcceptHandler(AcceptHandler handler)
  {
     
    onAcceptHandler_ = handler;
  }
private:
  EventLoop *loop_;
  SocketAddr addr_;
  Socket socket_;
  AcceptHandler onAcceptHandler_; 
};

3.1 echo服务器测试

现在我们用我们已有的东西来做一个测试。

class Server
{
     
public:
  Server(const SocketAddr& addr)
    : addr_(addr)
    , loop_()
    , acceptor_(&loop_, addr)
  {
     
    acceptor_.RegisterOnAcceptHandler(std::bind(&Server::OnAcceptHandle, this, placeholders::_1));
  }

  void OnReading(int fd)
  {
     
    char buff[1024] = {
     0};
    ::read(fd, buff, 1024);
    ::write(fd, buff, strlen(buff));
  }

  void OnAcceptHandle(int cfd)
  {
     
    auto event = make_shared<FileEvent>(&loop_, cfd);
    event->RegisterReadHandler(std::bind(&Server::OnReading, this, placeholders::_1));
    event->EnableReading();
  }

  void Start()
  {
     
    loop_.Loop();
  }
private:
  SocketAddr addr_;
  EventLoop loop_;
  Acceptor acceptor_;
};



int main()
{
     
  SocketAddr addr("127.0.0.1:8888");
  Server jedis(addr); 
  jedis.Start();
}

不难发现,如果我们再将这个Server类写成提供回调函数的方式的话,那么,我们就相当于直接可以使用Server类,然后注册几个回调函数,就能完成的我们的服务器设置了。

4. 结语

github
上述代码主要存放在Reactor_2目录下

你可能感兴趣的:(c++从零开始,c++)