開源SignalR-Client-CPP使用總結
一、使用背景
在使用C++對接專案平臺過程中需要使用SignalRClient接收平臺的事件資訊。C++版本的SignalRClient使用不是很多,國內網站也沒什麼資料可供參考。經過調研,專案中決定使用SignalR-Client-CPP開原始碼(https://github.com/SignalR/SignalR-Client-Cpp)。
二、SignalR簡介
ASP .NET SignalR 是一個ASP .NET 下的類庫,可以在ASP .NET 的Web專案中實現實時通訊。什麼是實時通訊的Web呢?就是讓客戶端(Web頁面)和伺服器端可以互相通知訊息及呼叫方法,當然這是實時操作的。通過SignalR技術伺服器將內容自動推送到已經連線的客戶端,而不是伺服器等待客戶端發起一個新的資料請求。
Websocket是HTML5提供的新的API,可以在Web網頁與伺服器端間建立Socket連線,當WebSockets可用時(即瀏覽器支援Html5)SignalR使用WebSockets,當不支援時SignalR將使用其它技術來保證達到相同效果。
SignalR可以使用最新的WebSocket 傳輸,同時也能夠讓你回退到原有的傳輸方式。你可以直接使用SignalR 使用 WebSocket,因為SignalR 已經替你封裝好許多你需要實現的方法。最重要的是你使用SignalR不用擔心為老的客戶端實現WebSocket而採用兩套不同的邏輯編碼方式。使用SignalR 實現WebSocket你不用擔心 WebSocket的更新而去修改程式碼,SignalR會在傳輸方式上使用WebSocket最新的傳輸方式,同時提供了一連串的介面能夠讓你來支援不同版本的客戶端。
SignalR-Client-CPP開原始碼,僅支援WebSocket環境以及要求SignalR服務端版本在2.0以上才可正常使用。
三、SignalRCPPClient使用
SignalR客戶端和服務端通訊可以有兩種方法HubConnection與PersistentConnection。其中,PersistentConnection方式更加偏向底層,程式設計模式和websocket類似,使用固定的傳送和接收方法,通過此方法編碼過程可控性大,但是編碼繁瑣,而且基本都是在重複造輪子。而HubConnection方法相對來說是一個封裝好了的方法,另一優勢就是hub連線可以在客戶端呼叫服務端的方法,或者服務端可以呼叫客戶端實現的方法。以下是SignalR-Client-CPP開源庫中兩種連線SignalR伺服器的簡單例項(http)。
PersistentConnection :
void send_message(signalr::connection &connection, const utility::string_t& message) { connection.send(message) // fire and forget but we need to observe exceptions .then([](pplx::task<void> send_task) { try { send_task.get(); } catch (const std::exception &e) { ucout << U("Error while sending data: ") << e.what(); } }); } int main() { signalr::connection connection{ U("[http://localhost:34281/echo](http://localhost:34281/echo)") }; connection.set_message_received([](const utility::string_t& m) { ucout << U("Message received:") << m << std::endl << U("Enter message: "); }); connection.start() // fine to capture by reference - we are blocking // so it is guaranteed to be valid .then([&connection]() { for (;;) { utility::string_t message; std::getline(ucin, message); if (message == U(":q")) { break; } send_message(connection, message); } return connection.stop(); }) .then([](pplx::task<void> stop_task) { try { stop_task.get(); ucout << U("connection stopped successfully") << std::endl; } catch (const std::exception &e) { ucout << U("exception when starting or closing connection: ") << e.what() << std::endl; } }).get(); return 0; }
持久連線的API(表現在PersistentConnection 類上)給了開發人員低價訪問SignalR所暴露的通訊協議的條件。我們使用set_message_received函式來設定一個方法,每當我們從伺服器接收到一條訊息時,它就會被呼叫對相關資料進行處理。然後通過使用connect.start()函式啟動連線。如果連線成功啟動,內部就會執行一個迴圈,從控制檯讀取訊息。
HubConnection:
void send_message(signalr::hub_proxy proxy, const utility::string_t& name, const utility::string_t& message) { web::json::value args{}; args[0] = web::json::value::string(name); args[1] = web::json::value(message); proxy.invoke<void>(U("send"), args) // fire and forget but we need to observe exceptions .then([](pplx::task<void> invoke_task) { try { invoke_task.get(); } catch (const std::exception &e) { ucout << U("Error while sending data: ") << e.what(); } }); } void chat(const utility::string_t& name) { signalr::hub_connection connection{U("[http://localhost:34281](http://localhost:34281/)")}; auto proxy = connection.create_hub_proxy(U("ChatHub")); proxy.on(U("broadcastMessage"), [](const web::json::value& m) { ucout << std::endl << [m.at](http://m.at/)(0).as_string() << U(" wrote:") << [m.at](http://m.at/)(1).as_string() << std::endl << U("Enter your message: "); }); connection.start() .then([proxy, name]() { ucout << U("Enter your message:"); for (;;) { utility::string_t message; std::getline(ucin, message); if (message == U(":q")) { break; } send_message(proxy, name, message); } }) // fine to capture by reference - we are blocking // so it is guaranteed to be valid .then([&connection]() { return connection.stop(); }) .then([](pplx::task<void> stop_task) { try { stop_task.get(); ucout << U("connection stopped successfully") << std::endl; } catch (const std::exception &e) { ucout << U("exception when starting or stopping connection: ") << e.what() << std::endl; } }).get(); }
其中hub_proxy的on方法可以實現服務端呼叫客戶端定義的函式方法,通過客戶端實現服務端定義的方法達到對資料處理的主動權。hub_proxy的invoke函式實現客戶端呼叫服務端的方法,函式定義在SignalR的服務端,客戶端通過invoke函式指定方法名稱及引數實現客戶端對服務端特定方法的呼叫。當服務端的程式碼訪問一個客戶端的方法時,一個數據包被自動傳輸,資料包中包含了函式方法引數的名稱(如果是一個物件,那麼這個物件會被序列化成JSON)。客戶端然後根據客戶端的程式碼匹配方法的名稱。如果找到相應的匹配方法,那麼久呼叫相應的函式執行反序列化的引數。
四、SignalRCPPClient使用過程中的問題及解決方法
再使用過程中,連線方式採用HubConnection,服務端採用了自簽名單向認證的https方式進行通訊,在開源的SignalRCpplient中使用https訪問伺服器時無法正常通訊。主要問題如下:
1、使用SignalR 對接伺服器時一直報出“WinHttpSendRequest: 12175”問題。
2、cpprest 內部爆出“set_fail_handler: 8: TLS handshake failed”錯誤。
第一個問題:通過排查發現出現12175報錯問題主要是因為安全連線過程中出錯,網上搜出的方法基本都是和winhttp的使用相關,通過嘗試Stack Overflow網站以及GitHub開源庫issues各種可能的原因都無法解決這一問題。最後通過抓包發現在建立通訊的過程中客戶端使用了TLS1.0嘗試建立安全連線,而對接的平臺不支援TLS1.0。TLS1.0於1999年發行,至今將近有20年。業內都知道該版本易受各種攻擊(如BEAST和POODLE)已有多年,除此之外,支援較弱加密,對當今網路連線的安全已失去應有的保護效力。
分析SignalRCPPClient原始碼後發現其主要是通過使用cpprestSDK(微軟的另一個開源庫)來完成http的互動。此處使用的cpprestSDK版本為2.9.0,此版本中未對TLS各個版本做好相容,預設使用的為TLS1.0。通過升級依賴庫後抓包發現實現了TLS1.2的握手過程,但cpprest 內部爆出“set_fail_handler: 8: TLS handshake failed”錯誤。
第二個問題:自簽名證書的使用需要繞過證書的認證,在SignalRClient庫中通過設定websocket_client_config以及http_client_config的set_validate_certificates值為false繞過證書的認證以後便可以正常通訊。
五、總結
本文件只涉及C++版SignalRClient的使用方法,未涉及SignalR服務端的開發與搭建。在使用過程中需要對各種異常情況進行捕獲處理。