前情提要
基于c++11的特性重写muduo的核心相关代码,在这里并且简单梳理一下。当然本人对于muduo库的理解还仅限于能看懂简单的使用。对于各个对象内存的管理,类的设计,以及这样设计的精髓等还差得很远。后面有了更深的体会还会来这里更新。总的来说muduo库实现了业务与网络通信的完全解耦,上层与下层之间有大量的回调函数,初看还是有点绕的。这里的梳理仅限于对muduo代码有一定的了解。小白可以直接忽略这篇文章。文末有一张全部的关系图。
Channel类
首先看一下channel类,该类是muduo库对于我们要监听的fd的一个封装。该类继承了 noncopyable类,表示该类的对象不能调用拷贝构造函数和拷贝赋值运算符。看一下成员变量:
class Channel : noncopyable
{
private:
static const int kNoneEvent;//这个是0
static const int kReadEvent;
static const int kWriteEvent;//这三个就是表示常量,对EPOLLIN EPOLLOUT 的封装
EventLoop *loop_;//该channel 所属的哪一个时间循环loop,也就是哪一个事件循环来监控这个
const int fd_; //封装的fd
int events_; //想要监听的事件
int revents_; //实际发生的事件 有一个问题是 想要监听的时间跟实际发生的时间还能不一样吗?
int index_; //是否有添加到poller中
std::weak_ptr<void> tie_; //自己绑定的那个 TCPconnection,为什么是弱指针?
bool tied_;//是否已经绑定
ReadEventCallback readCallback_;
EventCallback writeCallback_;
EventCallback closeCallback_;
EventCallback errorCallback_;//四个回调函数,也就是发生对应事件后 要执行的函数。
//两个私有方法
void update(); // 调用 自己所属的Loop来更新epoll中监听的事件
void handleEventWithGuard(Timestamp receiveTime);//根据revents_ 的事件来调用不同的函数。
public:
using EventCallback = std::function<void()>;
using ReadEventCallback = std::function<void(Timestamp)>;//读事件的回调函数还需要时间戳
Channel(EventLoop *loop, int fd); //构造函数只需要传入 Loop 和 fd就可
~Channel();//没有需要手动释放的资源,fd在哪里close?
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); }//设置各种回调,采用移动构造函数可以防止function对象被拷贝。但是拷贝也
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(); }//设置各种类型的监听事件,最后调用update更新到到loop的poll中
}
channel 类比较简单,就是对fd的一个封装,把要监听的事件也封装进去了。 当监听事件发生,通过channel调用不同的回调函数就能执行读写错误关闭操作。而这些回调函数是TCPconnection对象注册的。
poller 和 epollpoller
这两个简单来说就就是封装了epoll_wait 这个函数核心 以及一系列其他的功能,例如修改监听事件等。
poller 类是父类,其子类可以是poll 方法 可以是 epoll 方法,但是muduo中我们还是用epoll比较好。
class Poller : noncopyable
{
private:
EventLoop * ownerLoop_; //这个poll所属的loop是哪一个
protected:
using ChannelMap = std::unordered_map<int, Channel*>;
ChannelMap channels_; //用一个map来管理所有监听的socket
public:
Poller(EventLoop *loop);
virtual ~Poller() = default;
//下面三个纯虚函数,简单来说就是调用epll_wait 或者改变或者删除监听的fd的事件
//channel中改变event_就是如此
virtual Timestamp poll(int timeoutMs, ChannelList *activeChannels) = 0;
virtual void updateChannel(Channel *channel) = 0;
virtual void removeChannel(Channel *channel) = 0;
//传入的channel是否是该poll所监听的
bool hasChannel(Channel *channel) const;
//静态函数
static Poller* newDefaultPoller(EventLoop *loop); //构造一个子类的对象 通过父类指针返回
}
然后就是epollpoller
class EpollPoller : public Poller //共有继承表示基类的所有成员在子类中的所有访问方式不变
{
//成员变量
private:
int epollfd_; //epollfd,指向内核中的各种数据结构
using EventList = std::vector<epoll_event>;
EventList events_; //调用epollwait后活跃的事件集合,在时间的event中可以获取fd或者channel指针
void fillActiveChannels(int numEvents, ChannelList *activeChannels) const;//根据调用填写活跃的channel
// 更新channel通道
void update(int operation, Channel *channel);
public:
//重写父类纯虚函数
Timestamp poll(int timeoutMs, ChannelList *activeChannels) override;
void updateChannel(Channel *channel) override;
void removeChannel(Channel *channel) override;
}
epollpoller的总体思路作用如下:
该类中核心的成员是epollfd 和events_ 后者是一个调用epoll_wait()
的传出函数
通过updateChannel()
和removeChannel()
函数更新内核中的监听的事件,也就是要监听的fd以及对应的事件,通过调用poll 来实现epoll_wait,填充成员变量 events_,同时填充传入的数组 activeChannel ,将有事件的channel填入这个数组中,交给上层使用,至于谁调用了 poll 我们后面再说。
EventLoop
前面的几个类都是基础类,这个类就是一个核心的类了。
class EventLoop : noncopyable
{
private:
std::atomic_bool looping_; //原子操作标识是否处于loop循环中
std::atomic_bool quit_; //原子才做标识是否退出
const pid_t threadId_; //标识当前loop所在的线程id
Timestamp pollReturnTime_; // poller返回发生事件的channels的时间点
std::unique_ptr<Poller> poller_;//该loop的 epollloop
int wankeupFd_; //wakeupfd 用来唤醒epoll_wait
std::unique_ptr<Channel> wakeupChannel_;//上面fd的封装channel
ChannelList activeChannels_;//从epoll返回中活跃的channel
std::atomic_bool callingPendingFunctors_; //是否需要回调函数
std::vector<Functor> pendingFunctors_; //要执行的回调函数
std::mutex mutex_;//有锁 说明 该对象中可能被多个线程所操作。
}
EventLoop是 关联 poller 与 channel的桥梁 channel 中通过自己所属的 eventloop 去找到该eventlooop中的poller 然后修改fd 的事件。
此外 eventloop 还调用poller 的 epoll_wait方法,找出有事件发生的channel 并且执行对应的事件回调函数,并且执行上层交给eventloop的事件。总的来说 eventloop就这两个功能。
Thread
muudo库实现的是 one loop per thread 的思想。所以一个thread 就会对应一个eventloop。而类thread就是要实现这个。
class Thread : noncopyable
{
public:
using ThreadFunc = std::function<void()>; //开启线程要执行的函数
explicit Thread(ThreadFunc, const std::string &name = std::string()); //构造函数
//只需要传入构造函数和名字就可以
~Thread();//实现线程分离。
void start(); //开启线程
void join(); //join 线程等待线程结束。
bool started() const { return started_; }
pid_t tid() const { return tid_; }
const std::string& name() const { return name_; }
static int numCreated() { return numCreated_; }
private:
void setDefaultName(); //设置默认的名字并且将静态变量的线程数+1
bool started_;
bool joined_;
std::shared_ptr<std::thread> thread_; //线程的共享指针
pid_t tid_; //tid
ThreadFunc func_; //线程的调用函数
std::string name_; //名字
static std::atomic_int numCreated_; //静态变量 一共开启了多少个线程。
};
这里要重点说一下 start()
void Thread::start() // 一个Thread对象,记录的就是一个新线程的详细信息
{
started_ = true;// 这个函数就是要开启线程了
sem_t sem;
sem_init(&sem, false, 0); //要用到信号量
// 开启线程
thread_ = std::shared_ptr<std::thread>(new std::thread([&](){ //利用lambda表达式实现线程执行函数
// 获取线程的tid值
tid_ = CurrentThread::tid();
sem_post(&sem);//只有在获得线程名字后 才会然start ()执行完毕。
// 开启一个新线程,专门执行该线程函数
func_();
}));
// 这里必须等待获取上面新创建的线程的tid值
sem_wait(&sem);
}
也就是类Thread的对象构建完毕后,只有在start函数执行后才会正式的开启线程,执行的就是我们传入构造函数的func。
EventLoopThread
上面那个thread 类其实 可以算一个公共的目标,实际上做的事情跟muduo 没关系,(解耦拉满) 主要看你传入的是什么函数。而这里的eventLoop 就非常有用了。
//这给类的实现还是非常有趣的
class EventLoopThread : noncopyable
{
public:
using ThreadInitCallback = std::function<void(EventLoop*)>; //这里仍然有一个新的回调函数
EventLoopThread(const ThreadInitCallback &cb = ThreadInitCallback(),
const std::string &name = std::string());
~EventLoopThread();
EventLoop* startLoop();
private:
void threadFunc(); //线程的回调函数
EventLoop *loop_;//与线程关联的loop
bool exiting_; //
Thread thread_; //线程类
std::mutex mutex_;//锁
std::condition_variable cond_;//条件变量
ThreadInitCallback callback_; //传入的新回调函数
};
要具体看一下函数的实现才能了解全貌:
//构造函数 ,传入名字 和 新回调函数
EventLoopThread::EventLoopThread(const ThreadInitCallback &cb,
const std::string &name)
: loop_(nullptr)
, exiting_(false)
, thread_(std::bind(&EventLoopThread::threadFunc, this), name)//这个threadfunc后面再说
, mutex_()
, cond_()
, callback_(cb)
{
}
EventLoopThread::~EventLoopThread()
{
exiting_ = true;
if (loop_ != nullptr)
{
loop_->quit();
thread_.join();
}
}
EventLoop* EventLoopThread::startLoop()
{
thread_.start(); // 启动底层的新线程
EventLoop *loop = nullptr;
{
std::unique_lock<std::mutex> lock(mutex_);
while ( loop_ == nullptr )//等待上面开启的线程执行完绑定loop的操作 eventloop 栈空间
{
cond_.wait(lock);
}
loop = loop_;
}
return loop;
}
// 下面这个方法,是在单独的新线程里面运行的
void EventLoopThread::threadFunc()
{
EventLoop loop; // 创建一个独立的eventloop,和上面的线程是一一对应的,one loop per thread
if (callback_) //首先执行上面传入的新回调函数
{
callback_(&loop);
}
{
std::unique_lock<std::mutex> lock(mutex_); //锁住,为什么需要锁呢?
//因为我们条件变量判断的是Loop_ 主线程和 新开启的线程 都可以改变这个loop所以必须要用锁。
loop_ = &loop; //将本类的loop 绑定为 该栈中创建的loop
cond_.notify_one(); //只有绑定了栈loop才能让上面的函数成功返回loop
}
loop.loop(); // EventLoop loop => Poller.poll //执行loop函数,也就是epoll_wait
std::unique_lock<std::mutex> lock(mutex_);
loop_ = nullptr;
}
总的来说 EventLoopthread就是实现了one loop per thread。就是一个线程里面有一个loop。并且这个loop是保存在栈中的!
eventloop就是提供thread 线程执行函数,在函数内,栈中创建一个loop 并且绑定到该类的指针,然后执行loop死循环
,最后返回这个指针,供给外部操作这个loop。当然这里的loop已经执行了poll 所以是阻塞的,我们要操作这个loop 必须要通过runinloop 或者 queueinloop来向里面添加执行事件,才能够在dopendingfunctor 中执行。
EventLoopThreadPool
这个类就是一个线程池类,用来管理所有的EventLoopThread类别
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; } //设置subloop个数
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_; //subloop个数
int next_; //轮询的下一个thread是谁
std::vector<std::unique_ptr<EventLoopThread>> threads_; //储存的是threads_的指针
std::vector<EventLoop*> loops_; //储存了所有的subloop。
};
EventLoopThreadPool::EventLoopThreadPool(EventLoop *baseLoop, const std::string &nameArg)
: baseLoop_(baseLoop)
, name_(nameArg)
, started_(false)
, numThreads_(0)
, next_(0)
{}
EventLoopThreadPool::~EventLoopThreadPool()
{}
//这个函数比较重要,传入的就是回调函数,也就是在线程函数中执行的回调函数。
//根据设置的num数目来开启所有的线程。
void EventLoopThreadPool::start(const ThreadInitCallback &cb)
{
started_ = true;
for (int i = 0; i < numThreads_; ++i)
{
char buf[name_.size() + 32];
snprintf(buf, sizeof buf, "%s%d", name_.c_str(), i);
EventLoopThread *t = new EventLoopThread(cb, buf);
threads_.push_back(std::unique_ptr<EventLoopThread>(t));
loops_.push_back(t->startLoop()); // 底层创建线程,绑定一个新的EventLoop,并返回该loop的地址
}
// 整个服务端只有一个线程,运行着baseloop
if (numThreads_ == 0 && cb)
{
cb(baseLoop_);
}
}
// 如果工作在多线程中,baseLoop_默认以轮询的方式分配channel给subloop
EventLoop* EventLoopThreadPool::getNextLoop()
{
EventLoop *loop = baseLoop_;
if (!loops_.empty()) // 通过轮询获取下一个处理事件的loop
{
loop = loops_[next_];
++next_;
if (next_ >= loops_.size())
{
next_ = 0;
}
}
return loop;
}
std::vector<EventLoop*> EventLoopThreadPool::getAllLoops()
{
if (loops_.empty())
{
return std::vector<EventLoop*>(1, baseLoop_);
}
else
{
loops_;
}
}
这个类的功能就是开启设置好的subloop线程,然后可以通过轮询每次都返回一个eventloop,最后上层调用可以通过这个loop的runinloop来实现回调。
InetAdress
这个类就跳过,只是将linux是api封装一下更符合使用。
Acceptor
这个函数就是比较上层的函数调用,主要作用是创建listenfd,然后开启listen, 然后该fd注册的回调函数就是创建一个新的连接,当然这个回调函数也是上层传递的。
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(); //本类里面fd的回调函数
EventLoop *loop_; // Acceptor用的就是用户定义的那个baseLoop,也称作mainLoop
Socket acceptSocket_; //这个就是listen的fd
Channel acceptChannel_;//上面fd的channel
NewConnectionCallback newConnectionCallback_;
bool listenning_;
};
static int createNonblocking() //这个就是创建非阻塞 socket的函数
{
int sockfd = ::socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
if (sockfd < 0)
{
LOG_FATAL("%s:%s:%d listen socket create err:%d \n", __FILE__, __FUNCTION__, __LINE__, errno);
}
return sockfd;
}
Acceptor::Acceptor(EventLoop *loop, const InetAddress &listenAddr, bool reuseport)
: loop_(loop)
, acceptSocket_(createNonblocking()) // socket
, acceptChannel_(loop, acceptSocket_.fd()) //创建channel
, listenning_(false)
{
acceptSocket_.setReuseAddr(true);
acceptSocket_.setReusePort(true);//端口复用
acceptSocket_.bindAddress(listenAddr); // bind
// TcpServer::start() Acceptor.listen 有新用户的连接,要执行一个回调(connfd=》channel=》subloop)
// baseLoop => acceptChannel_(listenfd) =>
acceptChannel_.setReadCallback(std::bind(&Acceptor::handleRead, this)); //绑定写事件回调函数
}
Acceptor::~Acceptor()
{
acceptChannel_.disableAll();
acceptChannel_.remove();//回调函数就是移除channel
}
void Acceptor::listen()
{
listenning_ = true;
acceptSocket_.listen(); // listen 开启监听
acceptChannel_.enableReading(); // acceptChannel_ => Poller 向poller中注册fd
}
// listenfd有事件发生了,就是有新用户连接了
void Acceptor::handleRead()
{
InetAddress peerAddr;
int connfd = acceptSocket_.accept(&peerAddr); //传入参数
if (connfd >= 0)
{
if (newConnectionCallback_)
{
newConnectionCallback_(connfd, peerAddr); // 轮询找到subLoop,唤醒,分发当前的新客户端的Channel
}
else
{
::close(connfd);
}
}
else
{
LOG_ERROR("%s:%s:%d accept err:%d \n", __FILE__, __FUNCTION__, __LINE__, errno);
if (errno == EMFILE)
{
LOG_ERROR("%s:%s:%d sockfd reached limit! \n", __FILE__, __FUNCTION__, __LINE__);
}
}
}
这个类其实作用很简单,就是创建一个监听的fd,然后开启监听,如果监听事件发生就执行回调函数。
Buffer
这个类的实现比较有意思,并且思路可以去用到其他的的代码设计中。这个buf就是一个vector<char>
管理的连续空间用来储存消息,有一个readidex和一个writeindex来指向读取数据的位置和写入数据的位置。 一般来说一个TCPserver 中会有一个 inputbuffer 和一个 outputbuffer。其中inputbuffer 是TCPserver 从内核中读取数据到inputbuffer中,所以这里的write就是从内核中的数据转移到这个buffer中,read数据就是应用层从buffer中读取数据。 而outputbuffer就是TCPbuffer向内核写入数据时,如果内核的缓冲区满了,那么server就会注册可写事件,然后将剩余的数据写入到outputbuffer中。(何时可写数据会触发呢? 一旦内核的写缓冲区有空余就会一直触发写事件!)所以这里buffer的write是应用层写入,read是内核缓冲区读取数据。这里比较有一意思的是他的扩容过程,代码如下:
//何时这个vector要扩容呢,显然要写入的数据长度比空闲的数据长度大时就要进行扩容了。
//这里的比较是将wirteable的数据 加上前面 8字节后readidex前的数据也加进去比较。
void makeSpace(size_t len)
{
if (writableBytes() + prependableBytes() < len + kCheapPrepend)
{//如果空间还是小了,ok那么直接才后面扩容,readindex前的空余空间也可以不管了
buffer_.resize(writerIndex_ + len);
}
else
{//如果空间能用,那么我们就将readable 中的数据移动到 8字节处开始,再开始写入。
size_t readalbe = readableBytes();
std::copy(begin() + readerIndex_,
begin() + writerIndex_,
begin() + kCheapPrepend);
readerIndex_ = kCheapPrepend;
writerIndex_ = readerIndex_ + readalbe;
}
}
然后就是从fd中读取数据的函数了:
这里的设计比较巧妙。从内核中读取数据,我们肯定是想一次性就把数据读完,这样就只会有一次系统调用,但是我们在读取数据的时候不知道数据大小多少,所以buffer内存的设置不太清楚多少比较合适。muduo库中巧妙利用了栈空间的思维。将内核中的数据首先填充到buffer中,待buffer满了后,再填到栈中的extrabuf中,栈buf不论设置成多少最后都会自动的回收所以就不会有顾虑,这样我们通过extrabuf就能会知道内核的数据到底有多少,所有我们就根据这个数据来扩充buffer,最后拷贝数据到buffer中。
ssize_t Buffer::readFd(int fd, int* saveErrno)
{
char extrabuf[65536] = {0}; // 栈上的内存空间 64K
struct iovec vec[2];
const size_t writable = writableBytes(); // 这是Buffer底层缓冲区剩余的可写空间大小
vec[0].iov_base = begin() + writerIndex_;
vec[0].iov_len = writable;
vec[1].iov_base = extrabuf;
vec[1].iov_len = sizeof extrabuf;
const int iovcnt = (writable < sizeof extrabuf) ? 2 : 1;
const ssize_t n = ::readv(fd, vec, iovcnt);
if (n < 0)
{
*saveErrno = errno;
}
else if (n <= writable) // Buffer的可写缓冲区已经够存储读出来的数据了
{
writerIndex_ += n;
}
else // extrabuf里面也写入了数据
{
writerIndex_ = buffer_.size();
append(extrabuf, n - writable); // writerIndex_开始写 n - writable大小的数据
}
return n;
}
TCPconnection
这个类就是至关重要的类的。前面的很多回调函数都会在这里实现。先来看一下成员变量:
class TcpConnection : noncopyable, public std::enable_shared_from_this<TcpConnection>
{
private:
enum StateE {kDisconnected, kConnecting, kConnected, kDisconnecting};
void setState(StateE state) { state_ = state; }
void handleRead(Timestamp receiveTime); //channel中发生事件时的回调
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_; // 发送数据的缓冲区
};
每一个TCPconn都有 一个inputbuf 和 outputbuf。 非阻塞的io连接必须要有这些缓冲区。 成员变量中的 socket 是accept 返回的fd,其中loop是eventloopthreadpool 通过轮询下发的loop。其中的handle 四个函数就是注册给channel 的 读写错误关闭的回调,在这里实现,并且注册给channel。 此外还有 一系列的其他回调 ,例如连接回调,消息回调等等。这些要么是在channel的回调函数中执行,要么是在loop的dopendingfunctor中执行。 ok下面来看一下重点的几个函数:
void TcpConnection::handleRead(Timestamp receiveTime)
{
int savedErrno = 0;
ssize_t n = inputBuffer_.readFd(channel_->fd(), &savedErrno);
if (n > 0)
{
// 已建立连接的用户,有可读事件发生了,调用用户传入的回调操作onMessage
messageCallback_(shared_from_this(), &inputBuffer_, receiveTime);
}
else if (n == 0)
{
handleClose();
}
else
{
errno = savedErrno;
LOG_ERROR("TcpConnection::handleRead");
handleError();
}
}
首先是handleread()
,这个函数是channel监听read事件时发生的回调函数,简单来说就是将内核中的数据读取到inputbuffer中来。然后调用了 messagecallback,这个函数就是用户给的回调函数(如果是回射服务器的话,那么这里的回调执行的就是写入读取的数据)
void TcpConnection::handleWrite()
{
if (channel_->isWriting())
{//只有channel监听的是write事件才会执行
int savedErrno = 0;
ssize_t n = outputBuffer_.writeFd(channel_->fd(), &savedErrno);//从outputbuf中写入数据到内核
if (n > 0) //如果写入数据大于0 表示写入成功
{
outputBuffer_.retrieve(n); //修改buf的index
if (outputBuffer_.readableBytes() == 0)
{//如果此时的outbuffer中的所有数据全部被写进内核
channel_->disableWriting();//此时的channel就没必要继续监听write事件
if (writeCompleteCallback_)
{
// 唤醒loop_对应的thread线程,执行回调
loop_->queueInLoop( //为什么其他的回调函数都是直接执行,而这个回调需要在subeventloop中执行呢?
std::bind(writeCompleteCallback_, shared_from_this())
);
}
if (state_ == kDisconnecting)
{
shutdownInLoop();
}
}//反之,如果buffer中还有数据没有完全写完,此时的channel仍然监听写事件。
}
else
{
LOG_ERROR("TcpConnection::handleWrite");
}
}
else
{
LOG_ERROR("TcpConnection fd=%d is down, no more writing \n", channel_->fd());
}
}
然后就是handlewrite()
这个函数就是注册的写事件发生时channel执行的回调函数。一般来说初学者(包括我)都会把注意力集中在可读事件上,也就是内核缓冲区数据后会通知epoll,然后进行读操作。 那么写事件是如何通知的呢? 内核中同样有写缓冲区,当我们想内核写入数据,同时写缓冲区满了,那么我们就要注册可写事件,一旦内核的写缓冲区有空余地方,立刻通知channel执行写事件。(这里的实现好像跟之前写的Tinywebserver不太一样,tinywebsever都是统一应用层写入到buffer中,然后注册写事件)。这里是直接就写,只有缓冲区满了才会注册写事件,当然后面会提到实现。
ok看完了回调的读写。我们再来看一下如何直接执行写事件。
void TcpConnection::send(const std::string &buf)
{
if (state_ == kConnected)
{
if (loop_->isInLoopThread())
{//判断loop与线程是否是同一个,只有在单loop的时候才会执行这里,因为send函数是用户直接调用的
sendInLoop(buf.c_str(), buf.size());
}
else
{//不是在一个线程里,就去加入到对应loop线程 的functor中。
loop_->runInLoop(std::bind(
&TcpConnection::sendInLoop,
this,
buf.c_str(),
buf.size()
));
}
}
}
/**
* 发送数据 应用写的快, 而内核发送数据慢, 需要把待发送数据写入缓冲区, 而且设置了水位回调
*/
void TcpConnection::sendInLoop(const void* data, size_t len)
{
ssize_t nwrote = 0;
size_t remaining = len;
bool faultError = false;
// 之前调用过该connection的shutdown,不能再进行发送了
if (state_ == kDisconnected)
{
LOG_ERROR("disconnected, give up writing!");
return;
}
// 表示channel_第一次开始写数据,而且缓冲区没有待发送数据
if (!channel_->isWriting() && outputBuffer_.readableBytes() == 0)
{//如果channel没有监听write事件,同时 buff中也没有数据要写,那么就可以直接写入内核里面去。
nwrote = ::write(channel_->fd(), data, len);
if (nwrote >= 0)
{
remaining = len - nwrote; //剩余的有多少没有写入内核
if (remaining == 0 && writeCompleteCallback_)
{//如果所有的都写入到内核了,并且有回调函数,及执行回调函数
// 既然在这里数据全部发送完成,就不用再给channel设置epollout事件了
loop_->queueInLoop(
std::bind(writeCompleteCallback_, shared_from_this())
);
}
}
else // nwrote < 0
{//下面就是错误处理了,可以忽略
nwrote = 0;
if (errno != EWOULDBLOCK)
{
LOG_ERROR("TcpConnection::sendInLoop");
if (errno == EPIPE || errno == ECONNRESET) // SIGPIPE RESET
{
faultError = true;
}
}
}
}
// 说明当前这一次write,并没有把数据全部发送出去,剩余的数据需要保存到缓冲区当中,然后给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)
);
}
outputBuffer_.append((char*)data + nwrote, remaining);//将多余的没写入内核的数据写入到缓冲区
if (!channel_->isWriting())
{//注册可写事件,只要内核缓冲区有空余,那么就执行写回调
channel_->enableWriting(); // 这里一定要注册channel的写事件,否则poller不会给channel通知epollout
}
}
}
TCPserver
最后就是tcpserver了,这个也是总领所有下属类的,也是连接用户操作与底层的一个中间层,通过这一个类我们所有的操作都能够非常清晰。先看看成员变量
class TcpServer : noncopyable //不允许拷贝构造或者赋值
{
//所有connection的集合 用户名字来进行索引
using ConnectionMap = std::unordered_map<std::string, TcpConnectionPtr>;
EventLoop *loop_; // baseLoop 用户定义的loop
const std::string ipPort_;
const std::string name_;
//监听的fd以及监听的事件,以及监听事件发生后的回调函数
std::unique_ptr<Acceptor> acceptor_; //运行在mainLoop,任务就是监听新连接事件
//eventloop 的线程池
std::shared_ptr<EventLoopThreadPool> threadPool_; // one loop per thread
//注意这里的回调函数是用户注册给TCPserver的
ConnectionCallback connectionCallback_; // 有新连接时的回调
MessageCallback messageCallback_; // 有读写消息时的回调
WriteCompleteCallback writeCompleteCallback_; // 消息发送完成以后的回调
//这个也是用户注册的,在线程执行的时候执行
ThreadInitCallback threadInitCallback_; // loop线程初始化的回调
std::atomic_int started_;
int nextConnId_;
ConnectionMap connections_; // 保存所有的连接
}
让我们看看这里面的核心的实现过程:
//首先是构造函数,在用户区我们直接传入的是建立的baseloop 以及名字和 绑定端口和ip以及一个端口复用的标志
TcpServer::TcpServer(EventLoop *loop,
const InetAddress &listenAddr,
const std::string &nameArg,
Option option)
: loop_(CheckLoopNotNull(loop))
, ipPort_(listenAddr.toIpPort())
, name_(nameArg)
//创建一个新的accept,当然还没有运行监听的函数
, acceptor_(new Acceptor(loop, listenAddr, option == kReusePort))
//创建新的线程池,当然线程仍然没有执行
, threadPool_(new EventLoopThreadPool(loop, name_))
, connectionCallback_()
, messageCallback_()
, nextConnId_(1)
, started_(0)
{
// 当有先用户连接时,会执行TcpServer::newConnection回调,在accept中注册newconnection的回调函数
acceptor_->setNewConnectionCallback(std::bind(&TcpServer::newConnection, this,
std::placeholders::_1, std::placeholders::_2));
}
//开启server
void TcpServer::start()
{
if (started_++ == 0) // 防止一个TcpServer对象被start多次
{
threadPool_->start(threadInitCallback_); // 启动底层的loop线程池
loop_->runInLoop(std::bind(&Acceptor::listen, acceptor_.get()));//向baseloop中注册listen函数
}
}
//新连接进来时,也就是主线程监听的accept的fd发送读事件,执行下面的回调函数,创建新的连接并且生成的channel分发给下面的subloop
void TcpServer::newConnection(int sockfd, const InetAddress &peerAddr)
{
// 轮询算法,选择一个subLoop,来管理channel
EventLoop *ioLoop = threadPool_->getNextLoop(); //通过轮询找到一个分发的subloop
char buf[64] = {0};
snprintf(buf, sizeof buf, "-%s#%d", ipPort_.c_str(), nextConnId_);
++nextConnId_;
std::string connName = name_ + buf;
LOG_INFO("TcpServer::newConnection [%s] - new connection [%s] from %s \n",
name_.c_str(), connName.c_str(), peerAddr.toIpPort().c_str());
// 通过sockfd获取其绑定的本机的ip地址和端口信息
sockaddr_in local;
::bzero(&local, sizeof local);
socklen_t addrlen = sizeof local;
if (::getsockname(sockfd, (sockaddr*)&local, &addrlen) < 0)
{
LOG_ERROR("sockets::getLocalAddr");
}
InetAddress localAddr(local);
// 根据连接成功的sockfd,创建TcpConnection连接对象
TcpConnectionPtr conn(new TcpConnection(
ioLoop,
connName,
sockfd, // Socket Channel,在accept的回调函数,这个fd是accept返回的函数
localAddr,
peerAddr));
connections_[connName] = conn;
// 下面的回调都是用户设置给TcpServer=>TcpConnection=>Channel=>Poller=>notify channel调用回调
conn->setConnectionCallback(connectionCallback_);
conn->setMessageCallback(messageCallback_);
conn->setWriteCompleteCallback(writeCompleteCallback_);
// 设置了如何关闭连接的回调 conn->shutDown()
conn->setCloseCallback(
std::bind(&TcpServer::removeConnection, this, std::placeholders::_1)
);
// 直接调用TcpConnection::connectEstablished
ioLoop->runInLoop(std::bind(&TcpConnection::connectEstablished, conn));
}
梳理整个流程
通过echo 回射服务器代码的 连接建立 和读事件的发生 和关闭连接 来梳理整个流程
连接的建立,一个新的连接建立的过程。
在用户区创建一个baseloop,然后在自己的回射服务器中 写入TCPserver的 messageCallBack函数 和 ConnectedCallback函数,设置sub线程数目。 设置绑定的ip和端口。创建回射服务器。然后开启回射服务器, 调用了TCPserver::start()函数。
void TcpServer::start()
{
if (started_++ == 0) // 防止一个TcpServer对象被start多次
{
threadPool_->start(threadInitCallback_); // 启动底层的loop线程池
loop_->runInLoop(std::bind(&Acceptor::listen, acceptor_.get()));
}
}
该函数开启了所有的subloop子线程,创建了所有的subloop,此时这些loop都在epoll_wait()中阻塞。因为没有监听任何的channel。 然后 将创建的acceptor 中的listen函数,由于当前loop是base 同时也是主线程,所以直接执行acceptor的listen函数。
void Acceptor::listen()
{
listenning_ = true;
acceptSocket_.listen(); // listen
acceptChannel_.enableReading(); // acceptChannel_ => Poller在底层注册fd事件
}
该函数开启listen fd 监听新连接的到了,并且在主loop上注册 该fd的读事件。
ok 上面就是服务器开启的过程。正在等待一个连接的到来。
当一个连接到来时,listenfd的读事件发生,主loop中的epoll_wait()返回,执行listen fd 读事件的回调函数。accept新的连接,然后调用 TCPserver中的newconnetion函数。通过轮询算法找到要分发的loop,然后创建新的connection,在这个新的connection的构造汇总也会创建新的channel,并且注册该channel的各种事件。然后将下面的函数加入到的dopendingfunctor中,然后唤醒该线程:
void TcpConnection::connectEstablished()
{
setState(kConnected);
channel_->tie(shared_from_this());
channel_->enableReading(); // 向poller注册channel的epollin事件
// 新连接建立,执行回调
connectionCallback_(shared_from_this());
}
将channel绑定,并且注册到subloop的epoll中,调用连接建立回调函数。这时连接就正式建立,并且由subloop来监听该连接的读写事件。
读事件发生时的流程
当有读事件发生时,subloop的poll函数会返回,并且执行相应的hanleread() 函数,该函数就是读取数据到inputbuffer中,
然后执行我们的messageCallBack函数,回射服务器中该函数是 将收到的buffer‘数据全部序列化string拿出来然后再发送给对方。
关闭连接时发生的流程
当读事件返回时并且返回的整数是0。就执行handleclose()
void TcpConnection::handleClose()
{
LOG_INFO("TcpConnection::handleClose fd=%d state=%d \n", channel_->fd(), (int)state_);
setState(kDisconnected);
channel_->disableAll();
TcpConnectionPtr connPtr(shared_from_this());
connectionCallback_(connPtr); // 执行连接关闭的回调
closeCallback_(connPtr); // 关闭连接的回调 执行的是TcpServer::removeConnection回调方法
}
将连接状态设置成关闭连接,设置成不监听任何事件,调用closeCallback,也就是调用TCPserver中的removeconnection,然后调用
void TcpServer::removeConnectionInLoop(const TcpConnectionPtr &conn)
{
LOG_INFO("TcpServer::removeConnectionInLoop [%s] - connection %s\n",
name_.c_str(), conn->name().c_str());
connections_.erase(conn->name());
EventLoop *ioLoop = conn->getLoop();
ioLoop->queueInLoop(
std::bind(&TcpConnection::connectDestroyed, conn)
);
}
在server中删除connection,这个地方删除后 connection的析构函数就会执行(如果没有其他地方有share_ptr的话),然后调用
void TcpConnection::connectDestroyed()
{
if (state_ == kConnected)
{//在这次调用中不会进来
setState(kDisconnected);
channel_->disableAll(); // 把channel的所有感兴趣的事件,从poller中del掉
connectionCallback_(shared_from_this());
}
channel_->remove(); // 把channel从poller中删除掉
}
最后会调用
void EPollPoller::removeChannel(Channel *channel)
{
int fd = channel->fd();
channels_.erase(fd);
LOG_INFO("func=%s => fd=%d\n", __FUNCTION__, fd);
int index = channel->index();
if (index == kAdded)
{
update(EPOLL_CTL_DEL, channel);
}
channel->set_index(kNew);
}
从epoll中删除该fd。这样 channel 与connection都关闭了。
总的来说关闭事件发生时,就是要将connection 与 对应的channel 都从相关的集合中删除,然后执行析构。channel 与 connection 都是谁在管理呢?channel是在TCPconnection创建的时候创建的,并且用unique_ptr来管理这个channel,所以当connection销毁的时候 channel也会随之销毁。 那么connection又是如何销毁的呢,connection由 TCPserver 的connectionMap 来管理 通过 share_ptr来管理内存,当从TCPserver的map中删除一个connection时,如果其他地方没有share_ptr ,该connection就会立刻析构,那么这里会直接析构connection吗,答案是不会,因为在handleclose()
中新创建了一个share_ptr,然后根据这个ptr来执行closeCallback()
只有这个 handleclose()
执行完毕 connection才会析构,进而析构socket和channel。