指南:從源碼編譯到嵌入式WebSocket通信)
我最早接觸libwebsockets是在一個智能網關項目里需要讓設備端和云端保持實時通信。當時第一反應是用WebSocket但搜了一圈發(fā)現(xiàn)輕量級的方案里libwebsockets幾乎是最靠譜的選擇——純C實現(xiàn)、對嵌入式環(huán)境友好、支持SSL/TLS而且協(xié)議棧完整。后來我在不同平臺x86 Linux、ARM交叉編譯、Windows上都編譯過這個庫也踩了不少坑今天把整個過程整理出來從下載、編譯到測試一次性講透。1. libwebsockets是什么為什么值得用libwebsockets是一個用C語言寫的開源WebSocket協(xié)議庫由Andy Green維護在GitHub上長期保持活躍。它的核心定位是給資源受限的環(huán)境提供完整的WebSocket通信能力但又不只是在嵌入式里能用——你在普通的Linux服務器、macOS、Windows上都能編譯運行。它的幾個核心特性我實際用下來感受很深協(xié)議支持非常全RFC6455標準的WebSocket協(xié)議、H2HTTP/2多路復用、HTTP/3QUIC的實驗性支持還有MQTT over WebSocket。依賴極小只用到的核心依賴是OpenSSL如果啟用TLS和zlib如果啟用壓縮。不需要一堆沒完沒了的第三方庫。自帶測試用例和示例代碼源碼里有大量minimal example每個示例針對一種功能場景比如最小HTTP服務器、WebSocket客戶端、WS over TLS、異步DNS解析等對學習協(xié)議和庫的API非常有幫助。事件驅動模型基于自己的event loop也支持集成到外部event loop比如libuv、glib、ev/uv設計上不算臃腫。這個庫適合誰來用呢一是做嵌入式設備端通信的開發(fā)者二是需要在C/C服務端上直接集成WebSocket能力的人三是研究WebSocket協(xié)議本身、想通過實際代碼加深理解的入門者。相比直接用Boost.Beast或者OpenSSL裸手寫握手解析libwebsockets把協(xié)議細節(jié)封裝得足夠好同時在調用層面又保留了足夠的控制力。2. 下載libwebsockets源碼版本選擇與獲取方式2.1 從GitHub獲取最新代碼libwebsockets的官方倉庫地址是https://github.com/warmcat/libwebsockets。下載方式有兩種一種是直接clone git倉庫另一種是下載release tarball源碼包。git clone https://github.com/warmcat/libwebsockets.git如果你只需要某個特定版本不想把整個提交歷史都拉下來可以加--depth 1參數(shù)做淺克隆配合--branch指定分支或標簽git clone --depth 1 --branch v4.3-stable https://github.com/warmcat/libwebsockets.git2.2 穩(wěn)定版與開發(fā)版怎么選libwebsockets的發(fā)布節(jié)奏很有特點它有長期維護的stable分支類似v4.3-stable同時main分支上也持續(xù)加入新特性。根據(jù)我實際項目經驗做產品選型時用stable分支更穩(wěn)妥特別是要做長期維護的設備端固件時stable分支的API穩(wěn)定性明顯更好編譯依賴也更可控。而main分支上的代碼可能更早支持新的協(xié)議特性和優(yōu)化但偶爾會引入構建系統(tǒng)調整或者API改動如果只是跑測試玩一下無所謂但用于生產環(huán)境的項目我不建議直接跟蹤main。另外很多發(fā)行版Debian/Ubuntu的軟件源里也有l(wèi)ibwebsockets-dev包版本通常會滯后一些但勝在安裝省事sudo apt install libwebsockets-dev當然發(fā)行版自帶的版本對只想快速用到功能的人來說很合適但如果需要自定義編譯選項、裁剪功能或者交叉編譯到ARM平臺還是得從源碼自己編譯。2.3 下載后確認目錄結構源碼下載完成后先看一眼頂層目錄結構方便后面找東西CMakeLists.txt構建系統(tǒng)主文件整個編譯配置都靠它。cmake/CMake的輔助腳本和模塊包括找依賴庫的腳本。lib/libwebsockets核心庫的全部源碼這是我們最終編譯產物的來源。bin/一些測試工具和輔助程序的源碼比如測試證書生成腳本。minimal-examples/官方提供的大量極簡示例每個目錄對應一個獨立小項目特別適合學習。test-apps/早期版本里的測試程序目錄現(xiàn)在很多新功能示例都遷移到minimal-examples了。如果是老版本比如v3.x頂層目錄會略有差別但核心的lib/和CMakeLists.txt兩個單元永遠都在。拿到源碼先不急著編譯花兩分鐘看看minimal-examples里的示例對理解庫的能力邊界很有幫助。3. 編譯libwebsockets從CMake配置到生成產物3.1 前置依賴比想象中簡單libwebsockets的依賴真的不多但缺了會導致某些功能編譯不出來。我這里按功能分類列一下基礎編譯環(huán)境gcc/clang、make、cmake建議3.16以上版本舊版本有些選項不支持。TLS支持OpenSSL開發(fā)庫libssl-dev。如果不配置這個編譯出的庫默認不支持wss://WebSocket over TLS和https://。壓縮支持zlib開發(fā)庫zlib1g-dev。啟用后HTTP壓縮和permessage-deflate擴展才可用??蛇x能力libuv外部事件循環(huán)、libev、libevent、mbedtls輕量TLS、cjsonJSON解析、sqlite3存儲相關示例使用。在Ubuntu/Debian上安裝基礎依賴sudo apt update sudo apt install build-essential cmake libssl-dev zlib1g-dev如果后面交叉編譯主機上的依賴庫和最終目標板用的庫要區(qū)分清楚。交叉編譯時目標板跑的程序需要的是目標架構的庫而不是主機上的x86庫。3.2 標準編譯流程CMake三板斧libwebsockets從v3.x開始全面轉向CMake構建系統(tǒng)。原來的autotoolsconfigure/make在老版本里還有但新版本已經不推薦了。整個編譯過程其實就是三句話mkdir build cd build cmake .. make但這只是最基本的流程實際工程里基本不可能這么樸素地編譯——你幾乎總要配置一些選項比如禁用某些不需要的功能、開啟測試、指定安裝路徑等等。3.3 關鍵CMake選項解析選型必看我把自己常用的一些關鍵選項整理成了表格方便對照查閱。這里面的選項基本決定了你編譯出的庫是精簡版還是全功能版選項默認值作用說明我的建議LWS_WITH_SSLON啟用TLS/SSL支持依賴OpenSSL做產品建議打開現(xiàn)在wss幾乎是標配LWS_WITH_CLIENTON編譯客戶端模式支持需要主動連接WebSocket服務端時保留LWS_WITH_SERVERON編譯服務端模式支持服務端開發(fā)必須保留LWS_WITH_MINIMAL_EXAMPLESON同時編譯minimal示例程序開發(fā)調試階段打開方便驗證功能LWS_WITHOUT_TESTAPPSOFF是否跳過測試程序如果不想編譯test-apps里的工具設為ONLWS_WITH_SHAREDON編譯動態(tài)庫.so/.dll默認是動態(tài)庫設為OFF生成靜態(tài)庫LWS_WITH_STATICOFF編譯靜態(tài)庫.a/.lib需要靜態(tài)鏈接時設為ONCMAKE_INSTALL_PREFIX/usr/local指定安裝路徑交叉編譯時務必改成你的工具鏈sysrootLWS_IPV6ON啟用IPv6支持視實際網絡環(huán)境而定LWS_WITH_HTTP2OFF啟用HTTP/2支持有HTTP/2需求時打開功能相對獨立LWS_WITH_CJSONOFF集成cJSON以支持JSON相關示例示例代碼需要時自動開啟實際編譯時我一般這么配cmake .. \ -DCMAKE_BUILD_TYPERelease \ -DCMAKE_INSTALL_PREFIX/usr/local/libwebsockets \ -DLWS_WITH_MINIMAL_EXAMPLESON \ -DLWS_WITHOUT_TESTAPPSON \ -DLWS_WITH_SHAREDON \ -DLWS_WITH_STATICON這里-DLWS_WITH_STATICON和-DLWS_WITH_SHAREDON可以同時開啟這樣動態(tài)庫和靜態(tài)庫都會生成——調試時用靜態(tài)庫更方便部署到設備上可以用動態(tài)庫減小體積。3.4 遇到編譯錯誤時的排查思路有次編譯v4.3-stable時報錯提示找不到openssl/ssl.h但我明明確認過libssl-dev已經安裝了。后來發(fā)現(xiàn)是因為系統(tǒng)同時裝了多個OpenSSL版本CMake的find_package找到了錯誤的路經。解決辦法是指定OpenSSL根目錄cmake .. -DOPENSSL_ROOT_DIR/usr/local/openssl -DOPENSSL_INCLUDE_DIR/usr/local/openssl/include另一個常見問題是在比較老的glibc環(huán)境下編譯新版本libwebsockets會出現(xiàn)某些符號未定義的錯誤——這說明系統(tǒng)基礎庫太老要么升級系統(tǒng)要么換用更老的libwebsockets版本。最典型的經驗是libwebsockets的新版本通常依賴較新的OpenSSL3.x而很多老系統(tǒng)自帶的是OpenSSL 1.1.1。v4.3-stable對OpenSSL 1.1.1兼容性還不錯但v5.x以后最好直接上OpenSSL 3.x不然可能遇到API不兼容的編譯錯誤。3.5 交叉編譯到ARM目標板做嵌入式項目時交叉編譯是繞不開的。libwebsockets的CMake交叉編譯流程和大多數(shù)庫類似需要指定工具鏈文件關鍵是要讓CMake找到正確的編譯器、頭文件和庫路徑。我整理了一份通用的交叉編譯工具鏈文件放在cmake_toolchain_arm.cmakeset(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_PROCESSOR arm) set(CMAKE_C_COMPILER /opt/gcc-arm-none-eabi/bin/arm-none-eabi-gcc) set(CMAKE_CXX_COMPILER /opt/gcc-arm-none-eabi/bin/arm-none-eabi-g) set(CMAKE_SYSROOT /opt/gcc-arm-none-eabi/arm-none-eabi) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)編譯命令mkdir build-arm cd build-arm cmake .. -DCMAKE_TOOLCHAIN_FILE../cmake_toolchain_arm.cmake \ -DCMAKE_INSTALL_PREFIX$PWD/_install \ -DLWS_WITH_SSLOFF \ -DLWS_WITH_MINIMAL_EXAMPLESOFF make -j$(nproc) make install注意我把LWS_WITH_SSL關了——ARM板如果不需要wss關掉TLS能省下不少Flash空間和內存。實際上很多嵌入式場景確實用不到TLS因為TLS的握手和證書管理對MCU類設備來說開銷太大。4. 測試libwebsockets驗證功能的核心方法4.1 編譯自帶的測試工具libwebsockets源碼里帶了幾個非常實用的測試程序。在build/bin/目錄下編譯完成后會生成一些可執(zhí)行文件比如lws-minimal-http-server極簡HTTP服務器可以測試基礎的HTTP GET/POST請求。lws-minimal-ws-clientWebSocket客戶端支持連接外部ws://服務器。lws-minimal-ws-serverWebSocket服務器可以接受客戶端連接并收發(fā)消息。lws-minimal-ws-server-tls帶TLS加密的WebSocket服務器用于驗證wss功能。這些例子是認識libwebsockets功能邊界最好的入口每一個都是一個獨立的完整程序直接用CMake編譯好之后運行即可。4.2 啟動自帶的WebSocket服務器假設我們編譯時開啟了LWS_WITH_MINIMAL_EXAMPLESON在build目錄下找到示例程序cd build/bin ./lws-minimal-ws-server -p 7681這個命令會啟動一個WebSocket服務監(jiān)聽7681端口。啟動日志會顯示[2025/01/12 10:23:45:1234] NOTICE: lws-minimal-ws-server: listening on :7681如果要測試TLS版本需要先準備證書。libwebsockets源碼里帶了自簽名證書生成腳本在scripts/或者直接用系統(tǒng)的openssl命令生成openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 365 -subj /CNlocalhost ./lws-minimal-ws-server-tls -p 7682 --ssl --cert cert.pem --key key.pem4.3 用命令行工具測試連接驗證WebSocket服務最簡單的方式是用現(xiàn)成的WebSocket客戶端工具比如websocat或wscat。我用得比較多的是websocat安裝也簡單cargo install websocat # 或者 apt install websocat連接測試websocat ws://localhost:7681連接成功后客戶端輸入內容回車服務端會原樣回顯echo模式。這就是WebSocket的基本通信流程——握手升級、雙向消息傳遞。如果使用的是TLS版本連接命令要改一下websocat wss://localhost:7682 -k-k參數(shù)的作用是跳過證書校驗因為自簽名證書不被信任。4.4 寫一個簡單的C語言測試客戶端命令行工具能驗證基本連通性但如果你想測libwebsockets的C API是否調用正確、消息收發(fā)是否可靠建議寫一個簡單客戶端用庫的API來主動建立連接。我基于minimal-examples里ws-client示例簡化了一個測試客戶端核心邏輯如下#include libwebsockets.h #include string.h #include signal.h static int interrupted 0; static int callback_ws(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len) { switch (reason) { case LWS_CALLBACK_CLIENT_ESTABLISHED: lws_callback_on_writable(wsi); break; case LWS_CALLBACK_CLIENT_RECEIVE: printf(received: %.*s\n, (int)len, (char *)in); break; case LWS_CALLBACK_CLIENT_WRITEABLE: { unsigned char buf[LWS_PRE 64]; unsigned char *p buf[LWS_PRE]; size_t n sprintf((char *)p, hello from lws client); lws_write(wsi, p, n, LWS_WRITE_TEXT); break; } case LWS_CALLBACK_CLIENT_CONNECTION_ERROR: fprintf(stderr, connection error\n); interrupted 1; break; default: break; } return 0; } int main(void) { struct lws_context_creation_info info; struct lws_client_connect_info ccinfo; struct lws_context *context; struct lws *wsi; memset(info, 0, sizeof(info)); info.port CONTEXT_PORT_NO_LISTEN; info.protocols (struct lws_protocols[]) { { example-protocol, callback_ws, 0, 4096 }, { NULL, NULL, 0, 0 } }; info.options LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT; context lws_create_context(info); if (!context) { fprintf(stderr, context creation failed\n); return 1; } memset(ccinfo, 0, sizeof(ccinfo)); ccinfo.context context; ccinfo.address localhost; ccinfo.port 7681; ccinfo.path /; ccinfo.protocol example-protocol; ccinfo.ietf_version_or_minus_one -1; wsi lws_client_connect_via_info(ccinfo); if (!wsi) { fprintf(stderr, connection failed\n); lws_context_destroy(context); return 1; } while (!interrupted lws_service(context, 0) 0) { // 事件循環(huán)持續(xù)運行 } lws_context_destroy(context); return 0; }這段代碼做的事情是創(chuàng)建上下文、發(fā)起客戶端連接、在握手建立后發(fā)送一條文本消息、把服務端回顯的消息打印出來。編譯時鏈接庫gcc my_client.c -o my_client -I/usr/local/include -L/usr/local/lib -lwebsockets ./my_client如果一切正常服務端日志里能看到accepted client之類的連接日志客戶端則打印出received: hello from lws client——這是服務端echo回來的消息。4.5 壓力測試與連接穩(wěn)定性驗證測試WebSocket服務除了功能通斷還要看它能扛多少并發(fā)連接。libwebsockets自帶一個多線程壓力測試程序在某些版本位于test-apps下名字類似lws-mirror或lws-spawn。也可以通過外部工具做壓測websocat -n ws://localhost:7681 # 一次性發(fā)完消息斷開更實際的方案是自己寫一個壓測腳本用Python的websocket-client庫批量建立連接同時收發(fā)消息。我在實際項目里會關注幾個指標最大并發(fā)連接數(shù)受文件描述符上限影響ulimit -n。單連接長連穩(wěn)定性長時間不通信服務端和客戶端心跳保活。重連恢復能力服務端重啟后客戶端能否自動重連。libwebsockets的心跳PING/PONG機制默認開啟在server模式下每隔一段時間會給客戶端發(fā)PING客戶端回PONG這能保證連接不會因中間設備超時而斷開。如果在測試過程中出現(xiàn)連接頻繁斷開優(yōu)先檢查心跳間隔配置在lws_context_creation_info里設置ws_ping_pong_interval。5. 常見問題與踩坑記錄5.1 編譯階段的問題問題1找不到OpenSSL頭文件報錯特征fatal error: openssl/ssl.h: No such file or directory排查步驟確認libssl-dev有沒有安裝dpkg -l | grep libssl-dev搜索頭文件實際位置find /usr -name ssl.h 2/dev/nullCMake重新指定路徑或用sudo apt install libssl-dev問題2OpenSSL版本太新導致API編譯失敗報錯特征error: RSA {aka struct rsa_st} has no member named e之類。這是OpenSSL 3.x中很多結構體變?yōu)椴煌该鱫paque導致的。解決思路是換用支持OpenSSL 3的libwebsockets版本或者將OpenSSL降級到1.1.1。我個人更傾向換庫版本因為新系統(tǒng)的OpenSSL 3是安全更新基線沒必要為了舊庫強行降級系統(tǒng)組件。問題3鏈接階段找不到 -lwebsockets編譯自己的程序時提示cannot find -lwebsockets原因通常是庫文件沒有安裝到系統(tǒng)搜索路徑中或者動態(tài)庫運行時加載路徑沒配置。解決辦法export LD_LIBRARY_PATH/usr/local/libwebsockets/lib:$LD_LIBRARY_PATH或者在編譯時用-Wl,-rpath,/usr/local/libwebsockets/lib把庫路徑寫進可執(zhí)行文件。5.2 運行階段的問題問題1連接被拒絕客戶端報Connection refused檢查服務端是否真的在監(jiān)聽、端口是否正確netstat -tlnp | grep 7681問題2TLS握手失敗客戶端報TLS handshake failed優(yōu)先檢查證書路徑是否正確、證書格式是否是PEM。有次我用.crt格式證書直接指定結果libwebsockets只認PEM格式用openssl轉換一下就通過了。問題3發(fā)送消息亂序或丟失在低配設備上如果發(fā)送緩沖設置太小高頻消息可能被丟棄。libwebsockets的發(fā)送是異步的不能在一個回調里連續(xù)調用多次lws_write必須等LWS_CALLBACK_CLIENT_WRITEABLE再一次觸發(fā)后繼續(xù)寫。如果發(fā)現(xiàn)自己發(fā)的消息總是丟檢查是否在這個回調里一次性寫了過多數(shù)據(jù)或者沒有關注lws_write的返回值。5.3 我積累的幾個實用技巧技巧1開啟詳細日志調試libwebsockets提供lws_set_log_level接口可以動態(tài)調整日志級別。編譯時如果開啟LWS_WITH_DEBUG測試階段把日志級別調到最高能看到完整的手握包、數(shù)據(jù)幀收發(fā)過程lws_set_log_level(LLL_ERR | LLL_WARN | LLL_NOTICE | LLL_INFO | LLL_DEBUG, NULL);這比抓包工具直觀得多特別適合理解WebSocket協(xié)議細節(jié)。技巧2檢查文件描述符上限壓測連接數(shù)超過1024之后連接失敗十有八九是文件描述符限制。臨時調整ulimit -n 65535如果是生產環(huán)境需要改/etc/security/limits.conf。技巧3盡量用static庫嵌入產品固件我做嵌入式產品時有條經驗能用靜態(tài)庫就不用動態(tài)庫。動態(tài)庫在Linux桌面場景沒問題但到嵌入式環(huán)境版本管理和依賴關系很容易變成隱形炸彈。libwebsockets的靜態(tài)庫編譯出來體積在100KB~300KB左右取決于裁剪選項對現(xiàn)代設備來說完全可接受。6. 在項目里集成libwebsockets時的架構建議libwebsockets用起來不難但真正要跟自己的業(yè)務架構融合有幾個關鍵設計點需要想清楚?;卣{驅動的編程思維libwebsockets是事件驅動模型你的業(yè)務邏輯都掛在各種回調里。這和寫線性執(zhí)行的傳統(tǒng)C程序不太一樣新手容易在回調里做耗時操作結果把event loop卡住導致掉線或者消息延遲。后來我把耗時的業(yè)務處理全部丟到工作線程池回調里只做數(shù)據(jù)拷貝和狀態(tài)標記整個穩(wěn)定性一下就上來了。數(shù)據(jù)緩沖區(qū)的生命周期LWS_CALLBACK_CLIENT_RECEIVE回調里的in指針只在回調期間有效你不要存下來異步使用。正確做法是立即memcpy出來或者用lws_traffic之類機制管理緩沖。這個坑我踩過一次——當時把in指針直接傳給了工作線程結果兩分鐘后讀到的全是垃圾數(shù)據(jù)。與業(yè)務層解耦我建議把libwebsockets封裝成獨立的通信模塊對外只提供簡單的接口connect()、send()、on_message(callback)。業(yè)務層完全不需要知道WebSocket握手的細節(jié)也不需要關心底層是ws還是wss。這樣即使以后要換通信協(xié)議業(yè)務層代碼不受影響。7. 常見操作速查表最后整理一份速查表覆蓋最常用的操作方便你日常開發(fā)時翻查操作命令/配置克隆倉庫git clone https://github.com/warmcat/libwebsockets.git編譯release版cmake -DCMAKE_BUILD_TYPERelease .. make開啟所有示例-DLWS_WITH_MINIMAL_EXAMPLESON只編靜態(tài)庫-DLWS_WITH_SHAREDOFF -DLWS_WITH_STATICON指定安裝路徑-DCMAKE_INSTALL_PREFIX/your/path生成工程后查看可用選項ccmake ..或cmake -L ..運行ws服務器./lws-minimal-ws-server -p 7681運行ws客戶端測試websocat ws://localhost:7681啟用TLS-DLWS_WITH_SSLON運行時帶證書啟動安裝make install默認到/usr/local這套流程下來從源碼下載到服務跑通基本能覆蓋 libwebsockets 使用的主路徑。后續(xù)你完全可以基于minimal-examples里的代碼改出一個滿足具體業(yè)務需求的服務端或客戶端——我后面好幾次做項目都是直接在示例代碼上改出來的。根據(jù)我個人經驗libwebsockets這類庫最大的學習價值在于它把WebSocket這個協(xié)議完全透明地呈現(xiàn)在你面前——你可以在日志里看到每一個數(shù)據(jù)幀的流向在回調里感受每一次狀態(tài)機的切換。理解了這個過程之后無論是排查網絡問題、還是實現(xiàn)自己的長連接服務心里都會非常有底。