[Boost] ASIO學習筆記:network basic – client

posted in: ASIO, boost, C/C++程式設計, Server, 遊戲 | 0

對於網路程式設計而言,不外乎是server或者是client程式這二種。 而怎麼讓二台遠端的電腦透過網路溝通的程式設計,稱作socket programming。 現在就讓我們先從最基本的client端連接到遠端server的程式開始:

#include <boost/asio.hpp>
#include <boost/shared_ptr.hpp>
#include <iostream>
#include <string>
using namespace std;
 
int main( int argc, char * argv[] )
{
    boost::shared_ptr< boost::asio::io_service > io_service(new boost::asio::io_service);
    boost::asio::ip::tcp::socket sock( *io_service );
    boost::asio::ip::tcp::resolver resolver(*io_service);
    boost::asio::ip::tcp::resolver::query query( "www.yahoo.com.tw", "80");
    boost::asio::ip::tcp::resolver::iterator iterator = resolver.resolve( query);
    boost::asio::ip::tcp::endpoint endpoint = *iterator;
    std::cout << "Connecting to: " << endpoint << std::endl;
    sock.connect( endpoint);
    sock.shutdown( boost::asio::ip::tcp::socket::shutdown_both);
    sock.close();
    system("pause");
    return 0;
 
}

boost::asio::ip::tcp::socket sock( *io_service ):為我們電腦跟遠端主機連接的通道

boost::asio::ip::tcp::resolver resolver( *io_service ):域名解析器,功用是用來向DNS查詢一個網址的ip時用

boost::asio::ip::tcp::resolver::query query(“www.yaoo.com.tw”, “80”):輸入要查詢的網址及port

boost::asio::ip::tcp::resolver::iterator iterator = resolver.resolve( query ):向DNS查詢,回傳一組ip list,要特別注意的是,一個網址可能會對應到多個ip位置。

boost::asio::ip::tcp::endpoint endpoint = *iterator:取得list中第一個ip 在取得了endpoint之後,我們的sock就可以藉此來跟遠端的主機作連結:sock.connect( endpoint ); 然而,有時候我們的連結可能會有各種錯誤而導致連結失敗,如:的網址名稱可能輸入錯誤、對方的port未開端、網址遺失…等等。這時候我們在運行程式的時候,由於沒有任何錯誤的檢查機制,而導致程式的crash。幸好boost提供了一個方便的錯誤及例外的機制,只要在程式中加入boost::system::error_code ec,並且在需要檢查錯誤的功能呼叫中加入,不僅能夠讓我們取得系統的錯誤訊息,並且也可以藉此來對錯誤做相應的處理,demo程式如下:

 
int main( int argc, char * argv[] )
{
    boost::system::error_code ec;
    boost::shared_ptr< boost::asio::io_service > io_service(new boost::asio::io_service);
    boost::asio::ip::tcp::socket sock( *io_service );
    boost::asio::ip::tcp::resolver resolver(*io_service);
    boost::asio::ip::tcp::resolver::query query( "www.yahoo.com.tw", "80");
    boost::asio::ip::tcp::resolver::iterator iterator = resolver.resolve( query, ec );
    if(ec){std::cout << boost::system::system_error(ec).what() << std::endl;}
    boost::asio::ip::tcp::endpoint endpoint = *iterator;
    std::cout << "Connecting to: " << endpoint << std::endl;
    sock.connect( endpoint, ec );
    if(ec){std::cout << boost::system::system_error(ec).what() << std::endl;}
    sock.shutdown( boost::asio::ip::tcp::socket::shutdown_both, ec);
    if(ec){std::cout << boost::system::system_error(ec).what() << std::endl;}
    sock.close();
    system("pause");
    return 0;
 
}
 

另外要特別注意的是,一個網址可能會對應到多個ip位置,boost::asio::ip::tcp::resolver::iterator iterator中所保存的便是從DNS中所查詢到該網址所對應的ip,下面的程式試著將yahoo的http server所對應的所有ip列印出來:

int main( int argc, char * argv[] )
{
    boost::system::error_code ec;
    boost::shared_ptr< boost::asio::io_service > io_service(new boost::asio::io_service);
    boost::asio::ip::tcp::socket sock( *io_service );
    boost::asio::ip::tcp::resolver resolver( *io_service );
    boost::asio::ip::tcp::resolver::query query("www.yahoo.com.tw", "80");  //可能不只有一個ip
    boost::asio::ip::tcp::resolver::iterator iterator = resolver.resolve( query, ec );
     
    if(ec){std::cout < < boost::system::system_error(ec).what() << std::endl;}
     
    boost::asio::ip::tcp::endpoint endpoint = *iterator;
    std::cout << "Connecting to: " << endpoint << std::endl;
     
    cout << "ip list of www.yahoo.com.tw:" << endl;
    for(boost::asio::ip::tcp::resolver::iterator i = iterator;
    i != boost::asio::ip::tcp::resolver::iterator(); ++i) {
     
        boost::asio::ip::tcp::endpoint end = *i;
        std::cout << end.address() << ' ';
    }
     
    try{
        sock.connect(endpoint);
    }catch( std::exception & ex ){
        cout << ex.what() << endl;
    }
 
    sock.shutdown( boost::asio::ip::tcp::socket::shutdown_both, ec);
    if(ec){std::cout << boost::system::system_error(ec).what() << std::endl;}
    sock.close(ec);
    if(ec){ std::cout << boost::system::system_error(ec).what() << std::endl;}
     
    system("pause");
    return 0;
}
 

上面的做法,使用的是同步的socket連接方式,缺點是我們必須要等待遠端的系統回應之後才能夠進行接下來的工作。只是,在很多時候,我們不會希望程式浪費在等在系統回應的時間上,例如:當我們在線上遊戲中要加入一個四人團隊的副本時,不會希望在按下『加入』的同時,遊戲就像是當機一樣完全不能進行任何的操作;我們所期望的,是在等待連接的同時,我們人物的角色還是能夠在地圖上四處走動。

其實,要達成這個目的主要可以約略分為下面二個方法:1.使用ASync socket連接,2.使用Sync socket連接,但使用多個thread來處理連接時的等待。

1.使用ASync Socket連接:在boost中sock物件有提供async_connect方法讓我們做非同步的連接;當程式跟遠端主機連接上之後,遠端主機便會通知我們的程式,說明一個非同步的連接已完成:藉由將一個call back function加入到我們客端端io_service所保有的Completion Event Queue裡,我們便可以控制,在操作完成後該進行什麼動作。async_connect的使用方法如下:

void onConnect(boost::shared_ptr< boost::asio::ip::tcp::socket> sock, boost::system::error_code ec){

   if(ec){std::cout << boost::system::system_error(ec).what() << std::endl;}

   cout << "onConnect" << endl;
};

void main(){

   ...
    
   sock->async_connect( endpoint, boost::bind( &onConnect, sock, _1 ) );
    
   ...
}

其中在boost::bind( &onConnect, sock, _1 )中有_1這個變數,所代表的是bind時的placeholder--意指在呼叫時期才會決定的參數--在上面的程式中不一定要有,只是用來讓系統在呼叫call back(onConnect)時可以傳入boost::system::error_code ec而且,因此擺放的先後順序可以依我們自己的喜好決定。意即,我們也可以將boost::system::error_code ec放在boost::shared_ptr< boost::asio::ip::tcp::socket> sock前面。只要event handle跟bind中所擺放的順序是一樣的即可。

只是當我們將程式改成上面的樣子時,系統不僅不會呼叫onConnect,而且在sock->shutdown( boost::asio::ip::tcp::socket::shutdown_both, ec)之後會出現印出通訊端未連線的訊息。這是由於在我們的io_service還沒去執行completion event queue中的onConnect事件,因此client端的sock並不知道連線已經被建立了,所以沒辦法去shutdown一個已經不存在的連線。

因此我們必須讓我們的io_service去檢查completion event queue。還記得io_service->run()及io_service->poll()函數嗎?是的,我們必須要先轉系統轉移到kernel mode去檢查completion event queue裡是否有待處理的事件。這個時候我們必須要處理的議題是:到底要用的是Sync IO還是Async IO?

使用Sync IO很單純,只需要加上io_service->run()即可,完整的程式碼如下:

#include <boost/asio.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/bind.hpp>
#include <iostream>
#include <string>
 
using namespace std;
void onConnect(boost::shared_ptr< boost::asio::ip::tcp::socket> sock, boost::system::error_code ec){
    if(ec){std::cout << boost::system::system_error(ec).what() << std::endl;}

    cout << "onConnect:socket已與對方主機連接" << endl;
};
int main( int argc, char * argv[] )
{
    boost::system::error_code ec;
    boost::shared_ptr< boost::asio::io_service > io_service(new boost::asio::io_service);
    boost::shared_ptr< boost::asio::ip::tcp::socket > sock( new boost::asio::ip::tcp::socket( *io_service ));
    boost::asio::ip::tcp::resolver resolver(*io_service);
    boost::asio::ip::tcp::resolver::query query( "www.yahoo.com.tw", "80");
    boost::asio::ip::tcp::resolver::iterator iterator = resolver.resolve( query, ec );
    if(ec){std::cout << boost::system::system_error(ec).what() << std::endl;}
     
    boost::asio::ip::tcp::endpoint endpoint = *iterator;
     
    std::cout << "Connecting to: " << endpoint << std::endl;
    sock->async_connect( endpoint, boost::bind( &onConnect, sock, _1 ) );
     
    io_service->run(ec);
     
    if(ec){std::cout << boost::system::system_error(ec).what() << std::endl;}
     
    sock->shutdown( boost::asio::ip::tcp::socket::shutdown_both, ec);
    if(ec){std::cout << "shutdown:" << boost::system::system_error(ec).what() << std::endl;}
    sock->close();
    system("pause");
     
    return 0;
 
}

缺點非常明顯:程式必須要等待IO的完成。若要使用的是Async IO的話,那麼把run直接換成poll是行不通的,因為client端的socket與遠端主機的連接不可能在『瞬間』馬上完成,因此我們必須要不斷的去檢查Completion Event Queue裡是不是有非同步的事件已經完成並被加入到Queue裡。主要的概念是導入在while裡面並不斷的去執行poll,如下:

#include <boost/asio.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/bind.hpp>
#include <iostream>
#include <string>
using namespace std;
 
static bool bConnected = false;
void onConnect(boost::shared_ptr< boost::asio::ip::tcp::socket> sock, boost::system::error_code ec){
 
    if(ec){
        std::cout << boost::system::system_error(ec).what() << std::endl;
    }else{
        bConnected = true;
        cout << "onConnect:socket已與對方主機連接" << endl;
    }
};
int main( int argc, char * argv[] )
{
    boost::system::error_code ec;
    boost::shared_ptr< boost::asio::io_service > io_service(new boost::asio::io_service);
    boost::shared_ptr< boost::asio::ip::tcp::socket > sock( new boost::asio::ip::tcp::socket( *io_service ));
    boost::asio::ip::tcp::resolver resolver(*io_service);
    boost::asio::ip::tcp::resolver::query query( "www.yahoo.com.tw", "80");
    boost::asio::ip::tcp::resolver::iterator iterator = resolver.resolve( query, ec );
    if(ec){std::cout << boost::system::system_error(ec).what() << std::endl;}
     
    boost::asio::ip::tcp::endpoint endpoint = *iterator;
     
    std::cout << "Connecting to: " << endpoint << std::endl;
    sock->async_connect( endpoint, boost::bind( &onConnect, sock, _1 ) );
     
    //io_service->run(ec);
    while(!bConnected){
        io_service->poll(ec);
    }
     
    if(ec){std::cout << boost::system::system_error(ec).what() << std::endl;}
     
    sock->shutdown( boost::asio::ip::tcp::socket::shutdown_both, ec);
    if(ec){std::cout << "shutdown:" << boost::system::system_error(ec).what() << std::endl;}
    sock->close();
    system("pause");
    return 0;
}

這段程式為blocking,因為整個程式都在等待while迴圈裡的onConnect事件完成,但如果我們在迴圈裡也去做別的事的話(如:遊戲畫面的更新、人物的移動…等等),那麼這段程式就是non-blocking:

int main( int argc, char * argv[] )
{
    ...
    sock->async_connect( endpoint, boost::bind( &onConnect, sock, _1 ) );


    while(!bConnected){
    io_service->poll(ec);
    doSomething();
    }
     
    ...
}

只是當我們使用的是Async IO的時候,系統就必須不斷的在user mode跟kernel mode之間進行Context switch。如果這時候使用blocking的設計,那麼轉移就會變得非常頻繁。要知道,context swith的成本是很高的。

 

2.使用Sync socket連接,但使用多個thread來處理連接時的等待:

這個方法對main thread而言是屬於non-blocking的方法,但對sub thread而言則是要看程式設計師怎麼設計。以下所展示的是ASync socket / Sync IO / non-blocking的做法:

#include <boost/asio.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/bind.hpp>
#include <iostream>
#include <string>
#include <boost/thread.hpp>


using namespace std;
boost::mutex mutex;
static bool bConnected = false;




void onConnectThread(boost::shared_ptr< boost::asio::io_service > io_service){

    mutex.lock();
    cout << "\n" << __FUNCTION__ << " start." << endl;
    mutex.unlock();
    boost::system::error_code ec;
    io_service->run(ec);
    if(ec){std::cout << boost::system::system_error(ec).what() << std::endl;}
    bConnected = true;
    mutex.lock();
    cout << __FUNCTION__ << " end." << endl;
    mutex.unlock();
}

void onConnectEvent(boost::shared_ptr< boost::asio::ip::tcp::socket> sock, boost::system::error_code ec){

    if(ec){

        std::cout << boost::system::system_error(ec).what() << std::endl;
    }else{

        bConnected = true;
        mutex.lock();
        cout << "\nonConnect:socket已與對方主機連接" << endl;
        mutex.unlock();
    }

};



int main( int argc, char * argv[] ){
    boost::system::error_code ec;
    boost::shared_ptr< boost::asio::io_service > io_service(new boost::asio::io_service);
    boost::shared_ptr< boost::asio::ip::tcp::socket > sock( new boost::asio::ip::tcp::socket( *io_service ));
    boost::asio::ip::tcp::resolver resolver(*io_service);
    boost::asio::ip::tcp::resolver::query query( "www.yahoo.com.tw", "80");
    boost::asio::ip::tcp::resolver::iterator iterator = resolver.resolve( query, ec );
    boost::shared_ptr< boost::asio::io_service::work > work(new boost::asio::io_service::work( *io_service ));
    if(ec){std::cout << boost::system::system_error(ec).what() << std::endl;}
    boost::asio::ip::tcp::endpoint endpoint = *iterator;

    mutex.lock();
    std::cout << "Connecting to: " << endpoint << std::endl;
    mutex.unlock();

    sock->async_connect( endpoint, boost::bind( &onConnectEvent, sock, _1 ) );
    boost::thread t(boost::bind(&onConnectThread, io_service));
    while(!bConnected){
        cout << "do something and waiting for server to connect" << endl;
    }

    //work.reset();

    if(ec){std::cout << boost::system::system_error(ec).what() << std::endl;}
    sock->shutdown( boost::asio::ip::tcp::socket::shutdown_both, ec);
    if(ec){std::cout << "shutdown:" << boost::system::system_error(ec).what() << std::endl;}
    sock->close();
    system("pause");
    return 0;
}

基本上,由於我們有宣告boost::shared_ptr< boost::asio::io_service::work > work(new boost::asio::io_service::work( *io_service )),因此不會有太大的問題,只需要記得,如果想要結束執行onConnectThread--不要讓subThread繼續監聽event queue的話--只要下work.reset()就可以了。

不過若我們沒有宣告work的話,就要特別注意thread一定要在sock->async_connect執行後才宣告,因為當我們一宣告出t之後,thread就會馬上被create並執行onConnectThread這個function,所以,我們必須要先對Compleion Event Queue說:我們已經啟動了一個非同步的操作,請IO以同步--io_service->run(ec)--的方式執行。如果t宣告在async操作的前面的話,會由於還沒下async指令而導致Completion Event Queue為空,進而io_service在做Sync IO的時候找不到有非同步的操作完成通知就馬上返回,這是屬於邏輯上的程式錯誤。

當然,沒宣告work就表示我們的程式就只能針測到『一次』的非同步連結,在這實作上並沒有什麼意義。

Leave a Reply

Your email address will not be published. Required fields are marked *