总结使用libwebsockets开发接入层
作者:鄒祁峰
郵箱:Qifeng.zou.job@hotmail.com
博客:http://blog.csdn.net/qifengzou
日期:2015.12.13~2015.12.14
轉載請注明來自"祁峰"的CSDN博客
1 引言
????WebSockets是HTML5支持的一種讓瀏覽器與服務器全雙工通信的協議,其以更小的開銷高效的提供了Web連接。相較于經常推送實時數據到客戶端甚至通過維護兩個HTTP連接來模擬全雙工連接的舊的輪詢或長輪詢來說,這將有效的減少網絡流量與延遲。如果想知道更多關于WS協議的信息,請自行查找,在此不再復述。本篇的重點是講述如何使用C語言實現的libwebsockets進行進一步的開發。
???由于之前系統上線時間緊迫,為了縮短開發時間,因此使用nginx作為WS代理,讓其將接收WS連接請求,并將WS協議傳輸的數據BODY轉發到接入層。但是使用nginx作為WS代理時,當終端與nginx建立一個WS連接,nginx就會與接入層建立一個TCP連接。因此,這將造成不管單臺nginx服務器的配置多么牛逼,該服務器最多只能支持65535個并發,并且多了一個代理層,性能也會受到一定影響。
???解決以上問題的辦法就是:讓接入層直接支持WS協議。由于系統接入層使用的是C語言開發,因此,本人選擇以開源軟件libwebsockets為基礎,與原系統接入層進行融合。以下是系統改造前后的架構對比圖:[注:后文將libwebsockets簡寫為LWS]
圖1 改造對比圖
2 改造LWS
2.1 支持libev
???由于LWS庫默認情況下使用poll機制管理ws連接,為了讓系統更高效的管理更多的并發,需要開啟對libev的支持。可將CMakeLists.txt中的LWS_WITH_LIBEV選項由OFF改為ON來實現,如下圖所示:
?
圖2 開啟LIBEV
2.2 改造libev
???從圖1中的改造后的架構圖中可以看出,改造后的WS接入層需要管理與終端的ws連接,同時需要管理與路由層各服務器的TCP連接,還得負責相鄰層次間數據的轉發工作。而目前LWS并未支持多線程,如果采用多線程的方式改造WS接入層,將可能出現無法預估的后果。因此,本人決定使用單進程單線程的方式,并使用LWS架構接管WS接入層與路由層之間的TCP連接。而要實現讓LWS架構接管WS接入層與路由層之間的TCP連接,則需要對LWS庫進行如下改造:
2.2.1 函數定義
???由于LWS庫的Makefile編譯選項中設置了-fvisibility=hidden,因此,如果想讓LWS庫中實現的函數能被庫外調用,則在函數實現時必須使用LWS_VISIBLE進行顯示說明。在LWS庫中的libev.c中添加如下函數:[注:關于libev的用法, 請自行查詢]
/******************************************************************************
?**函數名稱: lws_libev_timer_start
?**功 ? ?能: 添加計時器
?**輸入參數:
?** ? ? context: lws上下文
?** ? ? timer: 計時器
?**輸出參數: NONE
?**返 ? ?回: VOID
?**實現描述: 將計時器加入到libev loop對象
?**注意事項:
?**作 ? ?者: # Qifeng.zou # 2015.12.10 #
?******************************************************************************/
LWS_VISIBLE void lws_libev_timer_start(struct lws_context *context, ev_timer *timer)
{
? ? ev_timer_start(context->io_loop, timer);
}
?
/******************************************************************************
?**函數名稱: lws_libev_timer_stop
?**功 ? ?能: 停止計時器
?**輸入參數:
?** ? ? context: lws上下文
?** ? ? timer: 計時器
?**輸出參數: NONE
?**返 ? ?回: VOID
?**實現描述: 將計時器從libev loop中刪除
?**注意事項:
?**作 ? ?者: # Qifeng.zou # 2015.12.10 #
?******************************************************************************/
LWS_VISIBLE void lws_libev_timer_stop(struct lws_context *context, ev_timer *timer)
{
? ? ev_timer_stop(context->io_loop, timer);
}
?
/******************************************************************************
?**函數名稱: lws_libev_io_start
?**功 ? ?能: 開啟IO幀聽
?**輸入參數:
?** ? ? context: lws上下文
?** ? ? io: IO對象
?**輸出參數: NONE
?**返 ? ?回: VOID
?**實現描述: 將io對象加入到libev loop中
?**注意事項:
?**作 ? ?者: # Qifeng.zou # 2015.12.10 #
?******************************************************************************/
LWS_VISIBLE void lws_libev_io_start(struct lws_context *context, ev_io *io)
{
? ? ev_io_start(context->io_loop, io);
}
?
/******************************************************************************
?**函數名稱: lws_libev_io_stop
?**功 ? ?能: 停止IO幀聽
?**輸入參數:
?** ? ? context: lws上下文
?** ? ? io: IO對象
?**輸出參數: NONE
?**返 ? ?回: VOID
?**實現描述: 將io對象從libev loop中刪除
?**注意事項:
?**作 ? ?者: # Qifeng.zou # 2015.12.10 #
?******************************************************************************/
LWS_VISIBLE void lws_libev_io_stop(struct lws_context *context, ev_io *io)
{
? ? ev_io_stop(context->io_loop, io);
}
代碼1 函數定義
2.2.2 函數聲明
???完成以上函數的定義后,還需按照如下的格式在libwebsockets.h中進行函數聲明。如下所示:
LWS_VISIBLE LWS_EXTERN void lws_libev_timer_start(struct lws_context *context, ev_timer *timer);
LWS_VISIBLE LWS_EXTERN void lws_libev_timer_stop(struct lws_context *context, ev_timer *timer);
LWS_VISIBLE LWS_EXTERN void lws_libev_io_start(struct lws_context *context, ev_io *io);
LWS_VISIBLE LWS_EXTERN void lws_libev_io_stop(struct lws_context *context, ev_io *io);
代碼2 函數聲明
???完成對LWS庫中libev的改造后,重新編譯和安裝LWS庫,這時便可利用以上函數將外部建立的TCP連接注入到LWS框架,讓LWS框架中的libev接管外部TCP連接的數據接收、數據發送、超時處理等處理。WS接入層與路由層之間的TCP連接,就是通過這種方式讓LWS框架接管的。[注意:一定要按照以上格式聲明函數,否則可能導致依賴此庫的程序無法訪問聲明的函數]
3 使用LWS
3.1 注冊協議回調
???終端向WS服務器發起ws連接請求時,一般會在協議頭中通過Sec-WebSockets-Protocol指明協議名。而開源libwebsockets庫通過對外提供注冊協議回調的接口為用戶自定義協議提供服務,注冊協議回調的接口中將會指明協議名、以及對應的處理回調、自定義數據的大小等字段。其注冊的方式如下所示:
/* 注冊協議回調配置表 */
struct libwebsocket_protocols g_aws_protocols[] =
{
? ? {
? ? ? ? "chat", ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? /* 協議名:其與Sec-Websockets-Protocol字段對應 */
? ? ? ? aws_callback_im_hdl, ? ? ? ? ? ? ? ? ? ? ? ? ? ?/* 回調函數:協議對應的回調處理函數 */
? ? ? ? sizeof(aws_im_session_data_t), ? ? ? ? ? ? ? ? ?/* 自定義數據空間大小:每個ws連接均會分配一個自定義數據空間 */
? ? ? ? 0, ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?/* max frame size / rx buffer */
? ? },
? ? {
? ? ? ? "push", ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? /* 協議名 */
? ? ? ? aws_callback_push_hdl, ? ? ? ? ? ? ? ? ? ? ? ? ?/* 回調函數 */
? ? ? ? sizeof(aws_push_session_data_t), ? ? ? ? ? ? ? ?/* 自定義數據空間大小 */
? ? ? ? 0, ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?/* max frame size / rx buffer */
? ? },
? ? { NULL, NULL, 0, 0 } ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?/* 結束標識 */
};
代碼3 注冊協議回調
???注意:如果終端向WS服務器發起的ws連接請求中并未指明Sec-WebSockets-Protocol字段,那么libwebsockets庫默認使用注冊協議回調配置表中的第一個配置項為該ws連接的提供服務。
3.2 回調函數參數
???回調函數用于處理對應協議各階段的數據。該回調函數的各參數含義見表1所示:
表1 回調函數參數
序號?? ?變量名?? ?類型?? ?含義
01?? ?context?? ?struct lws_context *?? ?含義:全局上下文
備注:這是libwebsockets框架的上下文,負責所有ws連接的維護和管理
02?? ?wsi?? ?struct lws *?? ?含義:WS連接對象
備注:每個ws連接均有一個wsi對象與之對應
03?? ?reason?? ?enum lws_callback_reasons?? ?含義:調用回調的原因
備注:一個協議只對應回調函數,但是調用回調函數的情況很多,且回調原因不同,參數in的含義也會發生變化。
04?? ?user?? ?void *?? ?含義:用戶自定義數據
備注:每個ws連接均會帶有一個用戶自定義數據,可在其中放入用戶關心的信息。其大小與協議回調注冊表中對應配置項中的sizeof()向對應。
05?? ?in?? ?void *?? ?含義:輸入數據
備注:根據調用回調的原因,該輸入數據的含義發生變化
06?? ?len?? ?size_t?? ?含義:輸入數據的長度
????回調函數參數中的reason指明了調用回調函數的原因,其也代表了ws連接正處于哪個處理階段或狀態。比如:在ws連接創建成功后,應該進行自定義數據的初始化;在ws連接銷毀階段,應該釋放自定義數據中用戶分配的空間等。因此,要正確的編寫協議回調函數就必須對reason各狀態值有正確的理解。以下將對服務器端開發者需要關心的reason狀態值的進行解釋:
表2 reason狀態值
序號?? ?狀態值?? ?含義
01?? ?LWS_CALLBACK_WSI_CREATE?? ?含義:正在創建ws連接對象
備注:此時表1中的wsi對象和user對象依然為空指針,因此,還不能初始化用戶自定義對象。
回調函數的參數含義:
????context: 全局上下文
????wsi: 空指針
????user: 空指針
in: 空指針
len: 0
02?? ?LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION?? ?含義:使用lws庫的人員可以在此過濾協議。
備注:在此處返回非0值時,lws庫將會關閉該鏈接;該處返回0時,表示ws連接已經建立成功。此時表1中的wsi對象和user對象已不為空,因此,此時可以對用戶自定義對象user進行初始化處理。
回調函數的參數含義:
????context: 全局上下文
????wsi: ws連接對象
????user: 用戶自定義數據
in: 空指針
len: 0
[注意:測試中發現同一個wsi可能多次由該reason調用回調,且該wsi對象的用戶自定義數據的指針會發生變化,導致用戶設置的數據丟失,造成嚴重后果。解決方案:不讓lws維護對象,而是我們自己申請和維護數據!]
03?? ?LWS_CALLBACK_LOCK_POLL?? ?含義:添加保護ws連接狀態的互斥鎖
備注:當采用的是多線程編程,則在此添加互斥鎖保護ws連接相關狀態,防止沖突。如果是單進程方式,則無需做任何操作。
04?? ?LWS_CALLBACK_UNLOCK_POLL?? ?含義:解除保護ws連接狀態的互斥鎖
備注:當采用的是多線程編程,則在此解除互斥鎖。如果是單進程方式,則無需做任何操作。
05?? ?LWS_CALLBACK_RECEIVE?? ?含義:收到一幀完整數據
備注:表示WS服務端收到客戶端發送過來的一幀完整數據,此時表1中的in表示收到的數據,len表示收到的數據長度。需要注意的是:指針in的回收、釋放始終由LWS框架管理,只要出了回調函數,該空間就會被LWS框架回收。因此,開發者若想將接收的數據進行轉發,則必須對該數據進行拷貝。
回調函數的參數含義:
????context: 全局上下文
????wsi: ws連接對象
????user: 用戶自定義數據
????in: 接收數據的起始地址
????len: 接收數據的長度
06?? ?LWS_CALLBACK_SERVER_WRITEABLE?? ?
含義:此ws連接為可寫狀態
備注:表示wsi對應的ws連接當前處于可寫狀態,即:可發送數據至客戶端。
回調函數的參數含義:
????context: 全局上下文
????wsi: ws連接對象
????user: 用戶自定義數據
in: 空指針
len: 0
07?? ?LWS_CALLBACK_CLOSED?? ?含義:ws連接已經斷開
備注:不能在此釋放內存空間,否則存在內存泄漏的風險!!!因為連接斷開時,并不總是會回調LWS_CALLBACK_CLOSED的處理!
??? ?LWS_CALLBACK_WSI_DESTROY?? ?含義:正在銷毀ws連接對象
備注:表示libwebsockets框架即將銷毀wsi對象。此時如果用戶自定義對象中存在動態分配的空間,則需要在此時進行釋放。
回調函數的參數含義:
????context: 全局上下文
????wsi: ws連接對象
????user: 用戶自定義數據
in: 空指針
len: 0
3.3 重要函數說明
???由于libwebsockets庫未能對其能控制ws連接狀態的函數進行重點說明,致使本人在使用過程中走了很多彎路。故,在此列出相關函數進行重點說明。
表3 重要函數列表
序號?? ?函數名?? ?功能
01?? ?lws_get_peer_write_allowance?? ?功能:該ws連接允許發送的字節數
備注:如果有發送字節限制,則返回正數;如果無發送字節限制,則返回-1。
02?? ?lws_send_pipe_choked?? ?功能:判斷ws連接是否阻塞
備注:如果ws連接阻塞,則返回1,否則返回0。
03?? ?lws_write?? ?功能:將數據發送給對端
備注:函數參數說明
??wsi: ws連接對象
??buf: 需要發送數據的起始地址。
?????????注意:必須在指針buf前預留長度為LWS_SEND_BUFFER_PRE_PADDING的空間,同時在指針buf+len后預留長度為LWS_SEND_BUFFER_POST_PADDING的空間。
??len: 需要發送數據的長度
??protocol: 如果該連接是http連接,則該參數的值為LWS_WRITE_HTTP;如果該連接是ws連接,則該參數的值為LWS_WRITE_BINARY,但如果第一次發送的數據長度n < len,則發送后續長度為(len - n)字節的數據時,該參數值改為LWS_WRITE_HTTP。
04?? ?lws_http_transaction_completed?? ?功能:當前連接為http連接,而非ws連接時,如果當前http請求的應答數據發送完畢,則可使用該函數重置http連接的相關狀態,只有收到新的http請求才能激活該http連接。
備注:如果當前連接為ws連接,則千萬不要調用此函數,其將導致服務端則無法再激活可寫事件。
05?? ?lws_callback_on_writable?? ?功能:將ws連接加入可寫事件監聽
06?? ?lws_callback_on_writable_all_protocol?? ?功能:將某個協議的所有ws連接加入可寫事件監聽
備注:在網絡中存在各種情況可能導致服務端并不知道與客戶端的連接已經斷開,比如:客戶端掉電。為了應對這種情況的存在,需要每隔一段時間執行該函數。再在該協議的回調函數的LWS_CALLBACK_SERVER_WRITEABLE事件中判斷一下連接是否超時,如果超時則返回非零值。
4 存在問題
?問題1:之前網上很多描述libwebsockets庫優點包括:節省內存空間。而從實際使用的情況上分析,libwebsockets庫消耗了大量的內存空間。比如:快速的建立1000個并發,將會導致消耗200MB的內存空間,且前1000個并發斷開后,再此建立1000個并發時,內存還會快速的增長!之前本人還以為存在內存泄漏的情況,后經反復使用valgrind進行內存泄漏的分析,發現并不存在內存泄漏的情況,因此,本人認為libwebsockets庫的內存管理策略存在嚴重問題。
??問題2:測試中發現同一個wsi可能多次由原因LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION調用回調,且該wsi對象的用戶自定義數據的指針會發生變化,導致用戶設置的數據丟失,造成嚴重數據丟失和程序CRASH。解決方案:不讓lws維護對象,而是我們自己申請和維護數據!
---------------------?
作者:祁峰?
來源:CSDN?
原文:https://blog.csdn.net/qifengzou/article/details/50281545?
版權聲明:本文為博主原創文章,轉載請附上博文鏈接!
總結
以上是生活随笔為你收集整理的总结使用libwebsockets开发接入层的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: boost log使用
- 下一篇: ios 渐变透明背景_骚气渐变色的海报设