首页 / 资讯中心 / 文章详情

从URL到HTTPS:排查网络问题必知的HTTP协议细节

从URL到HTTPS:排查网络问题必知的HTTP协议细节 ★ FEATURED ARTICLE
排查过几次网络问题的老手大概都有这种感觉真正让你卡住半天的往往不是那些高深的东西而是最基础的URL写法、HTTP报文字段、HTTPS证书状态这些没人在意但处处是坑的细节。我最近就为这件事花了整整一个周末把URL、HTTP报文与HTTPS加密技术从头到尾重新过了一遍整理了一份笔记。这篇内容就是把笔记里最实在的部分摊开来讲适合那些每天写接口、调接口、排查线上问题却很少回头盯着原始报文看的人。1. 起因一个被看不见的字符坑了的接口排查1.1 排查过程简述先讲这次让我决定整理笔记的事。前端同事反馈某个页面的搜索功能输入数码#好物这类带井号的关键词时一直搜不到结果。我一开始也怀疑是分词或者数据库的问题跑到后端去看日志发现接口根本报参数缺失——也就是说后端没有收到搜索关键字。于是我把抓包打开看真实的请求报文。浏览器地址栏里显示的完整地址是https://example.com/list?keyword数码#好物但抓包工具里实际发出的请求行却是GET /list?keyword数码 HTTP/1.1#好物不见了。问题就在于URL里的#号后面是fragment片段标识符它的作用是让浏览器定位页面内部的某个锚点浏览器在发HTTP请求时会把它整个丢弃压根不会发给服务器。前端把#好物接在查询参数后面等于是把一段永远到不了服务器的数据塞了进去。这个问题的修复很简单对#做一次转义或者放在请求参数的值里统一编码就行。但真正让我在意的是这个看似简单的问题背后团队里好几个写了多年业务代码的人都说不清URL各部分的确切作用。我意识到很多同事对HTTP的认知停留在发个请求、收个响应的黑盒层面底层报文长什么样、哪些字符会被协议特殊处理一概不清楚。1.2 为什么这些基础值得重新整理我后来做了个小范围摸底问了几个同事三个问题URL的fragment会不会发给服务器HTTP响应里的Content-Length到底数的是什么字节HTTPS握手之后真正加密业务数据的是对称加密还是非对称加密答案五花八门正确率不到一半。这其实不怪大家。现代开发框架把HTTP封装得太好了写业务的时候你传一个对象进去框架帮你序列化、拼URL、带Cookie、处理重定向底层报文几乎不可能暴露在面前。但一旦出了问题比如对接老系统、排查慢请求、分析第三方回调你就必须回到报文层面看问题。所以我把这套基础体系重新过了一遍不为了考试就是为了下一次排查问题时能少走弯路。这篇笔记聚焦三块URL的结构和编码规则、HTTP报文的组织方式、HTTPS加密技术的设计逻辑。每一块我都会结合自己实际踩过的坑来讲不是复述教科书。2. URL别只当作文本串七个组成部分和一个编码规则2.1 一个URL到底由哪些部分组成很多人把URL当作一个网址整体来看但它在规范里是被严格拆成多个部分的。看这个例子https://user:passwww.example.com:8080/path/to/page?name张三page2#detail拆开是这些组成部分示例作用常见坑schemehttps告诉客户端用什么协议http/https/ws等大小写不敏感userinfouser:pass早期HTTP认证信息现在基本废弃浏览器地址栏通常会隐藏hostwww.example.com服务器域名或IP发请求前要先做DNS解析port8080服务器监听端口默认端口与scheme关联80/443可省略path/path/to/page服务器上的资源路径以/开头空表示根queryname张三page2查询参数不会被路由匹配直接消费fragmentdetail浏览器本地锚点不会送到服务器先要记住的就是fragment因为它最反直觉。你在地址栏点一个带#的链接浏览器会去滚动到页面对应位置整个过程发生在本地。除非页面脚本用JavaScript主动读取location.hash否则服务器完全感知不到这部分内容。这也是前面那个搜索Bug的根因。再说query。query和path在服务器看来是两个独立的部分路由匹配通常只关心path参数解析关心query。很多人会把路径参数/user/123和查询参数/user?id123混为一谈实际上它们在REST接口设计里各有用途。路径参数适合表达资源层级查询参数适合表达过滤和分页条件。顺带把URI、URL、URN三兄弟的关系说清楚。URI是最大的概念表示统一资源标识符凡是能唯一标识一个资源的字符串都算URL是其中的一种它不仅标识资源还告诉你访问的位置URN则只给资源起一个持久不变的名字不关心怎么访问。实际开发中九成场景用的都是URL所以平时混着叫问题不大但心里要清楚边界。2.2 编码URL里只有ASCII字符是安全的URL的规范早年为ASCII设计后来补充了IDN国际化域名和百分号编码机制来容纳非ASCII内容。这里有一条硬规则URL中出现的非英文字符以及和结构冲突的保留字符都需要做百分号编码。保留字符包括这些被URL结构征用的符号: / ? # [ ] ! $ ( ) * , ; 什么意思呢如果你的查询参数值里本身包含一个符号比如keywordab直接拼进URL就变成了?keywordab服务端解析后得到的keyword值会是ab会被当作另一个参数的一部分。正确做法是对做百分号编码?keyworda%26b我见过太多人在此翻车。另一个高频坑是空格空格在query字符串里可以编码成%20但在表单方式提交的body里空格却要编码成。所以有的人用号传关键词服务端却收到了空格或者反过来。规范上application/x-www-form-urlencoded明确要求空格转而URL路径里的空格应转%20两者不能混。中文的处理也必须统一先按UTF-8编码成字节再做百分号编码。比如张三会变成%E5%BC%A0%E4%B8%89。如果前端页面是GBK编码后端按UTF-8解码中间必然乱码。关于这条后面踩坑部分我会再展开。2.3 我平时判断URL是否规范的三个习惯整理笔记时我给自己定了几条实操检查习惯分享出来眼睛扫一遍URL优先怀疑空格、#、中文、这四个字符它们在原始报文里几乎不可能原样出现。拿到一串%E6%9F%90这类的字符第一时间用在线解码工具或命令行工具反转确认解码后的内容是否符合预期。凡是拼接URL的地方一律用框架自带或自己封装好的encode函数不要手工拼字符串。手工拼URL是Bug温床这一点我后面还会重复强调因为它太重要了。3. HTTP报文解剖从头看到尾3.1 请求报文长什么样HTTP协议本身是文本协议HTTP/2以后改成二进制帧但语义不变报文结构非常直白。一次典型的POST请求抓包看到的原始报文长这样POST /api/login HTTP/1.1 Host: example.com Content-Type: application/json Content-Length: 31 User-Agent: Mozilla/5.0 Accept: */* Connection: keep-alive {username:admin,password:123}第一行叫请求行分为三部分方法POST、请求目标/api/login、协议版本HTTP/1.1。这一行最重要服务器就是靠它确定你想干什么、做在哪、按什么规矩来。中间若干行叫头字段每行都是字段名: 字段值。头字段分四类通用头请求响应都可能出现如Date、请求头如User-Agent、Accept、响应头如Set-Cookie、实体头描述body如Content-Type、Content-Length。空行之后是实体bodyPOST请求的参数就放在这里。注意空行是必须的它告诉接收方头结束了下面是实体。Content-Length的值就是body的字节数这个数字如果错了接收方解析就会跑偏。我最早读报文时容易忽略一个细节Content-Length数的是原始字节不是字符数。如果body里有中文一个张三在UTF-8下占6个字节在GBK下占4个字节Content-Length跟着变。这也是前后端编码不一致时各种诡异问题的来源之一。3.2 方法、状态码与它们的设计逻辑HTTP/1.1定义了若干方法常用的是GET、POST、PUT、DELETE、PATCH。这里值得理解的是幂等性概念方法是否幂等典型语义GET是获取资源没有副作用POST否创建资源或提交操作每次执行结果不同PUT是整体替换资源PATCH否部分修改资源DELETE是删除资源幂等的意思是执行一次和执行多次效果一样。为什么要关心因为网络是不可靠的请求可能超时重发。如果客户端不知道请求是否已到达服务器重发一个非幂等请求就可能把数据写两次。这也是设计接口幂等键、做重试策略时绕不开的理论基础。响应报文的第一行叫状态行格式是协议版本状态码原因短语HTTP/1.1 200 OK Content-Type: text/html; charsetutf-8 Content-Length: 1234 Set-Cookie: session_idabc123; Path/; HttpOnly html...状态码按首位数字分类范围含义常见码1xx信息性响应100 Continue, 101 Switching Protocols2xx成功200 OK, 201 Created, 204 No Content3xx重定向301, 302, 307, 308, 3044xx客户端错误400, 401, 403, 404, 4295xx服务端错误500, 502, 503, 504重定向这里有个细节值得展开301和302是老协议语义下的永久/临时重定向浏览器在遇到POST请求时会把方法改成GET再跳转这在很多场景下会丢参数。307和308是对应的新语义承诺方法不变、body不变。如果你的后端接口要处理提交表单后的重定向建议明确用307/308或者在前端用Fetch的redirect: manual自己接管。这个坑我后面还会提到一个真实案例。3.3 实体、分块传输与连接复用Content-Length之外还有一种常见的body描述方式是chunked分块传输编码。当服务器无法提前知道响应体长度时比如实时推送或动态生成的大文件就会用Transfer-Encoding: chunked把响应体切成一块块发送每块前面用十六进制标明长度最后以0长度块结束。至于Connection: keep-alive代表TCP连接复用。HTTP/1.1默认开启多个请求共用同一个TCP连接避免每次请求都重复握手。连接复用是性能基础但也会带来一个排查问题如果上层业务不主动释放连接连接池占用会悄悄升高线上出现连接耗尽的假象。查这类问题除了看应用日志建议直接看TCP层连接数和TIME_WAIT状态。HTTP/2则把报文彻底结构化把请求和响应拆成一个个帧用流ID标识所属请求在同一个TCP连接上多路复用还引入HPACK对头部做压缩。从报文可见性的角度看HTTP/2的原始形态不再是纯文本抓包工具显示的是解析后的结构但头和状态行的语义没有变。理解HTTP/1.1的报文结构对读HTTP/2的抓包结果依然有效。4. HTTPS的加密设计对称加密解决速度非对称解决信任4.1 HTTPS到底在解决哪三个问题HTTP报文是明文传输任何能截获网络流量的人都能直接读内容。HTTPS的加密技术要解决三个维度的问题机密性内容只有通信双方能读懂。完整性内容在传输中不被篡改。身份认证你能确认对方确实是你要访问的服务器而不是中间人冒充。这三个目标分别由加密算法和证书体系配合完成。注意身份认证这个点经常被忽略但恰恰是它让加密有意义——如果你和一个假冒的服务器建立了加密通道加密反而成了对手的保护伞。所以HTTPS的完整设计里证书验证和加密同样重要。4.2 对称加密与非对称加密一对互补的工具先说对称加密。AES这类算法加解密都用同一个密钥速度快、适合加密大量数据密钥就是一切。问题来了通信双方第一次见面怎么安全地传递这个密钥如果明文传中间人截获后就能解密后续所有内容。非对称加密解决密钥传递问题。RSA和椭圆曲线算法ECC使用一对公钥/私钥公钥可以公开分发私钥只有持有者有。用公钥加密的数据只有私钥能解用私钥签名的数据可以用公钥验真。这样服务器把自己的公钥给客户端客户端用它加密一个临时密钥发给服务器只有服务器私钥能解开密钥传递就安全了。但非对称加密慢得离谱不适合加密大量业务数据。所以实际方案是混合使用用途算法类型原因加密业务数据对称加密AES快适合大量数据交换会话密钥非对称加密RSA/ECC解决密钥安全传递完整性校验哈希算法SHA-256摘要不可逆防篡改身份签名非对称签名验证证书与握手参数真实性这也是HTTPS经常被误解的地方很多人以为HTTPS全程用RSA加密其实不是。真实业务数据全程走AES等对称加密所谓密钥协商发生在握手阶段非对称加密只负责把会话密钥安全地交给双方。4.3 数字证书与证书链信任是如何传递的如果服务器直接把公钥发给客户端中间人也能伪造一个公钥。所以需要一个机制确保这个公钥确实是目标服务器的。这个机制叫数字证书由证书颁发机构CA签发。证书里包含持有者域名、公钥、有效期、签发者信息以及CA对以上内容的数字签名。验证过程是链式的浏览器内置了一批根证书根证书的公钥可以验证下级CA证书的签名下级CA证书再验证服务器证书形成一条信任链。只要链上任一环对不上浏览器就报警。我第一次部署HTTPS时只给Web服务器配了服务器证书没配中间证书结果浏览器报证书链不完整。服务端只发了一张叶子证书客户端顺着签名找签发者发现签发者中间CA自己也没被浏览器信任链条断在中途。这个坑在自建证书、用免费证书时特别常见。这里还要解释两个概念。证书吊销是指某证书私钥泄露或域名变更后CA收回证书通过CRL吊销列表或OCSP在线查询协议让客户端查询证书状态。OCSP Stapling则是由服务器主动定期抓取OCSP结果并随握手发给客户端减少客户端额外查询的延迟。还有一类特殊情况是自签名证书内网系统常用开源工具生成一张没有CA背书的证书。浏览器默认不信任需要手动导入或配置例外。开发环境这么用没问题但凡是面向用户的正式服务都必须走正规证书链这不是要不要省事的问题而是安全底线。5. TLS握手全流程从ClientHello到加密通道建立5.1 一次完整的握手过程HTTPS的安全由TLS协议承载。一次完整握手在TCP三次握手之后再经历这么几步客户端发送ClientHello包含客户端随机数、支持的TLS版本列表、支持的加密套件列表。服务端回复ServerHello选定TLS版本和加密套件附上服务端随机数。服务端发送证书并把完整证书链一并发出。服务端发送密钥交换参数比如ECDHE的临时公钥并对以上握手消息做签名。客户端验证证书和签名生成预主密钥pre-master secret在ECDHE模式下双方各自用椭圆曲线参数算出同一个共享密钥。双方各自发送Finished消息证明自己掌握正确的会话密钥握手完成之后所有业务数据用对称加密传输。握手的精髓可以概括为双方先交换随机数和椭圆曲线参数各自独立计算出同一个密钥ECDHE这个密钥不经过网络传输中间人即使拿到了所有握手明文也算不出最终密钥。5.2 TLS 1.2和TLS 1.3的关键差异整理笔记时我把两个版本的区别列了个表比较直观维度TLS 1.2TLS 1.3握手往返次数2个RTT加TCP共3个RTT1个RTT加TCP共2个RTT密钥交换RSA或ECDHE仅ECDHE类强制前向保密加密套件写法cipher suite整体协商杂乱分块协商更清晰会话恢复Session ID或TicketPSK与0-RTT安全性需手动剔除弱套件默认只含安全套件TLS 1.3把RSA密钥交换整个移除了只保留ECDHE这种临时密钥交换方式。为什么因为RSA密钥交换有一个致命问题如果服务器私钥事后泄露攻击者可以回放之前抓下来的握手流量解开历史会话密钥把以前加密过的数据全部解密。临时密钥ephemeral用完就丢私钥泄露也不影响历史流量这就是前向保密PFS。现在所有主流服务都要求强制PFS。会话恢复机制方面TLS 1.3使用PSK预共享密钥客户端在第一次握手时拿到服务端签发的会话票据下次连接直接在ClientHello里带PSK省掉一次往返。0-RTT则是更进一步允许客户端在首次连接时就带上应用数据但代价是重放风险所以官方建议只用于幂等请求。5.3 如何亲眼观察握手过程纸上谈兵不如自己抓一次包。我建议顺手做两步验证第一步用命令行工具对任意HTTPS网站发起一次请求并开启详细输出。你会看到类似这样的关键信息SSL connection using TLSv1.3和对应的加密套件Server certificate的subject与issuer字段以及SSL certificate verify ok。这一行就告诉你TLS版本、加密套件、证书验证是否通过。第二步在浏览器地址栏点开锁形图标查看证书详情。你会看到证书的签发者、有效期、公钥算法和指纹。把证书详情和命令行输出对照能更直观理解证书链验证做了什么。每次看到证书验证通过这行输出我都会提醒自己这句简单的输出背后是一整条CA信任链的逐级签名验证以及时间、域名、用途等多个维度的校验。HTTPS的安全不是加密两个字而是一套环环相扣的工程体系。6. 实战踩坑记录五个我真实遇过的网络问题6.1 URL里的#问题呼应开头开头那个数码#好物的案例后来我进一步验证了把#放在路径里的效果和放在query里一样浏览器都会把它看作fragment并截断。如果不转义上游永远收不到完整参数。这件事给团队的教训是凡是用户输入的内容要拼进URL一律显式编码不能直接字符串拼接。6.2 中文参数编码不一致有一次对接第三方回调对方文档写着参数使用UTF-8但实际服务端拿到的中文始终乱码。排查到最后发现对方服务器默认字符集是GBK文档和实现不一致。更隐蔽的是URL编码层面看起来都正常都是%E5%BC%A0这种问题出在解码后按什么字符集解释字节。这个坑的排查方法很直接把接收方日志里解码后的字节流dump出来看十六进制再按不同字符集尝试还原。如果是UTF-8编码的字节被GBK解码会得到典型的乱码形态。认准这个特征基本一眼可以判断。6.3 HTTPS证书的时间问题有一次线上服务突然大面积报证书错误所有客户端都验证失败。服务端同事说证书刚续期过应该没问题。最后发现是服务器系统时间同步失败后偏差了好几个小时证书的有效期判断直接失败。TLS校验证书不只是看证书格式还会结合本机时间判断notBefore/notAfter。这个坑提醒我HTTPS证书问题排查顺序应该是先看本机时间再看证书链是否完整最后看域名和SAN是否匹配。如果你是负责服务端配置的人还要记得一件事证书续期后要检查是否包含完整链有的工具只更新叶子证书中间证书还在旧的过期文件里结果就是把新的叶子证书挂在一根断掉的链上。6.4 重定向导致请求方法变化有个支付回调对接服务端返回302让客户端跳转到另一个域名。前端用的是老式表单提交结果302之后浏览器把POST变成了GET参数全丢了。单据一直处于支付成功但业务未确认状态。这个问题的根源就是前面3.2里提到的301/302浏览器行为。修复方案有两条服务端改用307/308返回或者前端用Fetch配合手动接管重定向。两条路都要求双方对协议语义有明确共识这也是我为什么建议做接口对接时把重定向语义写进文档的原因。6.5 Host头、端口和虚拟主机还有一次开发环境访问正常测试环境打不开。最终发现测试环境的反向代理配置里漏了Host头转发导致后端按默认虚拟主机处理路由全部走到了一个不存在的站点。HTTP/1.1里Host头是唯一必需的请求头多站点共用同一IP时全靠它区分。很多网关和代理会改写Host排查时务必注意抓包结果里Host字段是否符合预期。端口问题也顺带提一句默认端口是服务的隐含前提https默认443http默认80。有人自定义端口后前端漏写请求就会打到默认端口连接直接失败或者落到别的服务上。URL里的端口一旦出现就必须和服务器监听端口完全一致没有智能猜测这回事。7. 笔记收尾值得反复翻看的几处细节7.1 我逐渐养成的三个排查习惯整理这套笔记我最大的收获不是记住了多少协议细节而是养成了一种回到报文去看问题的排查习惯。以前遇到接口问题第一反应是翻业务代码现在会先抓包确认请求和响应的原始形态再回头看代码。这个顺序的调整帮我省下的时间远超整理笔记的投入。如果只让我留下几条建议我会写这三条第一凡是要拼URL的地方不要手写字符串拼接用编码函数或URL构造对象。手工拼接URL等于主动生产Bug这是所有URL相关事故里最普遍的一条。第二排查HTTP问题优先确认四样东西请求行的方法和路径、Host头、Content-Type与Content-Length、状态码和后端日志是否一致。这四样对上九成的传输层问题就能定位。第三学习HTTPS时别只背非对称加密对称加密的口诀要能把握手每一步的目的讲清楚。讲不清楚的地方就用抓包工具抓一次真实的TLS握手对着报文看一遍比看十篇文章都管用。7.2 进一步深入的方向这份笔记还有几个方向可以延伸。一个是HTTP缓存策略Cache-Control、ETag、Expires这些字段的配合关系直接影响页面性能和接口耗时另一个是HTTP/3与QUIC它把传输层的很多机制重新设计了一遍理解起来需要先吃透TCP和TLS的旧模型再一个是证书管理的自动化从手工续期到全自动签发涉及的协议细节也很有意思。我个人在实际操作中的体会是网络基础这块东西越往底层走越能感受到设计逻辑的妙处。很多看似繁琐的规定背后都是为了对抗真实网络环境里的一个具体问题。把这些问题想明白再用抓包工具验证一遍这些知识才能真正变成你自己的判断力。这份笔记写到这就差不多了。后面如果再遇到值得记的网络问题我大概率还会回来补充。也希望这篇内容能帮你少踩几个我已经踩过的坑。
阅读完成 · 觉得有帮助?
咨询建站