用 net 手写极简 HTTP 客户端
本节目标
- 用 Node
net模块手写一个 HTTP 客户端 - 理解
fetch/axios底层其实在拼/解同样的报文 - 掌握"一行请求 → 一段响应"的收发闭环
思路:HTTP 客户端 = 建立 TCP 连接 → 拼一段符合规范的请求报文发出去 → 收回应答报文 → 解析。Node 的 net 给我们原始 TCP 通道。
// 运行环境:Node.js 14+(需联网,访问 httpbin 或任意站点)
// 保存为 aj-l2.js,执行:node aj-l2.js
const net = require('net')
function httpGet(host, port, path) {
return new Promise((resolve) => {
const socket = net.createConnection({ host, port }, () => {
// 拼请求报文(注意 \r\n 换行、末尾空行)
const req = `GET ${path} HTTP/1.1\r\nHost: ${host}\r\nConnection: close\r\n\r\n`
socket.write(req)
})
let data = Buffer.alloc(0)
socket.on('data', (chunk) => { data = Buffer.concat([data, chunk]) })
socket.on('end', () => {
const text = data.toString()
const sep = text.indexOf('\r\n\r\n')
const head = text.slice(0, sep)
const body = text.slice(sep + 4)
resolve({ statusLine: head.split('\r\n')[0], head, body })
})
})
}
// 调用示例(用公共测试服务)
httpGet('example.com', 80, '/').then(r => {
console.log('状态行:', r.statusLine) // HTTP/1.1 200 OK
console.log('body 前 60 字:', r.body.slice(0, 60))
})注意:真实客户端要处理重定向、HTTPS(需 tls)、分块、gzip 等。这里只演示"协议本质"——fetch 干的就是同样的事,只不过它替你处理了所有边界。
名词解释
- net 模块(Node):提供 TCP 套接字(socket)编程能力,是 Node 所有网络库的底座。类比:
http模块是在net之上按 HTTP 协议规则穿了件"外套"。 - Socket(套接字):网络连接的"两端端点",一端写、另一端读,像一根双向水管。
createConnection建立到服务器的水管。 - Connection: close:请求头,告诉服务器"响应发完就关连接"。配合
socket.on('end')即可判定响应收完,省去算 Content-Length。
课后练习
- 为什么这个客户端用
Connection: close而不是Keep-Alive?- 答案:close 时服务器发完就关闭 socket,
end事件即响应结束,无需自己按长度判断收尾;Keep-Alive 会复用连接、end不会触发,得靠 Content-Length/chunked 才能判定边界,初学者先用 close 最省心。
- 答案:close 时服务器发完就关闭 socket,
- HTTPS 站点(443)能直接用
net连吗?- 答案:不能,HTTPS 在 TCP 之上多了 TLS 加密握手;需用
tls.connect或在net之上手动做 TLS,否则收的是密文。
- 答案:不能,HTTPS 在 TCP 之上多了 TLS 加密握手;需用
总结
亲手写一个 HTTP 客户端,最大的收获不是"又会用一个 API",而是破除对 fetch 的神秘感。你会发现,所谓"发请求",底层不过是用 TCP 连上服务器、拼一段 GET /path HTTP/1.1\r\nHost: ...\r\n\r\n 发出去、再把收到的字节流按 \r\n\r\n 切开。这一层认知极其重要:当某天 fetch 行为诡异(重定向、压缩、超时),你能立刻下沉到字节层面定位,而不是只会骂"网络抽风"。我建议把 net 当成"网络世界的显微镜"——它让你看见 http 模块、甚至浏览器网络栈,都只是这层原始字节交互的封装。理解了底座,上层任何框架你都能一眼看穿。