libwebsockets(三)實現簡易websocket服務器

實現websocket服務器本身也是libwebsockets庫的初衷,本篇博客將介紹如何利用libwebsockets庫來實現一個簡單的ws服務器。

1、添加websocket協議

這裏創建服務器句柄的流程與http一致,需要修改的地方只有在創建服務器時傳入的協議數組,即

    struct lws_context_creation_info info;
    struct lws_context *context;

    static struct lws_protocols protocols[] =
    {
        /*http服務器庫中已做實現,直接使用lws_callback_http_dummy即可*/
        { "http", lws_callback_http_dummy, 0, 0 },
        LWS_PLUGIN_PROTOCOL_MINIMAL,
        { NULL, NULL, 0, 0 } /* 結束標誌 */
    };


    /*初始化內存*/
    memset(&info, 0, sizeof info);

    /*設置服務器端口*/
    info.port = 7681;

    /*設置http服務器的配置*/
    info.mounts = &mount;

    /*添加協議*/
    info.protocols = protocols;

    ...

struct lws_protocols的結構如下

struct lws_protocols {

    /*協議名稱*/
    const char *name;

    /*服務回調,協議事件處理*/
    lws_callback_function *callback;

    /*服務建立和斷開時申請內存大小,也是callback中user的內存*/
    size_t per_session_data_size;

    /*接收緩存區大小*/
    size_t rx_buffer_size;

    /*協議id,可以用來區分協議*/
    unsigned int id;

    /*自定義數據*/
    void *user; 

    /*發送緩存大小,爲0則與rx_buffer_size相同*/
    size_t tx_packet_size;
};

這裏我們重點關注的是callback成員,它是一個lws_callback_function類型的函數指針,協議的的數據交互處理都會使用該回調函數。該回調函數的原型是

/*
 * wsi: 連接的websocket的實例
 * reason: 回調的原因
 * user:用戶自定的數據,數據大小爲per_session_data_size,需在連接初始化時申請內存
 * in: 回調的傳入數據
 * len: in指向的內存大小
 */
typedef int
lws_callback_function(struct lws *wsi, enum lws_callback_reasons reason, 
    void *user, void *in, size_t len);

其中常用的reason值如下:

    /*協議初始化,只調用一次*/ 
    LWS_CALLBACK_PROTOCOL_INIT         

    /*連接已建立*/     
    LWS_CALLBACK_ESTABLISHED

    /*連接關閉*/
    LWS_CALLBACK_CLOSED

    /*可寫*/
    LWS_CALLBACK_SERVER_WRITEABLE

    /*有數據到來*/
    LWS_CALLBACK_RECEIVE

下面我們以官方的一個例子來說明如何寫回調函數。

2、websocket服務器實例

這裏我們將實現一個簡單的聊天室,即當一個頁面發送消息時,所有的連接的頁面都會收到該消息。

(1) 服務器結構體

struct per_vhost_data__minimal 
{        
        /*服務器,可由vhost與protocol獲取該結構體*/
        struct lws_vhost *vhost;

        /*使用的協議*/
        const struct lws_protocols *protocol;

        /*客戶端鏈表*/
        struct per_session_data__minimal *pss_list;

        /*接收到的消息,緩存大小爲一條數據*/
        struct msg amsg;

        /*當前消息編號,用來同步所有客戶端的消息*/
        int current; 
};

(2) 客戶端的結構體

struct per_session_data__minimal 
{
        /*下一個客戶端結點*/
        struct per_session_data__minimal *pss_list;

        /*客戶端連接句柄*/
        struct lws *wsi;

        /*當前接收到的消息編號*/
        int last; 
};

(3) 消息結構

struct msg 
{
        /*內存地址*/
        void *payload;

        /*大小*/ 
        size_t len;
};

整體代碼如下:


/*消息釋放*/
static void 
__minimal_destroy_message(void *_msg)
{
    struct msg *msg = _msg;

    free(msg->payload);
    msg->payload = NULL;
    msg->len = 0;
}

/*回調函數*/
static int
callback_minimal(struct lws *wsi, enum lws_callback_reasons reason,
            void *user, void *in, size_t len)
{
    /*獲取客戶端結構*/
    struct per_session_data__minimal **ppss, *pss =
            (struct per_session_data__minimal *)user;

    /*由vhost與protocol還原lws_protocol_vh_priv_zalloc申請的結構*/    
    struct per_vhost_data__minimal *vhd =
            (struct per_vhost_data__minimal *)
            lws_protocol_vh_priv_get(lws_get_vhost(wsi),
                    lws_get_protocol(wsi));
    int m;

    switch (reason) {

    /*初始化*/
    case LWS_CALLBACK_PROTOCOL_INIT:

            /*申請內存*/
            vhd = lws_protocol_vh_priv_zalloc(lws_get_vhost(wsi),
                lws_get_protocol(wsi),
                sizeof(struct per_vhost_data__minimal));
            vhd->protocol = lws_get_protocol(wsi);
            vhd->vhost = lws_get_vhost(wsi);

            break;

    /*建立連接,將客戶端放入客戶端鏈表*/
    case LWS_CALLBACK_ESTABLISHED:
        pss->pss_list = vhd->pss_list;
        vhd->pss_list = pss;
        pss->wsi = wsi;
        pss->last = vhd->current;
        break;

    /*連接關閉,將客戶端從鏈表中移除*/
    case LWS_CALLBACK_CLOSED:

        /*遍歷客戶端鏈表*/
        lws_start_foreach_llp(struct per_session_data__minimal **,
                      ppss, vhd->pss_list) {
            if (*ppss == pss) {
                *ppss = pss->pss_list;
                break;
            }
        } lws_end_foreach_llp(ppss, pss_list);
        break;

    /*客戶端可寫*/
    case LWS_CALLBACK_SERVER_WRITEABLE:
        if (!vhd->amsg.payload)
            break;

        if (pss->last == vhd->current)
            break;

        /* notice we allowed for LWS_PRE in the payload already */
        m = lws_write(wsi, vhd->amsg.payload + LWS_PRE, vhd->amsg.len,
                  LWS_WRITE_TEXT);
        if (m < vhd->amsg.len) {
            lwsl_err("ERROR %d writing to di socket\n", n);
            return -1;
        }

        pss->last = vhd->current;
        break;

    /*客戶端收到數據*/
    case LWS_CALLBACK_RECEIVE:
        if (vhd->amsg.payload)
            __minimal_destroy_message(&vhd->amsg);

        vhd->amsg.len = len;

        /* notice we over-allocate by LWS_PRE */
        vhd->amsg.payload = malloc(LWS_PRE + len);
        if (!vhd->amsg.payload) {
            lwsl_user("OOM: dropping\n");
            break;
        }

        memcpy((char *)vhd->amsg.payload + LWS_PRE, in, len);
        vhd->current++;

        /*
         *遍歷所有的客戶端,將數據放入寫入回調
         */
        lws_start_foreach_llp(struct per_session_data__minimal **,
                      ppss, vhd->pss_list) {
            lws_callback_on_writable((*ppss)->wsi);
        } lws_end_foreach_llp(ppss, pss_list);
        break;

    default:
        break;
    }

    return 0;
}

#define LWS_PLUGIN_PROTOCOL_MINIMAL \
    { \
        "lws-minimal", \
        callback_minimal, \
        sizeof(struct per_session_data__minimal), \
        128, \
        0, NULL, 0 \
    }

最後實現的效果如下,當一個窗口發送消息時,打開的頁面都會收到。

QQ截圖20180308123448.png

注:關於讀和寫時緩存區長度

However if you are getting your hands dirty with writing response headers, or
writing bulk data over http/2, you need to observe these rules so that it will
work over both http/1.x and http/2 the same.

1) LWS_PRE requirement applies on ALL lws_write().  For http/1, you don't have
to take care of LWS_PRE for http data, since it is just sent straight out.
For http/2, it will write up to LWS_PRE bytes behind the buffer start to create
the http/2 frame header.

This has implications if you treated the input buffer to lws_write() as const...
it isn't any more with http/2, up to 9 bytes behind the buffer will be trashed.

2) Headers are encoded using a sophisticated scheme in http/2.  The existing
header access apis are already made compatible for incoming headers,
for outgoing headers you must:

 - observe the LWS_PRE buffer requirement mentioned above

 - Use `lws_add_http_header_status()` to add the transaction status (200 etc)

 - use lws apis `lws_add_http_header_by_name()` and `lws_add_http_header_by_token()`
   to put the headers into the buffer (these will translate what is actually
   written to the buffer depending on if the connection is in http/2 mode or not)

 - use the `lws api lws_finalize_http_header()` api after adding the last
   response header

 - write the header using lws_write(..., `LWS_WRITE_HTTP_HEADERS`);

 3) http/2 introduces per-stream transmit credit... how much more you can send
 on a stream is decided by the peer.  You start off with some amount, as the
 stream sends stuff lws will reduce your credit accordingly, when it reaches
 zero, you must not send anything further until lws receives "more credit" for
 that stream the peer.  Lws will suppress writable callbacks if you hit 0 until
 more credit for the stream appears, and lws built-in file serving (via mounts
 etc) already takes care of observing the tx credit restrictions.  However if
 you write your own code that wants to send http data, you must consult the
 `lws_get_peer_write_allowance()` api to find out the state of your tx credit.
 For http/1, it will always return (size_t)-1, ie, no limit.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章