WebSocket++ 的 Client

由於從去年開始,Heresy 就開始有在寫網頁的東西了,所以也開始玩 HTML5 新的 WebSocket 這項技術。而由於當初自己透過 C 來建立 WebScoket Server 的需求,所以後來就去找了 WebSocket 這個函式庫(官網)來用。

當時也邊學邊寫了幾篇文章,算是自己的紀錄。有興趣的可以參考:

不過,當時 Heresy 都只有把它拿來建立 Server 過,並沒有用來建立 Client 端的經驗;而由於最近要用 C 寫個 WebSocket Client,所以就在來追加一下這系列的文章吧~

首先,過了一年後,WebSocket 在 GitHub(連結)的主線,已經換成 0.3 了~所以不用像之前一樣,還要自己切換到 experimental 的分支;而目前最新的版本,則是 0.3.0 Alpha 4。而一樣,由於他是基於 Boost 的 ASIO 來開發的,所以會需要額外準備 Boost C Libraries

要建立一個 WebSocket 的 client 端,和建立 Server 端時相同,需要 include 兩個檔案,一個是 config 的 hesrder 檔、另一個則是 client 的 header 檔。以一個使用 Boost ASIO、不加密的連線的 Client 來說,要 include 的檔案就是:

#include <websocketpp/config/asio_client.hpp>
#include <websocketpp/client.hpp>

而 client 端控制的 endpoint 物件型別,則是:

websocketpp::client<websocketpp::config::asio_tls_client>

由於他滿長的,所以會建議用 typedef 來做縮短;例如:

typedef websocketpp::client<websocketpp::config::asio_client> WSClient;

要注意的是,endpoint 的物件在 client 端基本上是用來控制 client 的連線用的,一個 endpoint 是可以建立多個連線的;而美建立一個連線,都會取得一個 connection_hdl 的物件,這個物件才是和 server 溝通時、用來操作的物件。

在程式碼的部分,一般來說流程大致上是:

  1. 建立 endpoint 的物件
  2. 初始化 endpoint
  3. 設定對應的 handler
  4. 取得連線、並且建立連線

Heresy 這邊寫成最簡單的程式的話,大概會像下面這樣:

// 1. cretae endpoint
WSClient mEndPoint;

// 2. initial endpoind
mEndPoint.init_asio();
mEndPoint.start_perpetual();
boost::thread wsThread( [&mEndPoint](){ mEndPoint.run(); } );

// 3. set handler
mEndPoint.set_message_handler(
  []( websocketpp::connection_hdl hdl, WSClient::message_ptr msg ){
cout << msg->get_payload() << endl;
}
);

// 4. get connection and connect
websocketpp::lib::error_code ec;
WSClient::connection_ptr wsCon = mEndPoint.get_connection( sUrl, ec);
mEndPoint.connect( wsCon );

首先,就是先建立出 endpoint 的物件、mEndpoint

而之後,則是要針對他進行初始化,這邊包含了三個步驟,第一步是初始化 ASIO,第二步則是將這個 endpoint 設定為永遠執行(perpetual),之後就建立另一個執行序、來執行 endpoint 的主迴圈。這邊應該也有其他的方法,像是有的方法就是不需要設定為永遠執行(perpetual)就可以用的,不過執行的順序會不太一樣,在這邊就先不提了(註一)。

而這邊 Heresy 是用 Boost 的 thread 來建立新的執行序,如果是使用 C 11 的環境的話,也可以用 STL 的 thread 就好。(參考

接下來,則是需要設定需要的 handler,指定在事件發生的時候,要做哪些事。在這個例子裡面,只有透過 set_message_handler() 設定收到訊息時要做的事;在這邊就是單純把收到的東西透過 ostream 輸出而已。
而如果要設定其他事件的處理方式的話,則就是要使用其他的 set_xxx_handler() 的函式、來進行設定。

到上面為止,基本上都還是針對 endpoint 進行設定、初始化。接下來,就是要建立連線了~要建立連線的話,基本上分成兩步,第一步是要先透過 get_connection() 這個函式,來取得一個 connection_ptr 型別的連線指標 wsCon,然後再透過 connect() 這個函式、來進行連線。

在這邊,會有一個 error_code 的物件 ec,他是用來做錯誤偵測之用的,有必要的話,可以透過判斷他的值、以及他的訊息(message())來判斷錯誤的狀態。

之後如果要傳送訊息的話,則就可以透過 wsConsend() 這個函式,來送出資料給 server 端;而收到來自 Server 端的資料的時候,則會去執行 set_message_handler() 所設定的函式。這邊的處理方法,和 Server 端是完全相同的,所以就不多提了。

最後,當連線結束的時候,則是要呼叫 wsConclose() 這個函式,來做中斷連線的動作。在呼叫 close() 的時候,需要給兩個參數,第一個是狀態的值,詳細的定義可以參考 close.hpp 這個檔案;而第二個參數則是中斷的理由,是一個字串,可以給最長 123 個字元。

而之後,要結束程式前,則是需要把 mEndPoint 停下來,這邊可以透過呼叫 stop_perpetual() 來讓它結束永遠執行的模式,這樣在沒有連線的時候,他就會自動停止了。

程式的最後,則是再呼叫

wsThread.join();

wsThread 這個執行序結束就可以了。

下面就是一個應該可以在 Visual C 2010 下運作的 WebSocket client 的完整範例了~

#define NOMINMAX
#define _WEBSOCKETPP_CPP11_FUNCTIONAL_
#define _WEBSOCKETPP_CPP11_MEMORY_  #include <iostream>
#include <string>

#include <boost/thread.hpp>

#include <websocketpp/config/asio_no_tls_client.hpp>
#include <websocketpp/client.hpp>

using namespace std;

typedef websocketpp::client<websocketpp::config::asio_client> WSClient;

int main( int argc, char** argv )
{
string sUrl = "ws://echo.websocket.org";

// cretae endpoint
WSClient mEndPoint;

// initial endpoind
mEndPoint.init_asio();
mEndPoint.start_perpetual();
boost::thread wsThread( [&mEndPoint](){ mEndPoint.run(); } );

// set handler
mEndPoint.set_message_handler(
  []( websocketpp::connection_hdl hdl, WSClient::message_ptr msg ){
cout << msg->get_payload() << endl;
}
);

// get connection and connect
websocketpp::lib::error_code ec;
WSClient::connection_ptr wsCon = mEndPoint.get_connection( sUrl, ec);
mEndPoint.connect( wsCon );

string sCmd;
while( true )
{
cout << "Enter Command: " << flush;
getline(cin, sCmd);

if( sCmd == "quit" )
{
wsCon->close( 0, "close" );
break;
}

auto ec = wsCon->send( sCmd );
cout << ec.message() << endl;
}

mEndPoint.stop_perpetual();
wsThread.join();
}

附註

  1. 如果不設定 perpetual 的話,endpoint 會在沒有連線時自動結束,而不會持續執行;所以如果需要用同一個 endpoint 建立多個連線的話,應該是採取這樣的寫法比較合適。
    但是如果這個 endpoint 只打算建立一個連線的話,其實可以不用做這個設定,但是相對的,執行 endoinpt 的 run() 的時間點,就需要放到建立連線(呼叫 connect())之後才行。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。