本章导读

全书最后一章。项目六(静态文件服务器 webserv):HTTPS 服务一个前端构建产物目录——安全入口(TLS + 证书)、静态映射(URL 前缀 ↔ 磁盘目录 + 穿越防护)、条件请求(ETag/304)、断点续传(Range/206)、压缩协商(gzip 可压文本)、访问日志与优雅停机。它是六个项目的合体:logstat 的行处理(日志)、configd 的配置(监听地址与目录映射从配置读)、xdl 的 Range 语义(这次在服务端给)、pushd 的长连接生命周期(HTTP keep-alive 与 TLS 会话)——加上静态层(第 105/132 章)与压缩(第 109 章)的专业件。也是全书知识地图的总收拢:从第 3 行代码到本章,十三章一百四十五个章节的知识在一个真实可部署的服务里汇合。验收之后是全书结语——你学到了什么、下一步去哪。

145.1引入

需求场景:前端团队构建产物(HTML/CSS/JS/图片,~5000 文件)要一个生产静态服务——要求 HTTPS(证书自动选)、并发(构建发布时的下载洪峰)、条件请求(客户端缓存协商——304 省 95% 流量)、断点续传(大文件下载中断可续)、压缩(文本 gzip 省 70%)、访问日志(审计与分析)。nginx 能做——但你要的是可编程:自定义缓存策略、按租户路由、与内部系统联动——通用服务器的配置 DSL 很快变成一门难调的方言;用 XRT 写一个,全部行为是 C 代码的透明逻辑。这不是教学练习的延续而是工程决策的示范:当通用件的配置复杂度超过自写的代码复杂度时,自写成为理性选择——XRT 的分层让"自写一个生产件"的成本可控(本章的webserv 核心不到三百行,其余全是库层)。

验收标准:① 全文件类型正确服务(MIME/编码);② ../ 与编码穿越全部 404(安全扫描器验证);③ ETag 协商:二次请求 304 零正文;④ Range:单区间 206 + 多区间 multipart;⑤ gzip:文本压缩、图片原样(Accept-Encoding 协商矩阵);⑥ 并发 500 连接下载洪峰稳定(keep-alive 复用);⑦ TLS:证书自动匹配 SNI、现代密码套件;⑧ 访问日志 JSON Lines(第 139 章形态);⑨ 优雅停机(Drain 全流程);⑩ 全程 stats 零泄漏。

145.2概念

架构:分层全图

1安全入口TLS Server(证书+SNI)——第 86/87 章
2HTTP 层Server + Router + 中间件(日志)——第 104/105 章
3静态层http_server_static——目录映射/穿越防护/MIME/ETag/Range——第 105 章
4压缩层reply_compress 协商——第 109 章
5观测logger JSON Lines + stats——第 38/6 章
6生命周期五段超时默认 + Drain——第 104 章

六层各是前文一章的正题——webserv 没有发明任何层,它做的全部工作是层间装配与业务粘合(目录映射的配置化、压缩策略的白名单)。这是全书"分层"哲学的终检:好的系统是装配出来的不是写出来的。

静态服务的语义清单

语义实现章节
路径映射URL 前缀 ↔ 目录前缀;规范化后验前缀(防穿越)104/43
MIME扩展名查表104
ETag内容指纹;If-None-Match → 304104/108
Range单区间 206 + Content-Range;多区间 multipart90/108
压缩Accept-Encoding q 协商 + 类型白名单108/91
缓存头Cache-Control 按类型(HTML 短/资产长)应用层
目录尾斜杠 → index.html;缺斜杠 → 重定向104

穿越防护的复查(安全核心):请求 /static/../secret.key——静态层的映射在拼接后做"规范化仍在根内"校验(第 105 章坑 3);编码穿越(%2e%22%2f——百分号编码的 ../):URL 解码发生在路由前(第 90 章)——解码后的路径同样过校验。双重防线:编码层不假设唯一入口(攻击者可以构造多层编码——每层解码后再验)。

条件请求与缓存策略

ETag 的生成:内容指纹(文件内容哈希——第 74 章)或元组指纹(大小+修改时间——零哈希成本)。决策:元组指纹——静态文件不变则内容不变(构建产物不可变假设);内容哈希留给高安全场景(防元数据欺骗)。Cache-Control 策略:index.html 短缓存(max-age=60——发布后一分钟生效);带指纹的资产(app.a3f9.js)长缓存(max-age=31536000, immutable——文件名变即缓存失效——前端构建的指纹命名惯例)。304 的流量意义:二次访问(有缓存)零正文——CDN/浏览器的缓存命中不耗服务器带宽(验收 ③ 的量化:5000 文件站点首访 50 MB、二次访 < 100 KB 的头开销)。

压缩协商的策略矩阵

第 109 章压缩层的 webserv 配置:类型白名单(text/css/js/json/svg 压;jpg/png/woff2 不压——已压格式);最小长度(< 256B 不压——开销大于收益);协商(Accept-Encoding 的 q 值——第 92 章千分位模型)。预压缩的升级路径:构建时生成 .gz 旁文件(app.js.gz)——服务时 Content-Encoding: gzip 直发文件(零在线压缩成本——CDN 惯例);本章基础版用在线压缩(构建产物量小可接受),预压缩是练习方向。

并发与生命周期

下载洪峰的形态:发布后 N 客户端同时拉全部资产——每连接多个请求(keep-alive 复用——第 99 章连接池的客户端侧配合)。服务端配置:MaxConnections(并发上限)、WriteSize(零复制租约——大文件分块)、五段超时默认(慢攻击防护——第 104 章)。文件发送的零复制路径:静态层 xrtHttpConnFile 族(第 105/108 章)——文件正文不进用户态缓冲(第 72 章 SendFile 的传输层兑现);内存水位与文件大小无关(stats 验证——验收 ⑩)。优雅停机:Drain(新连接拒、在途完成)→ 日志冲刷 → 销毁——发布重启的零中断窗口。

145.3示例

第一个完整程序:静态服务的最小形态

下面的程序来自 extlibs/xhttp/examples/http/static_file——静态文件响应的最小闭环:

extlibs/xhttp/examples/http/static_file/main.c源码 ↗
#include <xhttp.h>

#include <stdio.h>



/* 展示从受限文件根准备静态表示元数据和完整文件正文。 */
int main(void)
{
	static const char sPath[] =
		"xrt-http-static-file-example.tmp";
	xtaskpoolconfig Config = { 1, 8, 0 };
	const xhttprepresentation* pCurrent;
	xtaskpool* pPool;
	xhttpstaticfile* pFile;
	xhttpbody* pBody;
	xroot Root;
	xfile File;
	int iResult = 1;

	(void)xrtFileDelete(sPath);
	xrtClearError();
	File = xrtOpen(
		sPath,
		XFILE_WRITE |
		XFILE_CREATE |
		XFILE_EXCLUSIVE
	);
	if ( (File == NULL) ||
		!xrtWriteFull(
			File,
			"static resource",
			15,
			NULL
		) ||
		!xrtClose(File) ) {
		return 1;
	}
	Root = xrtRootOpen(".");
	pPool = xrtTaskPoolCreate(&Config);
	pFile = (Root != NULL) && (pPool != NULL) ?
		xrtHttpStaticFileOpen(
			pPool,
			Root,
			sPath
		) : NULL;
	pCurrent = pFile != NULL ?
		xrtHttpStaticFileRepresentation(
			pFile
		) : NULL;
	pBody = pFile != NULL ?
		xrtHttpStaticFileTakeBodyAll(
			pFile
		) : NULL;
	if ( (pCurrent != NULL) &&
		pCurrent->HasETag &&
		(pBody != NULL) ) {
		printf(
			"size=%llu weak-etag=%.*s\n",
			(unsigned long long)
				xrtHttpBodyLength(pBody),
			(int)pCurrent->ETag.Opaque.Size,
			pCurrent->ETag.Opaque.Data
		);
		iResult = 0;
	}
	xrtHttpBodyDestroy(pBody);
	xrtHttpStaticFileDestroy(pFile);
	if ( pPool != NULL ) {
		(void)xrtTaskPoolDestroy(pPool);
	}
	if ( Root != NULL ) {
		(void)xrtRootClose(Root);
	}
	(void)xrtFileDelete(sPath);
	return iResult;
}
$ gcc -O1 -DXRT_MODULE_ALL -DXHTTP_MODULE_ALL -I single -I single/extlibs -include xrt.h -include xhttp.h impl.c extlibs/xhttp/examples/http/static_file/main.c -lws2_32 -lbcrypt -ladvapi32 -liphlpapi(输出静态文件服务装配的自检结果)

刚才发生了什么。 ① 静态层的装配面:路径映射、MIME、文件响应的零复制路径——webserv 静态层的全部在库层现成。② 条件请求与 Range 由层内协商(If-None-Match/If-Range 的服务端镜像——xdl 是客户端侧、这里是服务端侧——同一协议的两端都走过了)。③ webserv 在此之上加:配置化映射(多目录)、压缩中间件、TLS 入口、日志——四件装配即完整服务。

第二个完整程序:路由 + 中间件 + 静态的完整装配

第二个程序来自 examples/http/server_static——可运行的静态服务骨架:

extlibs/xhttp/examples/http/server_static/main.c源码 ↗
#include <stdio.h>
#include <string.h>
#include <xhttp.h>

#include <math.h>
static inline double exampleTimerLimit(int64 Timeout)
{
    return Timeout == XRT_WAIT_FOREVER ? INFINITY : xrtTimer() + (double)Timeout / 1000.0;
}
static inline bool exampleTimerExpired(double Limit)
{
    return xrtTimer() >= Limit;
}
static inline int64 exampleTimerRemaining(double Limit)
{
    double Ms;
    if (Limit == INFINITY) return XRT_WAIT_FOREVER;
    Ms = ceil((Limit - xrtTimer()) * 1000.0);
    return Ms <= 0 ? 0 : Ms >= 0x1p63 ? INT64_MAX : (int64)Ms;
}



/* 静态站点示例在 Server 生命周期内拥有文件任务池和目录根。 */
typedef struct example_http_static {
	xtaskpool* Pool;
	xroot Root;
} example_http_static;



/* 一次调用完成安全映射、条件请求、Range、MIME 和异步正文。 */
static void exampleHttpStaticRequest(
	xhttpserver* pServer,
	xhttpconn* pConnection,
	const xhttpserverrequest* pRequest,
	ptr pData
)
{
	example_http_static* pState =
		(example_http_static*)pData;

	(void)pServer;
	(void)pRequest;
	if ( xrtHttpConnStatic(
		pConnection,
		pState->Pool,
		pState->Root,
		NULL
	) ) {
		return;
	}
	(void)xrtHttpConnReply(
		pConnection,
		XHTTP_STATUS_SERVICE_UNAVAILABLE,
		XRT_STR_LITERAL(
			"text/plain; charset=utf-8"
		),
		XRT_BYTES_LITERAL("Service Unavailable")
	);
}



/* 输出静态文件准备或 HTTP 传输错误。 */
static void exampleHttpStaticError(
	xhttpserver* pServer,
	xhttpconn* pConnection,
	const xerror* pError,
	ptr pData
)
{
	(void)pServer;
	(void)pConnection;
	(void)pData;
	fprintf(
		stderr,
		"HTTP error [%s/%s]: %s\n",
		xrtErrorDomain(pError),
		xrtErrorOperation(pError),
		xrtErrorMessage(pError)
	);
}



/* 启动静态站点,按回车后停止接收并排空连接。 */
int main(int iArgc, char** ppArgv)
{
	xtaskpoolconfig PoolConfig = { 2, 128, 0 };
	example_http_static State;
	xnetengineconfig EngineConfig;
	xhttpserverconfig ServerConfig;
	xhttpserverevents Events;
	xnetengine* pEngine = NULL;
	xhttpserver* pServer = NULL;
	xnetaddr Address;
	double Deadline;
	str sEndpoint = NULL;
	int iResult = 1;

	if ( iArgc != 2 ) {
		fprintf(
			stderr,
			"usage: server_static <directory>\n"
		);
		return 0;
	}
	memset(&State, 0, sizeof(State));
	State.Root = xrtRootOpen(ppArgv[1]);
	State.Pool = xrtTaskPoolCreate(&PoolConfig);
	if ( (State.Root == NULL) ||
		(State.Pool == NULL) ) {
		goto Cleanup;
	}
	xrtNetEngineConfigInit(&EngineConfig);
	pEngine = xrtNetEngineCreate(&EngineConfig);
	if ( (pEngine == NULL) ||
		!xrtNetEngineStart(pEngine) ) {
		goto Cleanup;
	}
	xrtHttpServerConfigInit(&ServerConfig);
	if ( !xrtNetAddrLoopback(
		&ServerConfig.Network.Listen.Address,
		XNET_FAMILY_IPV4,
		0
	) ) {
		goto Cleanup;
	}
	xrtHttpServerEventsInit(&Events);
	Events.Request = exampleHttpStaticRequest;
	Events.Error = exampleHttpStaticError;
	Events.Data = &State;
	pServer = xrtHttpServerStart(
		pEngine,
		&ServerConfig,
		&Events
	);
	if ( (pServer == NULL) ||
		!xrtHttpServerLocal(
			pServer, 0, &Address
		) ) {
		goto Cleanup;
	}
	sEndpoint = xrtNetAddrEndpointString(&Address);
	if ( sEndpoint == NULL ) {
		goto Cleanup;
	}
	printf(
		"serving %s on http://%s/\n"
		"press Enter to drain\n",
		ppArgv[1],
		sEndpoint
	);
	xrtFree(sEndpoint);
	sEndpoint = NULL;
	(void)getchar();
	if ( !xrtHttpServerDrain(pServer) ) {
		goto Cleanup;
	}
	Deadline = exampleTimerLimit(INT64_C(5000));
	while ( xrtHttpServerState(pServer) !=
		XHTTP_SERVER_CLOSED ) {
		if ( exampleTimerExpired(Deadline) ) {
			(void)xrtHttpServerAbort(pServer);
			break;
		}
		xrtThreadYield();
	}
	iResult = 0;

Cleanup:
	xrtFree(sEndpoint);
	if ( (pServer != NULL) &&
		(xrtHttpServerState(pServer) !=
		 XHTTP_SERVER_CLOSED) ) {
		(void)xrtHttpServerAbort(pServer);
		Deadline = exampleTimerLimit(
			INT64_C(5000)
		);
		while ( xrtHttpServerState(pServer) !=
			XHTTP_SERVER_CLOSED ) {
			if ( exampleTimerExpired(Deadline) ) {
				iResult = 2;
				break;
			}
			xrtThreadYield();
		}
	}
	xrtHttpServerDestroy(pServer);
	if ( (State.Pool != NULL) &&
		!xrtTaskPoolDestroy(State.Pool) &&
		(iResult == 0) ) {
		iResult = 2;
	}
	if ( (State.Root != NULL) &&
		!xrtRootClose(State.Root) &&
		(iResult == 0) ) {
		iResult = 2;
	}
	if ( (pEngine != NULL) &&
		!xrtNetEngineDestroy(pEngine) &&
		(iResult == 0) ) {
		iResult = 2;
	}
	return iResult;
}
$ gcc -O1 -DXRT_MODULE_ALL -DXHTTP_MODULE_ALL -I single -I single/extlibs -include xrt.h -include xhttp.h impl.c extlibs/xhttp/examples/http/server_static/main.c -lws2_32 -lbcrypt -ladvapi32 -liphlpapi(启动静态服务后按 Enter Drain 退出)

刚才发生了什么。 ① Router + 静态端点 + Engine 的三件装配——webserv 的骨架形态。② Drain 的收尾在示例末尾(按 Enter 触发——优雅停机的最小演示)。③ 中间件层的挂点(第 105 章的 Use)——日志中间件的插入位。webserv = 本示例 + TLS 配置 + 压缩层 + 目录映射数组——每个增量都是前文一章的配置面,无一新概念。

实现走查:装配主函数的源码形态

webserv 的装配主函数——配置读取到服务启动的完整流程(在教学示例之上的关键段走查):

int main(int argc, char** argv)
{
	wsvcfg Cfg;
	xnetengine* Eng = NULL;
	xhttpserver* Srv = NULL;
	xhttpserverrouter* Router = NULL;
	xtlsidentity* Ident = NULL;
	int Result = 1;

	/* 1 配置:configd 形态(默认→环境覆盖→校验) */
	wsv_defaults(&Cfg);
	wsv_env_override(&Cfg);            /* 第 43 章 */
	if ( !wsv_validate(&Cfg, stderr) ) { return 2; }  /* 启动即拦 */

	/* 2 Engine 与身份 */
	Eng = xrtNetEngineCreate(NULL);
	if ( (Eng == NULL) || !xrtNetEngineStart(Eng) ) { goto Cleanup; }
	Ident = wsv_load_identity(&Cfg);  /* 第 83 章:启动期验证 */

	/* 3 路由:静态映射 + 日志中间件 */
	Router = xrtHttpServerRouterCreate(NULL);
	if ( !wsv_mount(Router, &Cfg) ||          /* 目录映射数组 */
		!xrtHttpServerUse(Router, wsv_log, &Cfg) ||  /* 第 105 章 */
		!xrtHttpServerRouterFreeze(Router) ) { goto Cleanup; }

	/* 4 服务:TLS 入口 + 五段超时默认 */
	{
		xhttpservertlsconfig Tls;
		xrtHttpServerTlsConfigInit(&Tls);
		Tls.Handshake.Identity = Ident;
		Srv = xrtHttpServerRouterStartTls(
			Eng, &Cfg.Net, &Tls, Router, NULL);
	}
	if ( Srv == NULL ) { goto Cleanup; }
	wsv_print_endpoint(Srv);          /* 监听端点日志 */
	Result = wsv_wait_shutdown();     /* 信号/输入等待 */
	xrtHttpServerDrain(Srv);          /* 优雅停机 */
	wsv_log_flush(&Cfg);

Cleanup:
	xrtHttpServerDestroy(Srv);
	xrtHttpServerRouterDestroy(Router);
	xrtTlsIdentityRelease(Ident);
	xrtNetEngineDestroy(Eng);
	return Result;
}
$ gcc -O1 -DXRT_MODULE_ALL -DXHTTP_MODULE_ALL -I single -I single/extlibs -include xrt.h -include xhttp.h impl.c webserv.c -lws2_32 -lbcrypt -ladvapi32 -liphlpapiwebserv listening on https://127.0.0.1:8443 (root=dist)

走查要点。 ① 四步装配(配置→Engine/身份→路由→服务)每步失败 goto Cleanup——单出口纪律贯穿到主函数。② wsv_mount 是目录映射的配置化(Cfg.Mounts 数组循环挂静态端点——多目录/多前缀)。③ 日志中间件 wsv_log 在路由外层——每请求一行 JSON(时间/方法/路径/状态/字节/耗时——Close 后置记录完整状态)。④ TLS 入口的 StartSecure(身份挂上——SNI 自动匹配多证书)。⑤ Drain 后日志冲刷——停机期间的在途请求也进日志(最后一批完整记录)。⑥ 全部销毁按依赖逆序——第 104 章 Shutdown 语义的兑现。主函数六十行——webserv 的全部"业务":其余是库层与 wsv_* 辅助(配置/映射/日志——每个 < 100 行)。

与六项目经验的总对账

十七条工程常识在 webserv 的总检验表(每条的 webserv 落点):验收先行(十条标准含安全扫描与洪峰);形态决定容器(映射数组/ETag 元组/日志行构建器);视图即取即弃(日志中间件的请求视图回调内完成);分层换件(换存储后端/换压缩策略是单层);平台事实进设计(路径分隔符/权限/证书位置的系统差异);预算先算(吞吐=min(磁盘,网络)——CPU 在两端可忽略);测试四象限(语义矩阵+穿越靶场+洪峰压测);远程不确定性(客户端可能发任何东西——解码每层重验);协议原子点(ETag 协商由协议保证一致性);临时名+改名(发布的新目录原子切换——symlink 翻转);文件系统状态权威(ETag 从文件元数据来——不自维护缓存);测试替身(本地压测的确定性客户端);扇出先设计(洪峰的 keep-alive 复用预算);终态对账(Drain 后连接计数归零);序号自愈(发布版本号给客户端检测切换);测试客户端也被测(压测框架的请求对账);运维接口(端点打印+日志即观测)。十七条全部有落点——经验清单的完整性就是项目的完整性。

知识地图的总收拢

webserv(及六个项目整卷)走过的全部章节——按卷清点:卷一(3-8 章:第一个程序到裁剪——一切的地基);卷二(10-12 章:数值与哈希——ETag 的指纹原理);卷三(13-23 章:容器——映射表与请求队列的底座);卷四(25-36 章:文本与数据——URL/头部/JSON 日志的解析链);卷五(37-50 章:系统服务——日志/文件/路径/目录/异步 IO 的日常面);卷六(51-61 章:并发——连接并发的正确性根基);卷七(62-72 章:网络——TCP/缓冲/引擎的传输底座);卷八(73-88 章:安全——TLS 入口的全部层次);卷九(89-97 章:协议核心——HTTP/WS 的字节级语义);卷十(98-109 章:xhttp——本服务的直接工具箱);卷十一(110-129 章:扩展库——pushd/xdl 的工具来源);卷十二(130-137 章:工程实践——部署/测试/调优的纪律);卷十三(138-145 章:项目——经验的螺旋)。一百四十五个章节、六个项目、十七条常识、约 65 万字讲解——这就是这本教程的全部家底。你带走的不该是 API 速查(官网有)而该是分层的眼力与纪律的手感——它们在任何 C 代码库上都成立。

145.4契约

  • 穿越双防:拼接后规范化验前缀;多层解码每层重验——八形态穿越靶场全绿是验收基础。
  • ETag 元组:大小+修改时间(构建不可变假设);304 零正文。
  • 缓存双档:HTML 入口短缓存(max-age=60)/指纹资产长缓存(immutable)——文件名即失效机制。
  • 压缩白名单:可压文本族;已压格式原样;最小长度阈值。
  • Range 服务端:单区间 206+Content-Range;多区间 multipart——与 xdl 客户端互补成完整协议面。
  • 零复制:文件正文经 ConnFile 族——内存与文件大小无关。
  • 并发上限:MaxConnections+五段超时默认——慢攻击与洪峰防护。
  • TLS:证书 SNI 匹配;现代套件;验证器立场(服务端无验证器但有身份)。
  • 日志 JSON Lines:每请求一行(时间/方法/路径/状态/字节/耗时)——file_json 形态。
  • 停机:Drain 拒新完旧 → 日志冲刷 → 销毁——发布零中断窗口。

基准与容量数字的取得

webserv 的容量结论要用第 136 章方法取得(不是拍脑袋):吞吐基准——洪峰模拟(500 虚拟客户端拉全站)测 req/s 与带宽饱和点(磁盘 or 网络先满——预算指认扩容方向);延迟分布——P50/P99 请求延迟(日志的耗时字段聚合——第 42 章时间戳的产出);对比锚点——同负载下与 nginx(sendfile/epoll 的成熟实现)的差距度量(差距来源分析:库层调用开销/无内核调优/无多进程——多数场景差距 < 2 倍即可接受,因为可编程性是主要收益);sweep——连接数×文件大小两维扫描(小文件高并发 vs 大文件低并发的两种形态各有饱和点)。容量结论的形态:"单实例 4C8G:小文件场景 X 万 req/s(P99 < Y ms)、大文件 Z GB/s——超出加实例或前置 CDN"。这些数字进部署文档——基准的产出是运维的输入。

发布流程与版本策略

webserv 自身的发布(元工程——项目也是软件):原子目录切换——新版本构建到 releases/v{ts}/、symlink current 翻转指向(原子的目录级切换——第 143 章"临时名+改名"的目录版:任意时刻 current 指向完整的一个版本);回滚——symlink 指回旧目录(秒级——比重启回滚快两个数量级);健康检查——/health 端点(构建产物校验:index.html 存在性)——发布脚本等待健康才切流量;日志连续性——跨重启的日志按天滚动(第 39 章轮转)+ 版本号字段(哪个版本处理的请求——排查时序问题的锚点)。发布流程的自动化脚本本身就是 configd 思想(配置/版本/健康的状态机)——项目序列的知识在生产运维的最后一环合拢。

常见错误

坑 1:手拼目录映射漏了编码穿越

症状:安全扫描报 /static/%2e%2e%2fsecret 可访问——直拼 ../ 被拦但编码形态漏了。

原因:URL 在路由前解码一次(第 90 章)——解码发生在你的校验之前还是之后决定了防线位置。静态层的映射校验在解码后路径上(正确);自拼映射若在解码前验——编码穿越。

错误写法
snprintf(Path, "%s/%s", Root, RawUrlPath);  /* 原始未解码路径拼接 */
check_prefix(Path);                           /* 验的是编码形态 */
正确写法
/* 用静态层映射:解码后规范化校验内建 */
/* 自管时:先解码(第 102 章)→ 规范化 → 验前缀——每层解码都重验 */
坑 2:304 响应带了正文

症状:抓包发现 304 响应携带完整正文——缓存优化全部失效(带宽照耗)。

原因:条件请求命中后手工组装响应时忘了 304 语义是只有头。静态层的 304 路径是内建的;自拼响应时 304 分支必须走"仅头"组装。

错误写法
if ( etag_match ) { reply(304, body); }   /* 304 带正文:协议违例 */
正确写法
if ( etag_match ) {
	respond_headers_only(304, ETag);   /* 仅头 + ETag */
	return;
}
坑 3:把全部文件设为长缓存

症状:发布新版本——用户的 HTML 引用新资产但 HTML 自身被缓存——白屏或新旧混搭。

原因:缓存策略没区分入口文档与指纹资产。HTML 是入口(引用变则内容变)必须短缓存;资产文件名带指纹(变即新 URL)才敢 immutable。

错误写法
set_header("Cache-Control", "max-age=31536000");  /* 全部文件:发布即事故 */
正确写法
if ( is_html ) { cc("max-age=60"); }
else { cc("max-age=31536000, immutable"); }  /* 指纹资产 */
语义矩阵的设计意图:语义矩阵的设计意图

十六组语义矩阵的练习设计意图:类型(HTML/JS/JPG)× 条件(命中/未命中)× Range(无/单区间)× 压缩(要/不要)= 3×2×2×2 = 24 组(取十六组代表组合)。这个矩阵的价值不在通过而在通过的过程——你必须为每组先写期望(协议手推)再验证——任何一组不符都是协议理解的盲点。真实静态服务器的 bug 几乎都在这些组合的交集处(304+压缩的 Content-Encoding 头残留、Range+压缩的顺序错误、JPG+压缩的双压浪费)——矩阵是组合空间的系统覆盖,等价于第 133 章变异测试的响应侧版本。

编程练习

基础:全语义矩阵

对 static 服务跑语义矩阵:类型×条件×Range×压缩的十六组组合(每种各至少一请求)。验收标准:十六组响应(状态/头/正文有无)与协议手推一致。

进阶:穿越攻击靶场

构造八种穿越(直拼/一层编码/双层/尾点/反斜杠/空字节/超长/符号链接)对自拼映射与静态层各测。验收标准:静态层全 404;自拼版的漏洞各自定位并修复。

挑战:完整 webserv

六项目经验全合体:配置驱动(configd 形态)、TLS 入口、压缩、日志、Drain、压测(500 连接洪峰——第 136 章方法)。验收标准:十条验收全过;核心自写代码 < 400 行(其余库层)——"装配不是写"的量化证明。

性能与安全的双清单

上线前的最后两页清单。性能清单:① 零复制路径确认(大文件内存水位=stats 常数);② 压缩白名单核对(图片不压、文本压);③ keep-alive 复用率(日志的连接字段——复用 < 50% 查客户端或超时配置);④ ETag 命中率(304 占比——低于 80% 查缓存头策略);⑤ 五段超时显式(慢攻击防护不是默认值碰运气)。安全清单:① 穿越靶场全绿(八形态 404);② TLS 套件策略(第 85 章策略对象——旧套件关);③ 目录权限最小化(服务账户只读静态根);④ 日志脱敏(查询串中的令牌字段——第 77 章纪律);⑤ 错误页不泄路径(404 页通用文案——服务端路径信息是侦察材料)。两清单各五条——性能与安全都不是特性而是清单的执行度。

小结

知识点速查
定位六项目合体 + 全书知识总收拢——装配不是写
静态语义映射/MIME/ETag/Range/压缩/缓存头——第 105 章全责
穿越双防解码后规范化验前缀;多层解码每层重验
ETag元组指纹(大小+mtime);304 仅头零正文
缓存双档HTML 短 / 指纹资产 immutable——文件名即失效
压缩白名单+最小长度+q 协商;预压缩是升级路径
Range 双端本服务端 + xdl 客户端——完整协议面
零复制ConnFile 族——内存与文件大小无关
洪峰MaxConnections+五段超时+keep-alive 复用
收官十章验收 + 可编程服务器的工程决策示范 + 知识地图总收拢

最后一课:从读者到作者

教程的最后一条建议关于角色转换。读完全书的你已经具备把这套分层与纪律传授出去的能力——而传授是检验掌握的终极形式。三条路径:在团队内——拿一个真实模块按本书格式写一章(骨架八节、验收先行、bad/good 对照)——写作会暴露你以为懂了但讲不清的地方;在开源——为 XRT 贡献一个扩展或改进(第 130 章的扩展库清单就是接入点——manifest 挂上你的模块);在项目里——把十七章常识清单贴在团队 wiki(每条一个真实事故做注脚——你的事故库就是你的团队的教材)。教程会过时(API 会变、平台会换)但分层的眼力与纪律的手感不过时——把它们传下去,是这本书最好的收官。

全书结语

一百四十五个章节走完,webserv 是你能拿出的作品证明。回头看这条路径的形状:卷一的地基(错误/内存/裁剪——一切的词汇表);卷二卷三的确定性与容器(工程的积木箱);卷四的文本与数据(信息的世界);卷五的系统服务(与操作系统相处);卷六的并发(时间维度的正确性);卷七的网络(空间的延伸);卷八的安全(信任的建立);卷九卷十的协议栈(Web 世界的完整重建);卷十一的扩展库(生态的外延);卷十二的工程实践(把代码变成产品的纪律);卷十三的六个项目(约束的组合与经验的螺旋深化)。贯穿始终的三条线:分层(每章一层、项目装配)、纪律(验收先行/视图即取即弃/失败单一/预算先算——十七条工程常识)、可验证性(门禁/测试四象限/终态对账——一切主张都有机器证明)。你接下来去哪?三个方向:深入——本书每个章都是一扇门(TLS 的握手细节、io_uring 的内核机制——门后还有门);扩展——用 XRT 造自己的件(新协议/新容器/新工具——分层给你插位置);传授——把这套分层与纪律带到你的团队(能解释的架构才是拥有的架构)。无论哪个方向——代码在 github 等你,门一直是开的。

📖 本章涉及的 API 参考手册:xhttp-http_server(75 个条目)xhttp-http_server_static(6 个条目)xhttp-http_server_middleware(4 个条目)

点击查看完整签名、参数约束、返回值与错误说明