使用lws编码的注意事项

libwebsockets: Notes about coding with lws

守护进程

        如果你需要实现守护进程,那很简单,lws内置了一个API叫lws_daemonize ,它 能帮你完成守护进程的一切,包括创建锁文件。如果你正在创建一个基础的守护进程,那么,你只需要在初始化的早期调用它,它能fork出一个无头后台进程,并退出当前进程。

      注意:守护进程的stdout、stderr和stdin都被重定向到 /dev/null,因此,如果需要调试日志,你需要选择其它实现,比如说syslog。

最大连接数

        LWS能处理的最大连接数,是在启动时通过OS查询进程的最大文件描述符个数(比如说Fedora的1024)得到。然后,它分配一个数组来维护连接,剩下的文件描述符再给用户代码使用。

        如果你需要限制连接数,或提高连接数,你可以用ulimit或类似机制来改变OS的限制,在libwebsockets 重启之后,限制值就会随之调整。

Libwebsockets是单线程的

        Libwebsockets工作在一个序列化的事件循环里,是一个单线程实例。 

        不允许在其它线程执行websocket操作。除了forked()过程导致的内部数据不一致之外,wsi(websocket连接对象),在服务期间可能会随时因为socket的关闭或wsi喂数据而结束生命。

        Websocket写活动只能发生在LWS_CALLBACK_SERVER_WRITEABLE 回调里。

        这种将发送新数据与获取先前数据关联在一起的网络编程风格显然与一般用户习惯不同。因此,让我们换句话强调:

***只在WRITEABLE 回调时执行LWS写操作。***

还有另一个网络编程约定可能会让某些人感到惊讶:不能接收太多数据:

***你必须执行RX流量控制。***

相关实例代码,请参考 mirror protocol的实现。

        只有活跃连接才会出现在用户的回调中,这消除了所有尝试关闭wsi或向它喂数据的可能性。

        除了websocket之外,如果你需要服务于其它socket或文件描述符,你可以把它们与websocket结合,在同一个循环中轮询,请参考后续的“外部轮询循环支持”,并且,依然在同一线程/进程上下文里执行。

        如果你坚持尝试使用多线程,你需要特别注意多个线程同时创建多个context的场景。

      此外,创建context时会调用SSL_library_init(),这个API也是个不可重入的函数。因此,请至少保证串行创建contexts。

只在socket 可写时写入数据

        用户侧只能在LWS_CALLBACK_SERVER_WRITEABLE (或client端的LWS_CALLBACK_CLIENT_WRITEABLE)回调里向一条websocket连接写数据。

如果你需要发送什么数据,不要直接发出,而是要先请求socket的回调:

  • lws_callback_on_writable(context, wsi),这是对特定wsi的请求
  • lws_callback_on_writable_all_protocol(protocol) ,这是对同一protocol 下的所有连接同时发出的写请求。

        通常,下一次服务循环就会收到回调,但是,如果你的对端响应缓慢或临时失活,那么回调可能会延时。生成写入的内容并发送它应当在WRITEABLE回调里完成。

        请参考测试服务端代码,以加深理解。

不要仅依赖自己的WRITEABLE请求出现

       如果 LWS碰到需要缓存你所发送的数据的网络条件,它可能会生成额外的LWS_CALLBACK_CLIENT_WRITEABLE 事件。

        因此,你的LWS_CALLBACK_CLIENT_WRITEABLE 响应代码需要决定发送什么内容,不能假定writeable回调一定能发送出数据。

        任何时候,你都可能会收到“额外的”writeable回调,这时,你只需要return 0 即可,并耐心等待正常的回调。

从用户侧关闭连接

        如果你需要关闭一条连接,只需要在处理连接的回调时返回-1就可以。

        你可以对wsi调用lws_callback_on_writable ()来触发写回调,如果你想要在回调中关闭连接,只需返回-1。通常,在回调中直接决定关闭连接并返回-1也很简单。

        如果socket知道连接僵死,比如对端关闭,或者收到诸如FIN之类的网络错误,那么,LWS将会自动关闭连接。

        如果碰到静默死连接,可能是进入这样的状态:数据发出后没有收到ACK,这种连接永远不会再收到可写回调。要覆盖这类场景,你可能用TCP保活(后面会讲)或ping。

消息分片

        为了支持消息分片,你需要调用lws_is_final_fragment来检查。这个检查可以结合libwebsockets_remaining_packet_payload 来获取完整的消息内容。比如:

case LWS_CALLBACK_RECEIVE:
 {
     Client * const client = (Client *)user;
     const size_t remaining = lws_remaining_packet_payload(wsi);
 
     if (!remaining && lws_is_final_fragment(wsi)) {
         if (client->HasFragments()) {
             client->AppendMessageFragment(in, len, 0);
             in = (void *)client->GetMessage();
             len = client->GetMessageLength();
         }
 
         client->ProcessMessage((char *)in, len, wsi);
         client->ResetMessage();
     } else
         client->AppendMessageFragment(in, len, remaining);
 }
 break;

测试 app libwebsockets-test-fraggle展示了处理消息分片的方式。 

调试日志记录

        通过lws_set_log_level API,你可以自定义回调函数来处理日志。它缺省指向一个内部函数,并输出到stderr。可以把它设置为NULL。

        LWS库导出一个辅助函数 lwsl_emit_syslog() 它简化了syslog的处理。但你依然需要在用户侧代码中使用setlogmask、 openlog 和closelog 这些函数。

用户侧可以用的日志记录API有:

  • lwsl_err(...)
  • lwsl_warn(...)
  • lwsl_notice(...)
  • lwsl_info(...)
  • lwsl_debug(...)

        其中,notice和info的区别在于,缺省会记录notice而忽略info。

如果你编译LWS时没定义_DEBUG:

cmake .. -DCMAKE_BUILD_TYPE=DEBUG

那么,notice级别以下的日志不会编译进目标文件。

外部轮询循环支持

        libwebsockets 为它的socket维护了一个内部 poll() 数组,但是,你可以选择使用外部轮询数组。如果libwebsockets 需要与其他server维护的轮询协作,那么就会有这样的需求。

      对protocol 0 的回调会出现这样的四类回调: LWS_CALLBACK_ADD_POLL_FD、 LWS_CALLBACK_DEL_POLL_FD、 LWS_CALLBACK_SET_MODE_POLL_FD 和LWS_CALLBACK_CLEAR_MODE_POLL_FD 。它们允许接口代码在其它轮询循环中管理socket。

      你可以把所有需要的FD传给  lws_service_fd(),即使socket不属于libwebsockets ,也是安全的。

        如果libwebsocket 处理了,它在返回之前将pollfd 的revents 字段清零。因此,你可以让libwebsockets 大胆尝试,如果返回的pollfd->revents不为零,那么说明它需要你的代码处理。

        此外,请注意,当你集成的外部事件循环不使用 poll()语义时,比如说libev 或libuv,你必须返回一伪装的pollfd来映射真实的事件。

  • 请确保在合成的pollfd 里把  .events设置为 .revents的值。
  • 如果可能,检查内置的事件循环支持 (比如, ./lib/libuv.c),以确定它是如何与lws对接的。
  • 使用  libwebsockets.h 里定义的LWS_POLLHUP / LWS_POLLIN / LWS_POLLOUT 以避免缺少windows兼容性。

与 c++ 集成

        LWS集成C++应用很方便。你可以拷贝test-server的代码尝试一下:

cp test-server/test-server.c test.cpp

然后用g++编译它:

g++ -DINSTALL_DATADIR=\"/usr/share\" -ocpptest test.cpp -lwebsockets

INSTALL_DATADIR 只有在开放安装环境中才需要,如果你把代码放入你的工程中,那么你根本就不需要定义这个变量。

报头信息的可用性

        HTTP报头信息由一个"ah"结构组成的pool管理。这些资源是有限的,因此有重用的压力,释放报头时,需要把ah返回给pool。

        因此,在ESTABLISHED之后,也就是在升级为websocket之后,HTTP报头信息将会丢失。如果有任何重要信息后面处理需要,那么需要在前面的处理中把它拷贝出来,以备后用。

        对于没有升级的HTTP连接,报头信息则是一直可见的。

TCP 保活

        不用于发送数据的连接可能在什么时候静默死亡。这种场景TCP缺省不会报告,在你发送数据之前,再也收不到任何数据。

        为了应对这种场景,可以在创建context时选择对所有websocket使能TCP保活机制。

        要使通保活机制,在创建context时,把创建参数结构 的ka_time字段设置为非零值,单位为秒。你还应该填充ka_probes 和ka_interval。

        TCP保活机制使能之后,TCP层将发送控制包,在不影响链路层流量的前提下等待对端的模拟响应。如果没收到响应,socket会在poll()中触发错误,强制关闭连接。

   注意:BSD不支持像Linux这样的每个socket上的keepalive time / probes / interval机制。在这些系统中,你可以通过非零的ka_time来使能保活,但它使用系统内核层面的 time / probes/ interval,也就是说,它与ka_time中的具体值无关。

优化SSL 连接

        在 lws_context_creation_info 结构体里,有一个成员叫ssl_cipher_list ,它允许用户代码在context创建时限制加密算法的可能选择。

        你可能想探究一下它,以避免对端选择计算成功太高的加密算法。如果是这样,请把它指向一个类似这样的字符串:

`"RC4-MD5:RC4-SHA:AES128-SHA:AES256-SHA:HIGH:!DSS:!aNULL"`

如果把它设置为NULL,那么缺省的加密算法集合是所有可能的选择。

你也可以把它设置为"ALL",这样将允许“所有选择”(包括不安全算法)。

client 连接的异步性质

        当你调用 lws_client_connect_info(..) 并得到一个 wsi 指针时,它并不说明连接已经建立成功,而仅说明LWS开始尝试连接。

        事实上,client 端是在收到LWS_CALLBACK_CLIENT_ESTABLISHED 时才真正建立连接的。

        连接时有个5秒的超时,而且它随时可能因为某种错误而提前终止,如果是这样,你将会收到一个protocol上的LWS_CALLBACK_CLIENT_CONNECTION_ERROR 回调,而不是带有wsi的回调 。

        在尝试连接并得到wsi指针之后,你可以循环调用 lws_service() 以等待回调事件。

       请参考 test-client.c 的实例代码。

        请注意,client连接API在返回之前可能会尝试处理一些连接事务。这意味着,在用户代码获取WSI指针之前,可能会收到针对新的连接对象的CONNECTION_ERROR事件(事实上,如果连接早期尝试失败,会返回NULL指针)。

        为了避免引发问题,你可以在连接的info结构体中填充pwsi ,让它指向一个lws结构,在连接之前关联相关wsi。在错误回调触发时,可以检查之前填充的标识,以识别具体出错的事务。

Lws 的平台独立文件访问API

        LWS现在实现了自己的内部平台文件抽象,用户代码可以用写实现平台无关的文件访问,可以覆盖用户代码。它可以处理类似URI 目录空间作为虚拟文件系统的方法,可以作为常规文件系统的备份。具体的使用实例比如说从一个大的压缩归档文件内部提取文件,不需要对整个文件解压,只提取请求的文件。

         test server展示了它的使用方式,基本上,为LWS的平台相关部分准备了一个文件操作结构,它存在于lws context之中。

        用户代码可以获取指向文件操作结构的指针。

LWS_VISIBLE LWS_EXTERN struct lws_plat_file_ops *
         `lws_get_fops`(struct lws_context *context);

然后利用这些平台无关的文件操作API写代码:

static inline lws_filefd_type
 `lws_plat_file_open`(struct lws *wsi, const char *filename, unsigned long *filelen, int flags)
 
 static inline int
 `lws_plat_file_close`(struct lws *wsi, lws_filefd_type fd)
 
 static inline unsigned long
 `lws_plat_file_seek_cur`(struct lws *wsi, lws_filefd_type fd, long offset_from_cur_pos)
 
 static inline int
 `lws_plat_file_read`(struct lws *wsi, lws_filefd_type fd, unsigned long *amount, unsigned char *buf, unsigned long len)
 
 static inline int
 `lws_plat_file_write`(struct lws *wsi, lws_filefd_type fd, unsigned long *amount, unsigned char *buf, unsigned long len)

用户代码也可以覆盖文件操作,可以封装或取代它们。在est server里给出了一个具体示例。

ECDH 支持

目前支持ECDH 算法。可以通过CMAKE选项使能:

cmake .. -DLWS_SSL_SERVER_WITH_ECDH_CERT=1

并 在 info->options中设置 flag

LWS_SERVER_OPTION_SSL_ECDH

编译打包,并在运行时选择。

SMP / 多线程服务

        LWS 已经集成了SMP,不需要任何内部线程。使用起来非常简单。libwebsockets-test-server-pthread展示了如何使用它,用 -j <n>参数来控制服务的线程数量,最多为32个。

在info结构体中添加了两个新成员:

    unsigned int count_threads;
    unsigned int fd_limit_per_thread;

把它们的值设置为0,那么将得到通常的单线程服务循环。

设置 count_threads值为n,说明需要在context上并发n个服务线程。

无论启动多少个服务线程,一个socket端口依然只有一个线程监听。

当一条连接建立时,它由负载最小的service线程accepted,即活跃连接最少的线程。

        用户侧负责生成n个线程,运行关特定tsi(Thread Service Index, 0 .. n - 1)的service循环。请参考 libwebsockets-test-server-pthread以了解具体实现。

        如果fd_limit_per_thread值为0,那么进程的FD限制值就由service线程共享,如果进程最多允许1024个FD,那么,每个线程最多只能有1024/n个FD。

         你可以设置fd_limit_per_thread的值来控制每个线程的最在FD,建议设置的值小于进程允许的最大值。

        你可以在CMake中用DLWS_MAX_SMP=控制context的多线程基础数据分配,如果没有设置,缺省值为32。分给线程的serv_buf当前为4096,它是在运行时分配的,只作用于活跃线程。

        LWS会根据LWS_MAX_SMP限制请求线程的实际数量,它提供了一个API叫lws_get_count_threads(context),可以发现实际支持的线程数。

        用户代码要求实现锁机制来保护回调资源,就像ibwebsockets-test-server-pthread里那样。

        LWS本身没有线程相关的任何知识或依赖。锁的具体实现完全取决于用户代码。

Libev / Libuv 支持

你可以在cmake配置阶段选择支持其中一个,也可以选择都支持。

    -DLWS_WITH_LIBEV=1
    -DLWS_WITH_LIBUV=1

用户APP可以用以下context选项来指定它所用的事件库:

    LWS_SERVER_OPTION_LIBEV
    LWS_SERVER_OPTION_LIBUV

在用户代码中扩展选项控制

         现在,LWS提供了一个新的API叫  lws_set_extension_option(),用户代码可以用它设置每条连接的扩展选项。它应当在ESTABLISHED回调中调用,像这样:

lws_set_extension_option(wsi, "permessage-deflate",
                          "rx_buf_size", "12"); /* 1 << 12 */

        如果扩展没有激活(丢失、没有完成协商,或者扩展被禁用),那么调用返回-1。否则,连接的扩展选项会发生相应变化。

        扩展可以决定是否允许变化,上面的例子中,permessage-deflate限制它自己的rx缓存大小,同时考虑protocol的rx_buf_size成员。

Client端连接为HTTP[S] 而不是 WS[S]

      创建ws[s] 连接的结构体  lws_client_connect_info ,同样可用于创建HTTP连接。

      要保持http[s]连接,把info里的"method"成员设置为"GET",它的缺省值是NULL。

      在处理server报头之后,如果有来看成server的载荷,那么会触发LWS_CALLBACK_RECEIVE_CLIENT_HTTP回调。

        你可以选择立即处理数据,也可以请求一个写回调来提供流控制,并在writable回调中处理数据。

        无论选择哪种方式,你都要使用lws_http_client_read() API来访问数据,比如:

case LWS_CALLBACK_RECEIVE_CLIENT_HTTP:
         {
                 char buffer[1024 + LWS_PRE];
                 char *px = buffer + LWS_PRE;
                 int lenx = sizeof(buffer) - LWS_PRE;
 
                 lwsl_notice("LWS_CALLBACK_RECEIVE_CLIENT_HTTP\n");
 
                 /*
                  * Often you need to flow control this by something
                  * else being writable.  In that case call the api
                  * to get a callback when writable here, and do the
                  * pending client read in the writeable callback of
                  * the output.
                  */
                 if (lws_http_client_read(wsi, &px, &lenx) < 0)
                         return -1;
                 while (lenx--)
                         putchar(*px++);
         }
         break;

        请注意:如果你将用SSL CLIENT去连接一个vhost,那么,在vhost创建之后,你必须为vhost准备好client端的SSL context 。因为,如果vhost设置为listen / server,通常不会完成这个操作。调用  lws_init_vhost_client_ssl() API,它将允许建立vhost上的client SSL 。

使用lws vhosts

        如果你在创建context时设置了LWS_SERVER_OPTION_EXPLICIT_VHOSTS选项标签,那么,为了兼容,它不会用info结构成员创建缺省的 vhost。随后,你可以调用  lws_create_vhost() API来手工附加一个或多个vhost。

LWS_VISIBLE struct lws_vhost *
 lws_create_vhost(struct lws_context *context,
                  struct lws_context_creation_info *info);

lws_create_vhost() 所使用的info 结构对象,与 lws_create_context()相同。它自动忽略与context相关的成员,而使用对vhost有意义的成员(  libwebsockets.h 中以VH为标识的字段)。

 struct lws_context_creation_info {
         int port;                                       /* VH */
         const char *iface;                              /* VH */
         const struct lws_protocols *protocols;          /* VH */
         const struct lws_extension *extensions;         /* VH */
 ...

       在你附加vhost时,如果vhost的端口已经有监听socket存在,那么这些vhost共享端口,并使用SNI或者client端的Host报头来选择vhost。如果没有其它vhost监听端口,那么直接创建新的监听socket。

       还有一些新成员,但它们主要是你在创建context时可能需要设置的一些内容。

lws是如何匹配vhost 的hostname 或SNI 的

        LWS首先剥离端口:port。

        然后,尝试在监听当前端口上的vhost中查找名字精确匹配的对象。比如说,如果SIN或Host:报头值为abc.com:1234,那么它将会查找名为abc.com,并且监听在端口1234之上的vhost。

        如果没有精确匹配成功,lws会考虑模糊匹配,比如说,SNI或HOST报头值为 cats.abc.com:1234,那么,它接受监听在1234端口上的,名为 "abc.com"的vhost。如果有其它更好,更精确的匹配,则优先选择它。

        SSL连接依然会让client继续检查证书,如果不允许模糊匹配,则会报错。

Using lws mounts on a vhost

          lws_create_vhost() 的最后一个参数让你关联一个  lws_http_mount 结构的链表到vhost的URL  'namespace'是,这有点类似于unix中的文件系统挂载,让你可以透明地处理挂载的内容。

struct lws_http_mount {
         struct lws_http_mount *mount_next;
         const char *mountpoint; /* mountpoint in http pathspace, eg, "/" */
         const char *origin; /* path to be mounted, eg, "/var/www/warmcat.com" */
         const char *def; /* default target, eg, "index.html" */
 
         struct lws_protocol_vhost_options *cgienv;
 
         int cgi_timeout;
         int cache_max_age;
 
         unsigned int cache_reusable:1;
         unsigned int cache_revalidate:1;
         unsigned int cache_intermediaries:1;
 
         unsigned char origin_protocol;
         unsigned char mountpoint_len;
 };

        链表中的最后一个mount节点,必须把mount_next设置成NULL,至于其它节点,则把它指向后继节点。

        无论是mount结构,还是其它相关字符串,都必须持久化到context销毁为止,因为它们没有复制拷贝出来,但在生命周期内会引用到。

.origin_protocol 应当是以下值之一:

enum {
         LWSMPRO_HTTP,
         LWSMPRO_HTTPS,
         LWSMPRO_FILE,
         LWSMPRO_CGI,
         LWSMPRO_REDIR_HTTP,
         LWSMPRO_REDIR_HTTPS,
         LWSMPRO_CALLBACK,
 };
  • LWSMPRO_FILE :直接映射URL namespace到文件系统上
  • LWSMPRO_CGI : 把URL namespace关联到给定的CGI,当访问URL时,执行CGI,并把它的输出返回给client端。a
  • LWSMPRO_REDIR_HTTP 和LWSMPRO_REDIR_HTTPS 自动把client的请求重定向到给定的origin URL。
  • LWSMPRO_CALLBACK : 把请求关联到protocol 指定的回调函数上处理。c

LWSMPRO_CALLBACK mount的操作

        CALLBACK类型mount所提供的特性,把一部分URL namespace与命名的callback句柄绑定在一块。

        这一特性允许protocol插件处理URL namespace内容。比如,在test-server-v2.0.c里,URL域"/formtest"被关联到"protocol-post-demo"插件上:

 static const struct lws_http_mount mount_post = {
         NULL,           /* linked-list pointer to next*/
         "/formtest",            /* mountpoint in URL namespace on this vhost */
         "protocol-post-demo",   /* handler */
         NULL,   /* default filename if none given */
         NULL,
         0,
         0,
         0,
         0,
         0,
         LWSMPRO_CALLBACK,       /* origin points to a callback */
         9,                      /* strlen("/formtest"), ie length of the mountpoint */
 };

访问没有限制方法,也就是说,GET和POST都由回调处理。

在test server里,protocol-post-demo处理html表单的消息接收和响应。

      当连接访问与CALLBACK类型mount相关的URL时,连接的protocol会被更改,直到同一连接上下次访问CALLBACK区域之外的URL。连接上的用户空间大小,为特定protocol所指定的值。

This allocation is only deleted / replaced when the connection accesses a URL region with a different protocol (or the default protocols[0] if no CALLBACK area matches it).

        这个用户空间,只有在连接访问其它不同protocol区域时才会被删除/替换(或者缺省的 protocols[0] 没有与之匹配的CALLBACK 区域)。

连接丢失时调暗页面

        lws_test插件的html提供了一种页面反馈机制,反馈websocket的连接状态,如果连接断开,则页面会被置灰。你也可以方面地在你的html中添加类似的机制。

  • include lws-common.js from your HEAD section

    <script src="/lws-common.js"></script>

  • dim the page during initialization, in a script section on your page

    lws_gray_out(true,{'zindex':'499'});

  • in your ws onOpen(), remove the dimming

    lws_gray_out(false);

  • in your ws onClose(), reapply the dimming

    lws_gray_out(true,{'zindex':'499'});