在上一篇《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 的基本使用說明,可以參考《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 的 server 或 client 這兩種類別,都是 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 後,接下來要做的事情,包括了: -
初始化 ASIO 呼叫 init_asio() 這個函式,初始化內部的 Boost ASIO 的 io_service(官網),作為後續網路連線等功能之使用。 -
設定連接埠 呼叫 listen() 這個函式,指定要監聽的連接埠;這邊是設定成 9002。 而如果系統上有多個網卡的話,預設會監聽所有的網路介面;如果需要的話也可以特別指定要針對哪個介面做監聽。 -
開始接受連線 呼叫 start_accept() 開始接受輸入。 -
進入主迴圈 呼叫 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 端程式了~接下來,看看有什麼特殊的想法,會再做補充吧。
|