Libwebsockets 实现简单 HTTP 客户端 之前给 Libwebsockets 包装过一个 WebSocket 客户端,当时并没有深究 Libwebsockets 的设计,只是照着 Minimal Example 和 Github Issue 抄的,所以设计的一塌糊涂,这次打算重新看一看 Libwebsockets 的设计,包装一个 HTTP 客户端。
1. 各 Struct 关系 
1.1. lws_protocols 这个「 Protocol 」比较重要的只有两项:
协议名:用来区分各个协议,后面的每个 Instance 需要用协议名来决定自己运行哪个协议, i.e. 不能直接用 lws_protocols 指针。 回调:用来编程协议的行为,比如连接建立好要执行什么代码、连接将要断开时要执行什么代码 1.2. lws_context_creation_info 用来规定如何创建 Libwebsockets 的 Context ,比如要不要监听某个端口。里面比较重要的是需要指定 Context 关联的 lws_protocols 。
1.3. lws_context 通过 lws_context_creation_info 可以创建 Libwebsockets 的 Context ,这个 Context 上可以运行多个 Instance , i.e. 全局可以只维护唯一的 Context 。
想要让 Libwebsockets 动起来需要一个死循环不断对着这个 Context 调用 lws_service ,想要让 Libwebsockets 停下来需要对着这个 Context 调用 lws_cancel_service 。
1.4. lws 表示 Libwebsockets 的一个 Instance ,由 Libwebsockets 自行管理,甚至用户只能拿到 lws* ,看不到 struct lws 的定义。
上面 Protocol 编程自己的行为时主要通过 Callback , Callback 里就是通过这个 lws* 对 Instance 进行管理。
1.5. lws_client_connection_info 客户端特化配置,比如要连接到的服务端的 IP 或域名、端口、 SSL/TLS 细节等。主要作用是让 Libwebsockets 创建 lws 。
需要指定自己运行在的 Context (lws_context) ,指定 Protocol 名来确定自己运行什么协议;还可以关联一个 lws* 来拿到自己的 Instance 指针,这可能是唯一的在 Callback 以外拿到 Instance 指针的机会,可以用来保存到全局以便维护。
通过 lws_client_connect_via_info 即可启动这个 Instance 。
2. 让 Libwebsockets 动起来 2.1. lws_protocols 定义 自顶向下,首先定义一个 Protocol ,需要协议名,比如 foo ,以及回调,格式是这样的:
1 2 3 4 5 6 7 int  callback_foo (lws* wsi, lws_callback_reasons reason, void * user, void * in, std::size_t  len)    switch  (reason) {     case  LWS_CALLBACK_XXX:              break ;   } } 
lws* wsi 表示一个 Instance ,它运行在某个 Context 上(可以通过 lws_get_context(wsi) 拿到),且正好运行当前回调对应的协议。lws_callback_reasons reason 表示此次回调的事件,比如一个连接被建立、当前 Session 可以开始传输数据了、连接要被关闭了等。void* user 是 Per Session 的一段内存,由 Libwebsockets 进行管理,初始化 lws_protocols 时给第三个对象传非零值, Libwebsockets 就会给每个 Session 创建一块内存。void* in 表示输入数据,可能是任何类型。std::size_t len 表示输入数据大小。比如我想要给每个 Session 分配一个这样的数据:
1 2 3 struct  PerSessionData  {  std::uint64_t  flags; }; 
那么 Protocol 定义如下:
1 lws_protocols protocol {"foo" , callback_foo, sizeof (PerSessionData), 0 , 0 , nullptr , 0 }; 
Libwebsockets 的每个 Context 可以同时支持多个 Protocol ,所以需要定义一个以 LWS_PROTOCOL_LIST_TERM 结尾的 array :
1 2 3 4 lws_protocols protocols[]{   {"foo" , callback_foo, sizeof (PerSessionData), 0 , 0 , nullptr , 0 },   LWS_PROTOCOL_LIST_TERM }; 
2.2. lws_context 创建 首先需要定义 lws_context_creation_info 来定义 lws_context 细节:
1 2 3 4 5 6 7 8 9 lws_context_creation_info ctx_info{}; ctx_info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT | LWS_SERVER_OPTION_H2_JUST_FIX_WINDOW_UPDATE_OVERFLOW; ctx_info.port = CONTEXT_PORT_NO_LISTEN; ctx_info.protocols = protocols; ctx_info.user = &user_ctx; 
这里传了 user_ctx ,比如定义如下:
1 2 3 4 5 6 struct  UserCtx  {  std::string_view user_agent;   bool  ok; }; UserCtx user_ctx; 
然后使用 lws_create_context 即可创建 lws_context :
1 lws_context* ctx = lws_create_context (&ctx_info); 
可以象征性地检查一下是否出错:
1 2 3 if  (ctx == nullptr ) {  std::cerr << "fuck\n" ; } 
2.3. 开始运行 Libwebsockets 不会自动创建子线程,需要像这样:
1 2 while  (lws_service (ctx, 0 ) >= 0  && running) {} 
3. HTTP 客户端 现在来整一个 HTTP 客户端,去 GET https://httpbin.org/anything 。
3.1. 让 Libwebsockets 闭嘴 Libwebsockets 的日志吵得一批,而且有大量意义不明的缩写,所以先让 Libwebsockets 闭嘴:
1 lws_set_log_level (0 , nullptr ); 
3.2. HTTP 客户端的 Protocol 因为是 HTTP 客户端,所以给这个 Protocol 取名 http_client ,回调可以像这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 int  callback_http_client (lws* wsi, lws_callback_reasons reason, void * user, void * in, std::size_t  len)       lws_context* ctx = lws_get_context (wsi);      UserCtx& user_ctx = *reinterpret_cast <UserCtx*>(lws_context_user (ctx));      switch  (reason) {     case  LWS_CALLBACK_CLIENT_CONNECTION_ERROR: {            } break ;     case  LWS_CALLBACK_ESTABLISHED_CLIENT_HTTP: {            } break ;     case  LWS_CALLBACK_CLIENT_APPEND_HANDSHAKE_HEADER: {            } break ;     case  LWS_CALLBACK_CLIENT_HTTP_REDIRECT: {            } break ;     case  LWS_CALLBACK_CLIENT_FILTER_PRE_ESTABLISH: {            } break ;     case  LWS_CALLBACK_RECEIVE_CLIENT_HTTP: {              return  0 ;     } break ;     case  LWS_CALLBACK_RECEIVE_CLIENT_HTTP_READ: {              return  0 ;     } break ;     case  LWS_CALLBACK_COMPLETED_CLIENT_HTTP: {            } break ;     case  LWS_CALLBACK_CLOSED_CLIENT_HTTP: {            } break ;     default : break ;   }      return  lws_callback_http_dummy (wsi, reason, user, in, len); } 
3.2.1. LWS_CALLBACK_CLIENT_CONNECTION_ERROR 这时 in 表示错误信息(或者当 in == nullptr 时表示没有错误信息),需要返回非零值结束这个 Session ,封装一下可以像这样:
1 2 3 4 5 case  LWS_CALLBACK_CLIENT_CONNECTION_ERROR: {  std::cerr << "client connection error: "  << (in == nullptr  ? "(no reason)"  : reinterpret_cast <char *>(in)) << '\n' ;   lws_cancel_service (ctx);   *user_ctx.running = false ; } break ; 
3.2.2. LWS_CALLBACK_ESTABLISHED_CLIENT_HTTP 这时候可以拿到 HTTP Status Code 和对方的 IP :
1 2 3 4 5 6 7 8 case  LWS_CALLBACK_ESTABLISHED_CLIENT_HTTP: {  char  buf[128 ];      lws_get_peer_simple (wsi, buf, sizeof (buf));      int  status = static_cast <int >(lws_http_client_http_response (wsi));   std::cerr << "connected to "  << buf << ", status "  << status << '\n' ; } break ; 
比较奇怪的是这里拿不到对方的端口,而且这里非得整一个 buffer 让 Libwebsockets 把结果复制进去, void* in 的意义呢?
3.2.3. LWS_CALLBACK_CLIENT_APPEND_HANDSHAKE_HEADER 这里用来添加请求的 HTTP Header ,有两种格式:
1 2 lws_add_http_header_by_token (wsi, WSI_TOKEN_HTTP_XXX, "value" , 5 , p, end);lws_add_http_header_by_name (wsi, "name" , "value" , 5 , p, end);
Libwebsockets 又一大别扭的地方,非要整出一堆 Macro ,目前还看不出太大缺点,等后面遍历响应的 HTTP Header 就知道这是何等的脱裤子放屁。
比如添加 Accept 和 User-Agent Header :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 case  LWS_CALLBACK_CLIENT_APPEND_HANDSHAKE_HEADER: {  unsigned  char ** p = reinterpret_cast <unsigned  char **>(in);   unsigned  char * end = (*p) + len;   std::string_view accept {"*/*" };   std::string_view useragent {"芝士雪豹" };   if  (lws_add_http_header_by_token (wsi,                                    WSI_TOKEN_HTTP_ACCEPT,                                    reinterpret_cast <const  unsigned  char *>(accept.data ()),                                    static_cast <int >(accept.length ()),                                    p,                                    end)       != 0 ) {     std::cerr << "failed to add http header `Accept`\n" ;   }   if  (lws_add_http_header_by_token (wsi,                                    WSI_TOKEN_HTTP_USER_AGENT,                                    reinterpret_cast <const  unsigned  char *>(user_ctx.ua.data ()),                                    static_cast <int >(user_ctx.ua.length ()),                                    p,                                    end)       != 0 ) {     std::cerr << "failed to add http header `User-Agent`\n" ;   } } break ; 
3.2.4. LWS_CALLBACK_CLIENT_HTTP_REDIRECT 表示 Libwebsockets 接收到了 30X 的跳转响应,这里又是 Libwebsockets 的一大狗屎设计,因为跳转是不可控的,虽然在这里返回非零值表示拒绝跳转,但拒绝后 Libwebsockets 会直接断开连接,而不是完整读取 30X 的响应之后再断开, i.e. 你拿不到 30X 响应的 Body ;而如果不拒绝跳转的话 Libwebsockets 会自动跳转到 30X 响应的 Location Header 指向的位置,同样拿不到 30X 响应的 Body ,而且这时甚至拿不到跳转后的响应的 Header ,突出一个意义不明。
3.2.5. LWS_CALLBACK_CLIENT_FILTER_PRE_ESTABLISH 这个时候可以读取响应的 HTTP Header ,被 Libwebsockets 收录成 Macro 的只能通过 Macro 获取,像这样:
1 2 3 4 5 6 lws_token_indexes i = WSI_TOKEN_HTTP_SET_COOKIE; int  header_len = lws_hdr_total_length (wsi, i);char  header_buf[header_len + 4 ];header_len = lws_hdr_copy (wsi, header_buf, static_cast <int >(sizeof (header_buf)), i); const  char * header_name = reinterpret_cast <const  char *>(lws_token_to_string (i));std::cout << header_name << " -> "  << header_buf << '\n' ; 
而这个 Macro 没有办法方便地遍历,而且没有办法获取响应具体有哪些 Header ,所以必定要浪费一些 CPU 周期,只能像这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 for  (lws_token_indexes i = WSI_TOKEN_GET_URI; i < WSI_TOKEN_COUNT;     i = static_cast <lws_token_indexes>(static_cast <int >(i) + 1 )) {   int  header_len = lws_hdr_total_length (wsi, i);   if  (header_len <= 0 ) {          continue ;   }   char  header_buf[header_len + 4 ];   header_len = lws_hdr_copy (wsi, header_buf, static_cast <int >(sizeof (header_buf)), i);   const  char * header_name = reinterpret_cast <const  char *>(lws_token_to_string (i));   if  (header_name == nullptr ) {          continue ;   }   std::cout << header_name << " -> "  << header_buf << '\n' ; } 
没有被 Libwebsockets 收录成 Macro 的 Header 有一个更 make sense 的遍历方式:
1 2 3 4 5 6 7 8 9 10 lws_hdr_custom_name_foreach (  wsi,   [](const  char * name, int  nlen, void * opaque) -> void  {   lws* wsi = reinterpret_cast <lws*>(opaque);   int  header_len = lws_hdr_custom_length (wsi, name, nlen);   char  header_buf[header_len + 4 ];   header_len = lws_hdr_custom_copy (wsi, header_buf, static_cast <int >(sizeof (header_buf)), name, nlen);   std::cout << name << " -> "  << header_buf << '\n' ; },   wsi); 
3.2.6. LWS_CALLBACK_RECEIVE_CLIENT_HTTP 这时需要准备一块 buffer 读取响应 Body ,写法相对固定:
1 2 3 4 5 6 7 8 9 10 11 12 case  LWS_CALLBACK_RECEIVE_CLIENT_HTTP: {  char  buffer[1024  + LWS_PRE];   char * px = buffer + LWS_PRE;   int  lenx = sizeof (buffer) - LWS_PRE;   if  (lws_fi_user_wsi_fi (wsi, "user_reject_at_rx" ) != 0 ) {     return  -1 ;   }   if  (lws_http_client_read (wsi, &px, &lenx) < 0 ) {     return  -1 ;   }   return  0 ; } break ; 
调用 lws_http_client_read 之后会发起下一个 case :
3.2.7. LWS_CALLBACK_RECEIVE_CLIENT_HTTP_READ; 开始读取实际的响应 Body :
1 2 3 4 5 case  LWS_CALLBACK_RECEIVE_CLIENT_HTTP_READ: {  const  char * content = reinterpret_cast <const  char *>(in);   std::cerr << std::string_view{content, len};   return  0 ; } break ; 
3.2.8. LWS_CALLBACK_COMPLETED_CLIENT_HTTP 表示 HTTP 请求结束了,具体是怎么结束的不清楚, 200 情况下是会发起这个 case 的。
3.2.9. LWS_CALLBACK_CLOSED_CLIENT_HTTP 表示连接关闭,因为这次是 HTTP 客户端,只有一个 Context 、一个 Protocol 、一个 Instance 、一个 Session ,遇到这个 case 表示这个 Context 完成了它的工作,可以调用 lws_cancel_service 关闭这个 Context :
1 2 3 4 5 case  LWS_CALLBACK_CLOSED_CLIENT_HTTP: {  std::cerr << "connection closed\n" ;   lws_cancel_service (lws_get_context (wsi));   *user_ctx.running = false ; } break ; 
4. 连接到服务端 定义好 Protocol :
1 2 3 4 lws_protocols protocols[]{   {"http_client" , callback_http_client, 0 , 0 , 0 , nullptr , 0 },   LWS_PROTOCOL_LIST_TERM }; 
创建 Context :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 UserCtx user_ctx{}; user_ctx.running = &running; user_ctx.ua = "Foo/1.0" ; user_ctx.accept = "*/*" ; lws_context_creation_info ctx_info{}; ctx_info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT | LWS_SERVER_OPTION_H2_JUST_FIX_WINDOW_UPDATE_OVERFLOW; ctx_info.port = CONTEXT_PORT_NO_LISTEN; ctx_info.protocols = protocols; ctx_info.user = &user_ctx; ctx_info.connect_timeout_secs = 30 ; lws_context* ctx = lws_create_context (&ctx_info); if  (ctx == nullptr ) {  std::printf ("lws init failed.\n" );   std::terminate (); } 
准备连接到 httpbin.org :
1 2 3 4 5 6 7 8 9 10 11 12 13 lws_client_connect_info connect_info{}; connect_info.context = ctx; connect_info.address = "httpbin.org" ; connect_info.host = connect_info.address; connect_info.origin = connect_info.address; connect_info.path = "/anything" ; connect_info.port = 443 ; connect_info.method = "GET" ; connect_info.protocol = protocols[0 ].name; connect_info.ssl_connection |= LCCSCF_USE_SSL; connect_info.ssl_connection   |= LCCSCF_H2_QUIRK_OVERFLOWS_TXCR | LCCSCF_ACCEPT_TLS_DOWNGRADE_REDIRECTS | LCCSCF_H2_QUIRK_NGHTTP2_END_STREAM; connect_info.fi_wsi_name = "user" ; 
连接:
1 2 3 4 5 if  (lws_client_connect_via_info (&connect_info) == nullptr ) {  std::printf ("failed to connect\n" );   running = false ;   lws_cancel_service (ctx); } 
让 Libwebsockets 开始运行:
1 2 while  (lws_service (ctx, 0 ) >= 0  && running) {} 
事后清理:
1 lws_context_destroy (ctx);
5. 完整版 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 #include  <cstddef>  #include  <iostream>  #include  <string_view>  #include  "libwebsockets.h"  struct  UserCtx  {  bool * running;   std::string_view ua;   std::string_view accept; }; void  lws_dump_headers (lws* wsi)    for  (lws_token_indexes i = WSI_TOKEN_GET_URI; i < WSI_TOKEN_COUNT;        i = static_cast <lws_token_indexes>(static_cast <int >(i) + 1 )) {     int  header_len = lws_hdr_total_length (wsi, i);     if  (header_len <= 0 ) {       continue ;     }     char  header_buf[header_len + 4 ];     header_len = lws_hdr_copy (wsi, header_buf, static_cast <int >(sizeof (header_buf)), i);     const  char * header_name = reinterpret_cast <const  char *>(lws_token_to_string (i));     if  (header_name == nullptr ) {       continue ;     }     std::cout << header_name << " -> "  << header_buf << '\n' ;   }   lws_hdr_custom_name_foreach (     wsi,     [](const  char * name, int  nlen, void * opaque) -> void  {     lws* wsi = reinterpret_cast <lws*>(opaque);     int  header_len = lws_hdr_custom_length (wsi, name, nlen);     char  header_buf[header_len + 4 ];     header_len = lws_hdr_custom_copy (wsi, header_buf, static_cast <int >(sizeof (header_buf)), name, nlen);     std::cout << name << " -> "  << header_buf << '\n' ;   },     wsi); } int  callback_http_client (lws* wsi, lws_callback_reasons reason, void * user, void * in, std::size_t  len)    lws_context* ctx = lws_get_context (wsi);   UserCtx& user_ctx = *reinterpret_cast <UserCtx*>(lws_context_user (ctx));   switch  (reason) {     case  LWS_CALLBACK_CLIENT_CONNECTION_ERROR: {       std::cerr << "client connection error: "  << (in == nullptr  ? "(no reason)"  : reinterpret_cast <char *>(in)) << '\n' ;       lws_cancel_service (ctx);       *user_ctx.running = false ;     } break ;     case  LWS_CALLBACK_ESTABLISHED_CLIENT_HTTP: {       char  buf[128 ];       lws_get_peer_simple (wsi, buf, sizeof (buf));       int  status = static_cast <int >(lws_http_client_http_response (wsi));       std::cerr << "connected to "  << buf << ", status "  << status << '\n' ;     } break ;     case  LWS_CALLBACK_CLIENT_APPEND_HANDSHAKE_HEADER: {       unsigned  char ** p = reinterpret_cast <unsigned  char **>(in);       unsigned  char * end = (*p) + len;       if  (lws_add_http_header_by_token (wsi,                                        WSI_TOKEN_HTTP_ACCEPT,                                        reinterpret_cast <const  unsigned  char *>(user_ctx.accept.data ()),                                        static_cast <int >(user_ctx.accept.length ()),                                        p,                                        end)           != 0 ) {         std::cerr << "failed to add http header\n" ;         return  -1 ;       }       if  (lws_add_http_header_by_token (wsi,                                        WSI_TOKEN_HTTP_USER_AGENT,                                        reinterpret_cast <const  unsigned  char *>(user_ctx.ua.data ()),                                        static_cast <int >(user_ctx.ua.length ()),                                        p,                                        end)           != 0 ) {         std::cerr << "failed to add http header\n" ;         return  -1 ;       }     } break ;     case  LWS_CALLBACK_CLIENT_HTTP_REDIRECT: {       std::cout << "===== Redirect Headers =====\n" ;       lws_dump_headers (wsi);       std::cout << "====================\n" ;     } break ;     case  LWS_CALLBACK_CLIENT_FILTER_PRE_ESTABLISH: {       std::cout << "===== Headers =====\n" ;       lws_dump_headers (wsi);       std::cout << "====================\n" ;     } break ;     case  LWS_CALLBACK_RECEIVE_CLIENT_HTTP: {       char  buffer[1024  + LWS_PRE];       char * px = buffer + LWS_PRE;       int  lenx = sizeof (buffer) - LWS_PRE;       if  (lws_fi_user_wsi_fi (wsi, "user_reject_at_rx" ) != 0 ) {         return  -1 ;       }       if  (lws_http_client_read (wsi, &px, &lenx) < 0 ) {         return  -1 ;       }       return  0 ;     } break ;     case  LWS_CALLBACK_RECEIVE_CLIENT_HTTP_READ: {       const  char * content = reinterpret_cast <const  char *>(in);       std::cerr << std::string{content, content + len};       return  0 ;     } break ;     case  LWS_CALLBACK_COMPLETED_CLIENT_HTTP: {       std::cerr << "connection completed\n" ;     } break ;     case  LWS_CALLBACK_CLOSED_CLIENT_HTTP: {       std::cerr << "connection closed\n" ;       lws_cancel_service (lws_get_context (wsi));       *user_ctx.running = false ;     } break ;     default : break ;   }   return  lws_callback_http_dummy (wsi, reason, user, in, len); } int  main (int  argc, const  char * argv[])    lws_set_log_level (0 , nullptr );    bool  running = true ;   lws_protocols protocols[]{     {"http_client" , callback_http_client, 0 , 0 , 0 , nullptr , 0 },     LWS_PROTOCOL_LIST_TERM   };   lws* wsi = nullptr ;   UserCtx user_ctx{};   user_ctx.running = &running;   user_ctx.ua = "Foo/1.0" ;   user_ctx.accept = "*/*" ;   lws_context_creation_info ctx_info{};   ctx_info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT | LWS_SERVER_OPTION_H2_JUST_FIX_WINDOW_UPDATE_OVERFLOW;   ctx_info.port = CONTEXT_PORT_NO_LISTEN;   ctx_info.protocols = protocols;   ctx_info.user = &user_ctx;   ctx_info.connect_timeout_secs = 30 ;   lws_context* ctx = lws_create_context (&ctx_info);   if  (ctx == nullptr ) {     std::cerr << "lws init failed.\n" ;     std::terminate ();   }   lws_client_connect_info connect_info{};   connect_info.context = ctx;   connect_info.pwsi = &wsi;   connect_info.address = "httpbin.org" ;   connect_info.host = connect_info.address;   connect_info.origin = connect_info.address;   connect_info.path = "/anything" ;   connect_info.port = 443 ;   connect_info.method = "GET" ;   connect_info.protocol = protocols[0 ].name;   connect_info.ssl_connection |= LCCSCF_USE_SSL;   connect_info.ssl_connection     |= LCCSCF_H2_QUIRK_OVERFLOWS_TXCR | LCCSCF_ACCEPT_TLS_DOWNGRADE_REDIRECTS | LCCSCF_H2_QUIRK_NGHTTP2_END_STREAM;   connect_info.fi_wsi_name = "user" ;   if  (lws_client_connect_via_info (&connect_info) == nullptr ) {     std::cerr << "failed to connect\n" ;     running = false ;     lws_cancel_service (ctx);   }   while  (lws_service (ctx, 0 ) >= 0  && running) {   }   lws_context_destroy (ctx);   return  0 ; } 
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 ===== Headers ===== content-length: -> 339 content-type: -> application/json date: -> Mon, 27 May 2024 23:27:13 GMT :status -> 200 access-control-allow-origin: -> * server: -> gunicorn/19.9.0  -> http_client ==================== connected to 18.208.55.4, status 200 {   "args": {},   "data": "",   "files": {},   "form": {},   "headers": {     "Accept": "*/*",     "Host": "httpbin.org",     "User-Agent": "Foo/1.0",     "X-Amzn-Trace-Id": "Root=1-665516d1-05098c1a23b9ca0a3bb07a9e"   },   "json": null,   "method": "GET",   "origin": "114.514.1919.810",   "url": "https://httpbin.org/anything" } connection completed connection closed 
6. 总结 Libcurl 虽然 API 设计的很易用,但只能当 HTTP 客户端,而 Libwebsockets 虽然 API 设计的有些莫名其妙,但它从 HTTP 客户端,到 WebSocket 服务端都能干,所以我觉得各有各的优点吧。
补充 尝试用 Libwebsockets 发 HTTP Multipart ,用 httpbin 的 Anything 测试的话无论如何都没有成功,表现为 httpbin 貌似没有解析到我 POST 的内容;而用 Libwebsockets 的测试服务器来测试的话又是正常的,不清楚是 Libwebsockets 对 Multipart 协议的实现不够通用,但用 Curl 来测试的话无论是 httpbin 还是 Libwebsockets 的测试服务器都是正常的,所以我暂且得出不要用 Libwebsockets 做 HTTP Multipart 的结论。