websocket安裝與使用
- 1. 介紹
- 2. 安裝
- 3. websocketpp常用接口
- 4. Websocketpp使用
- 4.1 服務端
- 4.2 客戶端
1. 介紹
WebSocket 是從 HTML5 開始支持的一種網頁端和服務端保持長連接的 消息推送機制。
- 傳統的 web 程序都是屬于 “一問一答” 的形式,即客戶端給服務器發送了一個
HTTP 請求,服務器給客戶端返回一個 HTTP 響應。這種情況下服務器是屬于被動的一方,如果客戶端不主動發起請求服務器就無法主動給客戶端響應 - 像網頁即時聊天這樣的程序非常依賴 “消息推送” 的,即需要服務器主動推動消息到客戶端。如果只是使用原生的 HTTP 協議,要想實現消息推送一般需要通過 “輪詢” 的方式實現, 而輪詢的成本比較高并且也不能及時的獲取到消息的響應。
基于上述兩個問題, 就產生了 WebSocket 協議。WebSocket 更接近于 TCP 這種級別的通信方式,一旦連接建立完成客戶端或者服務器都可以主動的向對方發送數據。
原理解析
WebSocket 協議本質上是一個基于 TCP 的協議。為了建立一個 WebSocket 連接,客戶端瀏覽器首先要向服務器發起一個 HTTP 請求,這個請求和通常的 HTTP 請求不同,包含了一些附加頭信息,通過這個附加頭信息完成握手過程并升級協議的過程。
具體協議升級的過程如下:
報文格式
報文字段比較多,我們重點關注這幾個字段:
-
FIN: WebSocket 傳輸數據以消息為概念單位,一個消息有可能由一個或多個幀組成,FIN 字段為 1 表示末尾幀。
-
RSV1~3:保留字段,只在擴展時使用,若未啟用擴展則應置 1,若收到不全為 0的數據幀,且未協商擴展則立即終止連接。
-
opcode: 標志當前數據幀的類型
-
- 0x0: 表示這是個延續幀,當 opcode 為 0 表示本次數據傳輸采用了數據分片,當前收到的幀為其中一個分片
-
- 0x1: 表示這是文本幀
-
- 0x2: 表示這是二進制幀
-
- 0x3-0x7: 保留,暫未使用
-
- 0x8: 表示連接斷開
-
- 0x9: 表示 ping 幀
-
- 0xa: 表示 pong 幀
-
- 0xb-0xf: 保留,暫未使用
-
mask:表示 Payload 數據是否被編碼,若為 1 則必有 Mask-Key,用于解碼
Payload 數據。僅客戶端發送給服務端的消息需要設置。 -
Payload length:數據載荷的長度,單位是字節, 有可能為 7 位、7+16 位、7+64位。假設 Payload length = x
-
- x 為 0~126:數據的長度為 x 字節
-
- x 為 126:后續 2 個字節代表一個 16 位的無符號整數,該無符號整數的值為數據的長度
-
- x 為 127:后續 8 個字節代表一個 64 位的無符號整數(最高位為 0),該無符號整數的值為數據的長度
-
Mask-Key:當 mask 為 1 時存在,長度為 4 字節,解碼規則: DECODED[i] =
ENCODED[i] ^ MASK[i % 4] -
Payload data: 報文攜帶的載荷數據
Websocketpp 介紹
WebSocketpp 是一個跨平臺的開源(BSD 許可證)頭部專用 C++庫,它實現了
RFC6455(WebSocket 協議)和 RFC7692(WebSocketCompression Extensions)。它允許將 WebSocket 客戶端和服務器功能集成到 C++程序中。在最常見的配置中,全功能網絡 I/O 由 Asio 網絡庫提供。
WebSocketpp 的主要特性包括:
- 事件驅動的接口
- 支持 HTTP/HTTPS、WS/WSS、IPv6
- 靈活的依賴管理 — Boost 庫/C++11 標準庫
- 可移植性:Posix/Windows、32/64bit、Intel/ARM
- 線程安全
WebSocketpp 同時支持 HTTP 和 Websocket 兩種網絡協議,可以該庫作為項目的依賴庫用來搭建 HTTP 和 WebSocket 服務器。
總結
websocket是一個應用層的tcp長連接協議。搭建一個websocket服務器其實就是搭建一個tcp服務器,只不過應用層使用websocket協議格式進行數據處理。
與此對比的就是httplib,它是一個短連接,只是讓我快速搭建一個Http服務器,讓我們重點關注業務處理。假如在一個項目中,不單單是 請求 - 響應 的業務處理,還包含了數據的主動推送,而這種消息數據的主動推送,是Http協議無法實現的。它只能是客戶端發起請求,然后服務器收到給一個響應。因此需要搭建一個長連接的服務器,用于服務端主動向客戶端推送數據。
選擇websocket協議的考慮:因為Http通信支持websocket協議的切換。
websocket通信框架的選擇:websocketpp 即支持websocket通信,也支持http通信。
2. 安裝
sudo apt-get install libboost-dev libboost-system-dev libwebsocketpp-dev
3. websocketpp常用接口
namespace websocketpp
{typedef lib::weak_ptr<void> connection_hdl;template <typename config>class endpoint : public config::socket_type{typedef lib::shared_ptr<lib::asio::steady_timer> timer_ptr;//通信連接類型typedef typename connection_type::ptr connection_ptr;typedef typename connection_type::message_ptr message_ptr;typedef lib::function<void(connection_hdl)> open_handler;typedef lib::function<void(connection_hdl)> close_handler;typedef lib::function<void(connection_hdl)> http_handler;typedef lib::function<void(connection_hdl, message_ptr)> message_handler;/* websocketpp::log::alevel::none 禁止打印所有日志*/void set_access_channels(log::level channels); /*設置日志打印等級*/void clear_access_channels(log::level channels); /*清除指定等級的日志*//*設置指定事件的回調函數*/void set_open_handler(open_handler h); /*websocket 握手成功回調處理函數*/void set_close_handler(close_handler h); /*websocket 連接關閉回調處理函數*/void set_message_handler(message_handler h); /*websocket 消息回調處理函數*/void set_http_handler(http_handler h); /*http 請求回調處理函數*//*發送數據接口*/void send(connection_hdl hdl, std::string &payload,frame::opcode::value op);void send(connection_hdl hdl, void *payload, size_t len,frame::opcode::value op);/*關閉連接接口*/void close(connection_hdl hdl, close::status::value code,std::string &reason);/*獲取 connection_hdl 對應連接的 connection_ptr*///weak_ptr無法對對象直接操作,必須要獲得對應的shared_ptr才能對對象進行對應操作connection_ptr get_con_from_hdl(connection_hdl hdl);/*websocketpp 基于 asio 框架實現,init_asio 用于初始化 asio 框架中的 io_service 調度器*/void init_asio();/*設置是否啟用地址重用*/void set_reuse_addr(bool value);/*設置 endpoint 的綁定監聽端口*/void listen(uint16_t port);/*對 io_service 對象的 run 接口封裝,用于啟動服務器*/std::size_t run();/*websocketpp 提供的定時器,以毫秒為單位*/timer_ptr set_timer(long duration, timer_handlercallback);};//繼承endpointtemplate <typename config>class server : public endpoint<connection<config>, config>{/*初始化并啟動服務端監聽連接的 accept 事件處理*/void start_accept();};template <typename config>class connection: public config::transport_type::transport_con_type,public config::connection_base{/*發送數據接口*/error_code send(std::string &payload, frame::opcode::value op = frame::opcode::text);/*獲取 http 請求頭部*/std::string const &get_request_header(std::string const &key)/*獲取請求正文*/std::string const &get_request_body();/*設置響應狀態碼*/void set_status(http::status_code::value code);/*設置 http 響應正文*/void set_body(std::string const &value);/*添加 http 響應頭部字段*/void append_header(std::string const &key, std::string const &val);/*獲取 http 請求對象*/request_type const &get_request();/*獲取 connection_ptr 對應的 connection_hdl */connection_hdl get_handle();};namespace http{namespace parser{class parser{std::string const &get_header(std::string const &key)std::string const &get_body() typedef std::map<std::string, std::string,utility::ci_less> header_list;header_list const &get_headers()}; class request : public parser{/*獲取請求方法*/std::string const &get_method()/*獲取請求 uri 接口*/std::string const &get_uri()};}};class message_buffer{/*獲取 websocket 請求中的 payload 數據類型*/frame::opcode::value get_opcode();/*獲取 websocket 中 payload 數據*/std::string const &get_payload();};namespace log{struct alevel{static level const none = 0x0;static level const connect = 0x1;static level const disconnect = 0x2;static level const control = 0x4;static level const frame_header = 0x8;static level const frame_payload = 0x10;static level const message_header = 0x20;static level const message_payload = 0x40;static level const endpoint = 0x80;static level const debug_handshake = 0x100;static level const debug_close = 0x200;static level const devel = 0x400;static level const app = 0x800;static level const http = 0x1000;static level const fail = 0x2000;static level const access_core = 0x00003003;static level const all = 0xffffffff;};}namespace http{namespace status_code{enum value{uninitialized = 0,continue_code = 100,switching_protocols = 101,ok = 200,created = 201,accepted = 202,non_authoritative_information = 203,no_content = 204,reset_content = 205,partial_content = 206,multiple_choices = 300,moved_permanently = 301,found = 302,see_other = 303,not_modified = 304,use_proxy = 305,temporary_redirect = 307,bad_request = 400,unauthorized = 401,payment_required = 402,forbidden = 403,not_found = 404,method_not_allowed = 405,not_acceptable = 406,proxy_authentication_required = 407,request_timeout = 408,conflict = 409,gone = 410,length_required = 411,precondition_failed = 412,request_entity_too_large = 413,request_uri_too_long = 414,unsupported_media_type = 415,request_range_not_satisfiable = 416,expectation_failed = 417,im_a_teapot = 418,upgrade_required = 426,precondition_required = 428,too_many_requests = 429,request_header_fields_too_large = 431,internal_server_error = 500,not_implemented = 501,bad_gateway = 502,service_unavailable = 503,gateway_timeout = 504,http_version_not_supported = 505,not_extended = 510,network_authentication_required = 511};}}namespace frame{namespace opcode{enum value{continuation = 0x0,text = 0x1,binary = 0x2,rsv3 = 0x3,rsv4 = 0x4,rsv5 = 0x5,rsv6 = 0x6,rsv7 = 0x7,close = 0x8,ping = 0x9,pong = 0xA,control_rsvb = 0xB,control_rsvc = 0xC,control_rsvd = 0xD,control_rsve = 0xE,control_rsvf = 0xF,};}}
}
4. Websocketpp使用
4.1 服務端
websocketpp搭建服務器流程:
- 定義server類型
- 實例化服務器對象
- 初始化日志輸出 – 關閉日志輸出
- 初始化asio框架
- 設置消息處理/連接握手成功/連接關閉回調函數/連接異常回調函數
- 啟動地址重用
- 設置監聽窗口
- 開始監聽
- 啟動服務器
#include<iostream>
#include<websocketpp/config/asio_no_tls.hpp>
#include<websocketpp/server.hpp>
#include<sstream>// 1. 定義server類型
typedef websocketpp::server<websocketpp::config::asio> websocketsvr;//websocket握手成功回調函數
void onopen(websocketpp::connection_hdl hdl)
{std::cout<<"websocket長連接建立成功"<<std::endl;
}//websocket 連接關閉回調處理函數
void onclose(websocketpp::connection_hdl hdl)
{std::cout<<"websocket關閉連接"<<std::endl;
}// websocket 連接異常的回調函數
void onfail(websocketsvr *server,websocketpp::connection_hdl hdl)
{std::cout<<"websocket連接異常"<<std::endl;
}//websocket 消息回調處理函數
void onmessage(websocketsvr* server,websocketpp::connection_hdl hdl,websocketsvr::message_ptr msg)
{//1. 獲取有效消息載荷數據,進行業務處理std::string body = msg->get_payload();std::cout<<"收到客戶端消息: "<<body<<std::endl;//2. 對客戶端進行響應//獲取通信連接auto conn = server->get_con_from_hdl(hdl);//發送數據conn->send(body + "服務器回復",websocketpp::frame::opcode::text);
}//http 請求回調處理函數
void onhttp(websocketsvr* server,websocketpp::connection_hdl hdl)
{auto conn = server->get_con_from_hdl(hdl);std::stringstream ss;ss << "<!doctype html><html><head>"<< "<title>hello websocket</title><body>"<< "<h1>hello websocketpp</h1>"<< "</body></head></html>";conn->set_body(ss.str());conn->set_status(websocketpp::http::status_code::ok);
}int main()
{// 2. 實例化服務器對象websocketsvr server;// 3. 初始化日志輸出 -- 關閉日志輸出// all 表示打印全部級別日志// none 表示什么日志都不打印server.set_access_channels(websocketpp::log::alevel::none);// 4. 初始化asio框架server.init_asio();// 5. 設置消息處理/連接握手成功/連接關閉回調函數/連接異常回調函數server.set_open_handler(onopen);server.set_close_handler(onclose);server.set_fail_handler(std::bind(onfail,&server,std::placeholders::_1));server.set_message_handler(std::bind(onmessage,&server,std::placeholders::_1,std::placeholders::_2));server.set_http_handler(std::bind(onhttp,&server,std::placeholders::_1));// 6. 啟動地址重用server.set_reuse_addr(true);// 7. 設置監聽窗口server.listen(8080);// 8. 開始監聽server.start_accept();// 9. 啟動服務器server.run();return 0;
}
server:server.ccg++ -o $@ $^ -std=c++17 -lpthread -lboost_system
4.2 客戶端
Http 客戶端
使用瀏覽器作為 http 客戶端即可, 訪問服務器的 8080 端口。
WS 客戶端
HTML<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,
initial-scale=1.0"><title>Test Websocket</title></head><body><input type="text" id="message"><button id="submit">提交</button><script>// 創建 websocket 實例// ws://124.223.54.148:8080// 類比 http// ws 表示 websocket 協議// 192.168.51.100 表示服務器地址// 8888 表示服務器綁定的端口let websocket = new WebSocket("ws://124.223.54.148:8080");// 處理連接打開的回調函數websocket.onopen = function() {console.log("連接建立");} // 處理收到消息的回調函數// 控制臺打印消息websocket.onmessage = function(e) {console.log("收到消息: " + e.data);} // 處理連接異常的回調函數websocket.onerror = function() {console.log("連接異常");} // 處理連接關閉的回調函數websocket.onclose = function() {console.log("連接關閉");} // 實現點擊按鈕后, 通過 websocket 實例 向服務器發送請求let input = document.querySelector('#message');let button = document.querySelector('#submit');button.onclick = function() {console.log("發送消息: " + input.value);websocket.send(input.value);} </script>
</body>
</html>