總結使用libwebsockets開發接入層

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維護對象,而是我們自己申請和維護數據!

發佈了66 篇原創文章 · 獲贊 91 · 訪問量 33萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章