介紹
WebSocket 是從 HTML5 開始支持的一種網頁端和服務端保持長連接的消息推送機制。
- 傳統的 web 程序都是屬于 "一問一答" 的形式,即客戶端給服務器發送了一個 HTTP 請求,服務器給客戶端返回一個 HTTP 響應。這種情況下服務器是屬于被動的一方,如果客戶端不主動發起請求服務器就無法主動給客戶端響應。
- 像網頁即時聊天這樣的程序都是非常依賴 "消息推送" 的,即需要服務器主動推動消息到客戶端。如果只是使用原生的 HTTP 協議,要想實現消息推送一般需要通過 "輪詢" 的方式實現, 而輪詢的成本比較高并且也不能及時的獲取到消息的響應。
基于上述兩個問題, 就產生了 WebSocket 協議,旨在實現客戶端與服務器之間的 全雙工、持久化通信 。WebSocket 更接近于 TCP 這種級別的通信方式,一旦連接建立完成客戶端或者服務器都可以主動的向對方發送數據。
與 HTTP 對比
特性 | WebSocket | HTTP |
---|---|---|
連接方式 | 持久化長連接 | 短連接(請求-響應) |
通信模式 | 全雙工 | 半雙工(客戶端發起) |
頭部開銷 | 小(幀格式) | 大(每次攜帶完整頭) |
適用場景 | 實時交互 | 靜態資源獲取 |
核心特性
-
全雙工通信:客戶端和服務器可以同時雙向發送數據,無需等待請求-響應周期。
-
低延遲:建立連接后,數據可以即時傳輸,無需重復建立連接(HTTP 每次請求需握手)。
-
輕量級協議:數據幀頭部開銷小(最小僅 2 字節),適合高頻通信。
-
基于 TCP:工作在 OSI 模型的傳輸層之上,依賴 TCP 的可靠性。
原理解析
WebSocket 協議本質上是一個基于 TCP 的協議。為了建立一個 WebSocket 連接,客戶端瀏覽器首先要向服務器發起一個 HTTP 請求,這個請求和通常的 HTTP 請求不同,包含了一些附加頭信息,通過這個附加頭信息完成握手過程并升級協議的過程。

- HTTP 升級請求(客戶端發起)
客戶端發送一個包含?Upgrade: websocket?頭的 HTTP 請求,其中?Sec-WebSocket-Key?是隨機生成的 Base64 字符串,用于安全校驗:
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
- 服務器響應切換協議
服務器返回?HTTP 101 Switching Protocols?表示協議升級成功,其中 Sec-WebSocket-Accept?是客戶端 Key 與固定 GUID 拼接后通過 SHA-1 哈希生成的,用于驗證握手合法性:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
- 連接建立
此后,通信轉為 WebSocket 協議,數據以幀(Frame)形式傳輸。
數據幀格式
WebSocket 數據幀包含以下關鍵字段(二進制格式):
報文字段比較多,我們重點關注這幾個字段 :
- 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 是一個跨平臺的開源頭部專用 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
- 線程安全
下面是該項目的一些常用網站。
- github:https://github.com/zaphoyd/websocketpp
- 用戶手冊: http://docs.websocketpp.org/
- 官網:http://www.zaphoyd.com/websocketpp
安裝
sudo apt-get install libboost-dev libboost-system-dev libwebsocketpp-dev
安裝完畢后,若在 /usr/include 下有了 websocketpp 目錄就表示安裝成功了。
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*/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_handler callback);};template <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()};}};namespace message_buffer{/*獲取 websocket 請求中的 payload 數據類型*/frame::opcode::value get_opcode();/*獲取 websocket 中 payload 數據*/std::string const &get_payload();};
}
Websocketpp 使用
main.cc
#include <websocketpp/config/asio_no_tls.hpp>
#include <websocketpp/server.hpp>// 1. 定義server類型
typedef websocketpp::server<websocketpp::config::asio> server_t;void onOpen(websocketpp::connection_hdl hdl)
{std::cout << "websocket長連接建立成功!\n";
}
void onClose(websocketpp::connection_hdl hdl)
{std::cout << "websocket長連接關閉!\n";
}
void onMessage(server_t *server, websocketpp::connection_hdl hdl, server_t::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 + "-Hello!", websocketpp::frame::opcode::value::text);
}
int main()
{// 2. 實例化服務器對象server_t server;// 3. 初始化日志輸出,關閉日志輸出server.set_access_channels(websocketpp::log::alevel::none);// 4. 初始化asio框架server.init_asio();// 5. 設置消息處理/連接握手成功/關閉連接回調函數server.set_open_handler(onOpen);server.set_close_handler(onClose);auto msg_handler=std::bind(onMessage,&server,std::placeholders::_1,std::placeholders::_2);server.set_message_handler(msg_handler);// 6. 啟用地址重用server.set_reuse_addr(true);// 7. 設置監聽端口server.listen(9090);// 8. 開始監聽server.start_accept();// 9. 啟動服務器server.run();return 0;
}
makefile
main:main.ccg++ -o $@ $^ -std=c++17 -lpthread -lboost_system
test.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://192.168.22.129:9090// 類比 http// ws 表示 websocket 協議let websocket = new WebSocket("ws://192.168.22.129:9090");// 處理連接打開的回調函數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>