从 fetch到操作系统 Socket:TS网络编程入门
本文从TypeScript网络编程实践出发,深入解析了从应用层到传输层的网络通信原理。首先剖析了HTTP协议与fetch API的两阶段模型,解释了流式传输特性与状态码机制。接着探讨了TCP/UDP协议的核心差异,以及Socket作为操作系统接口的作用。文章还对比了WebSocket与HTTP的实时性差异,并揭示了事件循环如何调度异步网络请求。最后提供了类型安全封装、缓存策略、请求取消与超时处理等
从 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.ok 为 true 的条件是状态码在 200-299 之间。这是一个常见的面试陷阱。
三、传输层:TCP、UDP 与 Socket
应用层的下面一层是传输层。浏览器里的 JS 不能直接操作传输层,但理解这一层对排查网络问题至关重要。
3.1 TCP:面向连接的可靠传输
HTTP 建立在 TCP 之上。TCP 的核心特征可以概括为“三次握手,四次挥手”:
- 三次握手:客户端和服务端互相确认“我能发,也能收”
- 数据传输:每个包有编号,丢了自动重传,到达后按序重组
- 四次挥手:双方确认“我没有数据要发了”,优雅关闭连接
代价:建立连接需要时间(几十到几百毫秒),头部开销大。所以 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。为什么?
fetch把实际的网络请求交给浏览器的网络线程(C++ 层面,真正的多线程)去处理- JavaScript 主线程立刻返回,继续执行同步代码
console.log('C') - 网络线程完成请求后,把回调塞进微任务队列
- 同步代码结束,事件循环清空微任务队列,执行
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));
}
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)