用 net 手写极简 HTTP 客户端

本节目标

思路: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 干的就是同样的事,只不过它替你处理了所有边界。

名词解释

课后练习

  1. 为什么这个客户端用 Connection: close 而不是 Keep-Alive?
    • 答案:close 时服务器发完就关闭 socket,end 事件即响应结束,无需自己按长度判断收尾;Keep-Alive 会复用连接、end 不会触发,得靠 Content-Length/chunked 才能判定边界,初学者先用 close 最省心。
  2. HTTPS 站点(443)能直接用 net 连吗?
    • 答案:不能,HTTPS 在 TCP 之上多了 TLS 加密握手;需用 tls.connect 或在 net 之上手动做 TLS,否则收的是密文。

总结

亲手写一个 HTTP 客户端,最大的收获不是"又会用一个 API",而是破除对 fetch 的神秘感。你会发现,所谓"发请求",底层不过是用 TCP 连上服务器、拼一段 GET /path HTTP/1.1\r\nHost: ...\r\n\r\n 发出去、再把收到的字节流按 \r\n\r\n 切开。这一层认知极其重要:当某天 fetch 行为诡异(重定向、压缩、超时),你能立刻下沉到字节层面定位,而不是只会骂"网络抽风"。我建议把 net 当成"网络世界的显微镜"——它让你看见 http 模块、甚至浏览器网络栈,都只是这层原始字节交互的封装。理解了底座,上层任何框架你都能一眼看穿。