接项目时遇到一个挺典型的问题:想用 Shell 脚本接收 HTTP 请求、解析路径、然后返回不同的响应,脚本里用 nc -l -p $PORT 监听端口,结果每个请求处理完 nc 就退出,整个脚本也跟着死掉。这个现象其实不是 nc 的 bug,而是它的设计如此。问题背后牵扯到 netcat 的工作模式、HTTP 协议解析、循环结构设计、并发模型等一系列细节,如果你也在做轻量 HTTP 服务、IoT 设备上的小接口、或者只是想撸一个不依赖 Python/Node 的 Shell 脚本服务器,这篇文章应该能帮你把思路彻底理清。
1. nc 为什么"连一次就退出"——工作模式与问题复现
1.1 从"职责单一"理解 nc 的行为
netcat 设计哲学是"轻巧、单一、可组合",它不觉得自己是一个服务器框架。默认监听模式下,nc -l -p $PORT 的行为链路是这样的:
- 绑定指定端口并开始监听;
- 收到一个客户端连接后 accept;
- 在连接上双向转发数据(stdin 到 socket,socket 到 stdout);
- 当连接关闭、EOF 到达、或者数据交换结束,直接退出进程。
这种"一次连接一个进程"的设计让 nc 非常干净,但也正是"每个请求处理完就退出"的根源。你想让它自己保持监听状态继续服务下一个请求,等于要求它变成常驻服务器,这和它"单一连接工具"的定位天然冲突。
另外一个容易忽略的点是:nc 的退出也可能发生得非常早。如果客户端建立连接后什么也没发送就关闭了连接,nc 会立刻退出;如果客户端发送完请求但没有明确关闭连接(比如 HTTP keep-alive),nc 会一直等着对方发数据,看起来像卡住。这两种现象叠加,让"用 nc 写 HTTP 服务"的初版脚本表现非常不稳定。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
1.2 先复现一遍问题:你大概写过这样的脚本
假设你写了一个最简单的 HTTP 响应脚本:
bash复制#!/bin/bash
PORT=8080
echo -e 'HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK' | nc -l -p $PORT
运行后用 curl 访问:
bash复制curl -i http://127.0.0.1:8080/
第一次访问能收到响应,但脚本立刻结束,第二次再访问就连接拒绝。这就是标题描述的场景:nc 已经退出,Shell 进程也接着退出,端口自然被释放。
还有一种更迷惑的写法:
bash复制#!/bin/bash
PORT=8080
nc -l -p $PORT | while read line; do
echo "got: $line"
done
这个脚本也一样,处理完第一个连接就会退出。而且因为管道读取的存在,nc 的行为还会受到管道另一端结束时机的影响,表现更怪。
复现完你会发现,单纯把 nc 当成服务器的替代品是有问题的。它更像一个"网络连接原语",要让 HTTP 服务持续运转,必须自己解决"循环接受连接"这个问题。后面第 3 节会展开讲几种解决思路,但在那之前,先把"单次请求处理"本身做对,否则循环写得再好,请求解析不对,服务也没有意义。
2. 先把"单次请求"做对——完整 HTTP 请求接收与解析
2.1 HTTP 请求的结构与 Shell 读取策略
HTTP 请求文本的结构其实很简单:
code复制请求行 : GET /health HTTP/1.1
请求头 : Host: 127.0.0.1:8080
User-Agent: curl/8.0.1
空行 :
请求体 :(可选,POST 时通常有)
Shell 里最自然的读取方式就是按行读取:第一行是请求行,空行前都是请求头,剩下的才是请求体。用 read -r 就能可靠地读取请求行和请求头。
但这里有个关键前提:请求体不能按行读,因为 POST 过来的可能是任意二进制内容,必须按照 Content-Length 精确读取指定字节数。处理请求体的基础思路是这样:
bash复制# 读完请求头之后
content_length=0
# 从请求头中解析出 Content-Length
# 然后用 dd 读取指定数量字节
body=$(dd bs=1 count=$content_length 2>/dev/null)
dd bs=1 count=$content_length 是从 stdin 精确读 content_length 个字节的通用办法。不加 count 的话会把客户端发送的所有剩余内容都读走,容易出现错位。
2.2 一个可复用的 handler 脚本
把单次请求处理放在独立脚本里,是后面所有循环方案的基础。我习惯把 handler 单独抽出来,既方便测试,也能被 nc、ncat、socat 复用。下面这个 http_handler.sh 是我实际项目中的一个精简版本:
bash复制#!/bin/bash
# http_handler.sh - 处理单个 HTTP 请求,从 stdin 读取,响应写入 stdout
# 读取请求行
IFS= read -r request_line
if [ -z "$request_line" ]; then
exit 0
fi
# 解析请求行
method=$(echo "$request_line" | awk '{print $1}')
path=$(echo "$request_line" | awk '{print $2}')
# 继续读取请求头,直到遇到空行(可选,但为了后续扩展建议读掉)
while IFS= read -r header_line; do
# 空行代表请求头结束
if [ -z "$header_line" ]; then
break
fi
# 这里可以解析 Host、User-Agent 等
done
#
