快速开始
三个动作:构建宿主 → 写一份 xs.json 与 C 应用源码 → 运行。
1. 构建宿主
需要 GCC(可用 --cc 覆盖)与 Python 3。位置参数选择扩展变体,Windows / Linux 同一套流程:
build.bat /* 最小变体 */
build.bat sqlite xtp /* 按名挑选:SQLite 与 XTP2 */
build.bat all /* 全部十个可选库一键编入 */
build.sh all /* Linux 等价 */
xs --version /* 版本与已编入扩展清单 */
链接成功才原子替换产物 —— 构建失败永远保留上一个可用的 xs。产物是单文件可执行程序,TCC 编译器与全套 SDK 头已内嵌其中。
2. 最小配置与应用
/* xs.json —— 取自 demo-single 的真实配置 */
{
"services": [
{
"enabled": true,
"class": "http",
"name": "demo",
"ip": "127.0.0.1",
"port": 9080,
"host_default": {
"enabled": true,
"name": "app",
"path": "wwwroot",
"devlang": "c",
"devfile": "main.c"
}
}
]
}
/* main.c —— 最小可运行脚本 */
#include <xsbase.h>
static void ServiceInit(XS_HostInfo* pHost)
{
printf("[%s] service init\n", pHost->Name);
}
static XS_RequestResult RequestProc(XS_HttpReq* pReq)
{
/* 未处理的请求落回静态层(wwwroot),其余 404 */
return XS_FALLBACK;
}
3. 运行
./xs /* 默认读取应用目录下的 xs.json */
./xs my.json /* 指定配置文件 */
启动日志:[xs] config loaded → engine started → script loaded → 各端口 bound → [xs] running。浏览器访问 http://127.0.0.1:9080/ 即得到 wwwroot 下的静态页面。
应用结构
一个 XServer 应用 = 宿主 exe + 一个源码目录。目录约定由你自己定义,以下是 demo-single 验证过的组织方式:
myapp/
├── xs.exe /* 唯一的二进制(5.2MB,含编译器与 SDK)*/
├── xs.json /* 服务 · 端口 · TLS · vhost */
├── main.c /* 应用入口:include 编排 + ServiceInit/Unit */
├── route.h /* 路由注册清单 */
├── modules/ /* 复用模块(http/db/template/resource…)*/
├── route_http/ /* 请求处理器 */
├── page/ /* 受控页面(wwwroot 之外,防直链)*/
├── template/ /* xrtTemplate 模板 */
├── db/ /* SQLite 数据文件 */
└── wwwroot/ /* 静态资源根 */
全部源文件最终合成一个翻译单元(main.c include 一切),由内嵌 TCC 编译为原生机器码。静态变量按脚本代隔离:热重载会创建一套新的静态变量,旧代在全部请求离开后才执行 ServiceUnit —— 这正是热重载的语义基础。
dev_inc 目录、第三方库放 dev_lib 目录(xs.json 中声明),TCC 会自动搜索;#pragma comment(lib, ...) 可直接引用。配置参考(xs.json)
配置装载 fail-fast:任何字段非法都在启动时给出具体字段名的错误并退出,不带病运行。
根级字段
| 字段 | 类型 / 约束 | 说明 |
|---|---|---|
engine.workers | uint,0–1024 | 引擎工作线程数,0 = xrt 默认。热重载不可变,变更会被拒绝 |
services | 数组,必填 | 服务清单,缺失即启动失败 |
服务级字段(services[i])
| 字段 | 类型 / 约束 | 说明 |
|---|---|---|
enabled | bool,默认 true | 是否启用 |
class | 白名单 | http / ws / tcp / udp / custom |
name | 必填、全局唯一 | xsServerFind 的键 |
ip | 默认 0.0.0.0 | 监听地址 |
port | 1–65535,非 custom 必填 | 明文端口 |
tls | bool | 启用 TLS 端口(udp 上禁止) |
port_tls | 默认 443 | 与明文端口同 IP 冲突时拒绝启动 |
ip_tls | 缺省复用 ip | TLS 监听地址 |
backlog | uint32 | 直通 xrt 监听配置 |
recv_limit | ≤SIZE_MAX;udp ≤65535 | 流读上限 |
path / devlang / devfile / dev_inc / dev_lib | — | 服务级脚本缺省,合成进 DefaultHost |
主机级字段(host_default / hosts[i])
| 字段 | 类型 / 约束 | 说明 |
|---|---|---|
enabled | bool | 是否启用 |
name | server 内唯一、非空 | 主机名 |
host | 分号分隔域名 | http/ws 的启用 vhost 必填非空;路由按 Host 头 / SNI 匹配 |
path | 路径 | 静态资源 / 脚本根 |
devlang | static / c / 自定 | static = 纯静态;c = TCC 脚本 |
devfile | 路径 | 脚本入口文件 |
tls_ca / tls_cert / tls_key | PEM 路径 | host 级证书 → SNI 选择器数据源,支持热替换 |
dev_inc / dev_lib | 分号分隔目录 | 脚本额外 include / 库搜索路径(相对 appPath) |
应用自读旋钮(Custom 字段)
不弹出预设结构、键保留给应用读取(经 server->Custom / xsConfigRoot())。宿主与 demo 惯例:
| 字段 | 位置 | 约束 / 默认 |
|---|---|---|
header_limit | server | 请求头上限 |
path_limit | server | 路径长度上限,默认 2048 |
body_limit | server | 请求体上限(超限 413) |
idle_timeout | server | 空闲超时(http/ws/tcp),到点服务端主动关闭 |
ws_message_limit | server | WS 消息上限,超限以 close code 1009 关闭 |
ws_protocol | server | 强制要求的子协议(启动时校验合法性) |
static.headers | host | ≤8 字段,禁 8 种保留头(含 Transfer-Encoding) |
static.index | host | 目录索引数组,逐个回落(默认 index.html) |
static.error_pages | host | 400–599 状态码 → 相对路径自定义错误页 |
static.deny_dotfiles | host | 默认 true,点开头文件恒拒(含穿越) |
所有文件路径按配置文件所在目录转绝对路径 —— 配置目录自包含,可整体搬移。
脚本契约
宿主按符号名在编译产物中查找回调,全部可选导出、按需声明。这就是 XServer 与应用的全部约定 —— 没有基类、没有注册宏。
| 符号 | 适用协议 | 签名(xsbase.h) |
|---|---|---|
ServiceInit | 全部 | void (XS_HostInfo*) 装载后初始化 |
ServiceUnit | 全部 | void (XS_HostInfo*) 退役析构,全生命周期至多一次 |
ServiceSwap | 全部 | bool (XS_HostInfo*, xvalue**) 换代前导出状态快照 |
RequestProc | http/https | XS_RequestResult (XS_HttpReq*) 请求入口 |
WsOpen / WsText / WsBinary | ws/wss | (XS_HostInfo*, xwsstream*[, 消息视图]) |
WsPing / WsPong / WsClose | ws/wss | 同上;Close 含 uint16 状态码与原因视图 |
EventOpen / EventData / EventClose | tcp/tcps | (XS_HostInfo*, XS_StreamConn*[, xnetbuf*/结果]) |
EventDgram | udp | (XS_HostInfo*, xnetudp*, const xnetudpmessage*) |
RequestProc 三态返回
| 返回值 | 语义 |
|---|---|
XS_OK | 已写出完整响应,驱动消费 body 余量后继续 keep-alive |
XS_FALLBACK | 未处理:GET/HEAD 落入静态层,其余 404 |
XS_TAKEOVER | 应用接管连接:用 pull/Future 自行收发并最终 Close;Close 终态由 XServer 释放 lease |
HTTP 开发
XServer 不包装对象模型:XS_HttpReq 是一组 xrt 指针的视图结构体,你直接使用 xrt 全套 API。
| 字段 | 说明 |
|---|---|
tcp / tls | 传输层裸指针,二选一非空 —— 直接可用 xrt 全套流/发送 API |
head | 解析后的请求头(含 MethodCode);字段视图仅在回调内有效 |
body | 分帧状态机:定长 / chunked / 关闭 三种模式,xrtHttp1BodyRead 增量读取 |
host / server | 归属信息(vhost 路由结果) |
响应构造
响应由应用自行构造 —— xrtHttp1ResponseWrite 生成头、xrtHttp1ChunkWrite 分块、xrtNetStreamSend / xrtTlsStreamSend 发送。下面是 demo 的真实实现(节选):
/* modules/http.h —— 完整响应:Content-Length + Content-Type 统一生成,
* HEAD 保留 Content-Length 但不发正文 */
static bool ReplyRaw(XS_HttpReq* pReq, uint16 iStatus,
const char* sContentType, const void* pBody, size_t iBodySize)
{
char arrHead[1024], arrLength[32];
xhttpfield arrField[12];
size_t iFieldCount = 0, iHeadSize = 0;
snprintf(arrLength, sizeof(arrLength), "%llu",
(unsigned long long)iBodySize);
arrField[iFieldCount].Name = XRT_STR_LITERAL("Content-Length");
arrField[iFieldCount].Value = xrtStrView(arrLength);
iFieldCount++;
if ( !xrtHttp1ResponseWrite(XHTTP_VERSION_1_1, iStatus,
xrtHttpStatusText(iStatus), arrField, iFieldCount,
arrHead, sizeof(arrHead), &iHeadSize) ) return false;
if ( !ConnSend(pReq, arrHead, iHeadSize) ) return false;
return pReq->head->MethodCode == XHTTP_METHOD_HEAD ||
ConnSend(pReq, pBody, iBodySize);
}
路由(应用层模式)
宿主不内置路由器 —— 每个 host 一个 RequestProc,URL 分发由应用自建。demo-single 验证的模式:静态表(xrtMap)+ 动态 pattern(xrtPatternCompileMany)+ 方法槽:
AddStaticRouteHTTP("/api/v1/items", XHTTP_METHOD_GET, Handle_API_List);
AddDynamicRouteHTTP("/api/v1/items/{id}",
XHTTP_METHOD_PUT | XHTTP_METHOD_PATCH, Handle_API_Item_Update);
/* 注册完成后一次性编译,匹配耗时与路由数无关 */
(void)RouteHTTP_Compile();
- 方法掩码可自由组合,
XHTTP_METHOD_CRUD/XHTTP_METHOD_ANY为聚合掩码 - 匹配顺序:静态精确 → 动态 pattern → 方法槽;未命中返回
XS_FALLBACK落回 wwwroot - 万级路由实测:10 条 42.0µs → 10,000 条 59.1µs(基准详情)
vhost 与 TLS
Host 头 / absolute-form / authority-form 三来源路由;HTTP/1.1 强制 Host,未匹配回落默认 host。TLS 按 SNI 选择 host 级证书,服务端 Cookie 把 ClientHello 身份选择与 Accept 绑定到同一代。证书支持热替换(锁内原子换表)。
WebSocket 开发
XServer 完成握手校验(失败 400)并 xrtWsStreamAttach,掩码、分片重组、控制帧、permessage-deflate 全部在 xrt 层处理 —— 你只写业务回调:
static void WsOpen(XS_HostInfo* pHost, xwsstream* pWs)
{
(void)xrtWsStreamText(pWs, xrtStrView("connected"));
}
static void WsText(XS_HostInfo* pHost, xwsstream* pWs, xstrview tText)
{
(void)xrtWsStreamText(pWs, tText); /* echo */
}
- 发送:
xrtWsStreamText / xrtWsStreamBinary(另有 Ref / Take 所有权变体) - 限制:
ws_message_limit超限以 close 1009 关闭;ws_protocol可强制子协议 - 未导出 WsText/WsBinary 的 host 返回 503;vhost 路由失败 404;头超限 431
- 空闲扫描用跨线程安全的 Abort;累积器超 256KB 在消息结束后释放
TCP 流服务
class = tcp(TLS 由服务级 tls 开关决定 tcp / tcps)。XS_StreamConn 是视图结构体:tcp 与 tls 二选一非空,按非空一侧使用对应发送 API(tcps 下禁止绕过 tls 直写)。
static void EventOpen(XS_HostInfo* pHost, XS_StreamConn* pConn)
{
/* 连接建立即发欢迎横幅 */
if ( pConn->tcp != NULL )
(void)xrtNetStreamSend(pConn->tcp, "hello\n", 6);
}
static void EventData(XS_HostInfo* pHost, XS_StreamConn* pConn, xnetbuf* pBuffer)
{
/* pBuffer 为零拷贝缓冲链,仅回调内借用;
* 尺寸查询 xrtNetBufSize(pBuffer),数据经 xrt 缓冲 API 消费 */
}
static void EventClose(XS_HostInfo* pHost, XS_StreamConn* pConn,
xnetresult iResult, const xerror* pError)
{
/* 连接生命周期由 xs 持有,应用不负责 Destroy */
}
- 连接生命周期:Accept 接管 → 注册表 → Close 时析构,全部由 XServer 驱动持有
idle_timeout到点服务端主动关闭;recv_limit小于默认读块时自动收缩- host 证书构建 SNI 表,每连接按 SNI 出证书
UDP 服务
class = udp,最小驱动:bind → EventDgram 透传。每个数据报都经监听槽位取当前代 lease,无连接状态。
static void EventDgram(XS_HostInfo* pHost, xnetudp* pUdp,
const xnetudpmessage* pMsg)
{
/* pMsg->Remote 来源地址 · pMsg->Data/pMsg->Size 负载
* pMsg->Flags 含 XNET_UDP_MESSAGE_TRUNCATED 截断标志 */
(void)xrtNetUdpSendTo(pUdp, &pMsg->Remote,
"ack", 3); /* 回发到来源地址 */
}
recv_limit 上限 65535;发送完全由脚本经 xrtNetUdpSendTo 自理,支持任意目标地址。
热重载
reload 是期望状态协调任务,不是同步命令:Submit 返回稳定 ID,终态可查询。host / server 都以完整 server generation 为发布单元。
触发 API
| 函数 | 说明 |
|---|---|
xsReloadHostSubmit(pHost) | 重载一个 host(以所属 server 为发布单元) |
xsReloadServerSubmit(name) | 按名称重载整个 server |
xsReloadAllSubmit() | 整批事务:全部候选预编译,任何失败逆向回滚 |
xsReloadQuery(id, &result) | 查询终态与 Revision 指纹 |
终态枚举:ACCEPTED → PREPARING → SUCCEEDED / FAILED / SUPERSEDED / CANCELLED。同 server 的新意图覆盖未执行的旧意图(SUPERSEDED),pending 上限 64。
状态交接
/* 旧代:无破坏快照导出(导出后旧代仍可继续服务) */
static bool ServiceSwap(XS_HostInfo* pHost, xvalue** ppShared)
{
xvalue* pState = xrtValueObject();
xrtValueObjectSetNew(pState, XRT_STR_LITERAL("counter"),
xrtValueInt64(G_iCounter));
*ppShared = pState;
return true;
}
/* 新代:ServiceInit 内取回,无缝续跑 */
static void ServiceInit(XS_HostInfo* pHost)
{
xvalue* pState = xsSwapTake(pHost);
if ( pState != NULL ) {
G_iCounter = xrtValueInt64(xrtValueObjectGet(pState, "counter"));
xrtValueRelease(pState);
}
}
语义保证
- 先编译后换代:整个 server 的候选脚本全部编译成功才进入副作用区 —— 任一失败整体拒绝,旧代原样服务(回滚是语义保证)
- 双代并行:旧连接继续由旧代码服务完毕;端点不变时监听槽位无闪烁 handoff
- 引用归零回收:lease 归零后 GC 线程按序析构脚本 / 驱动 / 配置,三循环实测零泄漏零崩溃
- 定时器随代作废:
xsTimerAfter绑定创建时的旧代,换代自动取消,不会打进新代 - 不可变项:
engine.workers变更被拒绝;custom 类不支持在线重载(raw 资源无终态 lease)
单文件发布(站点 VFS)
把 xs.json 与整个站点打成一个 LZMA 归档追加到 xs 末尾,得到单文件可部署的 exe。无参启动自动探测应用包,从包内读配置、出文件。
xs.exe pack <站点目录> -o app.exe # 站点目录须含 xs.json;基底 = 当前 xs
xs.exe pack --list app.exe # 列出包内条目(路径/方法/大小/CRC)
xs.exe pack --extract app.exe -d out/ # 解包取证
xs.exe pack --strip app.exe -o orig.exe # 还原未打包二进制(字节级一致)
磁盘优先
放同名文件到 exe 旁即覆盖包内版本(热修 CSS / 续签证书免重打包),删除即恢复包内内容。证书路径经统一出口读取——磁盘优先、包内兜底,程序写文件后包内对应文件自然作废。
归档格式
- 压缩:逐条 LZMA(无收益自动 STORE);demo 站 1.7 MB → 542 KB
- 探测:读自身末尾 24 字节尾标,零成本;四级 CRC 校验(尾标/归档头/索引/条目)
- 内存:索引启动装载后不可变(线程安全);内容首次访问惰性解压并常驻缓存
--no-vfs忽略应用包强制目录模式(调试用)
格式逐字段规格见仓库 docs/站点VFS设计.md;端到端测试 python tools/test_site_vfs.py(19 项)。
桌面应用形态(class: "app")
服务节点声明 "class": "app" 即获得桌面窗口形态:本地 HTTP 服务 + WebView2 窗口同进程——启动加载窗、服务就绪自动开界面、关窗优雅停机。http 侧配置(host / static / TLS / 热重载)全部同义,仅多一个 window 描述符(9 字段约束见 字段参考)。
何时选 app 形态
- 面向最终用户交付工具类产品(管理面板、本地仪表盘、Agent 工作台),用户不该看见"打开浏览器输地址"
- 同一份产物要兼做服务器部署——Linux / 无 webview 变体自动 headless,配置不用改
- 需要本地端口隐身:app 类默认
ip 127.0.0.1(不触发防火墙弹窗)+port: 0OS 自动分配(实际端口在XS_ServerInfo.PortBound)
xsw.exe
Windows 发布包附带 xsw.exe——同一构建的 GUI 子系统变体(无控制台黑框),app 形态打包时以它为基底:xs.exe pack 站点/ -o app.exe 产物双击即用;配合 close: "stop" 全程无控制台窗口。
发布 checklist
window.devtools关闭(默认即关)——生产包不开 F12install_fallback按受众选:ask面向普通用户(缺 Runtime 弹窗问询)/silent无人值守 /headless明确不要窗口- 外部系统需要回调或地址要收藏 → 配固定端口(port:0 粘滞只在进程存活期)
close: "hide"场景确认停机路径(信号;v1 无托盘)- 服务器部署同一 xs.json 直接可用:无窗口后端的环境自动 headless,日志打印访问 URL
平台 / 变体降级矩阵与生命周期细节见 webview 参考页;端到端实战见教程 第 50 章。
证书自动续签(acme 节)
需 xacme 扩展。xs.json 根级 acme 节启用内置 ACME 守护——证书生命周期从"手工续期"变为无人值守:每日定时检查,剩余寿命不足 30 天自动 DNS-01 续签,热替换不断连接。全部字段约束见 xs.json 字段参考。
续签规则
| 项 | 行为 |
|---|---|
| 检查时机 | check_time "03:00" 每日固定时刻(或 check_interval_hours 12 间隔制,两者互斥);启动 5 分钟后首查 |
| 触发续签 | 剩余寿命 < 30 天(renew_before_days 可调 1–89),或 store 无证书 |
| 短路 | 证书充足时不联系任何 CA——纯本地判断 |
| 热替换 | 续签产物原子写盘(acme.sh 同构布局)→ 对引用该证书组的每个 server 调 xsTlsRefresh |
CA 回退链
directories[] 有序数组:按序尝试、首个成功者胜出;某 CA 失败(网络/限流/协议)自动落下一个。本地证书充足即短路;到期续签从链首重试——主 CA 恢复后自动回归。多 CA 并存零改造:xacme store 按 CA 分目录存账户,同一把账户私钥可在多个 CA 各自开户。
EAB 与账户导入
- 需 EAB 的 CA(如 litessl):
eab_kid_file / eab_hmac_file成对引用(凭据不进 xs.json) - 已有 acme.sh 账户免 EAB 导入:把账户私钥(SEC1/PKCS8 均可)预填
acme-store/accounts/<sha256(url) 前 16 hex>/account.pem——守护走"store 有账户即复用",EAB 全程不需要
运维速查
- 强制重签:删
acme-store/certs/<主域名>/后重启(判断依据是 store 不是 host tls 目录) - 守护线程自建网络引擎(Workers=1)、配置启动时深拷贝——改 acme 节须重启生效
- 服务化部署参考仓库
tools/xs.service(systemd,崩溃 5 秒自动拉起 + AmbientCapabilities 摆脱 setcap)
部署与运维
交付形态
单文件 xs / xs.exe(约 6.4MB,LZMA 内嵌 TCC + SDK + 系统库)+ 应用源码目录。目标机零安装:无运行时、无 DLL、无头文件依赖。
优雅停机
Ctrl+C / SIGTERM 触发五阶段:停止接入并退役脚本 → 等待网络终态并停引擎 → 退役 custom → 排空 reaper 上的析构 → 释放配置。ServiceUnit 在此路径中保证至多一次执行。
测试基线
func_test—— 40 个断言场景:21 个配置 fail-fast 用例、8 种路径逃逸向量、重载风暴reload_matrix—— 四协议 × TLS 共 7 端点同端点 handoff 与代隔离验证drill—— 6 小时长时演练:slowloris / pipeline 炸弹 / 坏脚本重载炸弹 + RSS 采样routebench—— 与 Go / PHP / FastAPI 的同机时延对比(结果)
平台
Windows 与 Linux 双平台(构建系统自动分支);macOS 在 xrt 层有 kqueue 后端支持。中文路径在 Windows 入口处统一转换为 UTF-8。