接着上篇,我们继续往HTTP协议的深处走。这一次,换个视角,站到服务器那头,看看一个HTTP请求到底是怎么被接住、拆开、再处理的。文章会从请求行讲起,把HTTP版本号、URI路径、网站资源的访问逻辑一层层铺开,再聊聊怎么才能完整读到一个HTTP请求报文。在此基础上,继续拆解响应状态码、异常处理、有效载荷,以及Host、User-Agent这些常见请求字段,把浏览器、代理服务器和Web服务器之间的实际交互过程,尽量讲清楚。
目录
1.1 理清三个核心概念:URL路径、服务器根目录与实际资源
三、HTTP响应与首部字段详解:状态码、异常处理及客户端信息
3.3 响应首部中的Content-Type:如何描述消息体的类型
3.7 User-Agent与爬虫基础:客户端身份标识与识别机制
一、HTTP资源路径解析:服务器如何找到请求的文件
网页内容,图片、CSS、JS这些,都存放在服务器的特定路径下,不是硬编码在代码里的。浏览器发HTTP请求的时候,靠URI告诉服务器:我要的是哪个资源。
1.1 理清三个核心概念:URL路径、服务器根目录与实际资源
URI(统一资源标识符):浏览器请求http://8.137.19.140:8081/a/b/c.html时,服务端收到的请求行长这样——GET /a/b/c.html HTTP/1.1其中/a/b/c.html就是URI。它指示的是请求资源在服务器上的相对位置,说白了,就是一份“资源地址”。
Web 根目录(Web Root):代码里定义的那个资源存储根目录,比如:const std::string webroot = "./wwwroot";注意,Web根目录不是Linux系统的绝对根目录/。它只是服务器程序自己划出来的一块地盘,所有静态资源都得放在这个目录下面。浏览器来的URI,也是相对这个根目录去找的。
资源路径:资源在服务器磁盘上的真实物理路径,由Web根目录和URI拼接而成。URI说“我要 /a/b/c.html”,Web根目录说“东西都在./wwwroot底下”,一拼,真实路径就是./wwwroot/a/b/c.html。服务器顺着这条路去磁盘上取文件,取到了,就返回;取不到,就是404。
1.2 HTTP资源路径的映射与拼接规则
服务器收到请求后,会根据URI的情况,走不同的拼接逻辑。
1.2.1 显式访问:客户端请求指定文件
当请求行是GET /a/b/c.html HTTP/1.1时,服务器直接把Web根目录webroot(./wwwroot)和URI(/a/b/c.html)拼在一起,最终得到的真实文件路径就是:./wwwroot/a/b/c.html简单直接,URI 指哪儿,服务器就去哪儿取。

1.2.2 隐式访问:请求根路径与默认首页的自动补全
当请求行是GET/HTTP/1.1时,情况有点不一样。URI只是一个/,指向的是Web根目录本身,可目录没法直接当文件返回。服务器得自己补一步:自动拼接一个默认首页,比如index.html。
代码里通常预设一个homepage = "/index.html",然后:filename = webroot + homepage;拼出来的真实路径就是./wwwroot/index.html。服务器读取这个文件,再返回给浏览器。这就是为什么我们访问一个网站首页时,地址栏只敲域名就行,index.html 是服务器在背后悄悄补上的。
1.3 小结
HTTP请求的本质,就是请求代码中./wwwroot目录下某个特定路径的资源。URI 里那串路径,就是服务器在磁盘上寻找该资源的索引。根目录请求会补默认首页,具体文件请求则直接拼接,规则就这么两条,不复杂。
二、HTTP请求报文进阶:从请求行到完整报文读取
搞清楚了服务器怎么根据URI解析文件路径,接下来该把HTTP请求报文本身拆开看看了。

2.1 HTTP请求行详解:请求方法、资源路径与协议版本
请求行就三样东西,各司其职:
请求方法,告诉服务器这次操作的意图。HTTP定义的方法不少,但最常用的是GET和POST。GET主要负责从远端拿内容,POST则负责把参数提交上去。一个取,一个送,方向正好相反。
URI,表明客户端要请求的是哪个资源。在请求行里,它通常表现为一段相对路径,比如/a/b/c.html,服务器拿到它,再跟Web根目录一拼,就能找到磁盘上的真实文件。
HTTP 版本号,指定客户端发请求时采用的协议版本,比如HTTP/1.0或HTTP/1.1。版本号不只是个标记,它决定了后续报文解析的规则和连接行为,比如1.1默认支持长连接,1.0则要显式声明。
2.2 HTTP为什么需要协议版本:从版本协商到兼容性
HTTP报文里,请求和响应都会明明白白标上版本号。原因不复杂:客户端和服务器,是两款各自独立运行的软件。
现实里,这种独立是常态。一个厂家把自家服务器升级了,可市面上大量用户还没更新客户端。于是,新老版本的客户端和服务器,就这么同时跑在同一张网络里,谁也管不着谁。
所以,客户端发请求时,必须把自己的版本号亮出来。服务器看到版本号,再跟客户端协商,按对方能理解的版本提供服务。这套版本协商机制要是缺了,客户端就可能拿着旧规则去解析新报文,或者服务器按新规则回应旧客户端,处理错误、解析失败,全跟着来了。
版本号看着不起眼,它其实是新老软件能在一张网里共存的那根安全绳。
2.3 网站资源的树状组织与浏览器的后续请求

我们在浏览器里发起请求的时候,首页就是整个站点的入口。一个网站,本质上就是一棵多叉树:首页是树根,各个栏目是枝干,具体页面是叶子,一级一级往下展开。
用户在网页上点一个链接,浏览器就会形成新的访问地址,发起二次请求,进而拼接出新的URI。服务器端请求的所有资源,最终都是通过HTTP请求里的URI来精准表示的。点哪片叶子,URI就指向哪片叶子。

2.4 如何正确读取一个完整的HTTP请求报文
HTTP的底层是TCP,而TCP是面向字节流的。直接调一次recv,谁也不敢保证一次就能把整个请求收全。那怎么办?
分三步走。
第一步,逐字节或按行读,找空行。一边读,一边盯着数据流,直到撞上那个代表报头结束的空行,也就是连续的\r\n\r\n。找到它,才算把完整的请求报头收齐了。
第二步,解析报头,抠出Content-Length。 对读到的报头做反序列化,从中提取Content-Length属性。这个属性白纸黑字写着后续请求正文(有效载荷)到底有多少字节,是读正文的唯一依据。
第三步,按长度精准提取正文。 拿着Content-Length的数值,从剩余的字符流缓冲区里,精准切出对应字节数的内容。拼上前面解析好的报头,一个完整的HTTP请求报文就到手了。
报头靠空行找边界,正文靠长度定大小。两步各管各的,字节流再零碎,也能拼出一个完整的请求。

Tips:/favicon.ico请求
浏览器向服务器发一次网页请求,除了请求网页本身,往往还会顺带请求一个/favicon.ico。这是浏览器的习惯动作,不影响页面主体,我们采取忽略的方式:
if (_targetfile == "./wwwroot/favicon.ico")
{
LOG(LogLevel::DEBUG) << "用户请求: " << _targetfile << " 忽略它";
return false;
}

那/favicon.ico到底是什么?说白了,就是你这个网页的小图标,浏览器标签页上那个小标志,收藏夹里显示的那个小图。
三、HTTP响应与首部字段详解:状态码、异常处理及客户端信息
Web服务器把请求解析完、业务逻辑也处理完,接下来就得给客户端回一份HTTP响应报文。这份报文的命门,就藏在状态行里,状态码和状态码描述,是服务器向客户端交代“事情办得怎么样”的核心。办成了、没办成、还是压根找不到东西,全在这三个数字和一句话里。

3.1 HTTP状态码详解:五大分类与常见状态码
状态码由三位数字组成,按首位数字的不同,划成五大类。首位是几,就代表哪一类,一眼就能看出这次请求大致是什么结局:
| 首位 | 类别 | 含义 |
|---|---|---|
| 1xx | 信息性状态码 | 服务器收到了请求,正在处理,先给客户端递个话。 |
| 2xx | 成功状态码 | 请求被正常接收、理解并处理,事情办成了。 |
| 3xx | 重定向状态码 | 请求的资源挪了地方,客户端得再走一趟新地址。 |
| 4xx | 客户端错误状态码 | 请求本身有问题,服务器没法处理。 |
| 5xx | 服务器错误状态码 | 请求没毛病,是服务器自己出了岔子。 |
五大类,各管一摊。1xx是“稍等,在处理”,2xx是“办好了”,3xx是“换个门”,4xx是“你那边不对”,5xx是“我这边出事了”。记住这个首字口诀,再看到具体状态码,大致方向就不会跑偏。
3.2 HTTP服务器异常处理:从错误响应到业务处理
3.2.1 代理服务器如何传递与生成错误响应
大型公司内部,通常不会让一台服务器单打独斗,而是用代理服务器加上机房里的多台服务器,共同搭一套负载均衡系统。请求先到代理服务器,再由它分发给后端的某一台。
如果其中某台后端服务器因为过载之类的原因出了异常,它不会直接跟终端用户对话,而是把错误码回给代理服务器。代理服务器收到之后,再统一把这份错误信息转交给终端用户。用户看到的那个错误,背后其实是代理服务器在替后端“传话”。这块内容,后面讲正向代理和反向代理的时候,还会再展开。

3.2.2 服务端异常捕获与错误处理回调机制
500错误有一种可能,是服务器创建子进程失败了。可这里有个尴尬的悖论:子进程都没创建出来,谁来给用户回这个500?
办法还是有的。在fork出错、进程准备退出之前,先调一下回调函数excepter()。让服务器自己把错误页面发回去,赶在咽气之前把话交代完。
pid_t id = fork();
if (id < 0)
{
LOG(LogLevel::FATAL) << "fork error ...";
// excepter(sock); // 退出前回调,由服务器自己发送错误页面
exit(FORK_ERR);
}
当然,如果父子进程双双挂掉,那就真没辙了。连发错误页的人都没了,客户端那边只能收到一个沉默的连接断开,什么状态码都等不到。
3.2.3 企业开发中的HTTP状态码与自定义业务错误码
很多大公司,服务器出错的时候,按规矩该返回5xx。可实际上呢,它们会故意返回4xx。
为什么?为了不暴露自己服务器出了毛病。5xx等于明晃晃地告诉外界“我这边出事了”,黑客一看,趁你病要你命,攻击可能紧跟着就来。返回4xx,把锅甩给客户端,至少表面上看起来是“你请求有问题”,而不是“我服务器扛不住”。这是一种自保策略,合规上未必漂亮,但现实里确实存在。
3.3 响应首部中的Content-Type:如何描述消息体的类型
如果你的应答带了数据,那响应报头里就必须写上Content-Length: XXX\r\n。不写,对面根本不知道正文到哪儿为止。
那就绕不开一个问题:一个文件到底有多大?办法有三种。
- C语言的文件读写位置接口:fseek到末尾,ftell一量,大小就出来了。
- C++的ifstream读写位置接口:seekg跳到末尾,tellg拿位置,同样能算出大小。
- stat系统调用:传个文件名进去,返回一个文件结构体,里面直接就有文件大小。
底层视角再看一眼:文件内容,就是一个char类型的数组;文件读写位置,就是数组下标。 这么想,很多接口就通了。
#ifndef __UTIL__
#define __UTIL__
#include "./BasicElement/Common.hpp"
namespace HttpUtilModule
{
class Util
{
public:
long GetFileSize(std::string filename)
{
std::ifstream in(filename, std::ios::binary);
in.seekg(0, std::ios::end);
std::streamoff size = in.tellg();
in.seekg(0, std::ios::beg);
return (long)size;
}
void GetFileContent(std::string &filename, std::string &content)
{
long n = GetFileSize(filename);
int size = n;
content.resize(size);
std::ifstream in(filename, std::ios::binary);
in.read(content.data(), size);
std::cout << "FileName: " << filename << std::endl;
std::cout << "FileSize: " << n << std::endl;
std::cout << "ContentSize: " << content.size() << std::endl;
}
};
}
#endif
Tips:一个必须提一嘴的小问题
前面给TCP服务器设计协议的时候,用的是字符串来读数据。严格讲,这有失偏颇。我们面向的是“字节流”,字节流的缓冲区,应该用vector<char>,而不是std::string。
static bool ReadFileContent(const std::string &filename /*std::vector<char>*/, std::string *out)
{
// version1: 默认以文本方式读文件,但图片是二进制的
}
为什么?因为字符串方式一旦碰到不可显示字符,或者\0,读取就可能出错,甚至直接停在那儿。读文本文件还好,读图片文件,这是致命的。图片里全是二进制字节,\0随处可见,用字符串去接,等于拿漏勺舀水。缓冲区换成vector<char>,字节流才接得住。
3.4 常见MIME类型:浏览器如何识别不同资源
Content-Type,应答里一般都得带上这个属性。
为什么?因为超文本传输,传的终究是字节流。字节流本身不会说话,你得告诉对面:这串字节流到底是什么东西。是 HTML、是图片、还是JSON?没了Content-Type,对面拿到一堆字节,只能干瞪眼。
3.5 客户端内容协商:Accept及相关请求首部
Accept-Encoding和Accept-Language,这两个字段说的是:客户端能支持哪些压缩模式、哪些语言。
网页一大,服务器通常会先把它打包压一压再发,省带宽也省时间。可压缩和解压得双方商量好,你用gzip压的,客户端得认 gzip 才行。Accept-Encoding就是客户端提前亮出的底牌:我能解哪些格式。语言也一样,Accept-Language告诉服务器:中文、英文,还是别的,你按我能看的来。
3.6 Host请求头:同一个服务器如何区分不同网站
Host字段,是客户端在告诉服务器:我要的资源,在哪个主机的哪个端口上。
假设浏览器向服务器发了一段请求,中间要经过代理服务器。代理服务器接到请求后,会看一眼Host,再决定把请求往后端哪台主机上送。后端目标主机收到请求,执行任务,返回响应。一层转一层,Host就是那张写着收件地址的标签,少了它,代理服务器不知道该往哪儿转。

3.7 User-Agent与爬虫基础:客户端身份标识与识别机制
我们在百度里搜东西,本质上也是在向百度的服务器发HTTP请求。服务器收到请求后,会通过User-Agent判断:这个客户端合不合法,是不是一个真正的客户端,跑在什么平台、什么版本上。判断完,再给出对应的搜索结果。
比如搜“微信”,手机上的结果会推安卓版下载,电脑上则会推Windows或macOS版本。同一句话,不同设备,答案不一样,背后就是User-Agent在起作用。

所以说,User-Agent就是一个描述客户端信息的属性。
让机器给百度发一条最简单的HTTP请求,请求行大概就是这样:
GET / HTTP/1.1
有了这个思路,我们就可以借助一些工具,向服务器发起请求,把想要的网页抓下来。手边正好有个现成的工具:wget https://tool.oschina.net/commons这就是爬虫(pachong)。本质很简单:用 HTTP 客户端模拟浏览器行为,去获取指定链接下的网页。
如果这篇文章对你有帮助,别忘了点赞、收藏、关注三连支持。你的每一次反馈,都是我继续硬核输出的最大动力。
本文深入解析HTTP协议在服务器端的处理流程,涵盖URI路径映射与Web根目录拼接规则、HTTP请求报文完整读取方法(基于空行与Content-Length)、五大类HTTP状态码语义及企业级异常处理策略,并重点剖析Host、User-Agent、Accept、Content-Type等关键首部字段的作用机制及其在内容协商、虚拟主机识别和爬虫检测中的实际应用。
:HTTP协议进阶——从资源路径解析到状态码与请求头详解&spm=1001.2101.3001.5002&articleId=169070321&d=1&t=3&u=6253f12b6c3145b7b92e5a6fa0319ea6)
247

被折叠的 条评论
为什么被折叠?



