Libwebsockets 实现简单 HTTP 客户端

Libwebsockets 实现简单 HTTP 客户端

RayAlto OP

之前给 Libwebsockets 包装过一个 WebSocket 客户端,当时并没有深究 Libwebsockets 的设计,只是照着 Minimal Example 和 Github Issue 抄的,所以设计的一塌糊涂,这次打算重新看一看 Libwebsockets 的设计,包装一个 HTTP 客户端。

1. 各 Struct 关系

Libwebsockets 客户端模式各 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{};
// 初始化 SSL 工具等
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;
// 传入上面定义的 Protocol
ctx_info.protocols = protocols;
// 这里可以传入 Per Instance 数据,由用户自行管理的那种
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); // shut the fuck up

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) {

// 可以拿到 Context
lws_context* ctx = lws_get_context(wsi);
// 可以拿到创建 Context 时放进去的用户数据
UserCtx& user_ctx = *reinterpret_cast<UserCtx*>(lws_context_user(ctx));

// 典中典的 switch + 114514 个 case
switch (reason) {
case LWS_CALLBACK_CLIENT_CONNECTION_ERROR: {
// 连接失败
} break;

case LWS_CALLBACK_ESTABLISHED_CLIENT_HTTP: {
// 连接成功
} break;

case LWS_CALLBACK_CLIENT_APPEND_HANDSHAKE_HEADER: {
// 添加 HTTP Header
} break;

case LWS_CALLBACK_CLIENT_HTTP_REDIRECT: {
// 遇到了 30X 跳转
} break;

case LWS_CALLBACK_CLIENT_FILTER_PRE_ESTABLISH: {
// 读取响应的 HTTP Header
} break;

case LWS_CALLBACK_RECEIVE_CLIENT_HTTP: {
// 准备读取响应的 Body
return 0;
} break;

case LWS_CALLBACK_RECEIVE_CLIENT_HTTP_READ: {
// 开始读取响应的 Body
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];
// 把对方的 IP 放进 buf 里
lws_get_peer_simple(wsi, buf, sizeof(buf));
// 拿到 HTTP Status Code
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 就知道这是何等的脱裤子放屁。

比如添加 AcceptUser-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) {
// 浪费 CPU 周期
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) {
// 浪费 CPU 周期
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. 完整版

点击展开:HTTP 客户端完整代码
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);
}

/* NOLINTNEXTLINE(misc-unused-parameters) */
int main(int argc, const char* argv[]) {
lws_set_log_level(0, nullptr); // shut up
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 的结论。