C++ WebSocket 函式庫:WebSocket++

| | 2 Comments| 16:31
Categories:

在上一篇《HTML5 WebSocket Client》裡,算是很簡單地介紹了 WebSocket 的概念,以及在網頁上、使用 JavaScript 開發 client 端程式的方法。而至於 Server 端呢?其實目前已經有很多方案,都可以用來建立 WebSocket Server 了。不過,由於 Heresy 這邊的需求,是要使用 C 搭配現有的函式庫來開發,所以不太適合使用一般的網站伺服器方案;而在稍微評估了一下後,後來是決定使用「WebSocket 」這個函式庫,來做為 C 環境的 WebSocket Server 開發方案。

WebSocket 的官方網站是:http://www.zaphoyd.com/websocketpp,他是採用 BSD License 的 OpenSource、跨平台函式庫,檔案則都放在 Github 上(網頁)。他目前最新的版本是 0.3.x,在 Github 上要切換到「experimental」這個 brahch;而這個版本的 WebSocket 基本上是使用 C 11 以及 Boost C Libraries 裡的 ASIO(官網)來實作的 Header-Only 的函式庫,所以在使用前不需要特別去建置這個函式庫、只要在需要時去 include 他的 Header 檔就可以了,相當地方便。

而如果使用的開發環境不支援 C 11 的話,也可以透過 Boost 的功能來做支援,所以實際上他的相容性算是相當不錯的~不過由於開發者本身是主要是在 Mac OS X 和 Linux 上做開發的,所以在 Windows、Visual Studio 上的支援,似乎沒有這麼好;不過由於他有盡量按照標準來寫,所以大多都還算好解決。

在 WebSocket 的功能方面,他除了有提供 Server 端的功能外,也可以用來開發 Client 端的程式,算是相當地完整;雖然他的板號還在 0.3,好像還很新,不過實際上功能應該算是夠用了~


檔案準備

如果要使用 WebSocket 的話,基本上就是先到 GitHub 上 0.3.x 這個分支:

https://github.com/zaphoyd/websocketpp/tree/experimental

去把檔案下載下來。

而下載下來的檔案裡面,「websocketpp」這個資料夾,就是要使用這個函式庫時,所有需要的檔案了~而文件的部分,則是要連到他的網頁(連結)去看,內容不算很完整,Heresy 算是看著範例程式和原始碼寫出來的;這點算是 Heresy 覺得這個函式庫做的比較差一點的地方,不過考慮到現在還是 0.3 版,也就不要要求太多了。

另外,由於他是基於 Boost ASIO 來開發網路的功能,所以也必須要下載 Boost C Libraries 來使用;Boost 的官方網站是:http://www.boost.org/

而如果有需要用到 TLS 的加密連線的話,應該是會需要使用 OpenSSL 這個函式庫(官網);如果不打算做加密連線的話,基本上是可以跳過這個函式庫的。(Heresy 沒試過這部分)


基本概念

WebSocket 的基本使用說明,可以參考《Building a program with WebSocket 》這份文件。Heresy 這邊算是整理一下,自己玩過後的想法。

首先,要使用 WebSocket 來開發程式的時候,基本上要 include 兩種檔案,一種是用來做組態設定(config)的,一種則是用來決定要開發的程式的腳色類型(Role)的。


Role

在 Role 的部分,主要就是分成 Server 和 Client 兩種;Server 就是用來開發 WebSocket 伺服器的,而 Client 則是可以用來開發 C 的 WebSocket 的用戶端程式、連線到其他的 WebSocket Server 做資料的存取。

如果要建立 Server 端的程式的話,就是要 include server 用的 header 檔:

#include <websocketpp/server.hpp>

而之後則是就可以建立出 websocketpp::server<> 的物件,拿來做操作。

如果是要建立 Client 端程式的話,則是要 include client 的 header 檔:

#include <websocketpp/client.hpp>

之後則是建立出 websocketpp::client<> 的物件來做連線。

而 WebSocket 的 serverclient 這兩種類別,都是 template 的 class,在建立時也需要指定要使用的 config 才可以。


Config

Config 的部分,WebSocket 主要提供了三種類型:

  • config::core
  • config::asio
  • config::asio_tls

上面這三種類型,在 WebSocket 是不同的結構,,config::core 基本上是提供有限功能的設定,相對的他只會用到 C 11 的功能。而 config::asio 則是使用 Boost ASIO 做基礎來提供完整的功能;config::asio_tls 則是 config::asio 再加上 TLS 的連線加密功能。

而根據組合的不同,不同的 config 也需要 include websocketpp/config 的目錄下、不同的 header 檔:

Server
Client
core
core.hpp
core_client.hpp
asio
asio_no_tls.hpp
asio_no_tls_client.hpp
asio_tls
asio.hpp
asio_client.hpp

而如果是以要建立一個使用 Boost ASIO、沒有 TLS 加密的 Server 的話,基本上就是要 include asio_no_tls.hpp 這個檔案,也就是:

#include <websocketpp/config/asio_no_tls.hpp>

其他的組合,也可以依此類推。


Endpoint

在決定要 include 哪兩個檔案後,接下來就可以在程式裡面,建立出需要使用的 WebSocket 物件了。

如果是要建立使用 Boost ASIO、沒有 TLS 加密的 Server 的話,基本上要 include 的檔案就是:

#include <websocketpp/server.hpp>
#include <websocketpp/config/asio_no_tls.hpp>

而在控制用的物件的部分,則就是:

websocketpp::server<websocketpp::config::asio> mServer;

之後,所有的功能就都是針對 mServer 這個物件進行操作。而在 WebSocket 裡面,則是把它稱為「endpoint」;透過組合出不同的 endpoint,就可以實作不同的功能了。


Server 的範例

基本上,因為 Heresy 的目的是要建立一個 WebSocket Server 讓網頁來連線,所以這邊就只講 Server 的部分了~而實際上,在《Building a program with WebSocket 》裡,官方就有提供一個很簡單的使用範例了~他的程式碼如下:

#include <iostream>
 
#include <websocketpp/config/asio_no_tls.hpp>
#include <websocketpp/server.hpp>
 
typedef websocketpp::server<websocketpp::config::asio> server;
 
void on_message(websocketpp::connection_hdl hdl, server::message_ptr msg)
{
std::cout << msg->get_payload() << std::endl;
}
 
int main()
{
server print_server;
 
print_server.set_message_handler(&on_message);
 
print_server.init_asio();
print_server.listen(9002);
print_server.start_accept();
 
print_server.run();
}

在這個範例裡面,他是透過 websocketpp::server<websocketpp::config::asio> 這個 Endpoint,來建立一個使用 Boost ASIO、沒有 TLS 加密的 WebSocket Server。這個 server 程式在執行後,會持續去監聽 port 9002,當有訊息傳遞進來的時候,就會觸發到 on_message() 這個函式、並把接到的訊息輸出到命令提示字元的視窗上。

如果想要測試連線的話,可以考慮用 WebSocket.org 提供的 Echo Test 這個網頁來做測試;要連線到本機的 server 的話,只要在「location」輸入「ws://localhost:9002」後、按「connect」就可以連上。而連上後,在下方的「Message」裡面、輸入訊息後按下「Send」按鈕,應該就可以看到 Server 端的命令提示字元的視窗裡,會出現在網頁上打的文字了~

不過實際上,由於 WebSocket 本身也有 log 的功能,所以除了收到的訊息會被輸出之外,還有很多紀錄用的訊息,也都會被輸出在畫面上,看起來可能會有點雜亂就是了。

另外,由於這個範例程式,只會從 client 接收訊息,並不會傳送資料給 Server 端,所以 Echo Text 的 Log 裡面,並不會像連到 ws://echo.websocket.org 一樣,送出訊息後,還會有回應。


在程式碼的地方,首先就是建立出一個 endpoint 的 server 物件 print_server,用來做後續的操作。

而在建立出 print_server 後,接下來要做的事情,包括了:

  1. 初始化 ASIO

    呼叫 init_asio() 這個函式,初始化內部的 Boost ASIO 的 io_service官網),作為後續網路連線等功能之使用。

  2. 設定連接埠

    呼叫 listen() 這個函式,指定要監聽的連接埠;這邊是設定成 9002。
    而如果系統上有多個網卡的話,預設會監聽所有的網路介面;如果需要的話也可以特別指定要針對哪個介面做監聽。

  3. 開始接受連線

    呼叫 start_accept() 開始接受輸入。

  4. 進入主迴圈

    呼叫 run(),進入 WebSocket Server 的主迴圈。
    之後程式就會進入主迴圈,直到 Server 被停下來。

那要怎麼處理連線進來的訊息呢?WebSocket 是透過提供各種「Handler」(callback function),來做事件的處理;在官方網站上,有列出可以使用的 handler 列表(頁面)。

而在這個範例裡,則是透過 set_message_handler(),來設定當 Server 收到訊息時,要執行的 callback function,這裡就是 on_message() 這個函式;這也是一般來說,一定會用到的 callback function。

而 message handler 的 callback function,會收到兩個資料:

一個是 websocketpp::connection_hdl 型別的資料,是用來識別目前的連線用的;如果之後要傳送訊息給 client 的話,就必須要透過這個物件,來設定要把訊息傳送給誰。而如果有需要的話,也可以藉由 server<>get_con_fromhdl() 來取得觸發這個事件的連線、以及他的資訊。

第二個資訊,則是 websocketpp::server<>::message_ptr,裡面儲存的是傳遞進來的訊息。一般來說,這會透過他的 get_payload() 函式,來取得傳遞進來的訊息,而得到的資料,會是 const string&。不過由於 WebSocket 也有可能是傳遞非文字的 binary 資料,所以可能會需要透過  get_opcode() 這個函式,來辨別傳遞進來的資料的形式。

而在這個範例裡面,on_message() 這個函式,就是很單純地把街道的資訊,透過 iostream 做輸出了~


在網頁上的這個範例裡面,這個 Server 只有做接收的功能,並不會送訊息給 Client 端。那如果要送訊息給 client 端要怎麼做呢?基本上就是呼叫 server<>send() 這個函式就可以了。

在官方的 example 資料夾裡,有個 echo_server 的目錄,裡面的 echo_server.cpp,就是一個更完整一點,在接到訊息後,會把訊息原封不動地回傳給 client 端的範例程式。

而他送出資料的方法,就是:

s->send(hdl, msg->get_payload(), msg->get_opcode());

這邊可以看到,要呼叫 sned() 這個函式來傳遞資料,基本上是需要給他三個參數:

  • websocketpp::connection_hdl 的物件,讓 Server 知道是要傳給哪個 client。

  • 要傳遞的資料,這邊就是直接把收到的訊息(msg->get_payload())再傳出去;實際上 send() 有提供不同的介面,實際的資料型別可以是 const void*const string&

  • 最後,則是要有一個 opcode,來指定要傳遞的資訊的形式;如果是純文字的話,基本上可以直接指定 websocketpp::frame::opcode::TEXT

而這個範例程式在執行後,如果一樣使用 WebSocket.org 的 Echo Test 來測試的話,就可以發現他的功能和 WebSocket.org 測試用的 ws://echo.websocket.org 一樣了~


Windows / Visual Studio 上的問題

上面基本上就是 WebSocket 使用上的基本用法。不過實際上,這樣的程式碼,在 Heresy 這邊的 Windows Visual Studio 2010 / 2012,都是沒辦法正確建置的。

最主要的問題,基本上應該算是 VC 本身對 Boost C Library 的支援性問題吧…在 Heresy 測試的結果是發現,如果希望在 VisualStudio 2010 或 2012 上使用的 WebSocket 的話,有部分的功能必須要強制讓 WebSocket 去使用 C 11 的內建函式庫,而不要去使用 Boost 的版本。

設定的方法,可以參考官方的《C 11 Support》這頁。以 Heresy 這邊的測試來說,至少 functional 和 memory 兩個函式庫,是需要使用 C 11 STL 的版本才行的;也就是說,必須要加上 _WEBSOCKETPP_CPP11_MEMORY__WEBSOCKETPP_CPP11_FUNCTIONAL_ 這兩個定義(因為 MSVC 不支援完整的 C 11,所以不能直接用 _WEBSOCKETPP_CPP11_STL_)。

但是,在加上這兩個定義後,實際上會產生另一個問題,那就是 std::min() 和巨集版本的 min() 衝到的問題(參考);這個問題,比較簡單的方法,就是在再額外定義一個 NOMINMAX,來取消掉巨集版本的 min()max() 了。

所以,實際上要讓上面的程式可以正常運作,一個方法就是在原始碼的一開始、include WebSocket 的 Header 之前,先加上下面三行:

#define NOMINMAX
#define _WEBSOCKETPP_CPP11_FUNCTIONAL_
#define _WEBSOCKETPP_CPP11_MEMORY_ 

或是在 VC 的專案屬性的「組態屬性」裡面,找到「C/C 」的「前置處理器」,在「前置處理氣定義」的欄位裡面,加上「NOMINMAX;_WEBSOCKETPP_CPP11_FUNCTIONAL_;_WEBSOCKETPP_CPP11_MEMORY_ 」了。

理論上,這兩種方法應該都可以讓 MSVC 可以正確地建置上面的範例程式。而這個問題 Heresy 也有回報給作者了(連結),就看之後有沒有辦法修正吧。


另外,Heresy 在使用 Visual Studio 2012 的時候,雖然可以正確編譯了,可是在執行階段,則是會當掉。稍微追了一下程式碼後,發現應該是 Visual Studio 2012 的 std::strftime() 這個函式(MSDN)有問題所造成的。

主要的問題是發生在  logger/basic.hpp 這個檔案,裡面定義的 get_timestamp() 這個函式裡面有透過 std::strftime() 來列印出目前的時間,以做為紀錄之用;而他定義的輸出字串,則是一個長度 30 的 C 字串 buffer

由於他有試著輸出時區的資訊(%z),而在 Visual Sutdio 裡,如果在台灣的環境的話,他會是一個「台北標準時間」這樣的文字;而這樣的文字,再加上前面的時間資訊的會,就導致整個結果會超過 30 個字元。而在這個狀況下,Visual Studio 2010 只是會無法輸出,但是在 Visual Studio 2012,卻可能是讓程式整個當掉… orz

而解決方法呢?基本上應該是兩種,一個是把 buffer 的大小改大、例如把它改成 40(要改兩個地方,一個是 105 行、一個是 111 行,參考),這樣可以讓字串夠長、不會出問題;另一種方法,則是把 105 行裡定義的時間格式字串「"%Y-%m-%d %H:%M:%S%z"」,最後面的「%z」拿掉,這樣就不會去處理時區的資訊,也就比較不容易出問題了。

而這個問題,也已經回報給 WebSocket 的作者了(連結)(MS 那邊姑且也回報了,會不會受理就不曉得了)。


基本上,這篇大概就這樣了。內容,算是對 WebSocket 的極簡單介紹的~實際上,由於官方文件實在不足,所以學習起來有點累;不過,至少已經成功地用 WebSocket 完成第一個 WebSocket 的 Server 端程式了~接下來,看看有什麼特殊的想法,會再做補充吧。

2 thoughts on “C++ WebSocket 函式庫:WebSocket++”

  1. Would you please tell me how to create user defined message?

    What I want is here.

    1) create a message_ptr;
    2) fill it with binary or text data.

    I am looking forward your reply. Thanks very much.

    49834715@qq.com

  2. to dariusli

    Sorry, I never do this.
    I don’t think that is required.

Leave a Reply

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *