第 4 章 / 共 51 章
卷一 · 起步与心智模型

最小应用解剖:两个文件构成一个服务

一个能跑、能响应请求、能回退静态文件的 XServer 应用,最小只要 xs.json 和 main.c 两个文件。本章把它们逐行拆开——这是你之后所有代码的地基。

本章导读

站位:把第 2 章"跑起来的 demo"缩小到最小可理解单元。读完你能不看书写出一个空白 XServer 应用。已经写过 demo 路由的读者重点看 4.4(三态)与常见错误①——它是新手最高频的坑。

4.1 引入

多数框架的 "hello world" 要穿过五层抽象:入口注册 → 路由匹配 → 中间件链 → 上下文对象 → 响应序列化。XServer 的哲学相反:先把协议的最小事实交到你手上——解析好的请求头、传输层指针、分帧状态机——然后退开。于是最小应用真的"最小":

xs.json 声明"在哪监听、脚本在哪" (装配事实) main.c 声明"请求来了做什么" (业务事实) └──── 两份事实,一个服务 ────┘

下面逐行解剖这两份事实。它们取自 demo-single 的真实文件,只是把业务部分裁掉了。

4.2 xs.json 逐行

xs.json 实机验证 出处:release/demo-single/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 逐行

main.c 实机验证 最小化自 demo-single/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 章)
三态是互斥且必须的:返回 XS_OK 之前必须真的写完响应;已经往连接写过字节就不能再返回 XS_FALLBACK(静态层会再写一份,破坏报文)。驱动会校验脚本声明消费的字节与线路实际的一致性,越界即断连。

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 除外)都在你的责任区内。

常见错误

忘记写 return(未定义行为)
症状
部分请求"随机"异常:偶发空响应、连接挂起或被断开。
原因
非 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 章完整封装一遍) */
写了一半响应又返回 FALLBACK
症状
响应体里混入文件内容,浏览器报解码错误。
原因
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 行为符合预期。
进阶/time 接口

提示:用 C 标准库的 time() 与 strftime()(TCC 环境可用);响应构造可先用一个极简头(Content-Length 必须正确)过渡,第 15 章再正规化。

验收:curl -s http://127.0.0.1:9080/time 返回当前时间且报文可被 curl 正确解析(无 trailing 垃圾)。
挑战/echo 雏形

把 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 延伸阅读