基础与报文结构)
Linux 网络学习HTTP一基础与报文结构引言TCP 回显程序已经可以完成“客户端发送字符串服务器再把字符串返回”的过程。但浏览器连接服务器后需要的远不止回显它要告诉服务器自己想访问哪个页面还要判断返回的是网页、图片还是一段错误信息。这些含义由应用层协议约定。前面网络计算器使用 JSON 表示计算请求HTTP 则提供了一套通用的请求与响应规则。浏览器和服务器遵守相同规则就能理解彼此发送的字节。一、HTTP 在网络通信中的位置HTTPHypertext Transfer Protocol超文本传输协议是一种应用层协议围绕资源的请求与响应组织通信。“超文本”可以通过链接关联其他资源实际传输的数据也可以是图片、音频、文件和接口结果。在 HTTP/1.1 的常见场景中可以把通信过程理解为浏览器生成 HTTP 请求 → TCP 发送字节 ↓ 服务器TCP 接收字节 → 解析请求 → 处理资源 ↓ 浏览器解析并使用响应 ← TCP ← 生成 HTTP 响应TCP 处理连接和有序字节传输HTTP 说明请求什么资源、处理结果是什么以及数据应该怎样解释。HTTP 的请求与响应语义见 RFC 9110。这里使用 HTTP/1.1 的文本报文学习协议。HTTP/2 使用二进制帧HTTP/3 则基于 QUIC后者使用 UDP因此“所有 HTTP 都运行在 TCP 上”并不准确。版本演进改变了传输与组织方式资源、方法和状态码等基本概念仍然保留。HTTP/2 标准、HTTP/3 标准分别定义了它们的机制。二、一次网页访问包含的工作假设用户访问一个网站首页。在需要建立新连接且没有缓存命中的简化场景下浏览器先确定服务器地址和端口建立连接再发送请求。服务器解析目标资源读取文件或执行业务最后返回响应。如果 URL 使用域名通常还需要通过 DNSDomain Name System域名系统获取对应地址若地址已经缓存则可能直接使用缓存结果。访问 HTTPS 时还需要完成相应的安全连接过程。浏览器拿到首页并不代表整张页面已经加载完成。首页可能包含h1Linux 学习笔记/h1imgsrc/image/th.jpgalt示例图片imgsrc/image/xlh.jpgalt另一张图片这份 HTMLHyperText Markup Language超文本标记语言只描述页面结构并引用两张图片。浏览器需要另外获取图片页面引用的样式表和脚本也可能触发请求。因此打开一个页面往往会看到多条 HTTP 请求。服务器不一定把图片内容直接放进首页响应中。一份响应通常对应一次请求处理浏览器根据页面里的引用继续取资源再组合出最终看到的内容。MDN 的 HTTP 概述介绍了这一资源获取过程。三、URL 与资源定位URLUniform Resource Locator统一资源定位符用于描述资源的位置。例如http://example.com:8080/blog/index.html?taglinuxpage1#intro它可以拆成几个部分部分示例作用方案http指定访问方案主机example.com指定目标主机端口8080指定服务入口路径/blog/index.html标识要访问的资源查询字符串taglinuxpage1向资源传递查询信息片段intro标识资源中的一个位置HTTP 默认端口为 80HTTPS 默认端口为 443。使用其他端口时需要在地址中明确写出。浏览器直接向源站发送普通 HTTP/1.1 请求时请求行里的目标通常是路径与查询字符串GET /blog/index.html?taglinuxpage1 HTTP/1.1 Host: example.com:8080其中没有#intro。片段通常由浏览器在获取资源后处理例如滚动到页面某个标题不作为该资源请求的目标内容发送给服务器。URI 片段说明明确区分了这一点。路径也不等于服务器操作系统中的绝对文件路径。若 Web 根目录设为wwwroot服务端可以把/index.html映射到wwwroot/index.html把/映射到默认首页。这个映射属于服务器实现。路径还可以代表业务接口例如/Search。服务器匹配该路径后调用搜索函数不需要存在一个名为 Search 的磁盘文件。资源地址与本地文件之间没有必须一一对应的关系。查询字符串与请求正文也应分开保存。?后面的taglinux属于请求目标的一部分正文位于报头后的空行之后。实现时即使把二者都放进一个字符串成员也不能据此把它们当成同一段协议内容。四、HTTP 请求的结构HTTP/1.1 请求由请求行、若干报头字段、空行和可选正文组成。请求行说明要做什么报头补充描述信息正文承载需要提交的数据。GET /index.html HTTP/1.1 Host: 127.0.0.1:8080 Accept: text/html Connection: close这是一份没有正文的请求。第一行的三个部分分别是GET请求方法这里表示获取目标资源的表示。/index.html请求目标。HTTP/1.1请求采用的协议版本。随后每行是一个报头字段。Host指定目标主机信息Accept表达客户端愿意接收的媒体类型Connection: close表示希望在本次交换后关闭连接。这些示例为了阅读方便显示成普通换行。真正构造 HTTP/1.1 报文时起始行和报头行使用\r\n即回车与换行两个字节。最后一条报头行之后再写一个\r\n便出现结束报头区的空行。这一格式见 RFC 9112 的报文定义。空行必须存在即使没有正文。接收方依靠它判断报头何时结束不能通过“没有其他内容了”来猜。再看一份提交 JSON 的请求POST /api/calc HTTP/1.1 Host: 127.0.0.1:8080 Content-Type: application/json Content-Length: 33 Connection: close {left:10,right:20,oper:}这里正文恰好是前面计算器使用的 JSON。它的序列化规则没有消失而是被放进 HTTP 的正文中。HTTP 负责组织外层消息JSON 负责表达里面的计算字段。方法与正文格式也不是绑定关系。POST 可以提交 JSON也可以提交表单或其他数据接收方需要结合报头和接口约定解释。方法的具体语义将在第二篇展开。五、HTTP 响应的结构响应与请求的外观相似只是第一行变成状态行HTTP/1.1 200 OK Content-Type: text/plain; charsetutf-8 Content-Length: 10 Connection: close Hello HTTP第一行由协议版本、三位状态码和原因短语组成。这里200表示请求成功OK是便于人阅读的描述。程序判断结果时应关注状态码不能只比较描述字符串。Content-Type描述正文类型Content-Length给出正文长度。空行之后的Hello HTTP才是正文本例不包含末尾换行长度为 10 字节。把正文改成 HTML并设置相应类型浏览器便能按网页内容处理Content-Type: text/html; charsetutf-8但只发送 HTML 字符串并不等于已经构造出规范的 HTTP/1.1 响应。状态行、报头、空行和正文组织起来客户端才能明确知道处理结果与内容边界。请求和响应的共同结构可参考 MDN 的 HTTP 消息说明。响应正文也不一定是网页。获取图片可以返回图片字节调用计算接口可以返回 JSON资源不存在时则可能返回错误页面。HTTP 报文本身与它承载的内容格式应当分别理解。六、三个常用报头1. Host网络连接已经指定了服务器地址为什么还需要Host因为同一台服务器、同一个 IP 地址可以为多个站点提供服务。服务器可以结合 Host 选择处理请求的站点。例如a.example.com和b.example.com可以解析到同一个 IP但请求中的 Host 不同服务端就能据此区分。HTTP/1.1 请求必须包含 Host端口不是默认端口时也应正确携带端口信息。Host 文档说明了相应要求。2. Content-TypeContent-Type回答“正文是什么类型”。常见取值包括正文内容媒体类型HTML 网页text/html普通文本text/plainJSON 数据application/jsonJPEG 图片image/jpegPNG 图片image/png类型声明不会自动转换正文。把 JPEG 图片字节标成text/html图片不会因此变成网页只会使客户端获得错误的解释信息。静态文件服务器可以根据扩展名选择合适的媒体类型再把文件字节放入正文。文本还可能需要字符编码参数。charsetutf-8表示按 UTF-8 解释文本不能把“内容是 HTML”和“字符怎样编码”混为一谈。Content-Type 文档给出了类型和参数的用法。3. Content-LengthContent-Length表示正文的字节长度不是整个 HTTP 报文长度也不是显示出来的汉字数量。若正文以 UTF-8 编码汉字通常占多个字节正文发生压缩时还需要按实际传输内容计算。使用 C 的std::string保存最终正文时可以用body.size()得到字节数。应在正文生成完成后计算避免正文后来改变长度字段却仍是旧值。Content-Length 文档明确以字节定义这个字段。若声明长度大于实际发送量客户端可能继续等待若声明更小剩余字节可能干扰后面的消息解析。保持连接复用时这种错误尤其容易让下一份响应也无法被正确识别。七、空行与正文完整性报头结束和整份消息结束是两个不同的判断。先找到 \r\n\r\n ↓ 报头已经完整 ↓ 根据协议规则确定正文长度 ↓ 正文收齐后才能处理完整消息例如请求声明Content-Length: 33但当前只收到正文的 12 字节就应该保留已有数据继续接收剩下的 21 字节。因此不能把一次recv的结果直接当成完整 HTTP 请求。TCP 可能把报头拆到多次接收也可能一次返回完整请求加下一条请求的一部分。上一节自定义计算器协议中使用的持久接收缓冲区在这里仍然需要。HTTP/1.1 还支持分块传输等正文定界方式并存在按请求方法、响应状态判断无正文的情况。Content-Length不是所有消息唯一的定界规则。实现教学服务器时可以先支持明确受限的报文但遇到不支持的形式要拒绝不能按已有规则硬猜。尤其不能仅凭 GET 或 POST 的名字判断整份消息长度。请求目标、报头和正文有各自的位置与规则方法语义和字节边界应分别处理。八、构造一份简单响应把报文结构落实到代码核心仍然是序列化。下面的函数构造一份返回 HTML 的固定成功响应#includestringstd::stringBuildHtmlResponse(conststd::stringbody){std::string responseHTTP/1.1 200 OK\r\n;responseContent-Type: text/html; charsetutf-8\r\n;responseContent-Length: ;responsestd::to_string(body.size());response\r\nConnection: close\r\n;response\r\n;responsebody;returnresponse;}调用时可以传入h1Hello HTTP/h1得到包含状态行、报头、空行和正文的完整字符串。正文里即使出现换行也不会重新开始一个报头因为接收方已经通过空行进入正文区域。这个函数只负责生成报文字节。外层服务端还要先收齐并解析请求处理适用的方法和资源再循环发送响应最后按Connection: close的约定关闭连接。不能认为构造好字符串一次send就一定发送完了。在程序结构上可以让HttpRequest保存解析后的方法、目标、报头和正文让HttpResponse保存状态码、响应报头和正文再由Serialize负责拼接。网络收发与协议格式分开后修改网页内容就不需要改动连接处理代码。九、观察实际的请求与响应本机已有 HTTP 服务监听 8080 端口时可以在 Linux 终端使用curl--http1.1--noproxy*-vhttp://127.0.0.1:8080/--http1.1指定使用 HTTP/1.1--noproxy *避免本次请求经过代理-v显示详细交互信息。输出中的标记发送的报头行标记接收的报头行这些符号是 curl 的展示标记不属于协议报文。curl 官方手册说明了这些选项。观察时先确认请求目标和 Host再看响应状态、Content-Type 与 Content-Length。随后核对正文内容是否与类型、长度一致。如果连接被拒绝应先排查监听端口和连接问题如果已经收到404说明服务器返回了 HTTP 层的结果需要检查请求路径及资源映射。浏览器里显示错误页面不代表 TCP 没有建立成功。十、无状态与连接复用HTTP 的“无状态”表示协议不会自动维护业务会话不能据此认定用户上一次做过什么。网站的登录状态需要应用层通过 Cookie、会话或令牌等机制实现第二篇会继续讨论。连接复用则是另一个层次的问题。HTTP/1.1 默认支持持久连接可以在同一条连接上进行多次请求与响应。保留 TCP 连接不等于保留登录身份关闭连接也不意味着用户退出登录。HTTP 连接管理说明解释了持久连接与短连接的区别。HTTPS 使用 TLSTransport Layer Security传输层安全协议提供通信保护。仅把 URL 中的http改为https或者把监听端口改成 443都不能让普通 TCP 服务器自动具备 HTTPS 能力。结语理解 HTTP/1.1可以从一份完整报文入手请求行表达目标与方法状态行表达处理结果报头补充描述空行分开报头与正文长度等定界规则说明消息何时完整。URL、HTTP 与 TCP 也各有职责URL 标识资源位置HTTP 组织资源交互TCP 在本例中传递字节。第二篇将进一步讨论请求方法、状态码、重定向以及登录状态第三篇再把这些规则落实到 Linux HTTP 服务器的解析、路由和资源返回中。