muduo網路庫學習之EventLoop(三):Socket、Acceptor、TcpServer、TcpConnection(連線建立,接收訊息)

s1mba發表於2013-11-08
1、Socket 操作封裝

Endian.h

封裝了位元組序轉換函式(全域性函式,位於muduo::net::sockets名稱空間中)。

SocketsOps.h/ SocketsOps.cc
裝了socket相關係統呼叫(全域性函式,位於muduo::net::sockets名稱空間中)。

Socket.h/Socket.cc(Socket類)
用RAII方法封裝socket file descriptor

InetAddress.h/InetAddress.cc(InetAddress類)
網際地址sockaddr_in封裝


2、Acceptor

Acceptor用於accept(2)接受TCP連線

Acceptor的資料成員包括acceptSocket_、acceptChannel_,Acceptor的acceptSocket_是listening socket(即server socket)。

acceptChannel_用於觀察acceptSocket_的readable事件,可讀事件發生,Channel::handleEvent()中回撥Acceptor::handleRead(),

後者呼叫accept(2)來接受新連線,並回撥使用者callback,注意callback 中傳入的第一個引數是accept返回的connfd。

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
 
void Acceptor::handleRead()
{
    loop_->assertInLoopThread();
    InetAddress peerAddr(0);
    //FIXME loop until no more
    int connfd = acceptSocket_.accept(&peerAddr);
    if (connfd >= 0)
    {
        // string hostport = peerAddr.toIpPort();
        // LOG_TRACE << "Accepts of " << hostport;
        if (newConnectionCallback_)
        {
            newConnectionCallback_(connfd, peerAddr);
        }
        else
        {
            sockets::close(connfd);
        }
    }
    else
    {
        // Read the section named "The special problem of
        // accept()ing when you can't" in libev's doc.
        // By Marc Lehmann, author of libev.
        if (errno == EMFILE)
        {
            ::close(idleFd_);
            idleFd_ = ::accept(acceptSocket_.fd(), NULLNULL);
            ::close(idleFd_);
            idleFd_ = ::open("/dev/null", O_RDONLY | O_CLOEXEC);
        }
    }
}

在建構函式中:

 C++ Code 
1
2
 
acceptChannel_.setReadCallback(
    boost::bind(&Acceptor::handleRead, this));

設定使用者回撥函式:

 C++ Code 
1
2
3
4
5
6
7
8
 
// 傳入connfd
typedef boost::function < void (int sockfd,
                                const InetAddress &) > NewConnectionCallback;

void setNewConnectionCallback(const NewConnectionCallback &cb)
{
    newConnectionCallback_ = cb;
}

開始監聽:

 C++ Code 
1
2
3
4
5
6
7
 
void Acceptor::listen()
{
    loop_->assertInLoopThread();
    listenning_ = true;
    acceptSocket_.listen();
    acceptChannel_.enableReading();
}


測試程式碼:
simba@ubuntu:~$ telnet 127.0.0.1 8888

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
 
#include <muduo/net/Acceptor.h>
#include <muduo/net/EventLoop.h>
#include <muduo/net/InetAddress.h>
#include <muduo/net/SocketsOps.h>

#include <stdio.h>

using namespace muduo;
using namespace muduo::net;

void newConnection(int sockfd, const InetAddress &peerAddr)
{
    printf("newConnection(): accepted a new connection from %s\n",
           peerAddr.toIpPort().c_str());
    ::write(sockfd, "How are you?\n"13);
    sockets::close(sockfd);
}

int main()
{
    printf("main(): pid = %d\n", getpid());

    InetAddress listenAddr(8888);
    EventLoop loop;

    Acceptor acceptor(&loop, listenAddr);
    acceptor.setNewConnectionCallback(newConnection);
    acceptor.listen();

    loop.loop();
}


使用telnet 連線伺服器,伺服器輸出如下:
simba@ubuntu:~/Documents/build/debug/bin$ ./reactor_test07
20131108 07:22:30.560145Z  3960 TRACE IgnoreSigPipe Ignore SIGPIPE - EventLoop.cc:51
main(): pid = 3960
20131108 07:22:30.675116Z  3960 TRACE updateChannel fd = 4 events = 3 - EPollPoller.cc:104
20131108 07:22:30.675684Z  3960 TRACE EventLoop EventLoop created 0xBFED7324 in thread 3960 - EventLoop.cc:76
20131108 07:22:30.676073Z  3960 TRACE updateChannel fd = 5 events = 3 - EPollPoller.cc:104
20131108 07:22:30.676577Z  3960 TRACE updateChannel fd = 6 events = 3 - EPollPoller.cc:104
20131108 07:22:30.676988Z  3960 TRACE loop EventLoop 0xBFED7324 start looping - EventLoop.cc:108
20131108 07:22:40.687957Z  3960 TRACE poll  nothing happended - EPollPoller.cc:74
20131108 07:22:41.606525Z  3960 TRACE poll 1 events happended - EPollPoller.cc:65
20131108 07:22:41.607053Z  3960 TRACE printActiveChannels {6: IN}  - EventLoop.cc:271
newConnection(): accepted a new connection from 127.0.0.1:56409
20131108 07:22:51.617500Z  3960 TRACE poll  nothing happended - EPollPoller.cc:74

telnet 端輸出如下:

simba@ubuntu:~$ telnet 127.0.0.1 8888
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
How are you?
Connection closed by foreign host.
simba@ubuntu:~$ 

從輸出可以看出,acceptSocket_.sockfd_ = 6,客戶端連線上來,監聽套接字發生可讀事件,呼叫accept() 接收連線後呼叫使用者回撥函式newConnection()。

3、TcpServer/TcpConnection

Acceptor類的主要功能是socket、bind、listen

一般來說,在上層應用程式中,我們不直接使用Acceptor,而是把它作為TcpServer的成員
 C++ Code 
1
 
boost::scoped_ptr<Acceptor> acceptor_; // avoid revealing Acceptor

TcpServer還包含了一個TcpConnection列表
 C++ Code 
1
2
3
 
typedef boost::shared_ptr<TcpConnection> TcpConnectionPtr;
typedef std::map<string, TcpConnectionPtr> ConnectionMap;
ConnectionMap connections_; // 連線列表

此外,還有一個IO執行緒池物件和一個acceptor Eventloop*, 通過setThreadNum()設定IO執行緒池的執行緒個數(不包括main Reactor)
關於EventLoopThread, EventLoopThreadPool 類參見這裡

 C++ Code 
1
 
boost::scoped_ptr<EventLoopThreadPool> threadPool_;
EventLoop* loop_;  // the acceptor loop

 C++ Code 
1
2
3
4
5
 
void TcpServer::setThreadNum(int numThreads)
{
    assert(0 <= numThreads);
    threadPool_->setThreadNum(numThreads);
}


TcpConnection與Acceptor類似,有兩個重要的資料成員,Socket(connfd)與Channel
 C++ Code 
1
2
 
boost::scoped_ptr<Socket> socket_;
boost::scoped_ptr<Channel> channel_;


時序圖分析:

在TcpServer 建構函式中先初始化acceptor_成員,acceptor_(new Acceptor(loop, listenAddr)),在建構函式體內:

 C++ Code 
1
2
3
4
 
// Acceptor::handleRead函式中會回撥用TcpServer::newConnection
// _1對應的是socket檔案描述符,_2對應的是對等方的地址(InetAddress)
acceptor_->setNewConnectionCallback(
    boost::bind(&TcpServer::newConnection, this, _1, _2));

呼叫TcpServer::start(),開始Acceptor::listen(), 已連線佇列不為空,TcpServer::acceptor_.acceptChannel_ 可讀,poll返回,呼叫
Channel::handleEvent()處理活動通道,呼叫Acceptor::handleRead(),函式中呼叫accept(2)來接受新連線,並回撥TcpServer::newConnection(), 函式中先建立一個TcpConnectionPtr 物件,在TcpConnection 建構函式體中:

 C++ Code 
1
2
3
 
// 通道可讀事件到來的時候,回撥TcpConnection::handleRead,_1是事件發生時間
channel_->setReadCallback(
    boost::bind(&TcpConnection::handleRead, this, _1));

新增進TcpServer::connections_, 設定連線回撥函式和訊息到來回撥函式,如下:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
 
//傳入connfd
void TcpServer::newConnection(int sockfd, const InetAddress &peerAddr)
{
    ......
    TcpConnectionPtr conn(new TcpConnection(ioLoop, connName, sockfd, localAddr, peerAddr));

    connections_[connName] = conn; // conn 是TcpConnectionPtr 物件
    conn->setConnectionCallback(connectionCallback_);
    conn->setMessageCallback(messageCallback_);
    conn->connectEstablished();
}

最後呼叫TcpConnection::connectEstablished()

 C++ Code 
1
2
3
4
5
6
 
void TcpConnection::connectEstablished()
{
    channel_->enableReading();  // TcpConnection所對應的通道加入到Poller關注

    connectionCallback_(shared_from_this());
}


現在已經建立了一個新連線,對等方傳送資料到connfd,核心接收緩衝區不為空,TcpConnection::channel_ 可讀事件發生,poll返回,呼叫Channel::handleEvent()處理活動通道,呼叫TcpConnection::handleRead()

 C++ Code 
1
2
3
4
5
 
void TcpConnection::handleRead(Timestamp receiveTime)
{
    ssize_t n = ::read(channel_->fd(), buf, sizeof buf);
    messageCallback_(shared_from_this(), buf, n);
}

shared_from_this()  會用當前物件的裸指標構造一個臨時智慧指標物件,引用計數加1,但馬上會被析構,又減1,故無論呼叫多少次,對引用計數都沒有影響。

測試程式:
simba@ubuntu:~$ telnet 127.0.0.1 8888

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
 
#include <muduo/net/TcpServer.h>
#include <muduo/net/EventLoop.h>
#include <muduo/net/InetAddress.h>

#include <stdio.h>

using namespace muduo;
using namespace muduo::net;

void onConnection(const TcpConnectionPtr &conn)
{
    if (conn->connected())
    {
        printf("onConnection(): new connection [%s] from %s\n",
               conn->name().c_str(),
               conn->peerAddress().toIpPort().c_str());
    }
    else
    {
        printf("onConnection(): connection [%s] is down\n",
               conn->name().c_str());
    }
}

void onMessage(const TcpConnectionPtr &conn,
               const char *data,
               ssize_t len)
{
    printf("onMessage(): received %zd bytes from connection [%s]\n",
           len, conn->name().c_str());
}

int main()
{
    printf("main(): pid = %d\n", getpid());

    InetAddress listenAddr(8888);
    EventLoop loop;

    TcpServer server(&loop, listenAddr, "TestServer");
    server.setConnectionCallback(onConnection);
    server.setMessageCallback(onMessage);
    server.start();

    loop.loop();
}

同樣地,使用telnet 去連線,伺服器端輸出如下:

simba@ubuntu:~/Documents/build/debug/bin$ ./reactor_test08
main(): pid = 7557
20131108 09:37:51.098888Z  7557 TRACE updateChannel fd = 4 events = 3 - EPollPoller.cc:104
20131108 09:37:51.099825Z  7557 TRACE EventLoop EventLoop created 0xBFAD3D08 in thread 7557 - EventLoop.cc:62
20131108 09:37:51.100692Z  7557 TRACE updateChannel fd = 5 events = 3 - EPollPoller.cc:104
20131108 09:37:51.101548Z  7557 TRACE updateChannel fd = 6 events = 3 - EPollPoller.cc:104
20131108 09:37:51.102063Z  7557 TRACE loop EventLoop 0xBFAD3D08 start looping - EventLoop.cc:94
20131108 09:38:01.116672Z  7557 TRACE poll  nothing happended - EPollPoller.cc:74
20131108 09:38:10.616161Z  7557 TRACE poll 1 events happended - EPollPoller.cc:65
20131108 09:38:10.616774Z  7557 TRACE printActiveChannels {6: IN}  - EventLoop.cc:257
20131108 09:38:10.616894Z  7557 INFO  TcpServer::newConnection [TestServer] - new connection [TestServer:0.0.0.0:8888#1] from 127.0.0.1:56410 - TcpServer.cc:93
20131108 09:38:10.617007Z  7557 DEBUG TcpConnection TcpConnection::ctor[TestServer:0.0.0.0:8888#1] at 0x827D7F8 fd=8 - TcpConnection.cc:62
20131108 09:38:10.617103Z  7557 TRACE newConnection [1] usecount=1 - TcpServer.cc:111
20131108 09:38:10.617152Z  7557 TRACE newConnection [2] usecount=2 - TcpServer.cc:113
20131108 09:38:10.617166Z  7557 TRACE connectEstablished [3] usecount=6 - TcpConnection.cc:78
20131108 09:38:10.617174Z  7557 TRACE updateChannel fd = 8 events = 3 - EPollPoller.cc:104
onConnection(): new connection [TestServer:0.0.0.0:8888#1] from 127.0.0.1:56410
20131108 09:38:10.617266Z  7557 TRACE connectEstablished [4] usecount=6 - TcpConnection.cc:83
20131108 09:38:10.617275Z  7557 TRACE newConnection [5] usecount=2 - TcpServer.cc:122
20131108 09:38:20.627567Z  7557 TRACE poll  nothing happended - EPollPoller.cc:74
20131108 09:38:30.638037Z  7557 TRACE poll  nothing happended - EPollPoller.cc:74
20131108 09:38:40.648523Z  7557 TRACE poll  nothing happended - EPollPoller.cc:74
20131108 09:38:46.891543Z  7557 TRACE poll 1 events happended - EPollPoller.cc:65
20131108 09:38:46.891599Z  7557 TRACE printActiveChannels {8: IN }  - EventLoop.cc:257
20131108 09:38:46.891611Z  7557 TRACE handleEvent [6] usecount=2 - Channel.cc:67
onMessage(): received 6 bytes from connection [TestServer:0.0.0.0:8888#1]
20131108 09:38:46.891744Z  7557 TRACE handleEvent [12] usecount=2 - Channel.cc:69
20131108 09:38:56.901306Z  7557 TRACE poll  nothing happended - EPollPoller.cc:74

可以看到,fd = 6 是監聽套接字,fd = 8是返回來的已連線套接字,那麼fd = 7去哪了呢?其實是被acceptor的 idleFd_ 佔據了。
連線建立的時候回撥onConnection(),我們在telnet 上輸入aaaa,伺服器端訊息到來,fd=8可讀事件發生,回撥onMessage(),加上\r\n 所以收到6個位元組資料。


參考:
《UNP》
muduo manual.pdf
《linux 多執行緒伺服器程式設計:使用muduo c++網路庫》


相關文章