muduo網路庫學習之EventLoop(七):TcpClient、Connector

s1mba發表於2013-11-10

Connector 主要用於發起連線,並帶有自動重連的功能,成員主要有一個channel_,

 C++ Code 
1
 
boost::scoped_ptr<Channel> channel_;    // Connector所對應的Channel

與Acceptor 相比少了一個acceptSocket_ 成員,因為Connector 是建立一個新的sockfd 並connect 它,如下:Connector::start()-->Connector::startInLoop()-->void Connector::connect()

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 
void Connector::connect()
{
    int sockfd = sockets::createNonblockingOrDie(); // 建立非阻塞套接字
    int ret = sockets::connect(sockfd, serverAddr_.getSockAddrInet());
    int savedErrno = (ret == 0) ? 0 : errno;
    switch (savedErrno)
    {
    case 0:
    case EINPROGRESS:   // 非阻塞套接字,未連線成功返回碼是EINPROGRESS表示正在連線
    case EINTR:
    case EISCONN:           // 連線成功
        connecting(sockfd);
        break;
        ....
    }
}

-->Connector::connecting()

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 
void Connector::connecting(int sockfd)
{
    setState(kConnecting);
    assert(!channel_);
    // Channel與sockfd關聯
    channel_.reset(new Channel(loop_, sockfd));
    // 設定可寫回撥函式,這時候如果socket沒有錯誤,sockfd就處於可寫狀態
    channel_->setWriteCallback(
        boost::bind(&Connector::handleWrite, this)); // FIXME: unsafe
    // 設定錯誤回撥函式
    channel_->setErrorCallback(
        boost::bind(&Connector::handleError, this)); // FIXME: unsafe

    channel_->enableWriting();      // 讓Poller關注可寫事件
}

現在connnect(sockfd) 沒有出錯,sockfd 就處於可寫狀態(核心緩衝區不為滿),而且poller 關注了可寫事件,觸發呼叫Connector::handleWrite()

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 
void Connector::handleWrite()
{
    LOG_TRACE << "Connector::handleWrite " << state_;

    if (state_ == kConnecting)
    {
        int sockfd = removeAndResetChannel();   // 從poller中移除關注,並將channel置空
        // socket可寫並不意味著連線一定建立成功
        // 還需要用getsockopt(sockfd, SOL_SOCKET, SO_ERROR, ...)再次確認一下。
        int err = sockets::getSocketError(sockfd);
        ......
        else    // 連線成功
        {
            setState(kConnected);
            if (connect_)
            {
                newConnectionCallback_(sockfd);     // 回撥
            }

        }
    }
}

注意:在handleWrite()裡面需要removeAndResetChannel(),因此此時連線建立,故不用再關注channel的可寫事件,最終會執行 channel_.reset();  即把channel析構了。此外函式需要返回sockfd, 讓TcpConnection來接管。

連線成功後呼叫newConnectionCallback_(sockfd); 通過下面函式設定:

 C++ Code 
1
2
3
4
 
void setNewConnectionCallback(const NewConnectionCallback &cb)
{
    newConnectionCallback_ = cb;
}

實際上 Connector 一般也不單獨使用,作為TcpClient 的成員:
 C++ Code 
1
2
 
typedef boost::shared_ptr<Connector> ConnectorPtr;
ConnectorPtr connector_;    // 用於主動發起連線

但TcpClient 與 TcpServer 不同的是隻有一個TcpConnection 成員:

 C++ Code 
1
 
TcpConnectionPtr connection_; // Connector連線成功以後,得到一個TcpConnection

即一個TcpClient 對應一個TcpConnection 和一個 Connector;而一個TcpServer 對應一個TcpConnection 列表 和 一個 Acceptor。

在TcpClient 建構函式中:

 C++ Code 
1
2
3
 
// 設定連線成功回撥函式
connector_->setNewConnectionCallback(
    boost::bind(&TcpClient::newConnection, this, _1));

也就是說現在會執行TcpClient::newConnectionn()

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 
void TcpClient::newConnection(int sockfd)
{
    ...........
    TcpConnectionPtr conn(new TcpConnection(loop_, connName, sockfd, localAddr, peerAddr));

    conn->setConnectionCallback(connectionCallback_);
    conn->setMessageCallback(messageCallback_);
    conn->setWriteCompleteCallback(writeCompleteCallback_);
    conn->setCloseCallback(
        boost::bind(&TcpClient::removeConnection, this, _1)); // FIXME: unsafe
    {
        MutexLockGuard lock(mutex_);
        connection_ = conn;     // 儲存TcpConnection
    }

    conn->connectEstablished();     // 這裡回撥connectionCallback_
}

此外與TcpServer 還有一點不同的是,TcpServer 可以有多個Reactor,即mainReactor+ThreadPool(subReactors),但TcpClient 只能有一個Reactor,即一個事件迴圈EventLoop,由它來處理這個TcpConnection 的事件(可讀事件(包括接收資料,連線關閉),可寫事件(核心傳送緩衝區不為滿),錯誤事件)。當然我們可以開多個TcpClient繫結在同個EventLoop上,這樣一個EventLoop 就管理多個TcpClient, 也就是多個TcpConnection,事件發生的處理流程與TcpServer 類似,可以參考以前筆記。

還需要說明一點是,使用者呼叫TcpServer/TcpClient 的setXXXCallback() 系列公有介面函式設定回撥函式,實際上最終設定的是TcpConnection 的XXXCallback_ 成員,這些回撥函式會在事件發生時被呼叫,比如連線建立,訊息到來等。

測試程式碼:

先開啟回射伺服器端如 ./reactor_test11

 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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include <muduo/net/TcpServer.h>
#include <muduo/net/EventLoop.h>
#include <muduo/net/InetAddress.h>

#include <boost/bind.hpp>

#include <stdio.h>

using namespace muduo;
using namespace muduo::net;

class TestServer
{
public:
    TestServer(EventLoop *loop,
               const InetAddress &listenAddr)
        : loop_(loop),
          server_(loop, listenAddr, "TestServer")
    {
        server_.setConnectionCallback(
            boost::bind(&TestServer::onConnection, this, _1));
        server_.setMessageCallback(
            boost::bind(&TestServer::onMessage, this, _1, _2, _3));
    }

    void start()
    {
        server_.start();
    }

private:
    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,
                   Buffer *buf,
                   Timestamp receiveTime)
    {
        string msg(buf->retrieveAllAsString());
        printf("onMessage(): received %zd bytes from connection [%s] at %s\n",
               msg.size(),
               conn->name().c_str(),
               receiveTime.toFormattedString().c_str());
        conn->send(msg);
    }

    EventLoop *loop_;
    TcpServer server_;
};


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

    InetAddress listenAddr(8888);
    EventLoop loop;

    TestServer server(&loop, listenAddr);
    server.start();

    loop.loop();
}

接著執行./tcpclient_test
分別輸入
aaaaaaaaaaa
XXXXXXXXXXXXXXXX

 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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
 
#include <muduo/net/Channel.h>
#include <muduo/net/TcpClient.h>

#include <muduo/base/Logging.h>
#include <muduo/net/EventLoop.h>
#include <muduo/net/InetAddress.h>

#include <boost/bind.hpp>

#include <stdio.h>

using namespace muduo;
using namespace muduo::net;

class TestClient
{
public:
    TestClient(EventLoop *loop, const InetAddress &listenAddr)
        : loop_(loop),
          client_(loop, listenAddr, "TestClient"),
          stdinChannel_(loop, 0)
    {
        client_.setConnectionCallback(
            boost::bind(&TestClient::onConnection, this, _1));
        client_.setMessageCallback(
            boost::bind(&TestClient::onMessage, this, _1, _2, _3));
        //client_.enableRetry();
        // 標準輸入緩衝區中有資料的時候,回撥TestClient::handleRead
        stdinChannel_.setReadCallback(boost::bind(&TestClient::handleRead, this));
        stdinChannel_.enableReading();      // 關注可讀事件
    }

    void connect()
    {
        client_.connect();
    }

private:
    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, Buffer *buf, Timestamp time)
    {
        string msg(buf->retrieveAllAsString());
        printf("onMessage(): recv a message [%s]\n", msg.c_str());
        LOG_TRACE << conn->name() << " recv " << msg.size() << " bytes at " << time.toFormattedString();
    }

    // 標準輸入緩衝區中有資料的時候,回撥該函式
    void handleRead()
    {
        char buf[1024] = {0};
        fgets(buf, 1024, stdin);
        buf[strlen(buf) - 1] = '\0';        // 去除\n
        client_.connection()->send(buf);
    }

    EventLoop *loop_;
    TcpClient client_;
    Channel stdinChannel_;      // 標準輸入Channel
};

int main(int argc, char *argv[])
{
    LOG_INFO << "pid = " << getpid() << ", tid = " << CurrentThread::tid();
    EventLoop loop;
    InetAddress serverAddr("127.0.0.1"8888);
    TestClient client(&loop, serverAddr);
    client.connect();
    loop.loop();
}

伺服器端輸出如下:
simba@ubuntu:~/Documents/build/debug/bin$ ./reactor_test11
20131110 07:57:14.970756Z  3400 TRACE IgnoreSigPipe Ignore SIGPIPE - EventLoop.cc:51
main(): pid = 3400
20131110 07:57:14.986047Z  3400 TRACE updateChannel fd = 4 events = 3 - EPollPoller.cc:104
20131110 07:57:14.986501Z  3400 TRACE EventLoop EventLoop created 0xBFADD094 in thread 3400 - EventLoop.cc:76
20131110 07:57:14.986822Z  3400 TRACE updateChannel fd = 5 events = 3 - EPollPoller.cc:104
20131110 07:57:14.987696Z  3400 TRACE updateChannel fd = 6 events = 3 - EPollPoller.cc:104
20131110 07:57:14.988252Z  3400 TRACE loop EventLoop 0xBFADD094 start looping - EventLoop.cc:108
20131110 07:57:17.022285Z  3400 TRACE poll 1 events happended - EPollPoller.cc:65
20131110 07:57:17.022988Z  3400 TRACE printActiveChannels {6: IN }  - EventLoop.cc:271
20131110 07:57:17.023190Z  3400 INFO  TcpServer::newConnection [TestServer] - new connection [TestServer:0.0.0.0:8888#1] from 127.0.0.1:54917 - TcpServer.cc:93
20131110 07:57:17.023348Z  3400 DEBUG TcpConnection TcpConnection::ctor[TestServer:0.0.0.0:8888#1] at 0x84417E0 fd=8 - TcpConnection.cc:65
20131110 07:57:17.023359Z  3400 TRACE newConnection [1] usecount=1 - TcpServer.cc:111
20131110 07:57:17.023387Z  3400 TRACE newConnection [2] usecount=2 - TcpServer.cc:113
20131110 07:57:17.023417Z  3400 TRACE connectEstablished [3] usecount=6 - TcpConnection.cc:238
20131110 07:57:17.023424Z  3400 TRACE updateChannel fd = 8 events = 3 - EPollPoller.cc:104
onConnection(): new connection [TestServer:0.0.0.0:8888#1] from 127.0.0.1:54917
20131110 07:57:17.023464Z  3400 TRACE connectEstablished [4] usecount=6 - TcpConnection.cc:243
20131110 07:57:17.023469Z  3400 TRACE newConnection [5] usecount=2 - TcpServer.cc:123
20131110 07:57:19.704918Z  3400 TRACE poll 1 events happended - EPollPoller.cc:65
20131110 07:57:19.704958Z  3400 TRACE printActiveChannels {8: IN }  - EventLoop.cc:271
20131110 07:57:19.704969Z  3400 TRACE handleEvent [6] usecount=2 - Channel.cc:67
onMessage(): received 11 bytes from connection [TestServer:0.0.0.0:8888#1] at 20131110 07:57:19.704916
20131110 07:57:19.705084Z  3400 TRACE handleEvent [12] usecount=2 - Channel.cc:69
20131110 07:57:22.728687Z  3400 TRACE poll 1 events happended - EPollPoller.cc:65
20131110 07:57:22.728725Z  3400 TRACE printActiveChannels {8: IN}  - EventLoop.cc:271
20131110 07:57:22.728735Z  3400 TRACE handleEvent [6] usecount=2 - Channel.cc:67
onMessage(): received 16 bytes from connection [TestServer:0.0.0.0:8888#1]at 20131110 07:57:22.728685
20131110 07:57:22.728786Z  3400 TRACE handleEvent [12] usecount=2 - Channel.cc:69
20131110 07:57:32.739020Z  3400 TRACE poll  nothing happended - EPollPoller.cc:74
^C


輸出中fd = 6是監聽套接字,fd=8是返回的已連線套接字,連線建立呼叫OnConnection(),因為客戶端輸入兩串資料,fd=8產生兩次可讀事件,呼叫兩次onMessage().

客戶端輸出如下:

simba@ubuntu:~/Documents/build/debug/bin$ ./tcpclient_test 
20131110 07:57:16.999262Z  3401 TRACE IgnoreSigPipe Ignore SIGPIPE - EventLoop.cc:51
20131110 07:57:17.001679Z  3401 INFO  pid = 3401, tid = 3401 - TcpClient_test.cc:77
20131110 07:57:17.002535Z  3401 TRACE updateChannel fd = 4 events = 3 - EPollPoller.cc:104
20131110 07:57:17.003035Z  3401 TRACE EventLoop EventLoop created 0xBFE52018 in thread 3401 - EventLoop.cc:76
20131110 07:57:17.003367Z  3401 TRACE updateChannel fd = 5 events = 3 - EPollPoller.cc:104
20131110 07:57:17.003846Z  3401 DEBUG Connector ctor[0x9A946D0] - Connector.cc:33
20131110 07:57:17.004215Z  3401 INFO  TcpClient::TcpClient[TestClient] - connector 0x9A946D0 - TcpClient.cc:72
20131110 07:57:17.004569Z  3401 TRACE updateChannel fd = 0 events = 3 - EPollPoller.cc:104
20131110 07:57:17.005017Z  3401 INFO  TcpClient::connect[TestClient] - connecting to 127.0.0.1:8888 - TcpClient.cc:106
20131110 07:57:17.024071Z  3401 TRACE updateChannel fd = 6 events = 4 - EPollPoller.cc:104
20131110 07:57:17.024375Z  3401 TRACE loop EventLoop 0xBFE52018 start looping - EventLoop.cc:108
20131110 07:57:17.024561Z  3401 TRACE poll 1 events happended - EPollPoller.cc:65
20131110 07:57:17.024980Z  3401 TRACE printActiveChannels {6: OUT}  - EventLoop.cc:271
20131110 07:57:17.025181Z  3401 TRACE handleWrite Connector::handleWrite 1 - Connector.cc:169
20131110 07:57:17.025326Z  3401 TRACE updateChannel fd = 6 events = 0 - EPollPoller.cc:104
20131110 07:57:17.025509Z  3401 TRACE removeChannel fd = 6- EPollPoller.cc:147
20131110 07:57:17.025804Z  3401 DEBUG TcpConnection TcpConnection::ctor[TestClient:127.0.0.1:8888#1] at 0x9A94808 fd=6 - TcpConnection.cc:65
20131110 07:57:17.026012Z  3401 TRACE connectEstablished [3] usecount=3 - TcpConnection.cc:238
20131110 07:57:17.026183Z  3401 TRACE updateChannel fd = 6 events = 3 - EPollPoller.cc:104
onConnection(): new connection [TestClient:127.0.0.1:8888#1] from 127.0.0.1:8888
20131110 07:57:17.026506Z  3401 TRACE connectEstablished [4] usecount=3 - TcpConnection.cc:243
aaaaaaaaaaa
20131110 07:57:19.704702Z  3401 TRACE poll 1 events happended - EPollPoller.cc:65
20131110 07:57:19.704765Z  3401 TRACE printActiveChannels {0: IN }  - EventLoop.cc:271
20131110 07:57:19.705370Z  3401 TRACE poll 1 events happended - EPollPoller.cc:65
20131110 07:57:19.705408Z  3401 TRACE printActiveChannels {6: IN }  - EventLoop.cc:271
20131110 07:57:19.705427Z  3401 TRACE handleEvent [6] usecount=2 - Channel.cc:67
onMessage(): recv a message [aaaaaaaaaaa]
20131110 07:57:19.705520Z  3401 TRACE onMessage TestClient:127.0.0.1:8888#1 recv 11 bytes at 20131110 07:57:19.705368 - TcpClient_test.cc:58
20131110 07:57:19.705538Z  3401 TRACE handleEvent [12] usecount=2 - Channel.cc:69
XXXXXXXXXXXXXXXX
20131110 07:57:22.728548Z  3401 TRACE poll 1 events happended - EPollPoller.cc:65
20131110 07:57:22.728616Z  3401 TRACE printActiveChannels {0: IN}  - EventLoop.cc:271
20131110 07:57:22.729010Z  3401 TRACE poll 1 events happended - EPollPoller.cc:65
20131110 07:57:22.729035Z  3401 TRACE printActiveChannels {6: IN }  - EventLoop.cc:271
20131110 07:57:22.729045Z  3401 TRACE handleEvent [6] usecount=2 - Channel.cc:67
onMessage(): recv a message [XXXXXXXXXXXXXXXX]
20131110 07:57:22.729070Z  3401 TRACE onMessage TestClient:127.0.0.1:8888#1 recv 16 bytes at 20131110 07:57:22.729009 - TcpClient_test.cc:58
20131110 07:57:22.729093Z  3401 TRACE handleEvent [12] usecount=2 - Channel.cc:69
20131110 07:57:32.739100Z  3401 TRACE poll  nothing happended - EPollPoller.cc:74
20131110 07:57:36.887794Z  3401 TRACE poll 1 events happended - EPollPoller.cc:65
20131110 07:57:36.887848Z  3401 TRACE printActiveChannels {6: IN }  - EventLoop.cc:271
20131110 07:57:36.887860Z  3401 TRACE handleEvent [6] usecount=2 - Channel.cc:67
20131110 07:57:36.887882Z  3401 TRACE handleClose fd = 6 state = 2 - TcpConnection.cc:369
20131110 07:57:36.887892Z  3401 TRACE updateChannel fd = 6 events = 0 - EPollPoller.cc:104
onConnection(): connection [TestClient:127.0.0.1:8888#1] is down
20131110 07:57:36.887948Z  3401 TRACE handleClose [7] usecount=3 - TcpConnection.cc:377
20131110 07:57:36.887966Z  3401 TRACE handleClose [11] usecount=3 - TcpConnection.cc:380
20131110 07:57:36.887984Z  3401 TRACE handleEvent [12] usecount=2 - Channel.cc:69
20131110 07:57:36.887994Z  3401 TRACE removeChannel fd = 6- EPollPoller.cc:147
20131110 07:57:36.888005Z  3401 DEBUG ~TcpConnection TcpConnection::dtor[TestClient:127.0.0.1:8888#1] at 0x9A94808 fd=6 - TcpConnection.cc:72
20131110 07:57:46.894605Z  3401 TRACE poll  nothing happended - EPollPoller.cc:74

fd=0是標準輸入,fd=6是客戶端連線的套接字,剛開始連線成功,fd=6可寫事件發生,但馬上把connector的channel移除關注並析構,並構造TcpConnection。在命令列輸入一串資料,標準輸入可讀事件發生,等伺服器回射回來,fd=6可讀事件發生,呼叫OnMessage(),重複兩次。我們首先ctrl+c 掉伺服器,客戶端發現此連線已經down掉,就會析構TcpConnection,順便關閉套接字,當然事件迴圈還在繼續,因為如前面所說,有可能EventLoop繫結了多個TcpClient。

可以稍微舉個例子,比如可以讓EventLoopThreadPool開兩個IO執行緒,每個IO執行緒管理4個TcpClient,如下程式中RecvFileClient 是一個封裝了TcpClient類的類。


 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
int main(int argc, char* argv[])
{
  LOG_INFO << "pid = " << getpid();
  EventLoop loop;
  g_loop = &loop;
  // 用兩個IO執行緒來發起多個連線
  EventLoopThreadPool loopPool(&loop);
  loopPool.setThreadNum(2);
  loopPool.start();

  boost::ptr_vector<RecvFileClient> clients(8);

  InetAddress serverAddr("127.0.0.1"2021);

  for (int i = 0; i < 8; ++i)
  {
    char buf[32];
    snprintf(buf, sizeof buf, "%d", i+1);
    clients.push_back(new RecvFileClient(loopPool.getNextLoop(), serverAddr, buf));
    clients[i].connect();
    usleep(200);
  }

  loop.loop();
  usleep(20000);
}


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


相關文章