应用层协议 HTTP
一、HTTP 概述
虽然我们说,应用层协议是程序员自己定的。但实际上,已经有大佬们定义了一些现成的、又非常好用的应用层协议,供我们直接参考使用。HTTP(HyperText Transfer Protocol,超文本传输协议)就是其中之一。
回顾上一章:我们自己设计了协议格式(长度 + 分隔符 + JSON),解决了粘包和序列化问题。这一章我们来看看"标准答案"------HTTP 是怎么做的。
在互联网世界中,HTTP 是一个至关重要的协议。它定义了客户端(如浏览器)与服务器之间如何通信,以交换或传输超文本(如 HTML 文档)。
HTTP 协议是客户端与服务器之间通信的基础。客户端通过 HTTP 协议向服务器发送请求,服务器收到请求后处理并返回响应。
1.1 什么是 HTTP
生活中的类比:餐厅点餐。
你走进一家餐厅,服务员递上菜单(这就是一个"网页")。你告诉服务员:"我要一份红烧肉"------这就是一个 HTTP 请求 。后厨做好菜,服务员端着盘子回来------这就是 HTTP 响应。
服务器 HTTP 协议 客户端(浏览器) 服务器 HTTP 协议 客户端(浏览器) #mermaid-svg-cBw22hBaNtV3zZMG{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-cBw22hBaNtV3zZMG .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-cBw22hBaNtV3zZMG .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-cBw22hBaNtV3zZMG .error-icon{fill:#552222;}#mermaid-svg-cBw22hBaNtV3zZMG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-cBw22hBaNtV3zZMG .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-cBw22hBaNtV3zZMG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-cBw22hBaNtV3zZMG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-cBw22hBaNtV3zZMG .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-cBw22hBaNtV3zZMG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-cBw22hBaNtV3zZMG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-cBw22hBaNtV3zZMG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-cBw22hBaNtV3zZMG .marker.cross{stroke:#333333;}#mermaid-svg-cBw22hBaNtV3zZMG svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-cBw22hBaNtV3zZMG p{margin:0;}#mermaid-svg-cBw22hBaNtV3zZMG .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-cBw22hBaNtV3zZMG text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-cBw22hBaNtV3zZMG .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-cBw22hBaNtV3zZMG .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-cBw22hBaNtV3zZMG .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-cBw22hBaNtV3zZMG .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-cBw22hBaNtV3zZMG #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-cBw22hBaNtV3zZMG .sequenceNumber{fill:white;}#mermaid-svg-cBw22hBaNtV3zZMG #sequencenumber{fill:#333;}#mermaid-svg-cBw22hBaNtV3zZMG #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-cBw22hBaNtV3zZMG .messageText{fill:#333;stroke:none;}#mermaid-svg-cBw22hBaNtV3zZMG .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-cBw22hBaNtV3zZMG .labelText,#mermaid-svg-cBw22hBaNtV3zZMG .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-cBw22hBaNtV3zZMG .loopText,#mermaid-svg-cBw22hBaNtV3zZMG .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-cBw22hBaNtV3zZMG .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-cBw22hBaNtV3zZMG .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-cBw22hBaNtV3zZMG .noteText,#mermaid-svg-cBw22hBaNtV3zZMG .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-cBw22hBaNtV3zZMG .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-cBw22hBaNtV3zZMG .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-cBw22hBaNtV3zZMG .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-cBw22hBaNtV3zZMG .actorPopupMenu{position:absolute;}#mermaid-svg-cBw22hBaNtV3zZMG .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-cBw22hBaNtV3zZMG .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-cBw22hBaNtV3zZMG .actor-man circle,#mermaid-svg-cBw22hBaNtV3zZMG line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-cBw22hBaNtV3zZMG :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} "我要一份红烧肉"(HTTP 请求)下单上菜(HTTP 响应------HTML 页面)
在这个类比中:
- 你 = 客户端(浏览器)
- 服务员 = HTTP 协议(传输载体)
- 菜单 = 网页(HTML 文档)
- 后厨 = 服务器
- 红烧肉 = 你请求的资源(文件、图片、数据......)
HTTP 协议是一个无连接、无状态的协议:
- 无连接:每次请求都需要建立新的 TCP 连接(HTTP/1.0),虽然后续版本做了改进。
- 无状态:服务器不会保存客户端的状态信息------每次请求都是"全新的",服务器不记得你上次来过。
知识点: HTTP 的本质是"请求-响应"模型------客户端主动发起,服务器被动应答。服务器不会主动给客户端推送消息(HTTP/2.0 之前)。
1.2 URL 与编码规则
平时我们俗称的"网址",其实就是 URL(Uniform Resource Locator,统一资源定位符)。

浏览器访问一个网址时,背后发生了什么?以 http://www.example.com:80/index.html 为例:协议 (http)告诉浏览器用 HTTP 通信,服务器地址 (www.example.com)通过 DNS 解析成 IP 找到目标主机,端口号 (80)是 HTTP 默认端口(浏览器通常不显示但不代表没有),文件路径 (/index.html)指明服务器上要获取哪个文件。IP + 端口 + 文件路径,三要素组合起来就可以唯一标识互联网上任一主机上的任一文件------这就是 URL(统一资源定位符)。
用
telnet直接连接服务器端口,返回的就是网页源码------网页本质就是服务器上的一个文件,HTTP 最核心的功能就是获取服务器上的文件。
生活类比:学号。 一个学校几万学生,每个人有一个唯一学号------学号就是一个典型的 URL,唯一标识了全校范围内的一个"资源"(学生)。
与之容易混淆的概念是 URI(统一资源标识符),它强调"唯一性",能唯一标识即可;URL 则不仅能标识,还能"定位"(找到)资源。所以 URL 一定是 URI,反过来不一定:身份证号是 URI(唯一标识你),但不是 URL(不能凭号找到你本人)。
人的上网行为归根结底就两种------把服务器的数据拿过来(GET),把我的数据传上去(POST)。这就是网络 I/O,和读写文件一个模型,只是对象从本地磁盘换成了远程服务器。
像 /、?、: 这样的字符在 URL 中有特殊含义,参数值里如果包含它们就不能直接写------浏览器会误解。比如搜索 C++,写成 /?q=C++,但 + 在 URL 中表示空格,浏览器会当空格处理。解决办法是 urlencode :将字符转 16 进制,从右到左取 4 位(不足直接处理),每 2 位一组,前面加 %,编码成 %XY 格式。

+ 被转义成了 %2B。常用字符编码速查:
| 字符 | URL 中含义 | 编码 |
|---|---|---|
+ |
空格 | %2B |
/ |
路径分隔 | %2F |
? |
参数起始 | %3F |
: |
协议分隔 | %3A |
= |
参数赋值 | %3D |
& |
参数分隔 | %26 |
| 空格 | 分隔符 | %20 |
知识点: urlencode 和 urldecode 是 HTTP 协议栈中最基础的工具函数,每个 HTTP 库都内置了这对编解码。理解原理后,URL 里的
%20、%2F就不再神秘。
1.3 HTTP 是无状态协议
HTTP 的无状态意味着:服务器不会记住你。
你第一次访问 www.example.com 和第一百次访问它,服务器对你的态度完全一样------每次都像第一次见到你。
生活类比:健身房的会员卡。
健身房前台不记得每个会员的脸(无状态),但你出示会员卡(Cookie),前台刷卡就能查到你的会员信息(Session)。每次去健身,你都得带着卡------不带?对不起,不认识你。
服务器 客户端 服务器 客户端 #mermaid-svg-w9Dc0W9uHUfZ65Xv{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-w9Dc0W9uHUfZ65Xv .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-w9Dc0W9uHUfZ65Xv .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-w9Dc0W9uHUfZ65Xv .error-icon{fill:#552222;}#mermaid-svg-w9Dc0W9uHUfZ65Xv .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-w9Dc0W9uHUfZ65Xv .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-w9Dc0W9uHUfZ65Xv .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-w9Dc0W9uHUfZ65Xv .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-w9Dc0W9uHUfZ65Xv .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-w9Dc0W9uHUfZ65Xv .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-w9Dc0W9uHUfZ65Xv .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-w9Dc0W9uHUfZ65Xv .marker{fill:#333333;stroke:#333333;}#mermaid-svg-w9Dc0W9uHUfZ65Xv .marker.cross{stroke:#333333;}#mermaid-svg-w9Dc0W9uHUfZ65Xv svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-w9Dc0W9uHUfZ65Xv p{margin:0;}#mermaid-svg-w9Dc0W9uHUfZ65Xv .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-w9Dc0W9uHUfZ65Xv text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-w9Dc0W9uHUfZ65Xv .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-w9Dc0W9uHUfZ65Xv .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-w9Dc0W9uHUfZ65Xv .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-w9Dc0W9uHUfZ65Xv .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-w9Dc0W9uHUfZ65Xv #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-w9Dc0W9uHUfZ65Xv .sequenceNumber{fill:white;}#mermaid-svg-w9Dc0W9uHUfZ65Xv #sequencenumber{fill:#333;}#mermaid-svg-w9Dc0W9uHUfZ65Xv #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-w9Dc0W9uHUfZ65Xv .messageText{fill:#333;stroke:none;}#mermaid-svg-w9Dc0W9uHUfZ65Xv .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-w9Dc0W9uHUfZ65Xv .labelText,#mermaid-svg-w9Dc0W9uHUfZ65Xv .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-w9Dc0W9uHUfZ65Xv .loopText,#mermaid-svg-w9Dc0W9uHUfZ65Xv .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-w9Dc0W9uHUfZ65Xv .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-w9Dc0W9uHUfZ65Xv .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-w9Dc0W9uHUfZ65Xv .noteText,#mermaid-svg-w9Dc0W9uHUfZ65Xv .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-w9Dc0W9uHUfZ65Xv .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-w9Dc0W9uHUfZ65Xv .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-w9Dc0W9uHUfZ65Xv .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-w9Dc0W9uHUfZ65Xv .actorPopupMenu{position:absolute;}#mermaid-svg-w9Dc0W9uHUfZ65Xv .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-w9Dc0W9uHUfZ65Xv .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-w9Dc0W9uHUfZ65Xv .actor-man circle,#mermaid-svg-w9Dc0W9uHUfZ65Xv line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-w9Dc0W9uHUfZ65Xv :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 第一次来 第二次来 我要进去(HTTP 请求)出示会员卡(无 Cookie → 拒绝 / 要求登录)这是我的卡(登录 → Cookie)好的请进(Set-Cookie + 响应)我要进去 + 会员卡(HTTP 请求 + Cookie)请进(识别身份 → 响应)
这带来了一个实际问题:登录怎么办? 如果服务器不记得你,那每次请求都得重新登录?答案在 Cookie 和 Session 机制中------简单来说:
- Cookie:服务器给你一个"号码牌",你每次请求都带上它
- Session:服务器拿着号码牌去后台查"这是谁"
知识点: HTTP 的无状态是设计哲学,不是缺陷。无状态让 HTTP 协议极其简单、易于扩展。状态管理(Cookie/Session)是"外挂"在 HTTP 之上的,而不是协议本身的一部分------后续章节会深入讲解。
二、HTTP 协议格式
HTTP 底层用的也是 TCP 套接字,所以它面对的同样是字节流。回忆上一章我们自己设计协议时遇到的两个核心问题:
- 怎么才算读完一条完整报文?(粘包/半包)
- 怎么把字符串反序列化成结构化数据?
HTTP 给出的答案优雅而简单------下面逐一拆解。
2.1 请求报文格式
站在客户端角度,一个 HTTP 请求该怎么表达?
HTTP 请求报文分为四个部分,按行 组织:

第一行 = 请求行,以空格为分隔,三列信息:
| 列 | 字段 | 示例 | 含义 |
|---|---|---|---|
| 1 | 请求方法 | GET |
想做什么(GET 获取 / POST 上传 / ......) |
| 2 | URI | /index.html |
URL 中去掉协议、域名、端口后剩下的路径 |
| 3 | HTTP 版本 | HTTP/1.1 |
客户端支持的协议版本,服务端据此决定响应方式 |
紧接着是请求报头 (Header),以行为单位,格式为 Key: Value(冒号+空格分隔),每行以 \r\n 结尾。比如 Host: www.example.com、Content-Length: 150。
空行之后是请求正文(Body),存放真正的有效载荷(POST 表单数据、JSON 等)。空行以前的全部是报头。
在我们认知里,一个 HTTP 请求就是一行字符串 ------只不过这行字符串里嵌了
\r\n来划分行。序列化就是把结构化成员(方法、URI、版本、KV 报头)拼接成这个大字符串。
2.2 响应报文格式
宏观轮廓上,响应报文和请求报文一模一样 ------状态行 + 响应报头 + 空行 + 响应正文,四段结构完全对应。

区别只在第一行:请求行是"方法 + URI + 版本",状态行是"版本 + 状态码 + 状态描述"。
| 列 | 字段 | 示例 | 含义 |
|---|---|---|---|
| 1 | HTTP 版本 | HTTP/1.1 |
服务端的协议版本------客户端和服务器各自告诉对方自己能说什么版本 |
| 2 | 状态码 | 200、404 |
HTTP 规定:只要网络没断、服务器没挂,无论成功还是失败都必须有应答。就像进程退出码------0 表示正常,非 0 表示出了什么问题 |
| 3 | 状态描述 | OK、Not Found |
状态码的简短解释,给人看的,程序不看它 |
状态码最有代表性的两个:200 OK (一切正常)和 404 Not Found(找不到资源)。
响应报头同样是 Key: Value 格式(冒号+空格分隔,每行 \r\n 结尾);空行后是响应正文(HTML 页面、JSON 数据等)。
HTTP 不依赖 JSON、XML 等外部序列化方案------它自己就是一套序列化/反序列化规则:按行分割,行内按空格或冒号提取字段。请求和响应共用同一套逻辑,自给自足,简洁高效。
2.3 Header 与 Body 的分隔------HTTP 如何解决粘包
回到本章开头的问题:怎么才算读完? TCP 是字节流,发送方一次 write 的内容,接收方可能分多次 read 才收完。HTTP 的解法分两步:
第一步:先保证读完报头。 报头和正文之间有一个空行 (\r\n\r\n,即连续两个换行符)。服务端收到数据后,只要扫描到 \r\n\r\n,就知道报头已经完整收到了。
第二步:从报头中拿到正文长度。 报头里有一个关键的 KV:Content-Length: 1024。它精确告知正文有多少字节。知道了长度,服务端就继续 read,直到凑满这 1024 字节------正文读完,一条完整的请求报文就拿到手了。
#mermaid-svg-wFe07tnKRG6z7e3x{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wFe07tnKRG6z7e3x .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wFe07tnKRG6z7e3x .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wFe07tnKRG6z7e3x .error-icon{fill:#552222;}#mermaid-svg-wFe07tnKRG6z7e3x .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wFe07tnKRG6z7e3x .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wFe07tnKRG6z7e3x .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wFe07tnKRG6z7e3x .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wFe07tnKRG6z7e3x .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wFe07tnKRG6z7e3x .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wFe07tnKRG6z7e3x .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wFe07tnKRG6z7e3x .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wFe07tnKRG6z7e3x .marker.cross{stroke:#333333;}#mermaid-svg-wFe07tnKRG6z7e3x svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wFe07tnKRG6z7e3x p{margin:0;}#mermaid-svg-wFe07tnKRG6z7e3x .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-wFe07tnKRG6z7e3x .cluster-label text{fill:#333;}#mermaid-svg-wFe07tnKRG6z7e3x .cluster-label span{color:#333;}#mermaid-svg-wFe07tnKRG6z7e3x .cluster-label span p{background-color:transparent;}#mermaid-svg-wFe07tnKRG6z7e3x .label text,#mermaid-svg-wFe07tnKRG6z7e3x span{fill:#333;color:#333;}#mermaid-svg-wFe07tnKRG6z7e3x .node rect,#mermaid-svg-wFe07tnKRG6z7e3x .node circle,#mermaid-svg-wFe07tnKRG6z7e3x .node ellipse,#mermaid-svg-wFe07tnKRG6z7e3x .node polygon,#mermaid-svg-wFe07tnKRG6z7e3x .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wFe07tnKRG6z7e3x .rough-node .label text,#mermaid-svg-wFe07tnKRG6z7e3x .node .label text,#mermaid-svg-wFe07tnKRG6z7e3x .image-shape .label,#mermaid-svg-wFe07tnKRG6z7e3x .icon-shape .label{text-anchor:middle;}#mermaid-svg-wFe07tnKRG6z7e3x .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-wFe07tnKRG6z7e3x .rough-node .label,#mermaid-svg-wFe07tnKRG6z7e3x .node .label,#mermaid-svg-wFe07tnKRG6z7e3x .image-shape .label,#mermaid-svg-wFe07tnKRG6z7e3x .icon-shape .label{text-align:center;}#mermaid-svg-wFe07tnKRG6z7e3x .node.clickable{cursor:pointer;}#mermaid-svg-wFe07tnKRG6z7e3x .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-wFe07tnKRG6z7e3x .arrowheadPath{fill:#333333;}#mermaid-svg-wFe07tnKRG6z7e3x .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wFe07tnKRG6z7e3x .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wFe07tnKRG6z7e3x .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wFe07tnKRG6z7e3x .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wFe07tnKRG6z7e3x .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wFe07tnKRG6z7e3x .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-wFe07tnKRG6z7e3x .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wFe07tnKRG6z7e3x .cluster text{fill:#333;}#mermaid-svg-wFe07tnKRG6z7e3x .cluster span{color:#333;}#mermaid-svg-wFe07tnKRG6z7e3x div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-wFe07tnKRG6z7e3x .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wFe07tnKRG6z7e3x rect.text{fill:none;stroke-width:0;}#mermaid-svg-wFe07tnKRG6z7e3x .icon-shape,#mermaid-svg-wFe07tnKRG6z7e3x .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wFe07tnKRG6z7e3x .icon-shape p,#mermaid-svg-wFe07tnKRG6z7e3x .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-wFe07tnKRG6z7e3x .icon-shape .label rect,#mermaid-svg-wFe07tnKRG6z7e3x .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wFe07tnKRG6z7e3x .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-wFe07tnKRG6z7e3x .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-wFe07tnKRG6z7e3x :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
否
是
字节流到达
找到空行\r
\r
?
报头完整
提取为 KV 结构
读取 Content-Length
正文收满\r
Content-Length 字节?
一条完整报文!
学协议归根结底就两件事:
- 如何把报头和有效载荷分开------HTTP 用空行
- 如何把有效载荷交给上层------通过回调(handler),上层不碰协议细节
HTTP 不需要依赖第三方的序列化库,按行、按空格、按冒号就能独立完成编解码。对比上一章我们用 JSON + 长度头的方案,HTTP 的方案更适合"人类可读"这个设计目标。
请求行三列速记:
| 列 | 含义 | 举例 |
|---|---|---|
| 方法 | 对资源的操作 | GET、POST(详见第三章) |
| URI | URL 的路径部分 | /a/b/c/d.html |
| 版本 | 客户端 HTTP 版本 | HTTP/1.0、HTTP/1.1、HTTP/2.0 |
为什么要有版本号?------客户端告诉服务器自己支持哪个版本,服务器根据版本来提供不同的服务。一个完善的协议都要有版本号,给未来的演进留空间。
HTTP 的工作模式是短连接 :客户端发 Request → 服务器回 Response → 服务器主动关闭 TCP 连接。浏览器拿到响应后,把 HTML 渲染成网页。短连接意味着每轮请求-响应走完就断开,不保持长连接(HTTP/1.1 的 Connection: keep-alive 是对此的改进,后续会讲)。
2.4 常用 Header 速查
前面几节反复出现了一些 Header 字段。这里统一梳理,作为后续章节的参考锚点。HTTP Header 本质都是 Key: Value 格式的键值对,按作用分为请求头、响应头和通用头:
| Header | 方向 | 示例值 | 作用 | 详解 |
|---|---|---|---|---|
Content-Type |
响应 | text/html |
告诉浏览器正文是什么格式(HTML / PNG / JSON......) | 见 6.3 节 |
Content-Length |
通用 | 1024 |
Body 的精确字节数------HTTP 解决粘包的核心字段 | 见 2.3 节 |
Host |
请求 | www.example.com:8080 |
客户端告知服务器:我要访问的是哪个主机和端口(一台服务器上可能跑多个网站) | --- |
User-Agent |
请求 | Mozilla/5.0 (Windows NT 10.0...) |
声明客户端的操作系统和浏览器版本------服务器据此做兼容或统计分析 | --- |
Referer |
请求 | https://www.google.com/search?q=xxx |
当前页面是从哪个页面跳转过来的------用于防盗链、流量分析 | --- |
Location |
响应 | /error.html |
搭配 3xx 状态码,告诉客户端接下来去哪里访问 |
见第三章 |
Cookie |
请求 | session_id=abc123 |
客户端把之前服务器给的"号码牌"带回------会话管理的载体 | 见第八章 |
Set-Cookie |
响应 | session_id=abc123; Path=/ |
服务器向客户端写入 Cookie------下次请求时浏览器自动回传 | 见第八章 |
Connection |
通用 | keep-alive / close |
控制 TCP 连接是否复用------close 即短连接,keep-alive 即长连接 |
--- |
这 9 个 Header 覆盖了 HTTP 最常见的场景:格式识别、长度定界、主机定位、跳转导航、状态保持。后续章节会逐个深入------这里先建立索引印象即可。
三、HTTP 状态码与重定向
3.1 状态码入门
HTTP 协议规定:只要网络没断、服务器没挂,无论成功还是失败都必须有应答。状态码就是服务器告诉客户端"结果怎么样"的方式。

最常见的状态码:200(OK)、404(Not Found)、403(Forbidden)、302(重定向)、504(Bad Gateway)。
3.2 301 vs 302------永久还是临时?
重定向的本质是自动跳转 ------服务器不返回正文,而是返回一个 3xx 状态码 + Location 头,浏览器拿到后自动发起第二次请求去访问新地址。用户不需要手动点击任何东西。
生活类比:饭店搬家。
你去一家饭店吃饭,门口贴了告示:
- 临时告示(302):"本店装修,临时搬到隔壁街 XX 号"------你这次去新地址吃,但下次你还是会先来老地址看看。
- 永久告示(301):"本店永久迁至隔壁街 XX 号"------你去了新地址,以后直接去新地址,不再来老地方了。
| 类型 | 状态码 | 含义 | 浏览器行为 | 典型场景 |
|---|---|---|---|---|
| 永久重定向 | 301 Moved Permanently | 资源已永久搬到新位置 | 更新书签,下次直接访问新地址;搜索引擎把索引 URL 替换为新地址 | 网站换域名 |
| 临时重定向 | 302 Found | 资源暂时搬到新位置 | 不更新书签,下次仍访问原地址;搜索引擎保留原 URL | 登录成功后跳转到首页 |
3.3 技术机制
无论是 301 还是 302,都依赖 Location 响应头来指定跳转目标:
流程:
服务器 客户端 服务器 客户端 #mermaid-svg-juTag5Gg5B6NJfka{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-juTag5Gg5B6NJfka .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-juTag5Gg5B6NJfka .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-juTag5Gg5B6NJfka .error-icon{fill:#552222;}#mermaid-svg-juTag5Gg5B6NJfka .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-juTag5Gg5B6NJfka .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-juTag5Gg5B6NJfka .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-juTag5Gg5B6NJfka .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-juTag5Gg5B6NJfka .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-juTag5Gg5B6NJfka .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-juTag5Gg5B6NJfka .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-juTag5Gg5B6NJfka .marker{fill:#333333;stroke:#333333;}#mermaid-svg-juTag5Gg5B6NJfka .marker.cross{stroke:#333333;}#mermaid-svg-juTag5Gg5B6NJfka svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-juTag5Gg5B6NJfka p{margin:0;}#mermaid-svg-juTag5Gg5B6NJfka .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-juTag5Gg5B6NJfka text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-juTag5Gg5B6NJfka .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-juTag5Gg5B6NJfka .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-juTag5Gg5B6NJfka .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-juTag5Gg5B6NJfka .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-juTag5Gg5B6NJfka #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-juTag5Gg5B6NJfka .sequenceNumber{fill:white;}#mermaid-svg-juTag5Gg5B6NJfka #sequencenumber{fill:#333;}#mermaid-svg-juTag5Gg5B6NJfka #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-juTag5Gg5B6NJfka .messageText{fill:#333;stroke:none;}#mermaid-svg-juTag5Gg5B6NJfka .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-juTag5Gg5B6NJfka .labelText,#mermaid-svg-juTag5Gg5B6NJfka .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-juTag5Gg5B6NJfka .loopText,#mermaid-svg-juTag5Gg5B6NJfka .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-juTag5Gg5B6NJfka .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-juTag5Gg5B6NJfka .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-juTag5Gg5B6NJfka .noteText,#mermaid-svg-juTag5Gg5B6NJfka .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-juTag5Gg5B6NJfka .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-juTag5Gg5B6NJfka .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-juTag5Gg5B6NJfka .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-juTag5Gg5B6NJfka .actorPopupMenu{position:absolute;}#mermaid-svg-juTag5Gg5B6NJfka .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-juTag5Gg5B6NJfka .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-juTag5Gg5B6NJfka .actor-man circle,#mermaid-svg-juTag5Gg5B6NJfka line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-juTag5Gg5B6NJfka :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 浏览器收到 3xx自动提取 Location GET /old-page HTTP/1.1HTTP/1.1 302 FoundLocation: /error.htmlGET /error.html HTTP/1.1HTTP/1.1 200 OK<html>错误页面</html>
知识点: 301 和 302 的区别在于"要不要记住新地址"。临时重定向不改变客户端的认知------下次还是先来老地址;永久重定向让客户端更新记录------以后直接去新地址,老地址不再访问。这在 SEO 中尤其重要:301 会让搜索引擎把权重和索引全部转移到新 URL。
四、HTTP 服务器实战
理论讲完了,开始编码。先定一个小目标:让浏览器访问我们的服务器,看到一句 GoodNight。
4.1 整体思路:复用 TcpServer
回顾上一章 NetCal 的架构------TcpServer 通过 handler_t 回调将网络层和业务层彻底解耦。既然 HTTP 底层也是 TCP 套接字,那 TcpServer 完全不需要改------只需要换一个 handler,让它返回 HTTP 格式的应答字符串即可。
先不解决粘包问题,把 service() 保持为"收一次 → 处理 → 回一次 → 关闭"的短服务模式。
4.2 HttpProtocol:从原始请求到 HTTP 应答
新建 Http.hpp,定义三个类(骨架先行,后续逐步填充):
cpp
#pragma once
#include <iostream>
#include <cstdio>
#include <string>
#include <sstream>
#include <unordered_map>
#include <fstream>
#include <functional>
#include "Logger.hpp"
class HttpRequest
{
// TODO:后续填充------请求行解析、报头 KV 提取、正文存储
};
class HttpResponse
{
// TODO:后续填充------状态码、报头、正文
};
class HttpProtocol
{
public:
std::string HandlerHttpRequest(std::string &req)
{
std::cout << "#########################" << std::endl;
std::cout << req << std::endl;
std::string status_line = "HTTP/1.1 200 OK\r\n";
status_line += "\r\n";
std::string html = "<html><body><h1>GoodNight</h1></body></html>";
return status_line += html;
}
};
HandlerHttpRequest暂时不做解析------先把收到的原始请求打印出来,然后返回一段硬编码的 HTML。最小可用版本,跑通了再迭代。
4.3 组装服务端 main
cpp
#include <memory>
#include "TcpServer.hpp"
#include "Http.hpp"
using namespace NS_LOG_MODULE;
int main(int argc, char *argv[])
{
if (argc != 2)
{
std::cout << "Usage: " << argv[0] << " <port>" << std::endl;
return -1;
}
uint16_t port = std::stoi(argv[1]);
// 1. 构建 HTTP 协议层
std::unique_ptr<HttpProtocol> protocol = std::make_unique<HttpProtocol>();
// 2. 构建网络层------注入 HTTP 处理回调
std::unique_ptr<TcpServer> tcp_server = std::make_unique<TcpServer>(port,
[&protocol](std::string &req){
return protocol->HandlerHttpRequest(req);
});
tcp_server->Loop();
return 0;
}
三层组装和 NetCal 如出一辙:HttpProtocol(协议层)→ 回调注入 → TcpServer(网络层)。区别只是这次还没有独立的业务层------HttpProtocol 自己就是全部。
4.4 验证:telnet + 浏览器
编译启动,先用 telnet 裸测:
shell
root@ALiServer:HttpServer# telnet 127.0.0.1 8080
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
GET / HTTP/1.1
HTTP/1.1 200 OK
<html><body><h1>GoodNight</h1></body></html>Connection closed by foreign host.
服务器通了!关键点:telnet 发过来的是一行 GET / HTTP/1.1,服务端用它根本没做解析------直接回了 HTTP/1.1 200 OK\r\n\r\n<html>...</html>。浏览器只认 HTTP 格式的应答,看到 HTTP/1.1 200 OK 就知道请求成功,然后把 <html> 渲染出来。

知识点: HTTP 服务器的最小原型只需要做一件事------按 HTTP 应答格式返回字符串。状态行 + 空行 + HTML,十几行代码就能让浏览器显示出内容。当然,这离一个真正的 HTTP 服务器还差得远:没有解析请求、没有路由、没有错误处理......这些就是后续小节要逐步解决的问题。
五、HTTP 请求解析------反序列化
在 4.2 节我们搭了一个骨架:HandlerHttpRequest 直接把收到的原始报文打印出来,然后返回硬编码的 HTML。这只能算"通了一个回路"------服务器根本没"理解"客户端说了什么。现在给它装上大脑:把那一大坨字符串拆开,解析成结构化数据。
5.1 HTTP 报文本质上是一个大字符串
收到一个 HTTP 请求,服务端看到的是什么?不是"方法"、"URI"、"报头"这些概念------而是一块连续的字符序列:
GET /index.html HTTP/1.1\r\nHost: localhost\r\n\r\n
TCP 是字节流协议,这串字符可能分好几次 read() 才收完。所以和上一章的 NetCal 一样,接收方必须回答两个问题:
- 怎么才算读完一条完整报文?(粘包/半包问题)
- 怎么把字符串反序列化成结构化数据?(提取 method、uri、version、KV 报头......)
HTTP 的解法完全自给自足------不需要 JSON、Protobuf 等第三方序列化方案:
- 问题 1 :用空行
\r\n\r\n标记报头结束,用Content-Length指定正文长度 - 问题 2:按行切割,行内按空格或冒号提取字段------全部是字符串操作
#mermaid-svg-hlkL19SXz2Dwchgm{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-hlkL19SXz2Dwchgm .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-hlkL19SXz2Dwchgm .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-hlkL19SXz2Dwchgm .error-icon{fill:#552222;}#mermaid-svg-hlkL19SXz2Dwchgm .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-hlkL19SXz2Dwchgm .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-hlkL19SXz2Dwchgm .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-hlkL19SXz2Dwchgm .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-hlkL19SXz2Dwchgm .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-hlkL19SXz2Dwchgm .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-hlkL19SXz2Dwchgm .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-hlkL19SXz2Dwchgm .marker{fill:#333333;stroke:#333333;}#mermaid-svg-hlkL19SXz2Dwchgm .marker.cross{stroke:#333333;}#mermaid-svg-hlkL19SXz2Dwchgm svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-hlkL19SXz2Dwchgm p{margin:0;}#mermaid-svg-hlkL19SXz2Dwchgm .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-hlkL19SXz2Dwchgm .cluster-label text{fill:#333;}#mermaid-svg-hlkL19SXz2Dwchgm .cluster-label span{color:#333;}#mermaid-svg-hlkL19SXz2Dwchgm .cluster-label span p{background-color:transparent;}#mermaid-svg-hlkL19SXz2Dwchgm .label text,#mermaid-svg-hlkL19SXz2Dwchgm span{fill:#333;color:#333;}#mermaid-svg-hlkL19SXz2Dwchgm .node rect,#mermaid-svg-hlkL19SXz2Dwchgm .node circle,#mermaid-svg-hlkL19SXz2Dwchgm .node ellipse,#mermaid-svg-hlkL19SXz2Dwchgm .node polygon,#mermaid-svg-hlkL19SXz2Dwchgm .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-hlkL19SXz2Dwchgm .rough-node .label text,#mermaid-svg-hlkL19SXz2Dwchgm .node .label text,#mermaid-svg-hlkL19SXz2Dwchgm .image-shape .label,#mermaid-svg-hlkL19SXz2Dwchgm .icon-shape .label{text-anchor:middle;}#mermaid-svg-hlkL19SXz2Dwchgm .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-hlkL19SXz2Dwchgm .rough-node .label,#mermaid-svg-hlkL19SXz2Dwchgm .node .label,#mermaid-svg-hlkL19SXz2Dwchgm .image-shape .label,#mermaid-svg-hlkL19SXz2Dwchgm .icon-shape .label{text-align:center;}#mermaid-svg-hlkL19SXz2Dwchgm .node.clickable{cursor:pointer;}#mermaid-svg-hlkL19SXz2Dwchgm .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-hlkL19SXz2Dwchgm .arrowheadPath{fill:#333333;}#mermaid-svg-hlkL19SXz2Dwchgm .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-hlkL19SXz2Dwchgm .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-hlkL19SXz2Dwchgm .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hlkL19SXz2Dwchgm .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-hlkL19SXz2Dwchgm .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hlkL19SXz2Dwchgm .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-hlkL19SXz2Dwchgm .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-hlkL19SXz2Dwchgm .cluster text{fill:#333;}#mermaid-svg-hlkL19SXz2Dwchgm .cluster span{color:#333;}#mermaid-svg-hlkL19SXz2Dwchgm div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-hlkL19SXz2Dwchgm .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-hlkL19SXz2Dwchgm rect.text{fill:none;stroke-width:0;}#mermaid-svg-hlkL19SXz2Dwchgm .icon-shape,#mermaid-svg-hlkL19SXz2Dwchgm .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hlkL19SXz2Dwchgm .icon-shape p,#mermaid-svg-hlkL19SXz2Dwchgm .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-hlkL19SXz2Dwchgm .icon-shape .label rect,#mermaid-svg-hlkL19SXz2Dwchgm .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hlkL19SXz2Dwchgm .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-hlkL19SXz2Dwchgm .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-hlkL19SXz2Dwchgm :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 原始字节流
GET / HTTP/1.1 (CRLF) Host: ... (CRLF)(CRLF) ...
ReadLine 逐行切割
请求行
GET / HTTP/1.1
请求头
Host: localhost
Content-Length: 150
空行 (CRLF)
请求正文
(Content-Length 字节)
空格分割
→ method, uri, version
冒号分割
→ KV 存入 unordered_map
HTTP 协议不依赖外部序列化库------它自己就是一套序列化/反序列化规则。规则极其简单:按行读,行内按分隔符拆。
5.2 核心工具:ReadLine
解析的第一步是把一个大字符串按 \r\n 切成一行一行。提取一个 Util::ReadLine 静态方法:
cpp
static std::string ReadLine(std::string &str)
{
auto pos = str.find(linesep);
if (pos == std::string::npos)
return std::string(); // 没找到 \r\n → 行不完整,返回空串
std::string line = str.substr(0, pos);
str.erase(0, line.size() + linesep.size()); // 从原串中切掉已提取的部分
if (line.empty())
return linesep; // 空行(只有 \r\n)→ 返回分隔符本身
else
return line; // 正常行 → 返回行内容
}
ReadLine 是一个引用语义 的工具方法:每次调用从传入的 str 中"切走"一行,同时返回这一行的内容。三种返回值约定:
| 返回值 | 含义 | 后续处理 |
|---|---|---|
""(空串) |
没找到 \r\n,行不完整 |
等待更多数据或报错 |
"\r\n" |
读到了空行 | 报头结束标志 / 请求行不允许为空行 |
| 其他字符串 | 读到一行具体内容 | 交给对应解析函数 |
知识点:
ReadLine是 HTTP 解析器的基石------后续解析请求行、请求头、判断空行全部依赖它。它通过同时返回行内容 + 修改原串(擦除已处理部分)实现了流式解析的语义。
5.3 解析请求行------第一行的三列信息
拿到请求行后,用 stringstream 按空格提取三列。请求行格式是 GET /index.html HTTP/1.1,天然就是空格分隔------和 stringstream >> 完美契合:
cpp
bool ParseReqLine(std::string &httpstr)
{
std::string req_line = Util::ReadLine(httpstr);
if (req_line.empty() || req_line == linesep)
return false;
std::stringstream ss(req_line); // 用 req_line 初始化 stringstream
ss >> _method >> _uri >> _version;
if (_method.empty() || _uri.empty() || _version.empty())
return false;
if (_uri == "/")
_uri += g_first_path; // "/" → "/index.html"(默认首页)
_uri = g_wwwroot + _uri; // "/index.html" → "./wwwroot/index.html"
return true;
}
紧接着两行路径转换------这是整个 HTTP 服务器最核心的映射逻辑:把 URL 路径转化为服务器本地磁盘路径。
请求方法 最常见的是 GET(获取资源)和 POST(上传数据)。HTTP 版本号 (HTTP/1.0、HTTP/1.1、HTTP/2.0)是客户端在告诉服务器自己支持的协议版本------一个完善的协议都要有版本号,服务器根据版本提供差异化的服务。
知识点: HTTP 请求行的反序列化只需要一个
>>操作符------字符串流天然按空格分隔,和请求行的格式完美契合。这就是为什么 HTTP 格式设计成空格分隔:不是为了好看,而是为了用最简单的字符串操作就能解析。
5.4 解析请求头------循环提取 KV
请求头可能有任意多行,格式统一为 Key: Value(冒号+空格分隔),以空行 \r\n 结束。
按照 HTTP 协议规范,我们是能保证读到报头结束的------因为空行 \r\n\r\n 是一个明确的分界标志。只要在字节流中扫描到连续的两个 \r\n,就知道报头已经完整收到了。至于正文有没有收完、还要收多少------那是拿了 Content-Length 之后才需要关心的事。
cpp
bool ParseHeader(std::string &httpstr)
{
std::string header_line;
do
{
header_line = Util::ReadLine(httpstr);
if (header_line.empty()) // 行不完整
return false;
if (header_line != linesep) // 不是空行 → 正常报头行
{
auto pos = header_line.find(headersep); // ": "
if (pos == std::string::npos)
return false;
std::string key = header_line.substr(0, pos);
std::string value = header_line.substr(pos + headersep.size());
_header[key] = value; // 存入 unordered_map
}
} while (header_line != linesep); // 遇到空行 \r\n 就停
return true;
}
循环逻辑:用 do-while 逐行读取,每次判断三种情况------行不完整(返回 false)、空行(循环终止)、正常 KV 行(按 : 分割存入 _header)。循环退出条件就是遇到空行------这正是 HTTP 协议中"报头以空行结束"的规则在代码中的直接翻译。
5.5 解析请求正文------Content-Length 定长读取
空行之后就是正文。但正文不是所有请求都有------GET 请求没有正文,POST 请求才在报头中携带 Content-Length 指示正文长度。
三个问题要回答:有没有正文?多大?从哪里开始?
cpp
bool ParseText(std::string &httpstr)
{
if (_header.find("Content-Length") == _header.end())
{
_text = std::string(); // 没有 Content-Length → 没有正文
return true; // GET 请求走这里,空正文即成功
}
int content_length = std::stoi(_header["Content-Length"]);
_text = httpstr.substr(0, content_length); // 截取正文
httpstr.erase(0, content_length); // 擦除已读部分
return true;
}
程序只认 Content-Length------无论从 TCP 字节流中分几次 read(),只要在空行后再读满 Content-Length 字节,正文就完整了。这正是 HTTP 协议解决粘包问题的唯一手段:用长度来界定边界,而不是靠特殊分隔符。
知识点: HTTP 协议的两阶段读取------先读到空行确定报头完整(第一层定界,用特殊字符
\r\n\r\n),再从报头中拿到Content-Length确定正文长度(第二层定界,用长度字段)------本质上是两层定界。这个模式在 TCP 应用层协议设计中非常经典。
5.6 组装:Deserialize 主流程
三个解析步骤串在一起就是完整的反序列化流程:
cpp
bool Deserialize(std::string &httpstr)
{
// 1. 解析请求行
if (!ParseReqLine(httpstr)) { /* error */ return false; }
// 2. 解析请求头
if (!ParseHeader(httpstr)) { /* error */ return false; }
_blank_line = linesep;
// 3. 解析请求体
if (!ParseText(httpstr)) { /* error */ return false; }
return true;
}
字节流信息转换为结构化信息,未来就可以直接通过成员变量提取所需字段------这个过程叫做反序列化。
六、HTTP 响应构建------序列化
请求解析完成后,HttpRequest 的成员变量(_method、_uri、_header......)被填满。下一步是根据请求构造响应------序列化过程,把结构化字段拼回 HTTP 应答字符串。
6.1 Web 根目录:从 URI 到磁盘文件
HTTP 协议的核心功能极其朴素:把服务器上某个目录下的文件,以 HTTP 应答的方式返回给客户端。
浏览器请求 URI: /index.html
↓
服务器映射路径: ./wwwroot/index.html
↓
ReadFile 读出内容 → 打包成 HTTP 响应 → 发回浏览器
wwwroot 就是 Web 根目录 ------它是服务器磁盘上专门存放前端文件(HTML、CSS、JS、图片、视频)的目录。URI 的正斜杠 / 就对应这个目录,不是系统根目录。如果用户请求的是 /,自动补上 index.html------这就是"首页"的由来。
HTTP(超文本传输协议)之所以叫"超文本",正是因为传输的内容不限于纯文本------可以是网页、音视频、二进制文件。但文件本身在哪里、怎么存储,HTTP 协议不关心------它只定义"怎么传"。
6.2 ReadFile:从文本到二进制
getline + 补 的方案在纯 HTML 场景下能跑,但一碰到图片、视频就崩------getline 是文本模式,遇到 0x1A 等特殊字节会截断,更不用说 JPEG 里到处是"换行符"。
根源:文本模式只适合纯文本。 真实网站的资源五花八门------HTML、CSS、JS、PNG、JPG、MP4、PDF......必须用二进制方式读取:
cpp
static std::string ReadFile(const std::string &filename)
{
// 采用二进制方式读取数据------支持图片、视频等所有文件类型
std::ifstream file(filename, std::ios::binary);
if (!file.is_open())
{
return std::string();
}
file.seekg(0, file.end); // 移到文件末尾获取大小
int filesize = file.tellg();
file.seekg(0, file.beg); // 回到文件开头
std::string content;
content.resize(filesize);
file.read((char *)content.c_str(), filesize); // 一次性读出全部字节
file.close();
return content;
}
知识点:
std::ios::binary+file.read()是处理 HTTP 静态资源的正确姿势。更彻底的方案是用std::vector<char>做缓冲区------string遇\n会截断,而vector<char>是纯字节容器。当前项目用string是因为 HTML/CSS/JS 等文本资源占了绝大多数,先够用再说。
6.3 Content-Type:告诉浏览器"我返回的是什么"
6.3.1 问题:浏览器怎么知道返回的是什么格式?
一个网页通常不止一个文件------首页 HTML 里可能嵌了 CSS、JS、PNG 图片、MP4 视频。浏览器请求首页拿到 HTML,解析后发现 <link>、<script>、<img> 标签,又向服务器发起新的 HTTP 请求去拉这些资源。
每种资源的格式各不相同。浏览器拿到响应正文后,怎么知道这是一段 HTML 还是一张 PNG?答案是 Content-Type 响应头:
HTTP/1.0 200 OK
Content-Type: image/png ← 浏览器看到这行,就知道用图片解码器来渲染
Content-Length: 28470
[PNG 二进制数据...]
那服务器又怎么知道每个文件对应什么 Content-Type?------靠文件后缀。每种后缀对应一个 MIME 类型(Multipurpose Internet Mail Extensions),RFC 标准化定义:
cpp
class MiniType
{
public:
static std::string Suffix2MimeType(const std::string &suffix)
{
auto iter = _mime_map.find(suffix);
if (iter != _mime_map.end())
return iter->second;
else
return "text/html"; // 未知后缀默认当 HTML 处理
}
private:
static std::unordered_map<std::string, std::string> _mime_map;
};
// 15 种常见后缀 → MIME 类型映射
std::unordered_map<std::string, std::string> MiniType::_mime_map = {
// 文本
{".txt", "text/plain"},
{".html", "text/html"},
{".htm", "text/html"},
{".css", "text/css"},
// 图片
{".jpg", "image/jpeg"},
{".jpeg", "image/jpeg"},
{".png", "image/png"},
{".gif", "image/gif"},
// 应用
{".pdf", "application/pdf"},
{".json", "application/json"},
{".xml", "application/xml"},
{".js", "application/javascript"},
// 其他
{".zip", "application/zip"},
{".mp3", "audio/mpeg"},
{".mp4", "video/mp4"},
};
| 大类 | 后缀示例 | MIME 类型 |
|---|---|---|
| 文本 | .html .css .js |
text/html text/css application/javascript |
| 图片 | .png .jpg .gif |
image/png image/jpeg image/gif |
| 音视频 | .mp3 .mp4 |
audio/mpeg video/mp4 |
| 文档 | .pdf .json .xml |
application/pdf application/json application/xml |
6.3.2 从 URI 提取后缀
HttpRequest 新增私有成员 _suffix 和公开方法 Suffix()。在 ParseReqLine 中通过 rfind(".") 从 URI 末尾截取后缀:
cpp
auto pos = _uri.rfind(suffixsep);
if (pos == std::string::npos)
_suffix = ".html"; // 没有后缀 → 默认首页
else
_suffix = _uri.substr(pos); // "/image/logo.png" → ".png"
6.3.3 串起来:HandlerHttpRequest 加入 Content-Type
现在服务端处理流程多了两环------提取后缀 + 查 MIME 映射表:
cpp
http_resp.SetCode(200);
http_resp.AddHeader("Content-Length", std::to_string(content.size()));
http_resp.AddHeader("Connection", "close");
http_resp.AddHeader("Content-Type", MiniType::Suffix2MimeType(http_req.Suffix()));
http_resp.SetBody(content);
一个网页 = 一个 HTML + N 个静态资源。浏览器先请求 HTML,解析到
<link>/<img>/<script>标签后再逐个请求这些资源。Content-Type让浏览器知道每个响应正文用什么解码器来处理。Connection: close告诉浏览器"一个资源请求处理完就关连接"------否则浏览器可能为了复用连接而干等,影响页面加载体验。
6.4 构建响应与状态码映射
HttpResponse 的构造函数初始化默认值(版本=HTTP/1.0、状态码=200),几个 setter 负责填充内容,CodeToDesc 管状态码→描述字符串的映射:
cpp
class HttpResponse
{
private:
std::string CodeToDesc(int code)
{
switch (code)
{
case 200: return "OK";
case 302: return "Found";
case 404: return "Not Found";
// ... 其余状态码省略(400/403/500/502/503 等均支持)
default: return "Unknown Status Code";
}
}
public:
HttpResponse() : _version(g_http_version), _code(200), _code_desc("OK"), _blank_line(linesep) {}
void AddHeader(const std::string &key, const std::string &value) { _header[key] = value; }
void SetCode(int code) { _code = code; _code_desc = CodeToDesc(_code); }
void SetBody(const std::string &content) { _text = content; }
// ... 序列化方法见 9.4
private:
std::string _version;
int _code; // int 而非 string------以后做状态码判断不用 stoi
std::string _code_desc;
std::unordered_map<std::string, std::string> _header;
std::string _blank_line;
std::string _text;
};
HttpRequest 侧新增了一个 RequestContent() 方法------根据 URI 去 wwwroot 目录读取对应的文件内容:
cpp
std::string RequestContent()
{
return Util::ReadFile(_uri);
}
这样 HttpProtocol::HandlerHttpRequest 的核心流程就非常清晰了:反序列化 → 读文件 → 判空设码 → 序列化返回:
cpp
std::string HandlerHttpRequest(std::string &req)
{
HttpRequest http_req;
http_req.Deserialize(req);
HttpResponse http_resp;
std::string content = http_req.RequestContent();
if (content.empty())
{
http_resp.SetCode(302); // 文件不存在 → 临时重定向
http_resp.AddHeader("Location", "/error.html"); // Location 搭配 3xx 使用
}
else
{
http_resp.SetCode(200);
http_resp.AddHeader("Content-Length", std::to_string(content.size()));
http_resp.AddHeader("Connection", "close"); // 短连接------响应完就关
http_resp.SetBody(content);
}
return http_resp.Serialize();
}
几个设计细节:
Content-Length响应头要带上:虽然浏览器能靠 HTML 闭合标签推断长度,但带上长度是最佳实践------浏览器可以精确知道接收多少字节后开始渲染。Connection: close:标识短连接模式,响应发送完毕就关闭 TCP 连接。- 302 重定向处理 404 :文件不存在时不再硬编码错误页面正文,而是返回
302+Location: /error.html------浏览器拿到后会自动发起第二次请求 去取/error.html。这就是重定向的核心机制(详见第三章)。
6.5 序列化:把结构化字段拼回 HTTP 字符串
反序列化的逆过程------将 _version、_code、_code_desc、_header 的 KV 对、_blank_line、_text 按 HTTP 响应格式拼接:
cpp
std::string Serialize()
{
// 1. 状态行:"HTTP/1.0 200 OK\r\n"
std::string resstr = _version;
resstr += spacesep + std::to_string(_code) + spacesep + _code_desc + linesep;
// 2. 响应头:遍历 unordered_map 逐行输出
for (auto &header : _header)
resstr += header.first + headersep + header.second + linesep;
// 3. 空行 + 4. 响应体
resstr += _blank_line + _text;
return resstr;
}
文件顶部定义了统一的常量:
linesep = "\r\n"、headersep = ": "、spacesep = " "------不用魔法字符串,一处修改全局生效。
四段拼接,顺序固定------状态行 → 报头 → 空行 → 正文。整个 HTTP 服务器的核心就是三个操作:反序列化 (字符串 → 结构体)→ 业务处理 (读文件、判断状态码)→ 序列化(结构体 → 字符串)。和上一章 NetCal 的自定协议思想完全一致------只是 HTTP 的格式是标准化的,不需要自己设计分隔符。响应构建和请求解析结构对称:都是四段(行 → 头 → 空行 → 正文),序列化和反序列化互为逆过程。
七、动态路由------从静态资源到 Web 服务
7.1 静态网站 vs 动态网站
到目前为止,我们的服务器只能做一件事:根据 URI 找到 wwwroot 下的对应文件,返回文件内容。HTML、CSS、JS、JPG------这些都是静态资源,用 GET 方法获取即可。
但真正的网站不可能全是静态页面------登录、注册、支付、搜索,这些需要用户和服务器交互 。交互意味着客户端要携带数据(用户名、密码、查询关键词)提交给服务器,服务器根据数据做出不同的响应。这就是动态站点的本质。
#mermaid-svg-eoQfjPeYvex5EORB{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-eoQfjPeYvex5EORB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-eoQfjPeYvex5EORB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-eoQfjPeYvex5EORB .error-icon{fill:#552222;}#mermaid-svg-eoQfjPeYvex5EORB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-eoQfjPeYvex5EORB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-eoQfjPeYvex5EORB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-eoQfjPeYvex5EORB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-eoQfjPeYvex5EORB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-eoQfjPeYvex5EORB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-eoQfjPeYvex5EORB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-eoQfjPeYvex5EORB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-eoQfjPeYvex5EORB .marker.cross{stroke:#333333;}#mermaid-svg-eoQfjPeYvex5EORB svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-eoQfjPeYvex5EORB p{margin:0;}#mermaid-svg-eoQfjPeYvex5EORB .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-eoQfjPeYvex5EORB .cluster-label text{fill:#333;}#mermaid-svg-eoQfjPeYvex5EORB .cluster-label span{color:#333;}#mermaid-svg-eoQfjPeYvex5EORB .cluster-label span p{background-color:transparent;}#mermaid-svg-eoQfjPeYvex5EORB .label text,#mermaid-svg-eoQfjPeYvex5EORB span{fill:#333;color:#333;}#mermaid-svg-eoQfjPeYvex5EORB .node rect,#mermaid-svg-eoQfjPeYvex5EORB .node circle,#mermaid-svg-eoQfjPeYvex5EORB .node ellipse,#mermaid-svg-eoQfjPeYvex5EORB .node polygon,#mermaid-svg-eoQfjPeYvex5EORB .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-eoQfjPeYvex5EORB .rough-node .label text,#mermaid-svg-eoQfjPeYvex5EORB .node .label text,#mermaid-svg-eoQfjPeYvex5EORB .image-shape .label,#mermaid-svg-eoQfjPeYvex5EORB .icon-shape .label{text-anchor:middle;}#mermaid-svg-eoQfjPeYvex5EORB .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-eoQfjPeYvex5EORB .rough-node .label,#mermaid-svg-eoQfjPeYvex5EORB .node .label,#mermaid-svg-eoQfjPeYvex5EORB .image-shape .label,#mermaid-svg-eoQfjPeYvex5EORB .icon-shape .label{text-align:center;}#mermaid-svg-eoQfjPeYvex5EORB .node.clickable{cursor:pointer;}#mermaid-svg-eoQfjPeYvex5EORB .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-eoQfjPeYvex5EORB .arrowheadPath{fill:#333333;}#mermaid-svg-eoQfjPeYvex5EORB .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-eoQfjPeYvex5EORB .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-eoQfjPeYvex5EORB .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eoQfjPeYvex5EORB .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-eoQfjPeYvex5EORB .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eoQfjPeYvex5EORB .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-eoQfjPeYvex5EORB .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-eoQfjPeYvex5EORB .cluster text{fill:#333;}#mermaid-svg-eoQfjPeYvex5EORB .cluster span{color:#333;}#mermaid-svg-eoQfjPeYvex5EORB div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-eoQfjPeYvex5EORB .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-eoQfjPeYvex5EORB rect.text{fill:none;stroke-width:0;}#mermaid-svg-eoQfjPeYvex5EORB .icon-shape,#mermaid-svg-eoQfjPeYvex5EORB .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eoQfjPeYvex5EORB .icon-shape p,#mermaid-svg-eoQfjPeYvex5EORB .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-eoQfjPeYvex5EORB .icon-shape .label rect,#mermaid-svg-eoQfjPeYvex5EORB .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eoQfjPeYvex5EORB .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-eoQfjPeYvex5EORB .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-eoQfjPeYvex5EORB :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 动态
POST /api/login
email=user@xx.com&pwd=123
查询数据库 → 返回结果
静态
GET /index.html
返回文件内容
7.2 浏览器如何提交参数:表单 <form>
前端的入口是 HTML 表单。用户在 <form> 中填写数据,点击提交按钮后,浏览器自动提取表单字段,构建带参数的 HTTP 请求:
html
<form class="auth-form active" id="loginForm" action="/api/login" method="POST">
<input type="email" name="email" placeholder="your@email.com">
<input type="password" name="password" placeholder="输入密码">
<button type="submit">登录</button>
</form>
action="/api/login"→ 告诉浏览器:提交后向这个 URI 发请求method="POST"→ 用 POST 方法,参数放在请求正文中name="email"→ 字段名,浏览器用它和用户输入值拼成 Key-Value 对
7.3 动态路由注册
URI 不再只对应磁盘上的文件------它还可以对应服务器上的一个服务方法 。比如 /api/login 不映射到 ./wwwroot/api/login,而是映射到 Login() 函数。
在 HttpProtocol 中新增服务映射表和相关方法:
cpp
using service_t = std::function<void(HttpRequest &req, HttpResponse &resp)>;
class HttpProtocol
{
public:
void RegisterService(const std::string &uri, service_t service)
{
std::string key = g_wwwroot + uri;
_http_services[key] = service; // URI → 回调函数
}
bool IsReqService(const std::string &uri)
{
auto iter = _http_services.find(uri);
return iter != _http_services.end(); // 查表判断是服务还是静态资源
}
std::string HandlerHttpRequest(std::string &req)
{
HttpRequest http_req;
http_req.Deserialize(req);
HttpResponse http_resp;
if (IsReqService(http_req.Uri()))
{
_http_services[http_req.Uri()](http_req, http_resp); // 走服务回调
}
else
{
// 静态资源:读文件 → 200/302
std::string content = http_req.RequestContent();
if (content.empty())
{
http_resp.SetCode(302);
http_resp.AddHeader("Location", "/error.html");
}
else
{
http_resp.SetCode(200);
http_resp.AddHeader("Content-Length", std::to_string(content.size()));
http_resp.AddHeader("Connection", "close");
http_resp.AddHeader("Content-Type", MiniType::Suffix2MimeType(http_req.Suffix()));
http_resp.SetBody(content);
}
}
return http_resp.Serialize();
}
private:
std::unordered_map<std::string, service_t> _http_services;
};
主线逻辑:收到请求 → IsReqService 判断 URI 是不是注册过的服务 → 是就回调服务函数,不是就走静态文件老路。
服务端 main 中注册:
cpp
std::unique_ptr<HttpProtocol> protocol = std::make_unique<HttpProtocol>();
protocol->RegisterService("/api/login", Login);
protocol->RegisterService("/api/register", Register);
这就是 RESTful 接口的雏形------URI 标识资源或服务,HTTP 方法(GET/POST)表达操作意图,JSON(或明文)携带数据。
7.4 POST 方法:参数在正文中
POST 请求将参数放在请求正文(Body)中。服务端拿到后,直接用 HttpRequest::Text() 读取正文即可------正文已经在 ParseText 中通过 Content-Length 截取好了:
cpp
void Login(HttpRequest &req, HttpResponse &resp)
{
std::string data = req.Text(); // 拿到 "email=xxx&password=yyy"
// 按 & 和 = 分隔解析出 email 和 password
// 查数据库验证...
resp.SetCode(200);
resp.AddHeader("Content-Type", MiniType::Suffix2MimeType(".txt"));
resp.SetBody("Login success!");
resp.AddHeader("Content-Length", std::to_string(resp.BodySize()));
}
7.5 GET 方法:参数拼在 URL 上
GET 请求也可以携带参数,但不是放在正文中,而是拼接在 URL 后面 。格式:/api/login?email=xxx&password=yyy。
? 之后的部分叫查询字符串 (query string),& 分隔不同参数,= 分隔 key 和 value。

浏览器实际访问的地址变为:
http://106.14.7.57:8080/api/login?email=1111%40163.com&password=11111
URI 里夹杂了查询参数,不再直接等于 /api/login。ParseText 要增加 GET 方法的参数剥离逻辑:
cpp
bool ParseText(std::string &httpstr)
{
if (strcasecmp(_method.c_str(), "GET") == 0) // GET 方法:无正文,但 URL 可能带 ?
{
auto pos = _uri.find(argsep); // argsep = "?"
if (pos == std::string::npos)
{
_text = std::string();
return true; // 没有查询参数,完事
}
else
{
std::string temp = _uri;
_uri = temp.substr(0, pos); // "/api/login?email=xxx" → "/api/login"
_text = temp.substr(pos + argsep.size()); // "email=xxx&password=yyy"
return true;
}
}
else
{
// POST 等方法:走 Content-Length 读取(不变)
if (_header.find("Content-Length") == _header.end())
{
_text = std::string();
return true;
}
int content_length = std::stoi(_header["Content-Length"]);
_text = httpstr.substr(0, content_length);
httpstr.erase(0, content_length);
return true;
}
}
知识点: GET 和 POST 携带参数的区别------GET 把参数拼在 URL 上(
?key=value&key=value),适合搜索、筛选等不敏感数据;POST 把参数放在正文中,适合登录、注册等敏感数据(不会被浏览器历史记录和服务器日志直接暴露)。URL 中%40是@的 urlencode 产物------email 中的@被自动转义了。strcasecmp是 POSIX 的不区分大小写字符串比较,比strcmp更适合处理 HTTP 的方法名(get/GET/Get都接受)。
7.6 GET vs POST 对比与安全
结论1: GET 方法常用于获取静态资源,也可以通过 URI 传递参数(?key=value)。
结论2: POST 方法通过请求正文传递参数,适合较大数据量和敏感场景。
| 维度 | GET | POST |
|---|---|---|
| 参数位置 | URL 查询字符串(? 后) |
请求正文(Body) |
| 可见性 | URL 明文暴露,浏览器历史记录、服务器日志均可见 | 不在 URL 中,普通用户不可见 |
| 长度限制 | URL 长度受浏览器和服务器限制(通常 ~2KB) | 无硬性限制 |
| 适用场景 | 搜索、筛选、分页 | 登录、注册、上传文件 |
| 幂等性 | 是(多次请求结果相同) | 否(多次 POST 可能创建多条记录) |
但两者都不安全------HTTP 本身是明文协议。 不管参数放在 URL 还是正文中,HTTP 请求在网络上传输时都是未经加密的。中间经过的每一台路由器、交换机都能看到完整内容。局域网内任意一台机器抓包就能截获密码。
验证工具(任选其一):
- Fiddler:Windows 平台经典 HTTP 抓包工具
- HTTP Toolkit:跨平台,界面更现代
#mermaid-svg-tZfm3uJ6P5xNpYlv{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-tZfm3uJ6P5xNpYlv .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-tZfm3uJ6P5xNpYlv .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-tZfm3uJ6P5xNpYlv .error-icon{fill:#552222;}#mermaid-svg-tZfm3uJ6P5xNpYlv .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-tZfm3uJ6P5xNpYlv .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-tZfm3uJ6P5xNpYlv .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-tZfm3uJ6P5xNpYlv .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-tZfm3uJ6P5xNpYlv .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-tZfm3uJ6P5xNpYlv .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-tZfm3uJ6P5xNpYlv .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-tZfm3uJ6P5xNpYlv .marker{fill:#333333;stroke:#333333;}#mermaid-svg-tZfm3uJ6P5xNpYlv .marker.cross{stroke:#333333;}#mermaid-svg-tZfm3uJ6P5xNpYlv svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-tZfm3uJ6P5xNpYlv p{margin:0;}#mermaid-svg-tZfm3uJ6P5xNpYlv .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-tZfm3uJ6P5xNpYlv .cluster-label text{fill:#333;}#mermaid-svg-tZfm3uJ6P5xNpYlv .cluster-label span{color:#333;}#mermaid-svg-tZfm3uJ6P5xNpYlv .cluster-label span p{background-color:transparent;}#mermaid-svg-tZfm3uJ6P5xNpYlv .label text,#mermaid-svg-tZfm3uJ6P5xNpYlv span{fill:#333;color:#333;}#mermaid-svg-tZfm3uJ6P5xNpYlv .node rect,#mermaid-svg-tZfm3uJ6P5xNpYlv .node circle,#mermaid-svg-tZfm3uJ6P5xNpYlv .node ellipse,#mermaid-svg-tZfm3uJ6P5xNpYlv .node polygon,#mermaid-svg-tZfm3uJ6P5xNpYlv .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-tZfm3uJ6P5xNpYlv .rough-node .label text,#mermaid-svg-tZfm3uJ6P5xNpYlv .node .label text,#mermaid-svg-tZfm3uJ6P5xNpYlv .image-shape .label,#mermaid-svg-tZfm3uJ6P5xNpYlv .icon-shape .label{text-anchor:middle;}#mermaid-svg-tZfm3uJ6P5xNpYlv .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-tZfm3uJ6P5xNpYlv .rough-node .label,#mermaid-svg-tZfm3uJ6P5xNpYlv .node .label,#mermaid-svg-tZfm3uJ6P5xNpYlv .image-shape .label,#mermaid-svg-tZfm3uJ6P5xNpYlv .icon-shape .label{text-align:center;}#mermaid-svg-tZfm3uJ6P5xNpYlv .node.clickable{cursor:pointer;}#mermaid-svg-tZfm3uJ6P5xNpYlv .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-tZfm3uJ6P5xNpYlv .arrowheadPath{fill:#333333;}#mermaid-svg-tZfm3uJ6P5xNpYlv .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-tZfm3uJ6P5xNpYlv .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-tZfm3uJ6P5xNpYlv .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tZfm3uJ6P5xNpYlv .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-tZfm3uJ6P5xNpYlv .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tZfm3uJ6P5xNpYlv .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-tZfm3uJ6P5xNpYlv .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-tZfm3uJ6P5xNpYlv .cluster text{fill:#333;}#mermaid-svg-tZfm3uJ6P5xNpYlv .cluster span{color:#333;}#mermaid-svg-tZfm3uJ6P5xNpYlv div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-tZfm3uJ6P5xNpYlv .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-tZfm3uJ6P5xNpYlv rect.text{fill:none;stroke-width:0;}#mermaid-svg-tZfm3uJ6P5xNpYlv .icon-shape,#mermaid-svg-tZfm3uJ6P5xNpYlv .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tZfm3uJ6P5xNpYlv .icon-shape p,#mermaid-svg-tZfm3uJ6P5xNpYlv .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-tZfm3uJ6P5xNpYlv .icon-shape .label rect,#mermaid-svg-tZfm3uJ6P5xNpYlv .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tZfm3uJ6P5xNpYlv .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-tZfm3uJ6P5xNpYlv .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-tZfm3uJ6P5xNpYlv :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} HTTP 明文
(密码可见)
HTTP 明文
客户端
路由器
服务器
局域网内任意主机
抓包即可看到密码
解决方案:HTTPS = HTTP + SSL/TLS。 SSL/TLS 层在 HTTP 和 TCP 之间插入加密:客户端和服务器先握手交换密钥,之后所有 HTTP 报文都用密钥加密传输,中间节点只能看到密文。
| 协议 | 默认端口 | 传输内容 |
|---|---|---|
| HTTP | 80 | 明文 |
| HTTPS | 443 | SSL/TLS 加密 |
知识点: 公网环境必须用 HTTPS------加密 + 身份认证(证书防中间人攻击)。内网环境可以用 HTTP------局域网相对可控,加密开销不划算。不存在"发给 HTTP 端口却收到 HTTPS 响应"的情况------端口和协议是绑定的。
八、会话管理------Cookie 与 Session
8.1 问题:HTTP 无状态,怎么保持登录?
回到 1.3 节的问题------HTTP 是无状态的,服务器不会记住你。但我们日常上网时,登录一次就能用很久,不需要每次刷新都重新输入密码。这是怎么做到的?
关键在于:登录和后续请求是两个独立的 HTTP 请求。 服务器需要在第二个请求到来时"认出"这个客户端就是刚才登录的那个------也就是维持会话状态。
8.2 Cookie:服务器给客户端的"号码牌"
服务器在登录成功的响应中,通过 Set-Cookie 响应头给浏览器写入一段数据(比如 session ID)。浏览器收到后会将这段数据保存在本地(可以是文件,也可以是内存)。之后每次向同一个域名发起请求时,浏览器自动把 Cookie 放在请求头中带回服务器:
服务器 客户端 服务器 客户端 #mermaid-svg-Lu5KarPuIA28ecgA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Lu5KarPuIA28ecgA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Lu5KarPuIA28ecgA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Lu5KarPuIA28ecgA .error-icon{fill:#552222;}#mermaid-svg-Lu5KarPuIA28ecgA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Lu5KarPuIA28ecgA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Lu5KarPuIA28ecgA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Lu5KarPuIA28ecgA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Lu5KarPuIA28ecgA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Lu5KarPuIA28ecgA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Lu5KarPuIA28ecgA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Lu5KarPuIA28ecgA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Lu5KarPuIA28ecgA .marker.cross{stroke:#333333;}#mermaid-svg-Lu5KarPuIA28ecgA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Lu5KarPuIA28ecgA p{margin:0;}#mermaid-svg-Lu5KarPuIA28ecgA .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Lu5KarPuIA28ecgA text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-Lu5KarPuIA28ecgA .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Lu5KarPuIA28ecgA .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-Lu5KarPuIA28ecgA .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-Lu5KarPuIA28ecgA .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-Lu5KarPuIA28ecgA #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-Lu5KarPuIA28ecgA .sequenceNumber{fill:white;}#mermaid-svg-Lu5KarPuIA28ecgA #sequencenumber{fill:#333;}#mermaid-svg-Lu5KarPuIA28ecgA #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-Lu5KarPuIA28ecgA .messageText{fill:#333;stroke:none;}#mermaid-svg-Lu5KarPuIA28ecgA .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Lu5KarPuIA28ecgA .labelText,#mermaid-svg-Lu5KarPuIA28ecgA .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-Lu5KarPuIA28ecgA .loopText,#mermaid-svg-Lu5KarPuIA28ecgA .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-Lu5KarPuIA28ecgA .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Lu5KarPuIA28ecgA .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-Lu5KarPuIA28ecgA .noteText,#mermaid-svg-Lu5KarPuIA28ecgA .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-Lu5KarPuIA28ecgA .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Lu5KarPuIA28ecgA .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Lu5KarPuIA28ecgA .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Lu5KarPuIA28ecgA .actorPopupMenu{position:absolute;}#mermaid-svg-Lu5KarPuIA28ecgA .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-Lu5KarPuIA28ecgA .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Lu5KarPuIA28ecgA .actor-man circle,#mermaid-svg-Lu5KarPuIA28ecgA line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-Lu5KarPuIA28ecgA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ① 登录 服务器处理请求返回 Set-Cookiesession_id=abc123user=shaner ② 后续请求 浏览器自动携带Cookie信息 提交账号密码 (POST /api/login)登录成功 (HTTP 200)请求首页 (GET /index.html)识别身份,返回个人主页
服务器收到后续请求时,解析 HTTP 请求头中的 Cookie 字段,拿到之前存的 session ID 或用户名密码,就能自动完成验证------用户感觉不到第二次登录。
Cookie 的几个关键属性:
| 属性 | 含义 |
|---|---|
Set-Cookie |
服务器向客户端写入 Cookie(响应头) |
Cookie |
客户端向服务器回传 Cookie(请求头) |
max-age / Expires |
过期时间------到时间浏览器自动删除,避免长期保存导致安全风险 |
| 存储位置 | 可写入磁盘文件,也可存在浏览器内存中(会话 Cookie) |
8.3 Session:Cookie 的更安全替代
Cookie 里直接存用户名+密码非常危险------明文暴露在网络中。更安全的做法是 Session:
- 服务器端:维护一个 session 表,key 是一个随机字符串(session ID),value 是用户的登录状态
- 客户端:Cookie 里只存 session ID,不存敏感信息
- 后续请求:客户端发来 session ID → 服务器查表 → 取出对应用户信息 → 自动验证
这样即使 Cookie 泄露,攻击者拿到的是一个随机字符串,而不是用户的密码。
知识点: HTTP 的无状态是设计哲学,Cookie/Session 是"外挂"在协议之上的状态管理方案。Cookie 和 Session 不互斥------Session ID 本身就是通过 Cookie 传递的。Cookie 做载体,Session 做内容。即便有了 Cookie,HTTP 协议本身仍然是无状态的------状态由应用层自己维护。
8.4 验证与安全:Cookie 不是银弹
如何验证 Cookie 生效? 服务器在响应中添加 Set-Cookie 报头后,浏览器会自动保存。打开浏览器开发者工具 → Application(或 Storage)→ Cookies,就能看到服务器写入的所有 Cookie 及其过期时间。
Cookie 的安全隐患: 如果黑客获取了你的 Cookie(比如通过 XSS 攻击、中间人抓包),就可以冒充你的身份访问网站------服务端只看 Cookie,不认人。
Session + Cookie 是标准方案: 纯 Cookie 方案把用户信息全存在客户端,风险极高。更好的做法是将用户会话数据保存在服务端 (文件、数据库、或内存 KV 存储如 Redis),只把一个全局唯一、随机生成的 session_id 通过 Set-Cookie 发给客户端。之后的认证流程变为:
Redis/Session存储 服务器 客户端 Redis/Session存储 服务器 客户端 #mermaid-svg-DkXg01zcswDpY1oW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-DkXg01zcswDpY1oW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DkXg01zcswDpY1oW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DkXg01zcswDpY1oW .error-icon{fill:#552222;}#mermaid-svg-DkXg01zcswDpY1oW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DkXg01zcswDpY1oW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DkXg01zcswDpY1oW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DkXg01zcswDpY1oW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DkXg01zcswDpY1oW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DkXg01zcswDpY1oW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DkXg01zcswDpY1oW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DkXg01zcswDpY1oW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DkXg01zcswDpY1oW .marker.cross{stroke:#333333;}#mermaid-svg-DkXg01zcswDpY1oW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DkXg01zcswDpY1oW p{margin:0;}#mermaid-svg-DkXg01zcswDpY1oW .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-DkXg01zcswDpY1oW text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-DkXg01zcswDpY1oW .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-DkXg01zcswDpY1oW .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-DkXg01zcswDpY1oW .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-DkXg01zcswDpY1oW .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-DkXg01zcswDpY1oW #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-DkXg01zcswDpY1oW .sequenceNumber{fill:white;}#mermaid-svg-DkXg01zcswDpY1oW #sequencenumber{fill:#333;}#mermaid-svg-DkXg01zcswDpY1oW #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-DkXg01zcswDpY1oW .messageText{fill:#333;stroke:none;}#mermaid-svg-DkXg01zcswDpY1oW .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-DkXg01zcswDpY1oW .labelText,#mermaid-svg-DkXg01zcswDpY1oW .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-DkXg01zcswDpY1oW .loopText,#mermaid-svg-DkXg01zcswDpY1oW .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-DkXg01zcswDpY1oW .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-DkXg01zcswDpY1oW .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-DkXg01zcswDpY1oW .noteText,#mermaid-svg-DkXg01zcswDpY1oW .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-DkXg01zcswDpY1oW .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-DkXg01zcswDpY1oW .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-DkXg01zcswDpY1oW .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-DkXg01zcswDpY1oW .actorPopupMenu{position:absolute;}#mermaid-svg-DkXg01zcswDpY1oW .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-DkXg01zcswDpY1oW .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-DkXg01zcswDpY1oW .actor-man circle,#mermaid-svg-DkXg01zcswDpY1oW line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-DkXg01zcswDpY1oW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} POST /api/login验证通过,创建 sessionkey = random_uuid, value = {user, expire}Set-Cookie: session_id=random_uuidGET /profile(自动带 Cookie: session_id=random_uuid)查 session_id → 取出用户信息返回个人页面
知识点: Session 并没有彻底解决安全问题------
session_id被窃取后攻击者仍能冒充身份。HTTP 协议层面无法做到绝对安全,最终防线在服务端:① 检测 IP 地址突变则让 session 失效、要求重新登录;② 检测异常行为模式(如短时间大量请求)则主动踢出。主动权始终在服务器一侧。
九、当前进展与下一步
到本章为止,项目已完成的核心能力:
| 模块 | 能力 | 对应方法 |
|---|---|---|
| 网络层 | 多进程 TCP Server(fork + handler 回调解耦) | TcpServer::Loop |
| 请求解析 | 请求行 + 报头 KV + 正文(Content-Length + GET 参数剥离) | HttpRequest::Deserialize |
| URI 路由 | / → ./wwwroot/index.html,后缀提取(rfind(".")) |
ParseReqLine |
| 文件读取 | 二进制方式读取任意类型文件 | Util::ReadFile |
| Content-Type | 15 种后缀 → MIME 类型自动映射 | MiniType::Suffix2MimeType |
| 响应构建 | 读文件 → 200/302 判断 → SetCode/Body/Header | HandlerHttpRequest |
| 响应序列化 | 结构化字段 → HTTP 应答字符串 | HttpResponse::Serialize |
| 动态路由 | URI → 服务回调映射(RegisterService) |
HttpProtocol |
| 重定向 | 文件不存在时 302 跳转 + Location 头 | SetCode(302) |
| 会话管理 | Cookie / Session 维持登录状态(概念已讲,代码待实现) | --- |
全文脉络回顾: 从 URL 概念入门 → HTTP 报文格式 → 状态码/重定向 → 手写最小原型 → 请求反序列化 → 响应序列化 → 动态路由 → 会话管理。一条线串起来:HTTP 是什么、长什么样、怎么解析、怎么构建、怎么让它"动起来"、怎么让它"记住你"。