欢迎光临
我们一直在努力

第 20-1 篇:极简 HTTP——socket/bind/select 轮询

上一篇:19-3《视频帧与媒体模块——默认不启用的 H.264》| 下一篇:20-2《路由与自研最小 JSON》

真机实测通过:本文实验已在 RK3588 板端实测完成(2026-09;方法学与原始记录见仓库 docs 与《实验脚本》目录)

一句话导读:极简 HTTP 服务:socket、bind、select 轮询与有界 worker 队列怎么撑起零外部库的自研服务器,板端实测 /health 往返,讲清自研与上框架在单机板端场景的成本取舍。

关键词:HTTP、socket、select 轮询、零依赖、推理引擎多模态与数据面收官,Day 20 进入服务化:引擎的 HTTP 服务在 src/serve/vllm_http.c,一个文件里装下了 socket 监听、自研 JSON 解析器、请求/响应解析、线程池、SSE 流式——零外部库,只依赖系统 socket API。这篇先回答"极简服务器怎么撑起来的",并用板端实测的 /health 往返时间把"自研 vs 上框架"的成本讲清楚。

1. 知识点:一个最小 HTTP 服务要几块砖

HTTP 服务最内核只有四件事:

socket() → bind() → listen() → accept() 循环
│
每来一个连接 → 读请求头/体 → 路由 → 写响应

  • listen:告诉内核"我在这端口等人连接",参数是积压队列长度;
  • accept:从内核拿一个已完成握手的连接;
  • select:一次等待多个 fd 就绪(本引擎用它在 accept 上等连接,附带超时做优雅停机轮询,vllm_http.c 854–868 行);
  • SO_RCVTIMEO:给每个已接受连接设接收超时(vnet_set_recv_timeout,511 行)——慢连接/半开连接会被超时踢掉,这是"不引库也能防呆"的典型写法。

引擎的取舍(文件头注释 4–9 行):accept 循环单线程串行 + 连接丢给有界队列 + 8 个 worker。HTTP 层并行度不归推理管——推理本身是单 SSE 流串行(Day 14 讲过"单机守则"),HTTP worker 只是把"慢客户端"与"推理线程"隔开。

为什么不上 libevent/nginx? 板端场景的并发模型是"单模型 + 少量并发请求",select + 有界队列 + SO_RCVTIMEO 已经覆盖;少一个第三方依赖,就少一分供应链审计成本(这是 Day 23 起国密/签名体系能自洽的前提)。

2. 对应代码:vllm_http.c 的砖块顺序

监听与主循环(vhttp_serve_ex,804–868 行):

listener.fd = socket(AF_INET, SOCK_STREAM, 0); // 812
setsockopt(listener.fd, SOL_SOCKET, SO_REUSEADDR, …);// 816
bind(listener.fd, (struct sockaddr *)&addr, sizeof(addr)); // 823
listen(listener.fd, 32); // 824
for (;;) {
fd_set rfds; FD_SET(listener.fd, &rfds);
select(listener.fd + 1, &rfds, NULL, NULL, &tv); // 860:等连接或超时
int cf = accept(listener.fd, (struct sockaddr *)&ca, &calen); // 868
queue_push(&ctx->q, conn); // 连接进有界队列,worker 取走
}

配套的三块:

  • 请求解析:read_request(554 行)+ find_header(519 行)——按 \\r\\n\\r\\n 切头、按 Content-Length 收体,不读 body 以外的字节;
  • 响应写回:send_response(636 行),socket 无缓冲,vhttp_stream_write 每写一次就到线缆(683 行注释);
  • worker:worker_main(791 行)从队列取连接、回调路由、关闭(serve_conn 765 行)。
  • "极简"的代价要在文档里说清(技术文档 §2.2):默认 –threads 8 的 worker 数只影响连接并发,推理仍串行;想要吞吐只能靠批处理而不是把 HTTP worker 堆到推理上。

    3. 改动后果:板端实测 /health 往返

    实测口径:RK3588 板 serve(model.vqf 已加载)+ x86 主机客户端,局域网 192.168.1.92:8801,2026-09-07。客户端记录的是 urlopen 完整往返(含 TCP 握手),不是服务端处理时间。

    GET /health ×6(模型已加载、无排队)
    42.6 ms / 29.3 ms / 16.8 ms / 27.8 ms / 7.2 ms / 22.5 ms
    响应体: {"status":"ok","model":"qwen3-vl-8b","busy":false,"queued":0,…}

    三个观察:

  • 7–43 ms 的抖动主要是网络与调度:同一板子同一路径,/health 服务端只做一次 JSON 拼装,远小于网络 RTT 的抖动——这告诉我们:单看一次 HTTP 往返测不出服务端快慢,要测服务端就该看服务端日志或压测多次取中位(Day 28 方法论预告);
  • busy/queued/active 字段让 /health 能当调度器探针用:queued 不为 0 说明请求在排队,这是比"进程活着"更有用的健康定义;
  • 模型 id 是配置标签:响应里 "model":"qwen3-vl-8b" 是服务启动时的模型 id 串(非架构声明),多模型共存时客户端靠它区分后端——兼容协议里模型字段的语义在 Day 21 细讲。
  • 4. 学员调试任务

    A 档(板端/本地动手):

  • 起 serve 后对 /health 连打 10 次,记录每次往返时间并取中位(对比本节的 7–43ms 量级);
  • 用 strace -e trace=network 或 /proc/<pid>/fd 看监听 socket 与已建立连接的状态,确认 select/accept 在干什么(netstat -tlnp | grep 8801 也行);
  • 读 vllm_http.c 812–868 行:把 listen(…, 32) 改成 1 再起服务,观察并发连接的表现差异(可在本机起两个 python 客户端同时连)。
  • B 档(纯读源码):回答:① 为什么 select 的超时对"优雅停机"是必要的(提示:g_vhttp_stop 标志 28–34 行 + vhttp_stop 在 vhttp_serve_ex 主循环哪里被检查)?② SO_RCVTIMEO 设多少秒合理、设成 0 会发生什么?③ 若把 queue_push 换成无界队列,worker 忙时连接会怎样堆积,内存上会出什么问题?

    预期输出:自己板子的 /health 往返直方图 + 监听 socket 的 netstat 截图,并能说出 select 在这里"等的是什么"。

    收尾

    本篇源码点名:vllm_http.c(socket/bind/listen 812–824、select 主循环 854–868、read_request 554、worker 队列 729–799)

    开源仓库:Kestrel-LLM (Gitee)(AGPL-3.0-or-later 或商业许可,二选一)

    下篇预告:连接进来了,URL 和 JSON 体怎么变成一次"对话"?20-2 讲路由表与自研最小 JSON 解析器,并用"改错字段名"实验演示兼容性断裂。

    赞(0)
    未经允许不得转载:171主机测评 » 第 20-1 篇:极简 HTTP——socket/bind/select 轮询
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址