一、使用背景
??在使用C++對接項目平臺過程中需要使用SignalRClient接收平臺的事件信息。C++版本的SignalRClient使用不是很多,國內(nèi)網(wǎng)站也沒什么資料可供參考。經(jīng)過調(diào)研,項目中決定使用SignalR-Client-CPP開源代碼(https://github.com/SignalR/SignalR-Client-Cpp)。
二、SignalR簡介
??ASP .NET SignalR 是一個ASP .NET 下的類庫,可以在ASP .NET 的Web項目中實現(xiàn)實時通信。什么是實時通信的Web呢?就是讓客戶端(Web頁面)和服務器端可以互相通知消息及調(diào)用方法,當然這是實時操作的。通過SignalR技術服務器將內(nèi)容自動推送到已經(jīng)連接的客戶端,而不是服務器等待客戶端發(fā)起一個新的數(shù)據(jù)請求。
??Websocket是HTML5提供的新的API,可以在Web網(wǎng)頁與服務器端間建立Socket連接,當WebSockets可用時(即瀏覽器支持Html5)SignalR使用WebSockets,當不支持時SignalR將使用其它技術來保證達到相同效果。
??SignalR可以使用最新的WebSocket 傳輸,同時也能夠讓你回退到原有的傳輸方式。你可以直接使用SignalR 使用 WebSocket,因為SignalR 已經(jīng)替你封裝好許多你需要實現(xiàn)的方法。最重要的是你使用SignalR不用擔心為老的客戶端實現(xiàn)WebSocket而采用兩套不同的邏輯編碼方式。使用SignalR 實現(xiàn)WebSocket你不用擔心 WebSocket的更新而去修改代碼,SignalR會在傳輸方式上使用WebSocket最新的傳輸方式,同時提供了一連串的接口能夠讓你來支持不同版本的客戶端。
??SignalR-Client-CPP開源代碼,僅支持WebSocket環(huán)境以及要求SignalR服務端版本在2.0以上才可正常使用。
三、SignalRCPPClient使用
??SignalR客戶端和服務端通信可以有兩種方法HubConnection與PersistentConnection。其中,PersistentConnection方式更加偏向底層,編程模式和websocket類似,使用固定的發(fā)送和接收方法,通過此方法編碼過程可控性大,但是編碼繁瑣,而且基本都是在重復造輪子。而HubConnection方法相對來說是一個封裝好了的方法,另一優(yōu)勢就是hub連接可以在客戶端調(diào)用服務端的方法,或者服務端可以調(diào)用客戶端實現(xiàn)的方法。以下是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(表現(xiàn)在PersistentConnection 類上)給了開發(fā)人員低價訪問SignalR所暴露的通信協(xié)議的條件。我們使用set_message_received函數(shù)來設置一個方法,每當我們從服務器接收到一條消息時,它就會被調(diào)用對相關數(shù)據(jù)進行處理。然后通過使用connect.start()函數(shù)啟動連接。如果連接成功啟動,內(nèi)部就會運行一個循環(huán),從控制臺讀取消息。
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方法可以實現(xiàn)服務端調(diào)用客戶端定義的函數(shù)方法,通過客戶端實現(xiàn)服務端定義的方法達到對數(shù)據(jù)處理的主動權。hub_proxy的invoke函數(shù)實現(xiàn)客戶端調(diào)用服務端的方法,函數(shù)定義在SignalR的服務端,客戶端通過invoke函數(shù)指定方法名稱及參數(shù)實現(xiàn)客戶端對服務端特定方法的調(diào)用。當服務端的代碼訪問一個客戶端的方法時,一個數(shù)據(jù)包被自動傳輸,數(shù)據(jù)包中包含了函數(shù)方法參數(shù)的名稱(如果是一個對象,那么這個對象會被序列化成JSON)??蛻舳巳缓蟾鶕?jù)客戶端的代碼匹配方法的名稱。如果找到相應的匹配方法,那么久調(diào)用相應的函數(shù)執(zhí)行反序列化的參數(shù)。
四、SignalRCPPClient使用過程中的問題及解決方法
??再使用過程中,連接方式采用HubConnection,服務端采用了自簽名單向認證的https方式進行通信,在開源的SignalRCpplient中使用https訪問服務器時無法正常通信。主要問題如下:
1、使用SignalR 對接服務器時一直報出“WinHttpSendRequest: 12175”問題。
2、cpprest 內(nèi)部爆出“set_fail_handler: 8: TLS handshake failed”錯誤。
??第一個問題:通過排查發(fā)現(xiàn)出現(xiàn)12175報錯問題主要是因為安全連接過程中出錯,網(wǎng)上搜出的方法基本都是和winhttp的使用相關,通過嘗試Stack Overflow網(wǎng)站以及GitHub開源庫issues各種可能的原因都無法解決這一問題。最后通過抓包發(fā)現(xiàn)在建立通信的過程中客戶端使用了TLS1.0嘗試建立安全連接,而對接的平臺不支持TLS1.0。TLS1.0于1999年發(fā)行,至今將近有20年。業(yè)內(nèi)都知道該版本易受各種攻擊(如BEAST和POODLE)已有多年,除此之外,支持較弱加密,對當今網(wǎng)絡連接的安全已失去應有的保護效力。
??分析SignalRCPPClient源碼后發(fā)現(xiàn)其主要是通過使用cpprestSDK(微軟的另一個開源庫)來完成http的交互。此處使用的cpprestSDK版本為2.9.0,此版本中未對TLS各個版本做好兼容,默認使用的為TLS1.0。通過升級依賴庫后抓包發(fā)現(xiàn)實現(xiàn)了TLS1.2的握手過程,但cpprest 內(nèi)部爆出“set_fail_handler: 8: TLS handshake failed”錯誤。
??第二個問題:自簽名證書的使用需要繞過證書的認證,在SignalRClient庫中通過設置websocket_client_config以及http_client_config的set_validate_certificates值為false繞過證書的認證以后便可以正常通信。
五、總結
??本文檔只涉及C++版SignalRClient的使用方法,未涉及SignalR服務端的開發(fā)與搭建。在使用過程中需要對各種異常情況進行捕獲處理。