应用层协议http

应用层协议 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 套接字,所以它面对的同样是字节流。回忆上一章我们自己设计协议时遇到的两个核心问题:

  1. 怎么才算读完一条完整报文?(粘包/半包)
  2. 怎么把字符串反序列化成结构化数据?

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.comContent-Length: 150

空行之后是请求正文(Body),存放真正的有效载荷(POST 表单数据、JSON 等)。空行以前的全部是报头。

在我们认知里,一个 HTTP 请求就是一行字符串 ------只不过这行字符串里嵌了 \r\n 来划分行。序列化就是把结构化成员(方法、URI、版本、KV 报头)拼接成这个大字符串。

2.2 响应报文格式

宏观轮廓上,响应报文和请求报文一模一样 ------状态行 + 响应报头 + 空行 + 响应正文,四段结构完全对应。

区别只在第一行:请求行是"方法 + URI + 版本",状态行是"版本 + 状态码 + 状态描述"。

字段 示例 含义
1 HTTP 版本 HTTP/1.1 服务端的协议版本------客户端和服务器各自告诉对方自己能说什么版本
2 状态码 200404 HTTP 规定:只要网络没断、服务器没挂,无论成功还是失败都必须有应答。就像进程退出码------0 表示正常,非 0 表示出了什么问题
3 状态描述 OKNot 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 字节?
一条完整报文!

学协议归根结底就两件事:

  1. 如何把报头和有效载荷分开------HTTP 用空行
  2. 如何把有效载荷交给上层------通过回调(handler),上层不碰协议细节

HTTP 不需要依赖第三方的序列化库,按行、按空格、按冒号就能独立完成编解码。对比上一章我们用 JSON + 长度头的方案,HTTP 的方案更适合"人类可读"这个设计目标。

请求行三列速记:

含义 举例
方法 对资源的操作 GETPOST(详见第三章)
URI URL 的路径部分 /a/b/c/d.html
版本 客户端 HTTP 版本 HTTP/1.0HTTP/1.1HTTP/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 一样,接收方必须回答两个问题:

  1. 怎么才算读完一条完整报文?(粘包/半包问题)
  2. 怎么把字符串反序列化成结构化数据?(提取 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.0HTTP/1.1HTTP/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/loginParseText 要增加 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 响应"的情况------端口和协议是绑定的。


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 是什么、长什么样、怎么解析、怎么构建、怎么让它"动起来"、怎么让它"记住你"。

相关推荐
abbgogo3 小时前
TCP/IP、OSI 与常见网络协议知识点总结
网络·网络协议·计算机网络
李昊哲小课4 小时前
fastapi sse websocket 奶茶店实时订单看板
人工智能·python·websocket·网络协议·fastapi·sse
FungLeo5 小时前
Flutter 接入 Alice 调试浮窗:一个顶层 final 抢跑,把 release 网络整没了
网络·flutter
abbgogo6 小时前
堆叠、DHCP 与链路聚合总结
网络·网络协议
xiaoxiangsiyan9 小时前
GitLab CI/CD 自托管(EE 企业版)+ Kubernetes Runner 集群 + ArgoCD(GitOps 部署)
运维·网络·ci/cd·容器·kubernetes·gitlab·argocd
XR12345678810 小时前
政府办公楼网络搭建方案对比:信锐、华为、华三选型指南
网络·华为
星恒讯工业路由器10 小时前
网络安全访问控制规则详解
网络·安全·智能路由器·访问控制·防火墙规则·工业网络安全
2601_9494999410 小时前
芯瑞科技400G VR4 QSFP-DD光模块:低功耗高密短距互联方案,赋能AI智算中心算力网络升级
大数据·运维·网络·人工智能·科技·光模块
神秘的MT10 小时前
C# WinForm Socket 服务器 + 串口转发
服务器·网络·学习·c#
上海云盾-小余10 小时前
混合网络攻击深度解析:WAF 流量清洗落地完整方案
网络