muduo原始碼閱讀
最近簡單讀了下muduo的原始碼,本文對其主要實現/結構簡單總結下。
muduo的主要原始碼位於net資料夾下,base資料夾是一些基礎程式碼,不影響理解網路部分的實現。muduo主要類包括:
- EventLoop
- Channel
- Poller
- TcpConnection
- TcpClient
- TcpServer
- Connector
- Acceptor
- EventLoopThread
- EventLoopThreadPool
其中,Poller(及其實現類)包裝了Poll/EPoll,封裝了OS針對裝置(fd)的操作;Channel是裝置fd的包裝,在muduo中主要包裝socket;TcpConnection抽象一個TCP連線,無論是客戶端還是伺服器只要建立了網路連線就會使用TcpConnection;TcpClient/TcpServer分別抽象TCP客戶端和伺服器;Connector/Acceptor分別包裝TCP客戶端和伺服器的建立連線/接受連線;EventLoop是一個主控類,是一個事件發生器,它驅動Poller產生/發現事件,然後將事件派發到Channel處理;EventLoopThread是一個帶有EventLoop的執行緒;EventLoopThreadPool自然是一個EventLoopThread的資源池,維護一堆EventLoopThread。
閱讀庫原始碼時可以從庫的介面層著手,看看關鍵功能是如何實現的。對於muduo而言,可以從TcpServer/TcpClient/EventLoop/TcpConnection這幾個類著手。接下來看看主要功能的實現:
建立連線
TcpClient::connect
-> Connector::start
-> EventLoop::runInLoop(Connector::startInLoop...
-> Connector::connect
EventLoop::runInLoop介面用於在this所在的執行緒執行某個函式,這個後面看下EventLoop的實現就可以瞭解。 網路連線的最終建立是在Connector::connect中實現,建立連線之後會建立一個Channel來代表這個socket,並且繫結事件監聽介面。最後最重要的是,呼叫Channel::enableWriting
。Channel
有一系列的enableXX介面,這些介面用於標識自己關心某IO事件。後面會看到他們的實現。
Connector監聽的主要事件無非就是連線已建立,用它監聽讀資料/寫資料事件也不符合設計。TcpConnection才是做這種事的。
客戶端收發資料
當Connector發現連線真正建立好後,會回撥到TcpClient::newConnection
,在TcpClient建構函式中:
connector_->setNewConnectionCallback(
boost::bind(&TcpClient::newConnection, this, _1));
TcpClient::newConnection
中建立一個TcpConnection來代表這個連線:
TcpConnectionPtr conn(new TcpConnection(loop_,
connName,
sockfd,
localAddr,
peerAddr));
conn->setConnectionCallback(connectionCallback_);
conn->setMessageCallback(messageCallback_);
conn->setWriteCompleteCallback(writeCompleteCallback_);
...
conn->connectEstablished();
並同時設定事件回撥,以上設定的回撥都是應用層(即庫的使用者)的介面。每一個TcpConnection都有一個Channel,畢竟每一個網路連線都對應了一個socket fd。在TcpConnection建構函式中建立了一個Channel,並設定事件回撥函式。
TcpConnection::connectEstablished
函式最主要的是通知Channel自己開始關心IO讀取事件:
void TcpConnection::connectEstablished()
{
...
channel_->enableReading();
這是自此我們看到的第二個Channel::enableXXX
介面,這些介面是如何實現關心IO事件的呢?這個後面講到。
muduo的資料傳送都是通過TcpConnection::send
完成,這個就是一般網路庫中在不使用OS的非同步IO情況下的實現:快取應用層傳遞過來的資料,在IO裝置可寫的情況下儘量寫入資料。這個主要實現在TcpConnection::sendInLoop
中。
TcpConnection::sendInLoop(....) {
...
// if no thing in output queue, try writing directly
if (!channel_->isWriting() && outputBuffer_.readableBytes() == 0) // 裝置可寫且沒有快取時立即寫入
{
nwrote = sockets::write(channel_->fd(), data, len);
}
...
// 否則加入資料到快取,等待IO可寫時再寫
outputBuffer_.append(static_cast<const char*>(data)+nwrote, remaining);
if (!channel_->isWriting())
{
// 註冊關心IO寫事件,Poller就會對寫做檢測
channel_->enableWriting();
}
...
}
當IO可寫時,Channel就會回撥TcpConnection::handleWrite
(建構函式中註冊)
void TcpConnection::handleWrite()
{
...
if (channel_->isWriting())
{
ssize_t n = sockets::write(channel_->fd(),
outputBuffer_.peek(),
outputBuffer_.readableBytes());
伺服器端的資料收發同客戶端機制一致,不同的是連線(TcpConnection)的建立方式不同。
伺服器接收連線
伺服器接收連線的實現在一個網路庫中比較重要。muduo中通過Acceptor類來接收連線。在TcpClient中,其Connector通過一個關心Channel可寫的事件來通過連線已建立;在Acceptor中則是通過一個Channel可讀的事件來表示有新的連線到來:
Acceptor::Acceptor(....) {
...
acceptChannel_.setReadCallback(
boost::bind(&Acceptor::handleRead, this));
...
}
void Acceptor::handleRead()
{
...
int connfd = acceptSocket_.accept(&peerAddr); // 接收連線獲得一個新的socket
if (connfd >= 0)
{
...
newConnectionCallback_(connfd, peerAddr); // 回撥到TcpServer::newConnection
TcpServer::newConnection
中建立一個TcpConnection,並將其附加到一個EventLoopThread中,簡單來說就是給其配置一個執行緒:
void TcpServer::newConnection(int sockfd, const InetAddress& peerAddr)
{
...
EventLoop* ioLoop = threadPool_->getNextLoop();
TcpConnectionPtr conn(new TcpConnection(ioLoop,
connName,
sockfd,
localAddr,
peerAddr));
connections_[connName] = conn;
...
ioLoop->runInLoop(boost::bind(&TcpConnection::connectEstablished, conn));
IO的驅動
之前提到,一旦要關心某IO事件了,就呼叫Channel::enableXXX
,這個如何實現的呢?
class Channel {
...
void enableReading() { events_ |= kReadEvent; update(); }
void enableWriting() { events_ |= kWriteEvent; update(); }
void Channel::update()
{
loop_->updateChannel(this);
}
void EventLoop::updateChannel(Channel* channel)
{
...
poller_->updateChannel(channel);
}
最終呼叫到Poller::upateChannel
。muduo中有兩個Poller的實現,分別是Poll和EPoll,可以選擇簡單的Poll來看:
void PollPoller::updateChannel(Channel* channel)
{
...
if (channel->index() < 0)
{
// a new one, add to pollfds_
assert(channels_.find(channel->fd()) == channels_.end());
struct pollfd pfd;
pfd.fd = channel->fd();
pfd.events = static_cast<short>(channel->events()); // 也就是Channel::enableXXX操作的那個events_
pfd.revents = 0;
pollfds_.push_back(pfd); // 加入一個新的pollfd
int idx = static_cast<int>(pollfds_.size())-1;
channel->set_index(idx);
channels_[pfd.fd] = channel;
可見Poller就是把Channel關心的IO事件轉換為OS提供的IO模型資料結構上。通過檢視關鍵的pollfds_
的使用,可以發現其主要是在Poller::poll介面裡。這個介面會在EventLoop的主迴圈中不斷呼叫:
void EventLoop::loop()
{
...
while (!quit_)
{
activeChannels_.clear();
pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);
...
for (ChannelList::iterator it = activeChannels_.begin();
it != activeChannels_.end(); ++it)
{
currentActiveChannel_ = *it;
currentActiveChannel_->handleEvent(pollReturnTime_); // 獲得IO事件,通知各註冊回撥
}
整個流程可總結為:各Channel內部會把自己關心的事件告訴給Poller,Poller由EventLoop驅動檢測IO,然後返回哪些Channel發生了事件,EventLoop再驅動這些Channel呼叫各註冊回撥。
從這個過程中可以看出,EventLoop就是一個事件產生器。
執行緒模型
在muduo的伺服器中,muduo的執行緒模型是怎樣的呢?它如何通過執行緒來支撐高併發呢?其實很簡單,它為每一個執行緒配置了一個EventLoop,這個執行緒同時被附加了若干個網路連線,這個EventLoop服務於這些網路連線,為這些連線收集並派發IO事件。
回到TcpServer::newConnection
中:
void TcpServer::newConnection(int sockfd, const InetAddress& peerAddr)
{
...
EventLoop* ioLoop = threadPool_->getNextLoop();
...
TcpConnectionPtr conn(new TcpConnection(ioLoop, // 使用這個選擇到的執行緒中的EventLoop
connName,
sockfd,
localAddr,
peerAddr));
...
ioLoop->runInLoop(boost::bind(&TcpConnection::connectEstablished, conn));
注意TcpConnection::connectEstablished
是如何通過Channel註冊關心的IO事件到ioLoop
的。
極端來說,muduo的每一個連線執行緒可以只為一個網路連線服務,這就有點類似於thread per connection模型了。
網路模型
傳說中的Reactor模式,以及one loop per thread,基於EventLoop的作用,以及執行緒池與TcpConnection的關係,可以醍醐灌頂般理解以下這張muduo的網路模型圖了:
總結
本文主要對muduo的主要結構及主要機制的實現做了描述,其他如Buffer的實現、定時器的實現大家都可以自行研究。muduo的原始碼很清晰,通過原始碼及配合陳碩部落格上的內容可以學到一些網路程式設計方面的經驗。
相關文章
- 【原始碼閱讀】AndPermission原始碼閱讀原始碼
- 【原始碼閱讀】Glide原始碼閱讀之into方法(三)原始碼IDE
- 【原始碼閱讀】Glide原始碼閱讀之with方法(一)原始碼IDE
- 【原始碼閱讀】Glide原始碼閱讀之load方法(二)原始碼IDE
- AmplifyImpostors原始碼閱讀原始碼
- stack原始碼閱讀原始碼
- AQS原始碼閱讀AQS原始碼
- delta原始碼閱讀原始碼
- CountDownLatch原始碼閱讀CountDownLatch原始碼
- HashMap 原始碼閱讀HashMap原始碼
- fuzz原始碼閱讀原始碼
- ConcurrentHashMap原始碼閱讀HashMap原始碼
- HashMap原始碼閱讀HashMap原始碼
- Mux 原始碼閱讀UX原始碼
- ReactorKit原始碼閱讀React原始碼
- Vollery原始碼閱讀(—)原始碼
- NGINX原始碼閱讀Nginx原始碼
- ThreadLocal原始碼閱讀thread原始碼
- 原始碼閱讀-HashMap原始碼HashMap
- Runtime 原始碼閱讀原始碼
- RunLoop 原始碼閱讀OOP原始碼
- PostgreSQL 原始碼解讀(3)- 如何閱讀原始碼SQL原始碼
- JDK原始碼閱讀:Object類閱讀筆記JDK原始碼Object筆記
- JDK原始碼閱讀:String類閱讀筆記JDK原始碼筆記
- muduo原始碼解析11-logger類原始碼
- Vollery原始碼閱讀(二)原始碼
- Laravel 原始碼閱讀 - QueueLaravel原始碼
- Laravel 原始碼閱讀 - EloquentLaravel原始碼
- 閱讀nopcommerce startup原始碼原始碼
- 再談原始碼閱讀原始碼
- Hive原始碼閱讀之路Hive原始碼
- 如何閱讀Java原始碼?Java原始碼
- buffer 原始碼包閱讀原始碼
- 使用OpenGrok閱讀原始碼原始碼
- express 原始碼閱讀(全)Express原始碼
- Kingfisher原始碼閱讀(一)原始碼
- 如何閱讀框架原始碼框架原始碼
- 如何閱讀jdk原始碼?JDK原始碼
- ArrayList原始碼閱讀(增)原始碼