fetch 到操作系统 Socket:TS网络编程入门


一、引言:网络编程不只是“调接口”

在学完 TypeScript 的类型系统、泛型和异步编程之后,我一度以为网络编程就是记住 fetch 的用法:

const res = await fetch('/api/user');
const data = await res.json();

直到我开始追问几个问题:fetch 返回的 Promise 到底是谁在 resolve?数据是怎么从网卡跑到我的回调函数里的?为什么 await 不会阻塞页面?我才意识到,网络编程的本质不是“调接口”,而是理解数据在不同层级之间的流动。

这篇文章从我最熟悉的 fetch 出发,向下梳理 HTTP、TCP/UDP、Socket,以及 JavaScript 事件循环如何调度网络请求,试图建立一张完整的网络编程知识地图。


二、应用层:HTTP 与 fetch 的两阶段模型

2.1 HTTP 是一套报文格式,不是“一个函数”

fetch('https://api.example.com/user') 在底层发送的是一段纯文本报文:

GET /user HTTP/1.1
Host: api.example.com
Accept: application/json
Connection: keep-alive

服务器返回的也是文本:

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 27

{"id":1,"name":"张三"}

fetch 帮我们把 JS 层面的调用翻译成 HTTP 报文发出去,再把返回的报文封装成 Response 对象。理解这一点很重要,因为当你遇到跨域、Content-Type 错误、或者状态码问题时,你其实是在和 HTTP 协议打交道,而不是 fetch API 本身的问题。

2.2 为什么 fetch 要分两个阶段?

这是我之前踩过的一个坑。fetch 的调用分成两步:

const res = await fetch(url);      // 阶段一:拿到 Response(只有头部)
const data = await res.json();     // 阶段二:解析 Body(流式读取)

原因是 HTTP 响应是流式传输的。响应头先到达(可能只有几百字节),Body 可能有几 MB。浏览器允许你在 Body 还在传输时,先根据 Header 做决策,甚至通过 AbortController 取消下载。

2.3 状态码:服务器的“肢体语言”

做网络编程必须学会读状态码,而不是只判断 === 200

  • 200 成功,201 已创建,204 无内容
  • 301/302 重定向,fetch 默认自动跟随
  • 400 请求参数错误,401 未授权(Token 过期),403 禁止访问,404 资源不存在
  • 500 服务端内部错误

另外,response.oktrue 的条件是状态码在 200-299 之间。这是一个常见的面试陷阱。


三、传输层:TCP、UDP 与 Socket

应用层的下面一层是传输层。浏览器里的 JS 不能直接操作传输层,但理解这一层对排查网络问题至关重要。

3.1 TCP:面向连接的可靠传输

HTTP 建立在 TCP 之上。TCP 的核心特征可以概括为“三次握手,四次挥手”:

  1. 三次握手:客户端和服务端互相确认“我能发,也能收”
  2. 数据传输:每个包有编号,丢了自动重传,到达后按序重组
  3. 四次挥手:双方确认“我没有数据要发了”,优雅关闭连接

代价:建立连接需要时间(几十到几百毫秒),头部开销大。所以 HTTP/1.1 引入了 Connection: keep-alive,复用同一个 TCP 连接发多个请求。

3.2 UDP:无连接的快速传输

UDP 没有握手、没有确认、没有重传,直接发:

  • 头部极小(8 字节),传输极快
  • 不保证送达,不保证顺序
  • 适用场景:视频直播(丢一帧不卡)、在线游戏(位置同步要实时性)、DNS 查询

3.3 Socket:操作系统提供的“通信插座”

这是我最开始困惑的概念。Socket 不是协议,也不是物理硬件,而是操作系统提供的编程接口(API)

当我在浏览器里写 fetch 时,浏览器底层通过 Socket API 告诉操作系统内核:“我要和目标 IP 的 443 端口建立 TCP 连接,然后发这段 HTTP 报文。”剩下的所有事情——TCP 握手、分包、IP 寻址、网卡驱动——都由操作系统负责。

在 Node.js 中,你可以直接摸到 Socket:

// TCP Socket
import * as net from 'net';
const socket = net.createConnection({ port: 8080 });
socket.write('hello');  // 内核负责 TCP 的可靠性保证

浏览器出于安全考虑,不允许 JS 直接创建裸 TCP/UDP Socket。我们只能使用浏览器封装好的高层 API:fetch(基于 TCP + HTTP)和 WebSocket(基于 TCP + WebSocket 协议)。


四、WebSocket:浏览器里的“实时双向管道”

fetch 是“请求-响应”模式:你问一句,服务器答一句,连接断开。但实时聊天、股票行情、多人协作需要服务器主动推数据

WebSocket 解决了这个问题:

const ws = new WebSocket('ws://localhost:8080');

ws.onopen = () => ws.send('hello');
ws.onmessage = (e) => console.log('收到:', e.data);

它与 fetch 的核心区别:

特性 fetch (HTTP) WebSocket
连接 短连接,每次新建 长连接,一次握手后保持
通信方向 客户端 → 服务端(单向请求) 双向:双方都能主动发
实时性 需要轮询才有实时性 真正的实时推送
建立过程 直接发 HTTP 先 HTTP 握手,再 Upgrade 到 WebSocket

值得注意的是,WebSocket 建立连接时,先通过 HTTP 发送一个 Upgrade: websocket 请求,服务器同意后,连接才从 HTTP 切换为 WebSocket。所以它仍然依赖 TCP。


五、事件循环:异步网络请求的调度中枢

网络编程和异步编程是绑定的。理解事件循环,才能真正明白为什么 await fetch() 不会阻塞页面:

console.log('A');
fetch('/api').then(res => console.log('B'));
console.log('C');

输出是 A → C → B。为什么?

  1. fetch 把实际的网络请求交给浏览器的网络线程(C++ 层面,真正的多线程)去处理
  2. JavaScript 主线程立刻返回,继续执行同步代码 console.log('C')
  3. 网络线程完成请求后,把回调塞进微任务队列
  4. 同步代码结束,事件循环清空微任务队列,执行 console.log('B')

这就是网络编程在单线程 JS 中的真相:我们的代码不负责“等待网络”,只负责“被通知”await 不是阻塞线程,而是“注册一个恢复函数,等数据到了继续执行”。

Node.js 的事件循环更复杂(6 个阶段 + process.nextTick),但核心逻辑一致:网络 I/O 的等待发生在操作系统层面,JS 主线程通过事件循环消费结果。


六、工程实践:类型安全、缓存、取消与超时

学完协议和原理,最终要落到代码上。以下是我在实际练习中总结的几个工程模式:

6.1 泛型封装:给网络层加上类型契约

async function http<T>(url: string): Promise<T> {
    const res = await fetch(url);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return res.json() as T;
}

接口返回的数据结构就是前后端的契约。用泛型在编译期约束这个契约,避免运行时访问不存在的属性。

6.2 Map 缓存:避免重复请求

const cache = new Map<string, Promise<any>>();

function fetchWithCache(key: string): Promise<any> {
    if (cache.has(key)) return cache.get(key)!;
    const promise = fetchData(key);
    cache.set(key, promise);
    return promise;
}

缓存的是 Promise 而不是结果数据,这样即使请求还没完成,并发调用也能共享同一个网络连接。

6.3 AbortController:取消竞态请求

const controller = new AbortController();
fetch(url, { signal: controller.signal });

// 用户快速切换时,取消上一个未完成的请求
controller.abort();

6.4 超时封装

function fetchWithTimeout(url: string, ms: number) {
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), ms);
    return fetch(url, { signal: controller.signal })
        .finally(() => clearTimeout(timeout));
}
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐