不管是刚入行的新人,还是写了五六年页面的老手,只要打开浏览器F12看两眼,就一定见过Network面板里那一条条密密麻麻的请求记录。可如果真的被问一句"一个HTTP请求到底由哪几部分组成",能答全的人反而不多。我在面试候选人的时候,超过一半的人能说出请求头和请求体,但能把请求行、空行、请求体层层拆清楚,顺带讲明白浏览器怎么组包、服务器怎么解析的,一上午几乎碰不到两三个。
说白了,前端几乎所有工作流——接口联调、数据获取、鉴权登录、文件上传、日志上报——都离不开HTTP请求。你写的那行axios.get(url, config),背后是浏览器替你拼装了一整套网络报文;你在Network面板里看到的若干条记录,每条背后都是一次完整的"请求-响应"循环。这篇内容就围绕前端的http请求组成这个主题来拆,从请求的四段式结构讲起,深入到请求行、请求头、请求体,再到浏览器实际发请求时经历的完整链路和排查技巧。适合正在准备前端面试、刚接手接口联调、或者想系统过一遍HTTP基础的同学,看完至少能做到一件事:下次打开F12,你能比身边大多数人多看懂三层信息。
1. HTTP请求的完整框架:先看懂这一个报文
1.1 一条真实请求的抓包截图长什么样
先别急着背概念。我习惯在讲任何协议细节之前,先让读者"看见"一条真实的HTTP请求。最简单的验证方式,是在浏览器里打开任何一个网站,按F12进入Network面板,刷新页面,随便点一条资源请求(比如一个JS文件或图片),然后找到Request Headers区域,点击"view source"或者"原始报文"。
你会看到类似这样的原始文本:
GET /api/user/info?userId=123 HTTP/1.1 Host: api.example.com Connection: keep-alive Accept: application/json, text/plain, */* User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ... Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 Cookie: sessionId=abc123; token=xyz789这四行看起来其貌不扬,但它就是一次完整HTTP请求的"骨架"。我们所有前端的请求代码、所有的鉴权逻辑、所有跨域配置,最终都会被压缩成这么一段纯文本,丢到网络上传输。
我踩过的一个典型误区是:很多同学以为HTTP请求是某种结构化的对象或者JSON,其实不是。HTTP协议从设计之初就是"纯文本协议",意思是你发给服务器的,本质上是这段有固定格式的ASCII文本。这正是它和HTTPS、HTTP/2之类协议的明显区别所在,也是理解一切网络调试的基础。
1.2 四段式组成:请求行、请求头、空行、请求体
官方定义里,一个标准的HTTP请求由四个部分组成,顺序绝对不能乱:
- 请求行(Request Line):方法 + 空格 + URI + 空格 + 协议版本,例如 GET /index.html HTTP/1.1
- 请求头(Request Headers):一堆"键: 值"形式的字段,每行一个,描述请求的各种属性和约束
- 空行(CRLF):一个不可见的回车换行,专门用来分隔"头部"和"主体"
- 请求体(Request Body):可选的部分,GET请求通常没有,POST/PUT等请求用来承载业务数据
很多人会漏掉"空行"这一层。别看它只是一个换行符,它的作用极其关键。服务器解析请求的时候,是靠"空行"来判断头部是否结束、消息体是否开始的。如果没有这个空行,服务器会一直读头字段,永远等不到数据。我见过有同学自己在Node里用raw TCP拼HTTP报文测试,结果一直报400 Bad Request,排查半天发现是拼接字符串时漏了\r\n\r\n这个分隔符——这就是典型的被"隐形的空行"坑了一次。
用生活化的方式来理解:请求行相当于快递单上"发件人-收件人-快递类型"那一栏,请求头是快递外包装上的所有备注标签(要不要冷藏、要不要签收确认、发件方信息),空行是"标签区域到此结束"的分隔条,请求体才是箱子里面塞的实际物品。没有分隔条,仓库人员就分不清哪里是标签、哪里是内容物。
2. 请求行拆解:方法与地址的前世今生
2.1 请求行的三个要素:方法、URI、协议版本
请求行的格式固定到每个字符都有意义,我甚至建议你把它当"固定句式"来记:
GET /api/user/info?userId=123 HTTP/1.1 方法 URI(含查询参数) 协议版本方法代表"你要干什么",URI代表"你要对哪个资源干",协议版本代表"你用什么语法的协议说这句话"。
这里有一个值得展开的点:大部分前端对URI的理解是"URL"的另一种叫法,这个说法不完全准确。URI是一个统称,它包含URL和URN。前端日常说的地址,如https://api.example.com/api/user/info?userId=123,严格来说是一个URL(统一资源定位符)。URL不仅指出了资源的位置,还包含了访问它的协议。而在HTTP请求行中,URI通常指的就是"路径+查询参数"这一部分,即/api/user/info?userId=123,协议版本单独放在末尾,和域名、端口剥离开。
再来说协议版本。虽然现在HTTP/2和HTTP/3已经在生产环境大规模落地,但在开发者工具里看到的绝大多数原始报文仍然是HTTP/1.1格式,因为HTTP/2的报文是二进制分帧的,默认不展示成纯文本。我建议你至少清楚三个版本的关系:HTTP/1.1最普及,解决了连接复用(keep-alive)问题;HTTP/2引入多路复用,解决了队头阻塞;HTTP/3改用UDP协议承载,进一步减少握手延迟。面试如果被问到,能讲清楚这条演进逻辑,比干背几个特性强得多。
2.2 GET和POST的底层差异,不只是"长度限制"
前端面试里有一道高频题:GET和POST的区别。新手都会背"GET参数在URL里,POST参数在Body里",但这个答案顶多算及格。往深一层看,两者最核心的区别其实是语义与幂等性。
从HTTP规范的角度,GET是"安全且幂等"的方法。安全的意思是:这个请求不应该在服务器上产生副作用,即不应该因为请求而改变服务器的数据状态。幂等的意思是:同一请求执行多次和执行一次效果完全一样。所以浏览器对GET请求会主动缓存,会允许你刷新、回退、收藏,而不会弹出安全提示。POST则没有这些保证,所以浏览器默认不会主动缓存POST请求,回退时也会提示"确认重新提交表单"。
还有一个容易被忽略的点:GET请求确实没有请求体,所有参数只能塞在URL的查询字符串里。这个"只能放URL"带来两个实际后果:一是长度受限——虽然HTTP协议本身没规定URL长度上限,但浏览器和服务器都有实现限制(Chrome大概支持几万个字符,但很多服务器默认限制在8KB或16KB),传大段文本或复杂结构容易出问题;二是安全性差——参数会出现在历史记录、日志、Referrer字段里,所以绝对不要用GET传密码、Token等敏感信息。
2.3 PUT、DELETE、OPTIONS:前端真正会用到的是哪几个
日常开发中,前端最常打交道的是GET和POST,但理解其他方法能帮你读懂框架源码和调试信息。
- PUT:一般用于整体替换资源,幂等。很多团队拿它做更新接口,但后端真实实现是否真的幂等,得看业务代码。
- DELETE:删除资源,幂等。不过浏览器里直接用axios/fetch发DELETE其实没问题,真正头疼的是"跨域时预检触发"的问题,后面单独讲。
- PATCH:部分更新资源。前端用得少,很多后端框架的约定也不统一。
- OPTIONS:预检请求(Preflight)专用。浏览器发现跨域请求是"非简单请求"时,会先自动发一条OPTIONS请求,问服务器"我能不能用这个方法和这些头",服务器允许后浏览器才发真实请求。这是整个前端请求体系里最容易让新手困惑的一环。
还有HEAD、CONNECT、TRACE这些,前端几乎接触不到,了解名字即可。真正深入业务时你会发现:你以为你在发PUT/DELETE,实际因为网络中间件与后端框架的限制,请求方法会被"降级"成POST,或者服务器只接收POST。之前我遇到过用RESTful风格设计的接口,前端发起DELETE请求,到了Nginx那层直接拦截返回405,最后后端改成POST + 自定义方法头才解决。
3. 请求头实用分类:哪些字段你真得记住
3.1 基础必备字段:Host、Connection、User-Agent、Accept
进入请求头部分,我不建议挨个字段背,而是按"功能类别"来记忆。开发中最常遇到的基础字段有四个。
Host字段表示"你要访问的域名和端口",是HTTP/1.1里新增的字段。它的作用是让一台物理服务器同时承载多个域名(虚拟主机)。如果Host和实际IP对应的证书/配置不匹配,服务器理都不理你。用Nginx配置过多个站点的人应该深有体会,错误配置Host导致返回默认站点是极常见的问题。
Connection字段在HTTP/1.1里默认值是keep-alive,意思是"这个TCP连接别用完就扔,留着给后续请求复用"。它解决了早期HTTP每次请求都要重新建连的性能问题。你在Network面板里看到某个请求的耗时特别短(比如几毫秒),很可能就是连接复用的功劳。
User-Agent就是用户代理标识,把操作系统、浏览器内核、版本等一堆信息塞给服务器。前端开发中它常被用来做兼容性处理或统计来源,但也要注意:这个头是可以伪造的,所以它只适合做业务统计,不适合做真正的安全校验。
Accept系列头表达的是"我能接受什么样的响应内容":Accept表示我能解析哪几种媒体类型,Accept-Encoding表示我支持哪些压缩算法(gzip、br等),Accept-Language表示我偏好的语言。如果后端返回的内容类型不在Accept范围内,有些严格的服务端会返回406 Not Acceptable。
3.2 Content-Type:让服务器读懂你的请求体
Content-Type是请求头里和前端业务绑定最深的一个,它告诉服务器"请求体里的数据是以什么格式组织的"。服务器拿到请求体后,就是靠这个字段来判断应该用什么解析器来读Body。常见的有四种:
| Content-Type | 请求体格式 | 典型场景 |
|---|---|---|
| application/x-www-form-urlencoded | key=value&key2=value2 | 原生HTML表单提交 |
| application/json | 标准JSON字符串 | axios/fetch传对象数据 |
| multipart/form-data | 由boundary分隔的多段数据 | 文件上传、混合表单 |
| text/plain | 纯文本 | 简单调试、部分WebSocket握手场景 |
我重点说两个容易翻车的点。第一,axios里如果你直接传一个JS对象,又没设置Content-Type,axios会默认把对象序列化成JSON,并自动设置application/json;但如果你用了axios.post(url, 'name=abc&age=18')这种字符串,它默认就是form-urlencoded。两种格式后端解析方式完全不同,后端如果拿到JSON却用表单解析器去读,就会得到一串解析不了的乱码。前后端联调时遇到"我明明传了参数,后端却说没收到",八成问题出在这里。
第二,multipart/form-data格式会有一个boundary参数,例如multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxk。这个boundary是浏览器随机生成的字符串,用来在请求体里分隔各段字段。如果你用代码手动拼接这种请求体,必须确保boundary一致,并且每段必须以--boundary开头、以--boundary--结尾。我见过有人用Postman调试上传接口没问题,一换成自己用fetch拼form-data就报错,原因就是boundary拼错了。
3.3 鉴权相关:Cookie、Authorization与Token的恩怨
前端项目里,鉴权方式一般就两种阵营:Cookie会话型和Token型(JWT或OAuth Token)。
Cookie是浏览器自动管理的,你只需要在登录时通过Set-Cookie种下去,之后浏览器在请求同域名时会在请求头里自动带上Cookie: sessionId=abc123。它最大的特点是"浏览器自动携带",省心,但也有明显弱点:跨域不自动携带(需要withCredentials配置)、CSRF风险需要防范、不适合跨端(App里没有Cookie这个概念)。
Authorization头一般用于Token鉴权,格式通常是Authorization: Bearer <token>。相比Cookie,它更灵活,Flutter、小程序、App都可以用,服务器端也无状态。前端在axios里做的"请求拦截器",很大一部分工作就是每次请求前从localStorage或store里取Token,手动塞进这个头里去。
一个我强烈建议你掌握的排查技巧:如果某个接口返回401,先看这个接口请求的Authorization头是否存在、Token是否过期;如果接口能通但权限不对,再看后端解析的是Authorization头还是Cookie里的session,因为有些老项目的网关只认其中一种。这类问题在微服务网关里特别常见,前端花在"Token带到了,但它没被识别"上的排查时间,远比写业务代码多。
顺便提一个不常见但很坑的情况:自定义请求头的名字若是带下划线(如user_token),在某些服务器或WAF(Web应用防火墙)的默认配置下会被直接丢弃。这个坑我是在排查一个生产环境问题时发现的,请求头里取不到自定义字段,后端日志里完全不显示。后来把请求头改名为X-User-Token,问题立刻消失。所以自定义请求头尽量用短横线命名,避免下划线。
4. 请求体的三种传参姿势与数据序列化细节
4.1 从URL Query到JSON Body再到FormData
请求体是可选的,它的格式完全取决于Content-Type。前端实际项目里,传参基本就三种姿势。
第一种是URL Query传参,即/api/user/info?userId=123&name=abc。它最常见于GET请求,但也有人会把参数全拼在URL里用POST请求(这种做法其实不规范,但不罕见)。优点是很直观,浏览器地址栏直接可见,接口调试最方便;缺点是只能传扁平字符串键值对,复杂嵌套结构几乎没法表达。
第二种是JSON Body传参,即axios.post('/api/user/save', { name: '张三', age: 30, address: { city: '北京' } })。这是SPA(单页应用)时代最主流的传参方式。它能表达任意嵌套结构,后端用Jackson/Gson/JSON库就能轻松映射成对象或Map。不过需要注意一点:JSON里的null值在传输时会被保留为字符串null,后端如果没有处理好空值策略,可能出现"该字段没传但成了null"的错觉。
第三种是FormData传参,即const fd = new FormData(); fd.append('file', fileInput.files[0])。文件上传几乎必须用它,因为只有multipart格式能处理二进制内容。它也能混合普通字段和文件字段,是前端上传功能的标准做法。
4.2 嵌套对象、数组序列化的隐藏坑
开发到一定阶段,一定会遇到这样的需求:传参数据结构是{ filters: { status: 'active', tags: ['a', 'b'] }, page: 1 }。如果你用GET请求,想把这种对象拼进URL Query,问题就来了。
常见的做法是手动拼:?filters[status]=active&filters[tags][0]=a&filters[tags][1]=b&page=1。但这种格式严格来说并不属于HTTP规范,只是各种框架约定俗成的序列化方式。React、Vue生态里常用的qs库、axios的paramsSerializer,各有一套对"嵌套对象转URL Query"的处理策略。后端如果用的框架不支持这种括号语法,解析出来的结构和你想象的可能完全不同。
我踩过一次经典的大坑:前端用axios默认序列化,对象是{ filters: { status: 'active' } },结果发出的URL是?filters%5Bstatus%5D=active(中括号被URL编码成了百分号形式)。Java后端的Map接收到的是filters[status]这样一个完整字符串键,而不是嵌套Map。最后前端改用了JSON字符串序列化:把filters直接JSON.stringify成一个字符串塞进参数,后端再解析这个字符串,才彻底解决。
所以我现在的传参规范是:
- 查询条件简单(不超过3个平铺字段):用URL Query
- 查询条件复杂、嵌套层级深:把整个查询条件对象打包成一个JSON字符串,或直接用POST + JSON Body
- 任何文件、二进制内容:用FormData
这三条规范看起来简单,但能避免大量让人抓狂的联调问题。
4.3 请求体的大小限制:为什么大文件上传要单独设计
HTTP协议本身对请求体大小没有限制,但服务器和网关通常会配置一个上限。Nginx默认的client_max_body_size是1MB,Tomcat默认也有maxPostSize限制,Spring Boot的spring.servlet.multipart.max-file-size默认1MB。也就是说,前端如果一上来就对接服务器默认配置,普通的JSON请求没问题,但上传一个3MB的图片或者几十MB的视频,很可能直接得到413 Request Entity Too Large。
这也解释了为什么大文件上传功能不能简单走"一次POST塞整个文件"的老路。生产环境的上传方案往往要做分片:把大文件切成若干块(例如每块2MB或5MB),逐块上传,后端再按顺序合并。分片上传还附赠一个好处——断点续传。前端用File.prototype.slice()切分文件,配合Worker、进度条和并发控制,这就是另一个可以单独展开的大话题了。
5. 浏览器组包与全链路:从一行代码到Network面板
5.1 从axios到真实网络报文,中间发生了什么
我们写的axios.get(url, config)只是一个"请求描述",真正让它变成网络报文的是浏览器或Node的底层实现。浏览器环境里,axios底层是基于XHR(XMLHttpRequest)来发请求的,而XHR本身也是一层API,真正做网络I/O的是浏览器内核的网络模块。
整个组包流程大致如下:
- 你调用axios方法,传入URL、参数、请求头配置
- axios合并默认配置和你的自定义配置,生成一个XHR请求对象
- 浏览器网络模块根据URL解析出协议、域名、端口、路径
- 如果是HTTPS地址,先完成TLS握手
- 如果请求需要走代理或已有连接池有可用连接,直接复用TCP连接;否则新建TCP连接(DNS解析、TCP三次握手)
- 浏览器按照HTTP协议格式,把请求行、请求头、请求体拼装成字节流
- 网络层把字节流交给操作系统,最终通过网卡发往服务器
这一段链路里,前端最容易感知到的性能瓶颈有三个:DNS解析耗时、TCP握手耗时、TLS握手耗时。Network面板的Timing标签页把这几段时间拆得清清楚楚,分别是Stalled、DNS Lookup、Initial Connection、SSL、Request Sent、Waiting(TTFB)、Content Download。如果你发现某个接口的整体耗时很长,先看是Waiting时间太长(后端慢)还是DNS/Connection时间太长(网络链路问题),再决定从哪端优化。
5.2 CORS预检:为什么一个POST请求在Network里会出现两条记录
跨域问题是前端绕不开的坎。当你从前端页面A(例如http://localhost:3000)向域名B发请求时,如果B的响应头里没有CORS相关的放行字段,浏览器就会拦截响应,前端表现为"接口报错、Network里看到红色请求"。
但更隐蔽的是"预检请求"。当一个实际请求满足以下任一条件:使用PUT/DELETE等方法、设置了自定义请求头(如Authorization、X-Requested-With)、Content-Type不是简单类型(如application/json)——浏览器就会先自动发起一条OPTIONS请求,去询问服务器:"我即将向你发一个这样的请求,你允许吗?"
于是你在Network面板里就会看到两个请求:第一个是OPTIONS(状态码往往是204或200),第二个才是真正的POST。很多新手第一次看到这种"多了个请求"的情况会以为代码有问题,其实这是CORS机制的正常行为。
排查跨域问题的正确姿势是:先看OPTIONS预检请求的响应头里有没有Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers这些字段。只要有一个缺失或值不匹配,真实请求就不会被放行。在我经手的联调里,最常见的问题是:后端把Access-Control-Allow-Headers写成了*,但某些框架版本不支持通配符匹配Authorization头,导致每次带Token的跨域请求都失败。
5.3 请求头来源追踪:哪些头是浏览器自动加的,哪些是代码加的
拿到一条请求报文后,我教前端同事的第一个技巧就是"区分哪些头是浏览器自动加的,哪些是你代码加的"。
浏览器自动加的头包括:Host、Connection、User-Agent、Accept(部分)、Accept-Language、Accept-Encoding,以及同域下自动携带的Cookie,还有来源信息Origin/Referer。你代码里通常只能手动设置:Content-Type、Authorization、自定义业务头(如X-Request-Id)、以及一些覆盖默认Accept的配置。
为什么这个区分很重要?因为有Bug时要快速定位责任方。如果发现请求里根本没有Authorization头,第一反应应该是"前端代码里是不是没执行请求拦截器,或者Token没取到",而不是跑去看后端;如果发现请求头里Content-Type是text/plain而不是application/json,第一反应应该是"axios的数据传参姿势不对"——比如你把对象又包了一层字符串。
我曾经在项目里排查过一个问题:登录接口总是401,我打开Network一看,Cookie字段带了,但Authorization头不见了。查了半小时,发现是请求拦截器在调用时被try-catch吞掉了异常,Token读取失败但代码继续往下走,导致最终发出的请求缺了关键头。日志打到浏览器控制台才能看见那个报错。所以,先把请求头的前后端职责边界画清楚,再排查问题,效率至少翻一倍。
6. 构建请求时的常见坑与排查技巧
6.1 高频问题速查表
下面这张表里的问题,是我这几年在实战中帮同事排查过至少十几次的典型案例,统一整理成速查表,可以直接对照自查。
| 现象 | 排查方向 | 常见原因 |
|---|---|---|
| Network面板看不到请求 | 看代码是否真的执行到发起请求的逻辑,是否被异常拦截 | 请求拦截器抛错、Promise提前reject |
| 接口显示CORS错误 | 看响应头Access-Control-*字段 | 缺少Allow-Headers、Origin配置错误、预检未通过 |
| 返回401 | 看请求头Authorization与Cookie | Token过期、Token没带上、后端不认自定义头 |
| 前端传了参数,后端收不到 | 对比Content-Type与请求体格式 | axios对象被序列化成JSON但后端用表单解析 |
| 大文件上传报413 | 查Nginx与后端限流配置 | client_max_body_size / multipart限制 |
| 刷新页面后老接口突然404 | 看URL是否写死、路由是否加了hash | 基路径变化、部署路径调整 |
| URL太长导致请求失败 | 看查询字符串长度 | 应改POST + JSON Body |
| 状态码200但数据异常 | 扒开响应体的真实内容 | 后端返回了JSON结构变更、网关篡改 |
务必理解一行字:Network面板里看到的状态码,反映的是"HTTP传输层"的结果。只要服务器返回了任何响应,哪怕状态码是500、502,对于浏览器来说请求也是"完成"的,真正的业务错误要进入响应体的JSON里去读code字段(很多团队用{ code: 0, data: ... }包裹)。很多新人看到500就觉得是网络问题,直接喊运维,结果点开响应体一看,是后端业务代码抛的异常。
6.2 抓包与调试工具的实用心得
调试HTTP请求,最常用的工具其实是浏览器自带的Network面板,但你真的用对了吗?我强调几个实用习惯:
一是"查阅原始报文"的习惯。默认视图里Headers是分类排布的,虽然好看,但容易掩盖真实格式。建议经常切到"view source"模式,直接看原始请求头和原始响应头,很多奇怪的字段问题一眼就能看到。比如前文说的自定义头带下划线被丢弃,只有在原始报文里才看得清。
二是养成"清空缓存后重放"的习惯。如果怀疑浏览器缓存导致拿不到最新数据,勾选Network面板里的"Disable cache",再进行一次完整的刷新。但注意,这个选项只在开发者工具打开时生效,而且只影响浏览器自身的HTTP缓存,不影响CDN层缓存。
三是使用抓包代理工具的时机。当场景涉及App内H5页面、小程序、跨端环境时,浏览器F12就不够用了。此时用Charles或Fiddler这类代理工具,把手机的代理指到电脑上,就能看到真实流量。这类工具的一个核心功能是"断点修改",可以拦截请求后手动改动请求头或响应体再放行,用来模拟后端异常返回特别顺手。
四是学会看Timing标签。前面提到的DNS、TCP、TLS、TTFB都是在这里展示的。如果TTFB(服务器首字节时间)很长,说明后端处理慢;如果Content Download很慢,说明响应体体积大或网络带宽受限;如果Stalled时间很长,说明有大量请求在排队。三类问题,优化方向完全不同。
6.3 一个"请求缓慢"案例的完整排查过程
我拿一个真实案例收尾这部分。曾经有个项目反馈"列表接口偶尔会卡住5秒以上"。
我打开Network面板追踪接口,发现TTFB显示约4.8秒,DNS和Connection都非常快。这个现象第一时间把矛头指向后端处理耗时,而不是网络问题。但奇怪的是,后端负责人表示接口平均耗时只有80毫秒。
后来我再看原始请求,发现Connection头是close,而不是keep-alive。继续查,发现请求走了代理网关,网关对某些路径会强制关闭连接,导致每次请求都要重新做TCP握手+TLS握手,中间还涉及网关和后端之间的连接重建,累计时间直接翻了几十倍。最终的解决办法是:修改网关配置,对该路径启用连接复用;同时前端把超时时间从默认值调大一些,以覆盖极端网络场景。
这个案例说明什么?排查HTTP请求慢的问题,不能只看一个指标,要把Network面板的Timing字段和原始报文结合起来,层层排除传输、网关、后端三个环节的问题。这正好呼应了整篇文章的主题:只有清楚一个HTTP请求从内到外的组成,你才知道它到底可能在哪个环节出问题。
写到这里,我最大的感受是:HTTP请求的组成看起来是个基础得不能再基础的话题,可一旦深入到真实项目中,你会发现每一个字段、每一个空行、每一次预检背后都有故事。我前几年带新人时,会专门安排半天让他们把Network面板里随便一条请求的原始报文抄一遍,再手写一个用TCP协议发送HTTP请求的小脚本,理解立刻不一样。如果你也想真正掌握这部分知识,我建议也可以这么来一遍——这比刷十道面试题都管用。