libwebsockets Secure Streams 静态策略实战:从 JSON 到嵌入式编译期策略
libwebsockets Secure Streams 静态策略实战:从 JSON 到嵌入式编译期策略
本篇以 lws minimal secure streams static policy 示例为主体,讲解如何用"静态编译期策略"驱动 libwebsockets 的 Secure Streams 客户端:应用只需说出"我要哪个 streamtype",而端点、端口、TLS 信任栈、协议、重试策略等全部由 policy 数据库决定。读完本文,你可以理解示例的完整运行流程、静态策略 JSON 的每个字段如何映射为编译期 C 结构,以及如何在空间受限平台上用 policy2c 工具离线生成策略头文件。
1. 示例定位:策略与载荷严格解耦
Secure Streams 是 libwebsockets 的一个客户端 API,它严格地把连接策略(policy)与载荷(payload)解耦。用户代码只关心 stream type 名称和 payload,而一组在 lws_context 创建时设定的 policy 数据库决定了连接的一切策略——包括端点、TLS CA、甚至线上协议本身(h1、h2、ws、mqtt 等),这一点在 secure-streams 总览 中有明确说明。
本示例(位于 minimal-secure-streams-staticpolicy 目录)做的是:
- 应用访问
https://warmcat.com,读取该处的index.html; - 它使用的是由 policy2c 示例从 JSON 生成的静态 Secure Streams policy,而不是运行期动态解析的 JSON policy。
这正好对应 lws_context_creation_info 中的两种策略入口形式(见 lws-context-vhost.h):
#if defined(LWS_WITH_SECURE_STREAMS_STATIC_POLICY_ONLY)
const struct lws_ss_policy *pss_policies; /**< CONTEXT: point to first
* in a linked-list of streamtype policies prepared by user code */
#else
const char *pss_policies_json; /**< CONTEXT: point to a string
* containing a JSON description of the secure streams policies. ... */
#endif
即:在 LWS_WITH_SECURE_STREAMS_STATIC_POLICY_ONLY 编译配置下,.pss_policies 指向一个由用户代码(通常是生成代码)准备好的 struct lws_ss_policy 链表;否则 .pss_policies_json 指向运行期解析的 JSON 字符串。本示例走的是前者。
2. 构建与运行
2.1 构建
$ cmake . && make
该示例的 CMakeLists.txt 声明了明确的构建前提(第 11~17 行),这些就是"适用前提":
require_lws_config(LWS_ROLE_H1 1 requirements)
require_lws_config(LWS_WITHOUT_CLIENT 0 requirements)
require_lws_config(LWS_WITH_SECURE_STREAMS 1 requirements)
require_lws_config(LWS_WITH_SECURE_STREAMS_STATIC_POLICY_ONLY 1 requirements)
# the generated static policy carries mqtt streamtypes
require_lws_config(LWS_ROLE_MQTT 1 requirements)
require_lws_config(USE_WOLFSSL 0 requirements)
也就是说,libwebsockets 库必须启用 LWS_ROLE_H1、保留客户端支持、启用 LWS_WITH_SECURE_STREAMS 且以 LWS_WITH_SECURE_STREAMS_STATIC_POLICY_ONLY 方式构建;由于生成的静态策略里还携带了 mqtt 类型的 streamtype,LWS_ROLE_MQTT 也必须启用。
2.2 命令行选项
| Commandline option | Meaning |
|---|---|
-d <loglevel> |
Debug verbosity in decimal, eg, -d15 |
2.3 运行输出(README 记录的完整示例)
$ ./lws-minimal-secure-streams-staticpolicy
[2020/03/26 15:49:12:6640] U: LWS secure streams static policy test client [-d<verb>]
[2020/03/26 15:49:12:7067] N: lws_create_context: using ss proxy bind '(null)', port 0, ads '(null)'
[2020/03/26 15:49:12:7567] N: lws_tls_client_create_vhost_context: using mem client CA cert 914
[2020/03/26 15:49:12:7597] N: lws_tls_client_create_vhost_context: using mem client CA cert 1011
[2020/03/26 15:49:12:7603] N: lws_tls_client_create_vhost_context: using mem client CA cert 1425
[2020/03/26 15:49:12:7605] N: lws_tls_client_create_vhost_context: using mem client CA cert 1011
[2020/03/26 15:49:12:9713] N: lws_system_cpd_set: setting CPD result OK
[2020/03/26 15:49:13:9625] N: ss_api_amazon_auth_rx: acquired 588-byte api.amazon.com auth token, exp 3600s
[2020/03/26 15:49:13:9747] U: myss_state: LWSSSCS_CREATING, ord 0x0
[2020/03/26 15:49:13:9774] U: myss_state: LWSSSCS_CONNECTING, ord 0x0
[2020/03/26 15:49:14:1897] U: myss_state: LWSSSCS_CONNECTED, ord 0x0
[2020/03/26 15:49:14:1926] U: myss_rx: len 1520, flags: 1
[2020/03/26 15:49:14:1945] U: myss_rx: len 1520, flags: 0
[2020/03/26 15:49:14:1946] U: myss_rx: len 1520, flags: 0
[2020/03/26 15:49:14:1947] U: myss_rx: len 1520, flags: 0
[2020/03/26 15:49:14:1948] U: myss_rx: len 1520, flags: 0
[2020/03/26 15:49:14:1949] U: myss_rx: len 583, flags: 0
[2020/03/26 15:49:14:2087] U: myss_rx: len 1520, flags: 0
[2020/03/26 15:49:14:2089] U: myss_rx: len 1520, flags: 0
[2020/03/26 15:49:14:2090] U: myss_rx: len 1520, flags: 0
[2020/03/26 15:49:14:2091] U: myss_rx: len 1520, flags: 0
[2020/03/26 15:49:14:2092] U: myss_rx: len 1520, flags: 0
[2020/03/26 15:49:14:2093] U: myss_rx: len 583, flags: 0
[2020/03/26 15:49:14:2109] U: myss_rx: len 1520, flags: 0
[2020/03/26 15:49:14:2110] U: myss_rx: len 1520, flags: 0
[2020/03/26 15:49:14:2111] U: myss_rx: len 1520, flags: 0
[2020/03/26 15:49:14:2112] U: myss_rx: len 1520, flags: 0
[2020/03/26 15:49:14:2113] U: myss_rx: len 1520, flags: 0
[2020/03/26 15:49:14:2114] U: myss_rx: len 583, flags: 0
[2020/03/26 15:49:14:2135] U: myss_rx: len 1520, flags: 0
[2020/03/26 15:49:14:2136] U: myss_rx: len 1358, flags: 0
[2020/03/26 15:49:14:2136] U: myss_rx: len 0, flags: 2
[2020/03/26 15:49:14:2138] U: myss_state: LWSSSCS_QOS_ACK_REMOTE, ord 0x0
[2020/03/26 15:49:14:2139] N: myss_state: LWSSSCS_QOS_ACK_REMOTE
[2020/03/26 15:49:14:2170] U: myss_state: LWSSSCS_DISCONNECTED, ord 0x0
[2020/03/26 15:49:14:2192] U: myss_state: LWSSSCS_DESTROYING, ord 0x0
[2020/03/26 15:49:14:2265] E: lws_context_destroy3
[2020/03/26 15:49:14:2282] U: Completed: OK
从这份日志可以读出一次完整生命周期:context 创建时为策略中的信任栈逐条装载内存 CA 证书(using mem client CA cert ...);CPD(captive portal detection, captive portal 探测)完成并置为 OK——这与策略中定义的 captive_portal_detect streamtype 相对应;随后流状态机依次经过 CREATING → CONNECTING → CONNECTED,myss_rx 回调按分片收到整个 index.html(最后一片 flags: 2 即 LWSSS_FLAG_EOM 消息结束标志),应用收到 EOM 后主动退出事件循环,流走 QOS_ACK_REMOTE → DISCONNECTED → DESTROYING 收尾,最终打印 Completed: OK。
3. 源码走读:minimal-secure-streams.c
示例主程序 minimal-secure-streams.c 不到 300 行,是理解"静态策略客户端写法"的最佳范本。
3.1 静态策略的引入
/*
* This is example builds with a static policy autogenerated from a JSON
* policy...
*/
#include "static-policy.h"
第 55 行直接 include 由 policy2c 生成的 static-policy.h,整个策略——证书、信任栈、重试、全部 streamtype——在编译期就是常量数据。
3.2 应用私有状态与 payload 三回调
typedef struct myss {
struct lws_ss_handle *ss;
void *opaque_data;
/* ... application specific state ... */
lws_sorted_usec_list_t sul;
} myss_t;
myss_t 携带 lws_ss_handle * 与不透明数据,通过 lws_ss_info_t 中的 offsetof 偏移告诉框架如何找到自己的字段。payload 接口是三个回调:
myss_rx(第 81~99 行):打印每片 rx 数据并 hexdump;关键是判断flags & LWSSS_FLAG_EOM——收到完整消息即把bad = 0并lws_default_loop_exit(context),表示示例任务完成;myss_tx:本例是纯读页面,直接返回 0;myss_state(第 110~139 行):流状态机回调,示例在三个状态点做事:
switch (state) {
case LWSSSCS_CREATING:
/* 设置 metadata:url 里的 ${uptag} 会用它展开 */
if (lws_ss_set_metadata(m->ss, "uptag", "myuptag123", 10))
lwsl_err("%s set metadata uptag failed\n", __func__);
if (lws_ss_set_metadata(m->ss, "ctype", "myctype", 7))
lwsl_err("%s set metadata ctype failed\n", __func__);
return lws_ss_client_connect(m->ss);
case LWSSSCS_ALL_RETRIES_FAILED:
/* 重试耗尽:退出应用并标记 FAIL */
lws_default_loop_exit(context);
break;
case LWSSSCS_QOS_ACK_REMOTE:
lwsl_notice("%s: LWSSSCS_QOS_ACK_REMOTE\n", __func__);
break;
default:
break;
}
注意 LWSSSCS_CREATING 时先 lws_ss_set_metadata() 再 lws_ss_client_connect():策略中 mintest 的 url 是 index.html?uptag=${uptag},${uptag} 就是在这里设置的 metadata 名 uptag 的占位符展开机制——应用不拼 URL,只给名字赋的值。
3.3 与 lws system 状态机集成:何时建流、何时备认证
示例用 register_notifier_list 注册了一个系统状态通知器(第 141~195 行),把"创建流"这一动作挂到系统到达 LWS_SYSTATE_OPERATIONAL 的时刻,而不是在 main 里立即做:
static int
app_system_state_nf(lws_state_manager_t *mgr, lws_state_notify_link_t *link,
int current, int target)
{
switch (target) {
case LWS_SYSTATE_REGISTERED:
/* 如果 AUTH root token 还没有,就注册一个现成的 root token */
...
lws_system_blob_direct_set(ab,
(const uint8_t *)canned_root_token_payload,
strlen(canned_root_token_payload));
break;
case LWS_SYSTATE_OPERATIONAL:
if (current == LWS_SYSTATE_OPERATIONAL) {
/* We're making an outgoing secure stream ourselves */
memset(&ssi, 0, sizeof(ssi));
ssi.handle_offset = offsetof(myss_t, ss);
ssi.opaque_user_data_offset = offsetof(myss_t,
opaque_data);
ssi.rx = myss_rx;
ssi.tx = myss_tx;
ssi.state = myss_state;
ssi.user_alloc = sizeof(myss_t);
ssi.streamtype = "mintest";
if (lws_ss_create(context, 0, &ssi, NULL, NULL,
NULL, NULL)) { ... }
}
break;
}
return 0;
}
这里体现了 Secure Streams 的典型协作模式:应用只声明 ssi.streamtype = "mintest",其余一切(连到 warmcat.com:443、走 h1、GET、TLS 信任 le_via_isrg)由策略决定。LWS_SYSTATE_REGISTERED 分支则演示了 lws_system blob 机制:AUTH root token 若未置,则从一段现成字符串(示例中 canned_root_token_payload,对应策略里 api_amazon_com_auth 的 refresh token 流程)直接写入——direct_set 只存指针,被指对象必须具有足够长的生命周期。
main 里同样设置了设备身份类 blob(第 290~309 行),策略里 http_dsn_header / http_fwv_header / http_devtype_header 三个头会分别携带它们:
lws_system_blob_direct_set(lws_system_get_blob(context,
LWS_SYSBLOB_TYPE_DEVICE_SERIAL, 0),
(const uint8_t *)"SN12345678", 10);
lws_system_blob_direct_set(lws_system_get_blob(context,
LWS_SYSBLOB_TYPE_DEVICE_FW_VERSION, 0),
(const uint8_t *)"v0.01", 5);
/* ..._heap_append() 方式追加(buflist 式,可多次追加、context 销毁时释放) */
lws_system_blob_heap_append(lws_system_get_blob(context,
LWS_SYSBLOB_TYPE_DEVICE_TYPE, 0),
(const uint8_t *)"spacerocket", 11);
3.4 context 创建与静态策略的挂钩
lws_context_info_defaults(&info, NULL);
lws_cmdline_option_handle_builtin(argc, argv, &info);
...
info.fd_limit_per_thread = LWS_FD_LIMIT_PER_THREAD_MIN + 6;
info.port = CONTEXT_PORT_NO_LISTEN;
...
info.pss_policies = &_ss_static_policy_entry;
info.options = LWS_SERVER_OPTION_EXPLICIT_VHOSTS |
LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT;
第 263~265 行是静态策略模式的两行关键配置:
info.pss_policies = &_ss_static_policy_entry;——把生成头里#define _ss_static_policy_entry _ssp_api_amazon_com_auth定义的链表头(见 static-policy.h 第 1587 行)挂进 context 信息;LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT——由于本进程自己完成 TLS 客户端连接(不经过 ss proxy),需要自行完成 TLS 库全局初始化。源码头注释也说明:同一份源码可以编成"自带静态策略"和"走 -proxy 示例"两种形态,走 proxy 的形态甚至不初始化 TLS 库,因为代理侧包办一切。
事件循环是最朴素的形态:
while (n >= 0)
n = lws_service(context, 0);
lws_context_destroy(context);
lwsl_user("Completed: %s\n", bad ? "failed" : "OK");
myss_rx 收到 EOM 后 lws_default_loop_exit() 让 lws_service 返回 -1,进程以 bad(0 = OK)为退出码结束。
4. static-policy.json:策略数据库的结构
static-policy.json 是"唯一事实源",policy2c 从它生成 C 代码。整体分五大块:
{
"release": "01234567",
"product": "myproduct",
"schema-version": 1,
"retry": [ { "default": {
"backoff": [1000, 2000, 3000, 5000, 10000],
"conceal": 5,
"jitterpc": 20,
"svalidping": 30,
"svalidhup": 35
} } ],
"certs": [ { "isrg_root_x1": "MIIFaz..." }, ... ],
"trust_stores": [
{ "name": "le_via_isrg", "stack": ["isrg_root_x2", "LEX3_isrg_root_x2"] },
{ "name": "api_amazon_com", "stack": ["digicert_global_ca_g2", "digicert_global_root_g2"] },
{ "name": "avs_via_starfield", "stack": ["starfield_class_2_ca", "starfield_services_root_ca"] },
{ "name": "mqtt_amz_iot", "stack": ["amazon_root_ca_1", "starfield_class_2_ca", "starfield_services_root_ca"] }
],
"s": [ ... 各 streamtype 定义 ... ]
}
retry:命名的重试策略集。default的backoff是毫秒级退避表(最多 5 级),conceal是"掩盖"重试次数的计数,jitterpc是抖动百分比,svalidping/svalidhup分别是距最近有效 ping / 有效挂起的秒数上限(空闲判活依据);certs:base64 编码的 DER 形式 X.509 证书池,每条以名字(如isrg_root_x2)索引。示例里只内置了实际用到的几个根/中间 CA——这正是嵌入式场景的做法:不依赖操作系统 CA 库,信任栈白名单化;trust_stores:把若干 certs 按名字组合成具名信任栈(如le_via_isrg),供 streamtype 以tls_trust_store引用;s:streamtype 数组。示例覆盖了 11 个 streamtype:api_amazon_com_auth、avs_event/avs_metadata/avs_audio(Alexa 语音服务的 h2 长轮询与 multipart 流)、mintest、h2longpolltest、mintest-fail、minpost、mqtt_test/mqtt_test1、captive_portal_detect。本应用真正使用的是mintest,其余条目用于展示策略的表达能力(也解释了 CMake 为何要求LWS_ROLE_MQTT)。
本例使用的 mintest 条目(static-policy.json 第 116~137 行):
"mintest": {
"endpoint": "warmcat.com",
"port": 443,
"protocol": "h1",
"http_method": "GET",
"http_url": "index.html?uptag=${uptag}",
"http_dsn_header": "x-dsn:",
"http_fwv_header": "x-fw-version:",
"http_devtype_header": "x-devtype:",
"metadata": [
{ "uptag": "X-Upload-Tag:" },
{ "ctype": "Content-Type:" },
{ "xctype": "X-Content-Type:" }
],
"plugins": [],
"tls": true,
"opportunistic": true,
"retry": "default",
"tls_trust_store": "le_via_isrg"
}
字段含义逐一对应到 struct lws_ss_policy(定义见 lws-secure-streams-policy.h 第 226~366 行):
| JSON 字段 | 作用 | 对应 C 结构成员 |
|---|---|---|
endpoint / port |
要连接的 DNS 名与端口 | .endpoint / .port |
protocol |
"h1" / "h2" / "h3" / "ws" / "mqtt" 等 |
.protocol(0=LWSSSP_H1,1=LWSSSP_H2…) |
http_method / http_url |
请求方法与 url,url 支持 ${name} 占位符 |
.u.http.method / .u.http.url |
http_dsn_header 等 |
用 lws_system blob(设备序列号/固件版本/设备类型)填充的请求头名 |
.u.http.blob_header[LWSSS_HBI_DSN/FWV/TYPE] |
metadata |
声明允许设置哪些 metadata、以及它们是否同时作为 HTTP 头发出(值如 "X-Upload-Tag:") |
.metadata 链表 / .metadata_count |
tls / tls_trust_store |
强制 TLS 并指定信任栈 | .flags 的 TLS 位 + .trust.store |
opportunistic |
连接按需惰性建立:不主动连,客户端要写时才连 | .flags 的 OPPORTUNISTIC 位 |
retry |
引用 retry 数组里的具名策略 |
.retry_bo |
http_auth_header / http_auth_preamble |
认证头与 Bearer 之类前缀(avs 系列使用) |
.u.http.blob_header[LWSSS_HBI_AUTH] / .u.http.auth_preamble |
long_poll / nailed_up |
长轮询、连接"钉住"整个流生命周期 | .flags 的 LONG_POLL / NAILED_UP 位 |
http_multipart_name / http_mime_content_type |
multipart 段名与 content-type | .u.http.multipart_* |
rideshare |
复用另一个已存在 streamtype 的连接 | .rideshare_streamtype |
5. static-policy.h:生成 C 的形态与 flags 解码
static-policy.h 文件头注明 "Autogenerated from the following JSON policy",#if 0 块内完整保留了源 JSON(含 Original JSON size: 15493 标注),其后的内容依次是:
① 重试策略——JSON retry.default 一比一映射为 lws_retry_bo_t(第 229~239 行):
static const uint32_t _rbo_bo_0[] = {
1000, 2000, 3000, 5000, 10000,
};
static const lws_retry_bo_t _rbo_0 = {
.retry_ms_table = _rbo_bo_0,
.retry_ms_table_count = 5,
.conceal_count = 5,
.secs_since_valid_ping = 30,
.secs_since_valid_hangup = 35,
.jitter_percent = 20,
};
② 证书与信任栈——每个 base64 证书被解码为 DER 字节数组 _ss_der_<name><a href="https://link.gitcode.com/i/7822ffde7824cefcc20380491e6d6828" target="_blank">],并包进 lws_ss_x509_t(见 [lws-secure-streams-policy.h 第 73~79 行 的 vhost_name / ca_der / ca_der_len 三字段);信任栈则用 lws_ss_trust_store_t 把若干 x509 指针按 stack 顺序装进 ssx509[6] 数组,例如:
static const lws_ss_trust_store_t _ss_ts_mqtt_amz_iot = {
.name = "mqtt_amz_iot",
.ssx509 = {
&_ss_x509_starfield_services_root_ca,
&_ss_x509_starfield_class_2_ca,
&_ss_x509_amazon_root_ca_1,
}
};
③ streamtype 策略链表——每个 JSON s 条目变成一个 lws_ss_policy 实例(_ssp_mintest、_ssp_avs_event 等),用 .next 指针串成链,且注意生成顺序与 JSON 中相反:链表尾是 api_amazon_com_auth,链表头经宏指向它。以 mintest 为例(static-policy.h 第 1488~1505 行):
_ssp_mintest = {
.next = (void *)&_ssp_h2longpolltest,
.streamtype = "mintest",
.endpoint = "warmcat.com",
.metadata = (void *)&_md_mintest_uptag,
.u = {
.http = {
.method = "GET",
.url = "index.html?uptag=${uptag}",
}
},
.retry_bo = &_rbo_0,
.flags = 0x11,
.port = 443,
.metadata_count = 3,
.protocol = 0,
.trust = {.store = &_ss_ts_le_via_isrg},
},
④ 入口宏与占用估算(文件尾两行):
#define _ss_static_policy_entry _ssp_api_amazon_com_auth
/* estimated footprint 10720 (when sizeof void * = 64-bit) */
整个策略数据库的静态占用约 10.7 KB——这就是"静态策略"模式在空间受限平台上成立的原因:无运行期 JSON 解析器、无字符串查找表,只有 const 数据。
flags 位解码:.flags 是 LWSSSPOLF_* 位集合(定义见 lws-secure-streams-policy.h 第 81~144 行)。以 mintest 为例,0x11 = OPPORTUNISTIC(1<<0) | TLS(1<<4),与 JSON 的 "opportunistic": true, "tls": true 完全对应;h2longpolltest 的 0x32 = NAILED_UP | TLS | LONG_POLL,对应 JSON 的 nailed_up + tls + long_poll;api_amazon_com_auth 的 0x1291 = X_WWW_FORM_URLENCODED | H2_QUIRK_OVERFLOWS_TXCR | HTTP_NO_CONTENT_LENGTH | TLS | OPPORTUNISTIC,对应 JSON 里的 http_www_form_urlencoded、h2q_oflow_txcr、http_no_content_length、tls、opportunistic 五个布尔量。对照这两处即可掌握任意策略条目的"翻译规则"。
6. 用 policy2c 重新生成策略头
示例自带的 static-policy.h 是随仓库提交的成品,实际维护流程由姊妹示例 minimal-secure-streams-policy2c 完成:它从 stdin 读入 JSON 策略,把等价 C 结构打印到 stdout。按其 README:
$ cmake . && make
$ cat mypolicy.json | lws-minimal-secure-streams-policy2c
stdout 输出即上文第 5 节所示形态的代码(_rbo_0、_ss_der_*、_ss_x509_*、信任栈与 _ssp_* 链表)。policy2c 依赖 libwebsockets 同时启用 LWS_ROLE_H1、LWS_ROLE_H2、LWS_ROLE_WS、LWS_ROLE_MQTT(它必须能处理任何策略内容)。工作流因此是:
- 在 static-policy.json 中维护/修改策略(加 streamtype、换 CA、调 backoff);
- 运行 policy2c 重新生成
static-policy.h; - 重新构建应用,
#include "static-policy.h"即可生效。
这样既保留了"以 JSON 为唯一事实源、便于人工审阅与维护"的优点,又让目标平台完全不付出运行期解析的成本。
7. 适用场景与注意事项
结合 CMakeLists.txt 的约束与源码注释,可以归纳出该模式的适用边界:
- 空间受限 / 无文件系统平台:策略是纯 const 数据,无需运行期 JSON 解析与文件读取,
LWS_WITH_SECURE_STREAMS_STATIC_POLICY_ONLY构建下 context 只接受pss_policies链表而不接受 JSON 字符串; - 策略白名单化:CA 证书池与信任栈在生成时固化,等价于"最小信任面",适合 IoT 设备只访问少数固定端点;
- 需要 TLS:示例用
LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT自行完成 TLS 全局初始化(不经过 ss proxy 的形态); - 同一源码双形态:源码注释(第 14~21 行)说明该文件也可编成走
-proxy示例的形态,此时应用侧不初始化 TLS,由 proxy 完成连接——即静态策略客户端既可"自带策略直连",也可"零策略连代理"。
调试时 -d15 级别日志能把 CPD 探测、TLS 信任栈装载、认证 token 获取、流状态迁移与 rx 分片细节全部呈现(见第 2.3 节输出),配合 myss_rx 中的 lwsl_hexdump_info() 即可定位到字节级问题。示例结束时以 bad 变量(收到 EOM 前为 1)作为进程退出码,方便脚本化验证。