实现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 */
        /* notice the 4th argument if you need send binary data, the value you
         * can refer at url at the bottom of the blog.
         */             
        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.

lws_write api doc