1. 程式人生 > >libwebsockets(三)實現簡易websocket伺服器

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.