最小应用解剖:两个文件构成一个服务
一个能跑、能响应请求、能回退静态文件的 XServer 应用,最小只要 xs.json 和 main.c 两个文件。本章把它们逐行拆开——这是你之后所有代码的地基。
本章导读
4.1 引入
多数框架的 "hello world" 要穿过五层抽象:入口注册 → 路由匹配 → 中间件链 → 上下文对象 → 响应序列化。XServer 的哲学相反:先把协议的最小事实交到你手上——解析好的请求头、传输层指针、分帧状态机——然后退开。于是最小应用真的"最小":
下面逐行解剖这两份事实。它们取自 demo-single 的真实文件,只是把业务部分裁掉了。
4.2 xs.json 逐行
{
"services": [
{
"enabled": true, /* 服务开关 */
"class": "http", /* 协议类:http/ws/tcp/udp/custom */
"name": "demo", /* 全局唯一名,xsServerFind 的键 */
"ip": "127.0.0.1",
"port": 9080,
"host_default": { /* 默认主机:vhost 未命中时回落到这里 */
"enabled": true,
"name": "app",
"path": "wwwroot", /* 静态资源根 */
"devlang": "c", /* c = TCC 脚本;static = 纯静态 */
"devfile": "main.c" /* 脚本入口(相对配置目录) */
}
}
]
}
刚才发生了什么:这份配置声明了一个事实链——在 127.0.0.1:9080 监听 HTTP,未匹配其他 vhost 的请求交给默认主机 app,app 的静态根是 wwwroot,业务逻辑在 main.c。任何字段写错(如 class 拼错、port 越界),启动会在 config loaded 之前失败并点名具体字段——完整字段表见指南 · 配置参考,卷二逐字段展开。
4.3 main.c 逐行
#include <xsbase.h> /* 契约头:自动拉入 xrt_decl.h 与 libtcc.h(都在 VFS 里)*/
/* 本代脚本装载后调用一次(装配 / reload 控制线程)*/
static void ServiceInit(XS_HostInfo* pHost)
{
printf("[%s] service init\n", pHost->Name);
}
/* 每个请求一次(引擎 worker 线程,可并发)*/
static XS_RequestResult RequestProc(XS_HttpReq* pReq)
{
/* 没处理的请求 → 落回静态层(wwwroot),其余 404 */
return XS_FALLBACK;
}
/* 本代退役时调用(GC 线程,全生命周期至多一次)*/
static void ServiceUnit(XS_HostInfo* pHost)
{
printf("[%s] service unit\n", pHost->Name);
}
刚才发生了什么:
#include <xsbase.h>不需要任何安装——这颗头文件连同 xrt 全量声明都在 exe 的 VFS 里,TCC 直接读取;- 宿主按符号名查找
ServiceInit / RequestProc / ServiceUnit(不认 static——TCC 内存模式下符号可见),全部可选导出; - 三个函数恰好对应第 3 章的代的三个时刻:诞生 / 服务 / 退役。
把它跑起来(输出为实机结果):
$ ./xs [xs] config loaded: .../xs.json (1 servers) [xs] script loaded: .../main.c (0.4 KB gen 1) [app] service init [xs] server 'demo' http bound on 127.0.0.1:9080 [xs] running, press Ctrl+C to stop $ curl -s http://127.0.0.1:9080/ <!DOCTYPE html> ... /* wwwroot/index.html —— 静态层接管了 / */
4.4 三态返回
RequestProc 的返回值不是"成功 / 失败",而是谁负责这个请求的余生:
| 返回值 | 语义 | 驱动随后做什么 |
|---|---|---|
XS_OK | 我已写出完整响应 | 消费掉 body 余量,继续 keep-alive 循环 |
XS_FALLBACK | 我没处理 | GET/HEAD 落入静态层;其他方法 404 |
XS_TAKEOVER | 我要接管整条连接 | 撤出驱动,应用自行收发并最终 Close(第 25 章) |
4.5 静态层回退
XS_FALLBACK 之后,宿主的静态层接手:在 host 的 path(这里是 wwwroot)里找文件,GET 取内容、HEAD 只发头,percent-decode、目录索引(默认 index.html)、点开头文件默认拒绝。/ 请求就是这么拿到 index.html 的。静态层的四个旋钮(自定义头、错误页、目录索引、deny_dotfiles)在卷二第 10 章展开。
这个"脚本优先、静态兜底"的组合,让你不必为静态资源写任何代码——也不必为动态路由配置任何重写规则。
4.6 契约
- 符号线:契约符号按名查找、全部可选——不导出 RequestProc 的 host 就是一个纯静态站。
- 所有权线:
XS_HttpReq及其内含指针只在回调栈内有效;本例直接 return 无风险,一旦开始使用视图就要遵守第 3 章的规则。 - 响应线:返回 XS_OK 意味着"完整响应已写出"——头 + 正文(HEAD 除外)都在你的责任区内。
常见错误
- 症状
- 部分请求"随机"异常:偶发空响应、连接挂起或被断开。
- 原因
- 非 void 函数漏写 return 是 C 的未定义行为——返回值是栈上的随机残留,三态被随机解释。
static XS_RequestResult
RequestProc(XS_HttpReq* pReq)
{
if ( path is mine ) ...;
/* 漏 return:残留值随机命中三态 */
}static XS_RequestResult
RequestProc(XS_HttpReq* pReq)
{
if ( path is mine ) return XS_OK;
return XS_FALLBACK; /* 显式收尾 */
}- 症状
- 客户端解析失败;keep-alive 下后续请求错乱。
- 原因
- 直接往
pReq->tcp拼字符串当响应——Content-Length、HEAD 语义、连接状态机都没有被正确维护。
xrtNetStreamSend(pReq->tcp,
"HTTP/1.1 200 OK\n\nhello", 22);
return XS_OK; /* 报文不完整、无长度 *//* 用 xrtHttp1ResponseWrite 生成头
* (第 15 章完整封装一遍) */- 症状
- 响应体里混入文件内容,浏览器报解码错误。
- 原因
- XS_FALLBACK 会让静态层继续写同一连接;此前你写的半截字节成了报文前缀。
ConnSend(pReq, head, n);
if ( !hasBody ) return XS_FALLBACK;/* 决策在写之前完成:
* 要么完整响应 + XS_OK,
* 要么什么都不写 + FALLBACK */编程练习
不看本章,新建目录写出 xs.json + main.c,跑通后用 curl 验证 / 返回静态页、/nope 返回 404。
service init 与 gen 1;两个 curl 行为符合预期。提示:用 C 标准库的 time() 与 strftime()(TCC 环境可用);响应构造可先用一个极简头(Content-Length 必须正确)过渡,第 15 章再正规化。
curl -s http://127.0.0.1:9080/time 返回当前时间且报文可被 curl 正确解析(无 trailing 垃圾)。把 POST 到 /echo 的请求体原样返回。提示:读取 pReq->body(定长 body 的读取 API 在 xrt:xrtHttp1BodyRead);读不完整可以先支持小 body。
curl -d "hello xs" .../echo 原样返回 hello xs;50 字节与 5000 字节各测一次。小结
| 要点 | 结论 |
|---|---|
| 最小应用 | xs.json(装配事实)+ main.c(业务事实),仅此两件 |
| 契约符号 | 按名查找、全部可选;Init/Request/Unit 对应代的三个时刻 |
| 三态返回 | XS_OK 已响应 / FALLBACK 归静态层 / TAKEOVER 接管连接 |
| 静态层 | FALLBACK 后接管 GET/HEAD;四旋钮卷二展开 |
| 头文件 | xsbase.h 及全部声明在 exe 的 VFS 内,零安装 |
- xrt 参考 · HTTP/1.1 — ResponseWrite / BodyRead 的完整签名
- xrt 教程 · HTTP 服务端 — 库层面如何组装 HTTP 服务(对照 XServer 宿主为你做的部分)