服务端是如何解析 HTTP 请求的数据:从 TCP 字节流到结构化请求对象

张开发
2026/4/5 23:16:09 15 分钟阅读

分享文章

服务端是如何解析 HTTP 请求的数据:从 TCP 字节流到结构化请求对象
服务端是如何解析 HTTP 请求的数据从 TCP 字节流到结构化请求对象01. 前言浏览器发出的字符串服务器怎么读懂02. 总体解析流程宏观视图03. 第一步从 TCP 接收字节流04. 第二步解析请求行Request Line解析流程代码伪逻辑05. 第三步解析请求头Request Headers解析流程特殊处理06. 第四步根据头部决定如何读取请求体6.1 有 Content-Length 时6.2 Transfer-Encoding: chunked 时6.3 无 body 的情况07. 第五步根据 Content-Type 解码请求体常见 Content-Type 处理方式解码流程图示例解析 JSON body示例解析表单 URL 编码08. 完整解析流程图服务端视角09. 常见边缘情况与处理10. 不同语言的解析实现差异11. 解析后的 Request 对象示例伪代码12. 总结The Begin点点关注收藏不迷路01. 前言浏览器发出的字符串服务器怎么读懂当你在浏览器输入https://example.com/api/user并回车浏览器构造了一个 HTTP 请求把它变成一段字节流发送给服务器。服务器如 Nginx、Tomcat、Node.js接收到这段数据后需要完成一个关键任务把原始的字节流解析成方便程序使用的结构化对象包含请求方法、URL、请求头、请求体等。这个过程涉及TCP 粘包处理、逐行解析、请求头键值对拆分、请求体按 Content-Type 解码、字符集处理、分块传输解码等。今天我们深入服务端内部模拟一遍完整的 HTTP 请求解析流程。02. 总体解析流程宏观视图客户端发送 HTTP 请求字节流 │ ▼ TCP 数据到达可能分多个包 │ ▼ ┌──────────────────────────────────────────────────────────┐ │ 1. 读取数据并拼接处理粘包/拆包 │ │ 2. 查找请求行method path version │ │ 3. 逐行读取请求头key: value直到空行 │ │ 4. 根据 Content-Length / Transfer-Encoding 读取请求体 │ │ 5. 按 Content-Type 解码请求体JSON/表单/文件/纯文本 │ │ 6. 封装成 Request 对象供业务代码使用 │ └──────────────────────────────────────────────────────────┘ │ ▼ 业务代码获取参数req.query / req.body / req.headers03. 第一步从 TCP 接收字节流HTTP 基于 TCP 传输服务器监听端口如 80 或 443。当客户端发送请求时数据可能被拆分成多个 TCP 包也可能多个请求在一个包中Nagle 算法、TCP 粘包。TCP 接收缓冲区 ┌─────────────────────────────────────────────────────────┐ │ [GET /api/user HTTP/1.1\r\nHost: example...] (第一个包) │ │ [\r\nContent-Type: application/json\r\n] (第二个包) │ │ [Content-Length: 18\r\n\r\n{name:张三}] (第三个包) │ └─────────────────────────────────────────────────────────┘服务端需要持续接收数据直到读到一个完整的 HTTP 请求处理粘包多个请求在一个包里需逐个解析处理拆包一个请求分多次到达需拼接04. 第二步解析请求行Request Line请求行格式固定METHOD SP Request-URI SP HTTP-Version CRLF解析流程原始字节流示例 GET /api/user?page2size10 HTTP/1.1\r\n 解析步骤 1. 读取直到第一个 \r\n 2. 按空格分割成三部分 - method GET - uri /api/user?page2size10 - version HTTP/1.1 3. 进一步解析 URI - path /api/user - queryString page2size10 - 解析 query 为键值对page2, size10代码伪逻辑function parseRequestLine(line): parts line.split( ) method parts[0] uri parts[1] version parts[2] queryStart uri.indexOf(?) if queryStart 0: path uri.substring(0, queryStart) queryString uri.substring(queryStart 1) queryParams parseQueryString(queryString) else: path uri queryParams {} return {method, path, queryParams, version}05. 第三步解析请求头Request Headers请求头格式Key: Value CRLF直到遇到单独的 CRLF空行表示头部结束。解析流程原始字节流示例 Host: api.example.com\r\n User-Agent: Mozilla/5.0\r\n Content-Type: application/json\r\n Content-Length: 42\r\n Authorization: Bearer xyz123\r\n \r\n ← 空行表示头部结束 解析步骤 1. 逐行读取每行以 \r\n 结尾 2. 遇到空行行长度为 0 或仅 \r\n→ 停止解析头部 3. 每行按第一个冒号分割 key 冒号前的字符串转小写去空格 value 冒号后的字符串去首尾空格 4. 存入 headers 对象如 headers[content-type] application/json特殊处理情况处理方式同一 header 出现多次合并为数组如Set-Cookie大小写不敏感通常转为小写存储值中包含冒号只按第一个冒号分割如Location: http://折叠头历史规范已废弃合并多行几乎遇不到06. 第四步根据头部决定如何读取请求体空行之后的数据是请求体Request Body。但并不是所有请求都有 bodyGET 无 bodyPOST/PUT 才有。读取 body 的方式取决于两个关键 headerHeader作用Content-Lengthbody 的字节数读取精确长度后结束Transfer-Encoding值为chunked时使用分块传输编码6.1 有 Content-Length 时Content-Length: 42\r\n \r\n {username:alice,password:123} ← 读取 42 字节不多不少6.2 Transfer-Encoding: chunked 时分块格式Transfer-Encoding: chunked\r\n \r\n 1A\r\n ← 第一个块长度16 进制26 字节 {username:alice,\r\n ← 26 字节数据 0D\r\n ← 第二个块长度13 字节 password:123}\r\n 0\r\n ← 长度 0表示结束 \r\n解析 chunked 流程循环 读取一行十六进制数→ 块大小 if 块大小 0 → 结束 读取 块大小 字节的数据 读取末尾的 \r\n 拼接数据到 body 继续循环6.3 无 body 的情况没有Content-Length且没有Transfer-Encoding或者方法是 GET/HEAD/DELETE按规范不应有 body直接跳过 body 解析07. 第五步根据 Content-Type 解码请求体获取到原始的 body 字节流后需要根据Content-Type转换成程序可用的数据结构。常见 Content-Type 处理方式Content-Type解码方式application/jsonJSON 解析 → 对象/Mapapplication/x-www-form-urlencodedURL 解码 按和拆分为键值对multipart/form-data边界解析 → 字段 文件text/plain直接作为字符串application/xml或text/xmlXML 解析 → DOM / 对象application/octet-stream保留原始二进制 Buffer解码流程图原始 Body字节数组 │ ▼ 读取 Content-Type 头 │ ┌───────────┼───────────┬───────────────┐ ▼ ▼ ▼ ▼ application/ x-www-form- multipart/ text/plain json urlencoded form-data │ │ │ │ ▼ ▼ ▼ ▼ 转为字符串 JSON.parse querystring 边界解析 .parse 提取字段/文件示例解析 JSON body原始数据 POST /api/user HTTP/1.1 Content-Type: application/json Content-Length: 27 {name:张三,age:25} 解析后得到的 Request 对象 method: POST path: /api/user headers: { content-type: application/json, ... } body: { name: 张三, age: 25 } ← 已解析为对象示例解析表单 URL 编码原始数据 POST /login HTTP/1.1 Content-Type: application/x-www-form-urlencoded usernamealicepassword123456 解析后 body: { username: alice, password: 123456 }08. 完整解析流程图服务端视角TCP 连接建立 │ ▼ ┌──────────────┐ │ 读取字节流 │ │ (循环接收) │ └──────┬───────┘ │ ▼ ┌───────────────────────┐ │ 尝试解析请求行 │ │ 查找第一个 \r\n │ └───────────┬───────────┘ │ ▼ 解析成功 ──否──→ 继续接收数据 │是 ▼ ┌───────────────────────┐ │ 逐行解析请求头 │ │ 存入 headers Map │ └───────────┬───────────┘ │ ▼ 遇到空行(\r\n\r\n) │是 ▼ ┌───────────────────────┐ │ 判断是否有 Body │ │ Content-Length 0 │ │ 或 Transfer-Encoding │ └───────────┬───────────┘ │ ┌───────────┴───────────┐ │ │ ▼ ▼ 有 Body 无 Body │ │ ▼ ▼ 根据 Content-Length body null 或 chunked 读取 body │ ▼ ┌───────────────────┐ │ 根据 Content-Type │ │ 解码 Body │ │ (JSON/Form/File) │ └─────────┬─────────┘ │ ▼ ┌───────────────────┐ │ 封装 Request 对象 │ │ { method, path, │ │ headers, query, │ │ body } │ └─────────┬─────────┘ │ ▼ 交给业务代码处理09. 常见边缘情况与处理场景服务端如何处理请求行超长返回414 URI Too Long请求头超长返回431 Request Header Fields Too LargeContent-Length 限制返回413 Payload Too LargeContent-Type 不匹配尝试解析失败返回400 Bad Request分块传输编码格式错误返回400 Bad Request多个请求在一个 TCP 包中解析完第一个后继续从剩余字节解析第二个HTTP 流水线请求体未完全到达继续等待 TCP 数据直到读完 Content-Length 或 chunked 结束字符编码如 UTF-8/GBK读取Content-Type中的charset参数没有则按默认通常 UTF-810. 不同语言的解析实现差异语言/框架解析位置特点NginxC 语言手写事件驱动高性能非阻塞生产级Node.jshttp模块 /body-parser流式解析req对象可读流TomcatJava Servlet 容器标准 Servlet 规范自动封装 HttpServletRequestPythonhttp.server/Django简单解析库或框架全自动处理Gonet/http标准库自动解析r.Form/r.Body访问Spring BootDispatcherServlet 转换器注解驱动自动绑定到RequestBody11. 解析后的 Request 对象示例伪代码Request { method: POST, url: /api/user, httpVersion: 1.1, headers: { host: api.example.com, content-type: application/json, content-length: 27, user-agent: Mozilla/5.0, authorization: Bearer xyz123 }, query: { token: abc // 来自 /api/user?tokenabc }, body: { // 已按 Content-Type 解析 name: 张三, age: 25 }, rawBody: Buffer 7b 22 6e... // 原始字节可选 }业务代码直接使用name request.body.name age request.body.age token request.query.token12. 总结服务端解析 HTTP 请求的过程本质上是一个有状态的状态机解析器接收字节流 → 解析请求行 → 解析请求头 → 空行检测 → 解析请求体 → 解码 body → 封装 Request 对象关键要点TCP 粘包/拆包需要正确处理请求行和请求头的分隔符是\r\n空行\r\n\r\n标志着头部结束Body 的读取依赖Content-Length或Transfer-Encoding: chunkedBody 的解析依赖Content-Type头所有解析必须防御性防止超长/恶意输入面试常见追问如何解析multipart/form-data边界匹配 文件流如果Content-Length和Transfer-Encoding同时存在优先用哪个规范说忽略 Content-Length如何支持 HTTP 管道化Pipelining解析完一个请求后不关闭连接继续从剩余字节解析下一个The End点点关注收藏不迷路

更多文章