代码链接:https://github.com/ynighter/mymuduo.git
目录
1.前言
本项目对muduo库中核心部分进行的重新书写,muduo库是基于Reactor模式实现的TCP网络编程库,在多线程环境下有比较好的性能表现,其主要模式如下图所示。
此模式的特点是one loop per thread, 一个main Reactor负责accept连接, 然后把该连接挂在某个sub Reactor中,这样该连接的所有操作都在哪个sub Reactor所处的线程中完成。多个连接可能被分配到多个线程中,充分利用CPU。在应用场景中,Reactor的个数可以采用 固定的个数,比如跟CPU数目一致。此模式与模式二相比,减少了进出thread pool两次上线文切换,小规模的计算可以在当前IO线程完成并且返回结果,降低响应的延迟。并可以有效防止当IO压力过大时一个Reactor处理能力饱和问题。
其中Reactor模型的组件分布情况如下所示:

其中的调用关系如下:
1.将事件及其处理方法注册到Reactor,Reactor中存储了连接套接字connfd以及其感兴趣的事件Event;
2.Reactor向其所对应的Demultiplex去注册相应的connfd+Event,并启动反应堆;
3.当Demultiplex检测到connfd上有事件发生,就会返回相应事件;
4.Reactor根据事件去调用Eventhandler处理函数
本项目实现需要依赖于boost库,同时在vscode上开发实现。
2.核心部分
本文在文章开始附上了代码的链接,在这里只简单表示每个类中需要实现的类方法。
手写muduo库项目之中,存在三个核心部分,分别是Channel类,Poller/EpollPoller类和Eventloop类,这三大类组合起来实现出一种reactor用以监听fd并同时处理相应的回调函数。这三部分的内容关系如下图所示。
由图可见,Eventloop之中存储一个Poller和一组Channel,当Poller想要与Channel交互时,都要通过Eventloop执行,在Channel上注册相应的fd与事件,当Poller中sockfd监听到事件发生时,就可通过fd调用相应的回调。
Channel
作用:
在网络编程中,IO多路复用监听某个文件描述符fd和其感兴趣的事件,并通过epoll_ctl注册到IO多路复用模块(事件监听器)上。当事件监听器监听到有事件发生了,则返回“发生事件的fd集合”和“每个fd都发生了什么事件”。
Channel类封装了一个sockfd、这个sockfd感兴趣事件、事件监听器监听到该sockfd实际发生的事件、每种事件对应的处理函数。
Channel类的主要功能要求如下代码所示,其中继承的不可拷贝函数是为了防止拷贝构造函数,本项目中许多类都有这种使用方法。
#pragma once
#include "noncopyable.h"
#include "Timestamp.h"
#include <functional>
#include <memory>
class EventLoop;
/**
* 理清楚 EventLoop、Channel、Poller之间的关系 《= Reactor模型上对应 Demultiplex
* Channel 理解为通道,封装了sockfd和其感兴趣的event,如EPOLLIN、EPOLLOUT事件
* 还绑定了poller返回的具体事件
*/
class Channel : noncopyable
{
public:
using EventCallback = std::function<void()>;
using ReadEventCallback = std::function<void(Timestamp)>;
Channel(EventLoop *loop, int fd);
~Channel();
// fd得到poller通知以后,处理事件的
void handleEvent(Timestamp receiveTime);
// 设置回调函数对象
void setReadCallback(ReadEventCallback cb) { readCallback_ = std::move(cb); }
void setWriteCallback(EventCallback cb) { writeCallback_ = std::move(cb); }
void setCloseCallback(EventCallback cb) { closeCallback_ = std::move(cb); }
void setErrorCallback(EventCallback cb) { errorCallback_ = std::move(cb); }
// 防止当channel被手动remove掉,channel还在执行回调操作
void tie(const std::shared_ptr<void>&);
int fd() const { return fd_; }
int events() const { return events_; }
int set_revents(int revt) { revents_ = revt; }
// 设置fd相应的事件状态
void enableReading() { events_ |= kReadEvent; update(); }
void disableReading() { events_ &= ~kReadEvent; update(); }
void enableWriting() { events_ |= kWriteEvent; update(); }
void disableWriting() { events_ &= ~kWriteEvent; update(); }
void disableAll() { events_ = kNoneEvent; update(); }
// 返回fd当前的事件状态
bool isNoneEvent() const { return events_ == kNoneEvent; }
bool isWriting() const { return events_ & kWriteEvent; }
bool isReading() const { return events_ & kReadEvent; }
int index() { return index_; }
void set_index(int idx) { index_ = idx; }
// one loop per thread
EventLoop* ownerLoop() { return loop_; }
void remove();
private:
void update();
void handleEventWithGuard(Timestamp receiveTime);
static const int kNoneEvent;
static const int kReadEvent;
static const int kWriteEvent;
EventLoop *loop_; // 事件循环
const int fd_; // fd, Poller监听的对象
int events_; // 注册fd感兴趣的事件
int revents_; // poller返回的具体发生的事件
int index_;
std::weak_ptr<void> tie_;
bool tied_;
// 因为channel通道里面能够获知fd最终发生的具体的事件revents,所以它负责调用具体事件的回调操作
ReadEventCallback readCallback_;
EventCallback writeCallback_;
EventCallback closeCallback_;
EventCallback errorCallback_;
};
成员变量:
fd_:这个Channel对象照看的文件描述符,存在两种fd:listenfd --> acceptorChannel、connfd --> connectionChannel。
events_:Channel对应的fd所监听(感兴趣)的事件类型集合。
revents_:代表事件监听器实际监听到该fd发生的事件类型集合,即poller返回的发生的事件。当事件监听器监听到一个fd发生了什么事件,通过Channel::set_revents()函数来设置revents的值。
loop_:fd所属的EventLoop对象。
read_callback_、write_callback_等回调函数_:代表着这个Channel为这个文件描述符保存的各事件类型发生时的处理函数,当事件发生时可回调相应事件对应的回调函数
Poller和EPollPoller
muduo提供了epoll和poll两种IO多路复用方法来实现事件监听。默认使用epoll来实现,也可以通过选择poll。这里只支持epoll。
关于为何使用epoll,主要原因有:
当有很多客户端同时与一个服务器进程保持着TCP连接,而每一时刻只有几百上千个TCP连接是活跃的,实现这样的高并发采用epoll则是最佳选择。
在select/poll和中,服务器进程每次都把全部的连接告诉操作系统,让操作系统内核去查询这些套接字上是否有事件发生,轮询完成后,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大。
除此之外,在EPollPoller类中,重写一部分Poller类方法,同时需要注意在Poller类中存在一个newDefaultPoller方法,需要返回EPollPoller类型,要是将它写在Poller的cc文件之中,会在基类之中包含派生类,不符合设计的常理,因此将这一类函数重新开辟文件书写,同时包含基类和派生类的头文件。
作用:
其负责监听文件描述符事件是否触发以及返回发生事件的文件描述符以及具体事件的模块就是Poller,故一个Poller对象可看作对应一个事件监听器。在multi-reactor模型中,有多少reactor就有多少Poller。
这里Poller是个抽象虚类,由EpollPoller继承来实现,与监听文件描述符和返回监听结果的具体方法是在派生类EPollPoller中实现的,其内部封装了用epoll方法实现的与事件监听有关的各种方法。Poller的类功能要求如下:
#pragma once
#include "noncopyable.h"
#include "Timestamp.h"
#include <vector>
#include <unordered_map>
class Channel;
class EventLoop;
// muduo库中多路事件分发器的核心IO复用模块
class Poller : noncopyable
{
public:
using ChannelList = std::vector<Channel*>;
Poller(EventLoop *loop);
virtual ~Poller() = default;
// 给所有IO复用保留统一的接口
virtual Timestamp poll(int timeoutMs, ChannelList *activeChannels) = 0;
virtual void updateChannel(Channel *channel) = 0;
virtual void removeChannel(Channel *channel) = 0;
// 判断参数channel是否在当前Poller当中
bool hasChannel(Channel *channel) const;
// EventLoop可以通过该接口获取默认的IO复用的具体实现
static Poller* newDefaultPoller(EventLoop *loop);
protected:
// map的key:sockfd value:sockfd所属的channel通道类型
using ChannelMap = std::unordered_map<int, Channel*>;
ChannelMap channels_;
private:
EventLoop *ownerLoop_; // 定义Poller所属的事件循环EventLoop
};
Epollpoller的类功能要求如下所示:
#pragma once
#include "Poller.h"
#include "Timestamp.h"
#include <vector>
#include <sys/epoll.h>
class Channel;
/**
* epoll的使用
* epoll_create
* epoll_ctl add/mod/del
* epoll_wait
*/
class EPollPoller : public Poller
{
public:
EPollPoller(EventLoop *loop);
~EPollPoller() override;
// 重写基类Poller的抽象方法
Timestamp poll(int timeoutMs, ChannelList *activeChannels) override;
void updateChannel(Channel *channel) override;
void removeChannel(Channel *channel) override;
private:
static const int kInitEventListSize = 16;
// 填写活跃的连接
void fillActiveChannels(int numEvents, ChannelList *activeChannels) const;
// 更新channel通道
void update(int operation, Channel *channel);
using EventList = std::vector<epoll_event>;
int epollfd_;
EventList events_;
成员变量:
epollfd_: 就是用epoll_create方法返回的epoll句柄。
channels_:是unordered_map<int, Channel*>类型(键值对:<fd,对应的channel>),即保管所有注册在这个Poller上的Channel。
ownerLoop_:定义Poller所属的事件循环EventLoop。
Eventloop
作用:
Eventloop是对epoll的一个封装,原来的EpollLoop在epoll_create,注册各个channel之后,就处于epoll_wait处于阻塞状态。如果这个时候,之前的channel没有事件发生,而上层又想唤醒当前的EventLoop去执行新的连接,就调用wakeup,唤醒当前的EventLoop。
Channel和Poller通过EventLoop关联和驱动,其整合封装了二者并向上提供了更方便的接口。
作为一个网络服务器,需要有持续监听、持续获取监听结果、持续处理监听结果对应的事件,这就需要循环的去调用Poller:poll()获取实际发生事件的Channel集合,然后调用这些Channel中注册的不同类型事件的处理函数。
#pragma once
#include <functional>
#include <vector>
#include <atomic>
#include <memory>
#include <mutex>
#include "noncopyable.h"
#include "Timestamp.h"
#include "CurrentThread.h"
class Channel;
class Poller;
// 时间循环类 主要包含了两个大模块 Channel Poller(epoll的抽象)
class EventLoop : noncopyable
{
public:
using Functor = std::function<void()>;
EventLoop();
~EventLoop();
// 开启事件循环
void loop();
// 退出事件循环
void quit();
Timestamp pollReturnTime() const { return pollReturnTime_; }
// 在当前loop中执行cb
void runInLoop(Functor cb);
// 把cb放入队列中,唤醒loop所在的线程,执行cb
void queueInLoop(Functor cb);
// 用来唤醒loop所在的线程的
void wakeup();
// EventLoop的方法 =》 Poller的方法
void updateChannel(Channel *channel);
void removeChannel(Channel *channel);
bool hasChannel(Channel *channel);
// 判断EventLoop对象是否在自己的线程里面
bool isInLoopThread() const { return threadId_ == CurrentThread::tid(); }
private:
void handleRead(); // wake up
void doPendingFunctors(); // 执行回调
using ChannelList = std::vector<Channel*>;
std::atomic_bool looping_; // 原子操作,通过CAS实现的
std::atomic_bool quit_; // 标识退出loop循环
const pid_t threadId_; // 记录当前loop所在线程的id
Timestamp pollReturnTime_; // poller返回发生事件的channels的时间点
std::unique_ptr<Poller> poller_;
int wakeupFd_; // 主要作用,当mainLoop获取一个新用户的channel,通过轮询算法选择一个subloop,通过该成员唤醒subloop处理channel
std::unique_ptr<Channel> wakeupChannel_;
ChannelList activeChannels_;
std::atomic_bool callingPendingFunctors_; // 标识当前loop是否有需要执行的回调操作
std::vector<Functor> pendingFunctors_; // 存储loop需要执行的所有的回调操作
std::mutex mutex_; // 互斥锁,用来保护上面vector容器的线程安全操作
};
在Multi-Reactor中的关系:

EventLoop模块:起到驱动循环的功能。
Poller模块:负责从事件监听器上获取监听结果。
Channel模块:将fd及其相关属性封装起来,即将fd、其感兴趣事件events、发生的事件revents、不同事件对应的回调函数handlers封装在一起,供EventLoop调用。
图中的EventLoop与线程为1对1绑定,每个线程循环箭头一组文件描述符的集合,由此体现出muduo的one loop per thread。
loop()方法
根据One Loop Per Thread的设计思想,每个EventLoop对象都唯一绑定了一个线程,并在该线程里一直执行的while循环。
// 开启事件循环:持续循环的获取监听结果并根据结果调用处理函数
void EventLoop::loop()
{
...
while(!quit_)
{
activeChannels_.clear(); // 清空vector<Channel*>
/* Poller监听哪些channel发生事件了,然后上报给EventLoop,并通知Channel处理相应的事件 */
// 监听两类fd:一种是client的fd,一种wakeupfd
pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);
for (Channel *channel : activeChannels_)
{
// Poller监听哪些channel发生事件了,然后上报给EventLoop,通知channel处理相应的事件
channel->handleEvent(pollReturnTime_);
/*
每一个Channel的处理函数会根据其类中封装的实际发生的事件,执行其类中封装的各事件处理函数。
1. 一个Channel发生了可读、可写事件,则这个Channel类的HandlerEvent()就会调用提前注册在这个Channel的可读事件和可写事件处理函数。
2. 一个Channel只发生了可读事件,那么HandlerEvent()就只会调用提前注册在这个Channel中的可读事件处理函数。
*/
}
...
}
LOG_INFO("EventLoop %p stop looping. \n", this);
...
}
eventfd()方法
eventfd 是一个计数相关的fd,计数不为零是有可读事件发生,类似信号量的使用。read() 之后计数会清零,write() 则会递增计数器。
// EFD_SEMAPHORE标志位的作用:
#include <sys/eventfd.h>
#include <unistd.h>
#include <iostream>
using namespace std;
int main() {
int efd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC | EFD_SEMAPHORE);
eventfd_write(efd, 2); // 写入2,计数器为2
eventfd_t count;
int read_result = eventfd_read(efd, &count); // count = 1,计数器自减1,为1
cout << "read_result=" << read_result << endl; // 0
cout << "count=" << count << endl; // 1
read_result = eventfd_read(efd, &count); // count = 1,计数器自减1,为0
cout << "read_result=" << read_result << endl; // 0
cout << "count=" << count << endl; // 1
read_result = eventfd_read(efd, &count); // 读取失败
cout << "read_result=" << read_result << endl; // -1,读取失败
cout << "count=" << count << endl; // 1
close(efd);
}
write()向eventfd中写值:
1.如果写入值的和小于0xFFFFFFFFFFFFFFFE,则写入成功。
2.如果大于0xFFFFFFFFFFFFFFFE,
2.1设置了EFD_NONBLOCK标志位,则直接返回-1。
2.2没有设置EFD_NONBLOCK标志位,则一直阻塞到read操作执行。
read()读取eventfd的值:
1.如果计数器中的值大于0,1.1设置了EFD_SEMAPHORE标志位,则返回1,且计数器的值减去1。
1.2没有设置EFD_SEMAPHORE标志位,则返回计数器中的值,并将其清零。
2. 如果计数器中的值为0,
2.1设置了EFD_NONBLOCK标志位,则直接返回-1且errno=EAGAIN(即当前没有可读的数据)。
2.2 没有设置EFD_NONBLOCK标志位,则一直阻塞直到计数器中的值大于0。
wakeupFd_
假如EventLoop A线程阻塞在EventLoop::loop()中的epoll_wait()调用上(即此时EventLoop A监听的文件描述符没有任何事件发生),这时候其他EventLoop线程要求EventLoop A线程赶紧执行某个函数,那要怎么唤醒这个阻塞住的EventLoop A线程呢?
EventLoop A线程的EventLoop对象既然阻塞在事件监听上,因wakeupFd_已经注册在这个EventLoop中的事件监听器上,那就可通过wakeup()函数向待唤醒的线程所绑定的EventLoop对象持有的wakeupFd_随便写入一个8字节数据,这时EventLoop A线程的EventLoop对象的事件监听器监听到有文件描述符的事件发生,epoll_wait()阻塞结束而返回,即起到了唤醒线程的作用。
3.其余类
Eventloopthreadpool
作用:
这个类首先是调用底层的pthread,生成thread类,用来处理一些线程的简单操作,在此基础上,将eventloop与thread类型结合,成为eventloopthread类,用来开启线程,最后得到eventloopthreadpool类,在初始化的时候,eventloop会给一个baseloop来执行基础的循环,每当创建一个新线程的时候,就会提供一个新的eventloop,其中getnextloop方法比较重要,其通过轮询的方式来获取下一个subloop。
在这个类中,初始化时会设置线程数量,每一个线程对应一个loop,实现了one loop per thread 的方法。
结合三大核心模块中EventLoop中的图可知,这个类可以理解为sub reactor池,通过设置numThreads_,可以创建相应数量的subreactor。
#pragma once
#include "noncopyable.h"
#include <functional>
#include <string>
#include <vector>
#include <memory>
class EventLoop;
class EventLoopThread;
class EventLoopThreadPool : noncopyable
{
public:
using ThreadInitCallback = std::function<void(EventLoop*)>;
EventLoopThreadPool(EventLoop *baseLoop, const std::string &nameArg);
~EventLoopThreadPool();
void setThreadNum(int numThreads) { numThreads_ = numThreads; }
void start(const ThreadInitCallback &cb = ThreadInitCallback());
// 如果工作在多线程中,baseLoop_默认以轮询的方式分配channel给subloop
EventLoop* getNextLoop();
std::vector<EventLoop*> getAllLoops();
bool started() const { return started_; }
const std::string name() const { return name_; }
private:
EventLoop *baseLoop_; // EventLoop loop;
std::string name_;
bool started_;
int numThreads_;
int next_;
std::vector<std::unique_ptr<EventLoopThread>> threads_;
std::vector<EventLoop*> loops_;
};
Acceptor
作用:
Accetpor封装了服务器监听套接字fd以及相关处理方法,内部其实没有贡献什么核心的处理函数,主要是对其他类的方法调用进行封装。 Acceptor接受新用户连接后,“轮询”选择一个sub Reactor(sub EventLoop)并将该新连接分发到其上面
#pragma once
#include "noncopyable.h"
#include "Socket.h"
#include "Channel.h"
#include <functional>
class EventLoop;
class InetAddress;
class Acceptor : noncopyable
{
public:
using NewConnectionCallback = std::function<void(int sockfd, const InetAddress&)>;
Acceptor(EventLoop *loop, const InetAddress &listenAddr, bool reuseport);
~Acceptor();
void setNewConnectionCallback(const NewConnectionCallback &cb)
{
newConnectionCallback_ = cb;
}
bool listenning() const { return listenning_; }
void listen();
private:
void handleRead();
EventLoop *loop_; // Acceptor用的就是用户定义的那个baseLoop,也称作mainLoop
Socket acceptSocket_;
Channel acceptChannel_;
NewConnectionCallback newConnectionCallback_;
bool listenning_;
};
成员变量与方法:
acceptSocket_:该服务器监听的文件描述符所在的Socket对象。
acceptChannel_:封装了acceptSocket_(其中存在sockfd_)、其感兴趣事件和事件对应的处理函数的Channel类。
EventLoop *loop:监听套接字的fd由哪个EventLoop负责循环监听以及处理相应事件,这里的EventLoop就是main EventLoop。
newConnectionCallback_: TcpServer构造函数中将TcpServer::newConnection()函数注册给了这个成员变量。
TcpServer::newConnection函数的功能:“轮询”选择一个subEventLoop,并把已经接受的连接分发给这个subEventLoop。
listen():开启对acceptSocket_(其中存在sockfd_)的监听,同时将acceptChannel_及其感兴趣事件(“可读”事件)注册到main EventLoop的事件监听器上,即让main EventLoop事件监听器去监听acceptSocket_。
handleRead():要注册到acceptChannel_上的, 同时内部还调用了成员变量newConnectionCallback_保存的函数。当main EventLoop监听到acceptChannel_上发生了可读事件时(新用户连接事件),就会调用handleRead()方法。
Tcpconnection
作用:
一个连接的客户端,对应一个TcpConnection,封装了一个Socket(调用的是sys文件夹中的socket.h)、一个Channel、各种回调、发送和缓冲区。
Acceptor用于main EventLoop中,对服务器监听套接字listenfd及其相关方法进行封装(监听、接受连接、分发连接给sub EventLoop等)。
TcpConnection用于sub EventLoop中,对连接套接字connfd及其相关方法进行封装(读消息事件、发送消息事件、连接关闭事件、错误事件等)。
当有一个新用户连接,其通过accept函数拿到connfd,然后通过TcpConnection 设置回调,然后通过channel到poller实现channel的回调操作。
#pragma once
#include "noncopyable.h"
#include "InetAddress.h"
#include "Callbacks.h"
#include "Buffer.h"
#include "Timestamp.h"
#include <memory>
#include <string>
#include <atomic>
class Channel;
class EventLoop;
class Socket;
/**
* TcpServer => Acceptor => 有一个新用户连接,通过accept函数拿到connfd
* =》 TcpConnection 设置回调 =》 Channel =》 Poller =》 Channel的回调操作
*
*/
class TcpConnection : noncopyable, public std::enable_shared_from_this<TcpConnection>
{
public:
TcpConnection(EventLoop *loop,
const std::string &name,
int sockfd,
const InetAddress& localAddr,
const InetAddress& peerAddr);
~TcpConnection();
EventLoop* getLoop() const { return loop_; }
const std::string& name() const { return name_; }
const InetAddress& localAddress() const { return localAddr_; }
const InetAddress& peerAddress() const { return peerAddr_; }
bool connected() const { return state_ == kConnected; }
// 发送数据
void send(const std::string &buf);
// 关闭连接
void shutdown();
void setConnectionCallback(const ConnectionCallback& cb)
{ connectionCallback_ = cb; }
void setMessageCallback(const MessageCallback& cb)
{ messageCallback_ = cb; }
void setWriteCompleteCallback(const WriteCompleteCallback& cb)
{ writeCompleteCallback_ = cb; }
void setHighWaterMarkCallback(const HighWaterMarkCallback& cb, size_t highWaterMark)
{ highWaterMarkCallback_ = cb; highWaterMark_ = highWaterMark; }
void setCloseCallback(const CloseCallback& cb)
{ closeCallback_ = cb; }
// 连接建立
void connectEstablished();
// 连接销毁
void connectDestroyed();
private:
enum StateE {kDisconnected, kConnecting, kConnected, kDisconnecting};
void setState(StateE state) { state_ = state; }
void handleRead(Timestamp receiveTime);
void handleWrite();
void handleClose();
void handleError();
void sendInLoop(const void* message, size_t len);
void shutdownInLoop();
EventLoop *loop_; // 这里绝对不是baseLoop, 因为TcpConnection都是在subLoop里面管理的
const std::string name_;
std::atomic_int state_;
bool reading_;
// 这里和Acceptor类似 Acceptor=》mainLoop TcpConenction=》subLoop
std::unique_ptr<Socket> socket_;
std::unique_ptr<Channel> channel_;
const InetAddress localAddr_;
const InetAddress peerAddr_;
ConnectionCallback connectionCallback_; // 有新连接时的回调
MessageCallback messageCallback_; // 有读写消息时的回调
WriteCompleteCallback writeCompleteCallback_; // 消息发送完成以后的回调
HighWaterMarkCallback highWaterMarkCallback_;
CloseCallback closeCallback_;
size_t highWaterMark_;
Buffer inputBuffer_; // 接收数据的缓冲区
Buffer outputBuffer_; // 发送数据的缓冲区
};
成员变量与主要方法:
socket_:用于保存已连接套接字文件描述符。
channel_:封装了上面的socket_及其各类事件的处理函数(在TcpConnection对象构造函数中,注册的读/写/错误/关闭等事件处理函数)。
loop_:是一个EventLoop*类型,即该Tcp连接的Channel分发到得sub EventLoop。 inputBuffer_:是一个Buffer类,该TCP连接对应的用户接收缓冲区。
outputBuffer_:是一个Buffer类,用于暂存待发送的数据。 因为Tcp发送缓冲区是有水位线限制的,如果达到了高水位线,就无法把待发送的数据通过send()直接拷贝到Tcp发送缓冲区,而是暂存在outputBuffer_中,等TCP发送缓冲区有空间了,触发可写事件了,再把outputBuffer_中的数据拷贝到Tcp发送缓冲区中。 它避免发送数据过快,导致数据丢失,通过水位线highWaterMark限制发送的数据量。
state_:标识当前TCP连接的状态(kConnected、kConnecting、kDisconnecting、kDisconnected)。
connetionCallback_、messageCallback_、writeCompleteCallback_、closeCallback_ : 保存用户注册的 [连接建立的处理函数] 、[收到消息后的处理函数]、[消息发送完后的处理函数]、[连接关闭后的处理函数](Muduo库提供)。
handleRead():负责处理Tcp连接的可读事件,它会将客户端发送来的数据拷贝到用户缓冲区inputBuffer_中。
handleWrite( ):负责处理Tcp连接的可写事件。
handleClose( ):负责处理Tcp连接关闭的事件。大概的处理逻辑就是将这个TcpConnection对象中的channel_从事件监听器中移除。然后调用connectionCallback_和closeCallback_保存的回调函数。closeCallback_中保存的函数是由Muduo库提供的,connectionCallback_保存的回调函数则由用户提供的。
Tcpserver
作用:
包含Acceptor、EventLoopThreadPool、一些回调、以及一个存储channel连接信息的ConnectionMap connections_。
#pragma once
/**
* 用户使用muduo编写服务器程序
*/
#include "EventLoop.h"
#include "Acceptor.h"
#include "InetAddress.h"
#include "noncopyable.h"
#include "EventLoopThreadPool.h"
#include "Callbacks.h"
#include "TcpConnection.h"
#include "Buffer.h"
#include <functional>
#include <string>
#include <memory>
#include <atomic>
#include <unordered_map>
// 对外的服务器编程使用的类
class TcpServer : noncopyable
{
public:
using ThreadInitCallback = std::function<void(EventLoop*)>;
enum Option
{
kNoReusePort,
kReusePort,
};
TcpServer(EventLoop *loop,
const InetAddress &listenAddr,
const std::string &nameArg,
Option option = kNoReusePort);
~TcpServer();
void setThreadInitcallback(const ThreadInitCallback &cb) { threadInitCallback_ = cb; }
void setConnectionCallback(const ConnectionCallback &cb) { connectionCallback_ = cb; }
void setMessageCallback(const MessageCallback &cb) { messageCallback_ = cb; }
void setWriteCompleteCallback(const WriteCompleteCallback &cb) { writeCompleteCallback_ = cb; }
// 设置底层subloop的个数
void setThreadNum(int numThreads);
// 开启服务器监听
void start();
private:
void newConnection(int sockfd, const InetAddress &peerAddr);
void removeConnection(const TcpConnectionPtr &conn);
void removeConnectionInLoop(const TcpConnectionPtr &conn);
using ConnectionMap = std::unordered_map<std::string, TcpConnectionPtr>;
EventLoop *loop_; // baseLoop 用户定义的loop
const std::string ipPort_;
const std::string name_;
std::unique_ptr<Acceptor> acceptor_; // 运行在mainLoop,任务就是监听新连接事件
std::shared_ptr<EventLoopThreadPool> threadPool_; // one loop per thread
ConnectionCallback connectionCallback_; // 有新连接时的回调
MessageCallback messageCallback_; // 有读写消息时的回调
WriteCompleteCallback writeCompleteCallback_; // 消息发送完成以后的回调
ThreadInitCallback threadInitCallback_; // loop线程初始化的回调
std::atomic_int started_;
int nextConnId_;
ConnectionMap connections_; // 保存所有的连接
};
成员变量与方法:
start():启动EventLoopThreadPool,调用acceptor_的listen()方法,去监听客户端的连接套接字。
newConnection():该方法被注册到了acceptor_中,当acceptor_监听到新用户连接时会执行该回调函数
其使用轮询算法选择一个sub reactor;
根据连接成功的sockfd,创建一个连接对象并加入到TcpServer的存储连接信息的connections_中;
给这个连接设置connectionCallback、messageCallback、writeCompleteCallback、CloseCallback回调;
然后在main loop中执行connectEstablished()方法;
上面提到的关闭连接的回调函数,真实的调用过程:TcpConnection::setCloseCallBack() --> TcpServer::removeConnection() --> TcpServer::removeConnectionInLoop() --> TcpConnection::connectionDestroyed()
Buffer
作用:
Buffer类与其他类的交互比较少,主要是随着写入数据的不断增加,可写缓冲区的空间可能会被耗尽此时我们需要扩容,扩充缓冲区的长度。由于我们的缓冲区使用的是vector,所有我们扩容就直接调用vector的resize函数就可以实现扩容了。
#pragma once
#include <vector>
#include <string>
#include <algorithm>
// 网络库底层的缓冲器类型定义
class Buffer
{
public:
static const size_t kCheapPrepend = 8;
static const size_t kInitialSize = 1024;
explicit Buffer(size_t initialSize = kInitialSize)
: buffer_(kCheapPrepend + initialSize)
, readerIndex_(kCheapPrepend)
, writerIndex_(kCheapPrepend)
{}
size_t readableBytes() const
{
return writerIndex_ - readerIndex_;
}
size_t writableBytes() const
{
return buffer_.size() - writerIndex_;
}
size_t prependableBytes() const
{
return readerIndex_;
}
// 返回缓冲区中可读数据的起始地址
const char* peek() const
{
return begin() + readerIndex_;
}
// onMessage string <- Buffer
void retrieve(size_t len)
{
if (len < readableBytes())
{
readerIndex_ += len; // 应用只读取了刻度缓冲区数据的一部分,就是len,还剩下readerIndex_ += len -> writerIndex_
}
else // len == readableBytes()
{
retrieveAll();
}
}
void retrieveAll()
{
readerIndex_ = writerIndex_ = kCheapPrepend;
}
// 把onMessage函数上报的Buffer数据,转成string类型的数据返回
std::string retrieveAllAsString()
{
return retrieveAsString(readableBytes()); // 应用可读取数据的长度
}
std::string retrieveAsString(size_t len)
{
std::string result(peek(), len);
retrieve(len); // 上面一句把缓冲区中可读的数据,已经读取出来,这里肯定要对缓冲区进行复位操作
return result;
}
// buffer_.size() - writerIndex_ len
void ensureWriteableBytes(size_t len)
{
if (writableBytes() < len)
{
makeSpace(len); // 扩容函数
}
}
// 把[data, data+len]内存上的数据,添加到writable缓冲区当中
void append(const char *data, size_t len)
{
ensureWriteableBytes(len);
std::copy(data, data+len, beginWrite());
writerIndex_ += len;
}
char* beginWrite()
{
return begin() + writerIndex_;
}
const char* beginWrite() const
{
return begin() + writerIndex_;
}
// 从fd上读取数据
ssize_t readFd(int fd, int* saveErrno);
// 通过fd发送数据
ssize_t writeFd(int fd, int* saveErrno);
private:
char* begin()
{
// it.operator*()
return &*buffer_.begin(); // vector底层数组首元素的地址,也就是数组的起始地址
}
const char* begin() const
{
return &*buffer_.begin();
}
void makeSpace(size_t len)
{
if (writableBytes() + prependableBytes() < len + kCheapPrepend)
{
buffer_.resize(writerIndex_ + len);
}
else
{
size_t readalbe = readableBytes();
std::copy(begin() + readerIndex_,
begin() + writerIndex_,
begin() + kCheapPrepend);
readerIndex_ = kCheapPrepend;
writerIndex_ = readerIndex_ + readalbe;
}
}
std::vector<char> buffer_;
size_t readerIndex_;
size_t writerIndex_;
};
3.逻辑梳理
Muduo整体架构one loop per thread + threadpool的架构,即一个mainEventLoop和包含多个subEventPool的EventLoopPool。
mainEventLoop 管理 Acceptor,主要是listen和accept新的连接,然后在EventLoopPool中**“轮询”**一个subEventPool去处理新Channel的读写事件。

对于整个muduo库:
1.用户创建一个main loop,主线程作为main reactor,主要用来接受/断开用户连接。
2.给TcpServer设置连接和读写事件回调,TcpServer再给TcpConnection设置回调(发生事件时,用户设置要执行的),TcpConnection再给channel设置回调(在发生事件时,会先执行这个回调,再执行用户设置的回调)。
3.TcpServer根据用户设置传入的线程数,去threadPool中开启几个线程(即开启的sub eventloop的个数),如果没有设置则用户传入的main loop还要承担读写事件的任务。
4.当有新连接进来时,创建一个TcpConnection实例对象,然后由Acceptor去轮询唤醒一个sub reactor给它服务。
5.同时,每个sub reactor在服务时,其所包含的那个Poller如果没有事件就会处于循环阻塞状态,发生事件之后,根据类型再去执行相应的回调操作。
1.在MainEventLoop中接受新连接请求之后,将这条Tcp连接封装成TcpConnection对象。TcpConnection对象的内容如上图所示,主要就是封装了连接套接字的fd(上图中的socket_)、连接套接字的channel_等。在TcpConnection的构造函数中会TcpConnection::handleRead()等四个上图中的蓝色方法注册进channel_内。
2.当TcpConnection对象建立完毕之后,MainEventLoop的Acceptor会将这个TcpConnection对象中的channel_注册到某一个SubEventLoop。
3.SubEventLoop中的EventLoop::loop()函数内部会循环的执行上图中的步骤1和步骤2。步骤1就是调用Poller::poll()方法获取事件监听结果,这个事件监听结果是一个Channel集合,每一个Channel封装着 [一个fd] 及 [fd感兴趣的事件events] 和 [事件监听器监听到该fd实际发生的事件revents]。步骤2就是调用每一个Channel的Channel::HandlerEvent方法。该方法会根据每一个Channel的感兴趣事件以及实际发生的事件调用提前注册在Channel内的对应的事件处理函数(readCallback_、writeCallback_、closeCallback_、errorCallback_)。
建立连接

TcpServer server(&loop, listenAddr)调用了TcpServer的构造函数创建一个TcpServer对象,主要是实例化了一个Acceptor acceptor_对象,并往这个acceptor_对象注册了一个回调函数TcpServer::newConnection()。
在实例化acceptor_对象时,其构造函数中实例化了一个Channel acceptChannel_对象,封装了服务器监听套接字文件描述符(尚未注册到main EventLoop的事件监听器上)。
接着Acceptor构造函数将Acceptor::handleRead()方法注册进Channel acceptChannel_中,这意味着之后如果事件监听器监听到Channel acceptChannel_发生可读事件将会调用AcceptorC::handleRead()函数。
至此,TcpServer对象创建完毕,用户调用TcpServer::start()方法,开启TcpServer。
本质上是调用Acceptor::listen()函数(底层是调用了linux的函数listen())监听服务器套接字,以及将acceptChannel_注册到main EventLoop的事件监听器上监听它的可读事件(新用户连接事件)。
接着用户调用loop.loop(),即调用了EventLoop::loop()函数,该函数就会循环的获取事件监听器的监听结果,并且根据监听结果调用注册在事件监听器上的Channel对象的事件处理函数。
如果程序执行到了Acceptor::handleRead()函数,说明acceptChannel_发生可读事件,程序处理新客户连接请求。该函数首先调用了Linux的函数accept()接受新客户连接。接着调用了TcpServer::newConnection()函数,其主要是“轮询”一个sub EventLoop并和客户端的连接一起封装成TcpConnection对象,之后调用TcpConnection::connectEstablished()将TcpConnection::channel_注册到刚刚选择的sub EventLoop上。
发送消息
当用户调用了TcpConnetion::send(msg)函数时,相当于要求Muduo库把数据msg发送给该Tcp连接的客户端。
// 发送消息
void TcpConnection::send(const std::string& msg)
{
if (state_ == kConnected)
{
if (loop_->isInLoopThread())
{
sendInLoop(msg.c_str(), msg.size());
}
else
{
loop_->runInLoop(std::bind(&TcpConnection::sendInLoop, this, msg.c_str(), msg.size()));
}
}
}
// 由于应用层写的快,内核发送数据慢,故需要将待发送的数据先写入缓冲区,且设置了水位回调
void TcpConnection::sendInLoop(const void* data, size_t len)
{
loop_->assertInLoopThread();
ssize_t nwrote = 0;
size_t remaining = len;
bool faultError = false;
// 之前调用过该connection的shutdown,不能再进行发送了
if (state_ == kDisconnected)
{
LOG_ERROR("disconnected, give up writing!");
return;
}
// if no thing in output queue, try writing directly
// 此时,channel_第一次开始写数据,而且缓冲区没有待发送数据
if (!channel_->isWriting() && outputBuffer_.readableBytes() == 0)
{
nwrote = ::write(channel_->fd(), data, len);
if (nwrote >= 0) // 成功发送了
{
remaining = len - nwrote;
if (remaining == 0 && writeCompleteCallback_)
{
// 既然在这里数据全部发送完成,就不用再给channel设置epollout事件了,这样epoll_wait就不用监听可写事件并且执行handleWrite了,算是提高效率了!!!
loop_->queueInLoop(std::bind(writeCompleteCallback_, shared_from_this()));
}
}
else // nwrote < 0
{
// 未能一次性将data全部拷贝到socket发送缓冲区中
nwrote = 0;
// 如果是非阻塞模式,socket发送缓冲区满了就会立即返回并且设置EWOULDBLOCK
if (errno != EWOULDBLOCK)
{
LOG_ERROR("TcpConnection::sendInLoop");
if (errno == EPIPE || errno == ECONNRESET) // SIGPIPE RESET
{
faultError = true;
}
}
}
}
// 说明当前这一次write,并没有把数据全部发送出去,剩余的数据需要保存到outputBuffer_缓冲区当中,然后给channel。
// 给channel_注册epollout事件,poller发现tcp的发送缓冲区有空间,会通知相应的sock-channel,调用writeCallback_回调方法即TcpConnection::handleWrite()方法,把发送缓冲区中的数据全部发送完成。
if (!faultError && remaining > 0)
{
// 目前发送缓冲区剩余的待发送数据的长度
size_t oldLen = outputBuffer_.readableBytes();
if (oldLen + remaining >= highWaterMark_
&& oldLen < highWaterMark_
&& highWaterMarkCallback_)
{
loop_->queueInLoop(std::bind(highWaterMarkCallback_, shared_from_this(), oldLen+remaining));
}
// 将message[nwrote, nwrote+remaining]的数据,写入到outputbuffer_中
outputBuffer_.append((char*)data + nwrote, remaining);
// 向poller注册channel的写事件,否则poller不会给channel通知epollout
if (!channel_->isWriting())
{
channel_->enableWriting();
}
}
}
1.如果TCP发送缓冲区能一次性容纳msg,那这个write()函数将msg全部拷贝到发送缓冲区中。此时该TcpConnection注册在事件监听器上的感兴趣事件中,没有可写事件。
2.如果TCP发送缓冲区内不能一次性容纳msg:
2.1这时候write()函数msg数据尽可能地拷贝到TCP发送缓冲区中。如果是非阻塞模式,socket发送缓冲区满了就会立即返回并且设置EWOULDBLOCK。
2.2剩余未拷贝到TCP发送缓冲区中的msg数据会被存放在TcpConnection::outputBuffer_中,并且向事件监听器上注册该TcpConnection::channel_的可写事件。
2.3事件监听器监听到该Tcp连接可写事件,就会调用writeCallback_回调函数即TcpConnection::handleWrite()函数,其会通过调用Buffer::writeFd()函数把TcpConnection::outputBuffer_中剩余的数据写入Tcp发送缓冲区。
2.3.1如果Tcp发送缓冲区能容纳全部剩余的未发送数据,就再好不过了。
2.3.2如果Tcp发送缓冲区依旧没法容纳剩余的未发送数据,那就继续保持可写事件的监听。
当数据全部拷贝到Tcp发送缓冲区之后,就会调用用户自定义的【写完后的事件处理函数writeCompleteCallBack_】,并且移除该TcpConnection在事件监听器上的可写事件。(移除可写事件是为了提高效率,避免让epoll_wait()毫无意义的频繁触发可写事件。因为大多数时候是没有数据需要发送的,频繁触发可写事件但又没有数据可写,毫无意义。)
连接断开
被动断开
服务端TcpConnection::handleRead( )函数内部调用了Linux的函数readv(),其返回值为0时,服务端就知道客户端断开连接了,就会调用TcpConnection::handleClose()被动断开连接。调用顺序为:
1.subeventloop::closecallback(Tcpconnection &conn)
2.Tcpserver::removeconnection(Tcpconnection &conn),使用其中的loop_->runinloop(std::bind(Tcpserver::removeconnection,this,conn))
3.Tcpserver::removeconnection(Tcpconnection &conn),使用其中ioloop->queueinloop(std::bind(Tcpconnection::connectiondestoryed,conn))
4.Tcpconnection::connectiondestoryed(),最终channel_->remove(),至此连接断开
main eventloop线程和sub eventloop线程之间的切换充分体现了One Loop Per Thread的设计理念。
removeConnectionInLoop()要在MainEventLoop中运行,因为该函数主要是从TcpServer对象中删除某条数据(将该TcpConnection conn对象从connections_中删掉),而TcpServer对象是属于MainEventLoop的。
TcpConnection::connectDestroyed()函数的执行是又跳回到了subEventLoop线程中。该函数就是将Tcp连接的监听描述符从事件监听器中移除。另外,SubEventLoop中的Poller类对象还保存着这条Tcp连接的channel_,所以调用channel_.remove()将这个Tcp连接的channel对象从Poller内的数据结构中删除。
服务器主动关闭导致连接断开
当服务器主动关闭时,调用TcpServer::~TcpServer()析构函数, 这个析构函数巧妙利用了shared_ptr的特点,当这个TcpConnection对象引用计数为0时,这个TcpConnection对象就会被析构删除(堆内存释放)
/* 彻底删除一个TcpConnection对象,必须要调用该对象的connecDestroyed()方法,执行完后才能释放该对象的堆内存。*/
TcpServer::~TcpServer()
{
for(auto &item : connections_) // connections_类型为unordered_map<string, TcpConnectionPtr>,其中TcpConnectionPtr是指向TcpConnection的shared_ptr共享智能指针。
{
// 这个局部的shared_ptr智能指针对象,出右括号则会自动释放,即引用计数减一。
TcpConnectionPtr conn(item.second);
// 在MainEventLoop的TcpServer::~TcpServer()函数中,调用item.second.reset(),释放掉TcpServer中保存的该TcpConnection对象的智能指针。
item.second.reset();
/* 此时,只有“conn”持有TcpConnection对象,即引用计数为1 */
// 让这个TcpConnection对象conn所属的SubEventLoop线程,执行TcpConnection::connectDestroyed()函数。
conn->getLoop()->runInLoop(bind(&TcpConnection::connectDestroyed, conn));
/* 此时,“conn”和“subEventLoop中的TcpConnection::connectDestroyed()成员函数的this指针”共同持有TcpConnection对象,即引用计数为2 */
}
/* 此时,只“subEventLoop中的TcpConnection::connectDestroyed()成员函数的this指针”持有TcpConnection对象,即引用计数为1。在该函数执行结束后,引用计数减为0,即会自动释放TcpConnection对象。*/
}
每一个TcpConnection对象都被一个共享智能指针TcpConnetionPtr持有,当执行了TcpConnectionPtr conn(item.second)时,这个TcpConnetion对象就被conn和这个item.second共同持有,但conn的生存周期很短一旦离开了当前for循环就会被释放。
调用item.second.reset()方法,释放掉TcpServer中保存的该TcpConnection对象的智能指针。此时,只剩下conn还持有这个TcpConnection对象,因此当前TcpConnection对象还不会被析构。
调用conn->getLoop()->runInLoop(bind(&TcpConnection::connectDestroyed, conn)),即让SubEventLoop线程去执行TcpConnection::connectDestroyed()函数。当将这个conn传进去时,conn所指向的资源的引用计数会加1。
SubEventLoop线程开始运行TcpConnection::connectDestroyed()。
MainEventLoop线程当前这一轮for循环跑conn离开代码块而被析构,但TcpConnection对象还不会被释放,因为还有一个共享智能指针指向这个TcpConnection对象,而且这个智能指针在TcpConnection::connectDestroyed()中是一个隐式的this的存在,该函数执行完后,智能指针就真的被释放了。
到此,就没有任何智能指针指向这个TcpConnection对象了,即TcpConnection对象就彻底被析构删除了。
怎么保证在触发TcpConnection关闭机制后,能把数据发送完再释放其对象的资源?
1.shared_from_this():
// TcpConnection类继承了这个类后,才能使用shared_from_this()函数
class TcpConnection : public std::enable_shared_from_this<TcpConnection>
{ ... }
在TcpConnection对象(我们管这个对象叫TCA)中的成员函数中调用了shared_from_this(),该函数可以返回一个shared_ptr,并且这个shared_ptr指向的对象就是TCA。
2.结合“/建立连接/用智能指针管理TcpConnection对象/TcpConnection对象的多线程安全问题”分析。
/***** TcpConnection.cpp *****/
void TcpConnection::connectEstablished()
{
setState(kConnected);
channel_->tie(shared_from_this());
channel_->enableReading(); // 向poller注册channel的epollin事件
// 新连接建立,执行回调
connectionCallback_(shared_from_this());
}
/***** Channel.h ******/
std::weak_ptr<void> tie_;
/***** Channel.cpp ******/
// channel的tie方法什么时候调用过? 一个TcpConnection新连接创建的时候 TcpConnection => Channel 。
void Channel::tie(const std::shared_ptr<void> &obj)
{
tie_ = obj;
tied_ = true;
}
当事件监听器返回监听结果,就要对每一个发生事件的channel对象调用它们的HandlerEvent()函数。函数中,会先把tie_这个weak_ptr提升为shared_ptr强共享智能指针,其会指向当前的TcpConnection对象。即使外面调用删除析构了其他所有的指向该TcpConnection对象的智能指针,只要HandleEventWithGuard()函数没执行完,这个TcpConnetion对象都不会被析构释放堆内存。
/* 当fd_得到poller的事件后,处理事件,即调用相应的回调函数 */
void Channel::handleEvent(Timestamp receiveTime)
{
if (tied_)
{
// 将weak_ptr提升为shared_ptr,并通过判断返回值来衡量tie_是否还存在。
std::shared_ptr<void> guard = tie_.lock();
if (guard)
{
handleEventWithGuard(receiveTime);
}
}
else
{
handleEventWithGuard(receiveTime);
}
}
// 根据poller通知的channel发生的具体事件, 由channel负责调用具体的回调操作
void Channel::handleEventWithGuard(Timestamp receiveTime)
{
LOG_INFO("channel handleEvent revents:%d\n", revents_);
// 在使用epoll机制进行I/O多路复用时,当文件描述符上出现EPOLLHUP事件时,通常意味着连接已经被对端关闭,或者一些错误导致连接异常断开。
if ((revents_ & EPOLLHUP) && !(revents_ & EPOLLIN))
{
if (closeCallback_)
{
closeCallback_();
}
}
if (revents_ & EPOLLERR)
{
if (errorCallback_)
{
errorCallback_();
}
}
/*
EPOLLPRI是Linux系统中epoll的一种事件类型,它表示有一个高优先级的带外(out-of-band)数据可供读取。
带外数据是一种特殊类型的数据,它不是按照普通数据流来发送和接收的,而是可以在普通数据的外部被发送和接收。
这种数据通常用于向应用程序传递紧急信息或控制命令,因此具有更高的优先级。
*/
if (revents_ & (EPOLLIN | EPOLLPRI))
{
if (readCallback_)
{
readCallback_(receiveTime);
}
}
if (revents_ & EPOLLOUT)
{
if (writeCallback_)
{
writeCallback_();
}
}
}
4.运行效果
在运行autobuild.sh之后,在example文件夹下直接make即可,运行该文件夹下testserver可执行文件,在当前终端运行testserver,然后开辟新终端使用telnet ip port进行连接,最终可实现消息的交互,证明了各种类函数手写的有效性。
感谢您耐心看完,如果存在问题欢迎指正!



552

被折叠的 条评论
为什么被折叠?



