资讯动态

Java开发中的HTTP协议与接口排障实战

发布时间:2026/10/23 13:51:35 来源:尧图企业网站定制
线上接口突然开始报 400日志里只打印了一行Bad Request没有堆栈、没有字段名、没有任何线索——这种场景我遇到过不止一次。第一次碰上时我盯着代码看了两个小时最后发现问题出在请求头里少了一个Content-Type服务端拿不到正确的类型参数绑定直接失败。从那以后我就明白一件事Java 开发里调用接口谁都会但真要出了问题还能快速定位靠的不是熟练背 API而是对 HTTP 协议本身的理解深度。这篇内容聊的就是 Java 开发中的 HTTP 协议从报文结构、客户端选型、服务端处理链路一直到实际排查手法把这条链路上容易踩的坑一次性摊开讲。不管你是刚写第一个接口的新手还是天天跟超时和连接池打交道的老手都能在里面找到能直接抄走的东西。1. 先把 HTTP 当成一门手艺而不是一门课1.1 一次接口调用的真实链路很多人学 HTTP 的方式是打开一份文档从上往下读方法、状态码、头部字段读完觉得懂了一写代码还是懵。问题在于这种学法把协议当成了一堆定义而实际开发中它是一条完整的时空链路。你在 Java 里写下一行restTemplate.getForObject(...)这行代码往后要经过这些环节应用层的 HTTP 客户端组装报文连接池里取一条已经建立好的 TCP 连接或者新建一条TLS 握手如果是 HTTPS把字节写进 socket经过本机网络栈、可能存在的代理、负载均衡、网关最终到达服务端的监听端口服务端容器解析出请求对象交给框架分发到具体方法方法执行完再按原路返回。链路上任何一环出问题你看到的都是同一个现象调用失败。所以真正决定排查效率的是你知不知道这条链路上每一环分别负责什么、各自会留下什么痕迹。比如超时这件事连接超时和读取超时是两回事连接超时说明 TCP 层面就没握上手多半是网络不通、端口不对、服务没起读取超时说明连接通了但对面没在约定时间内返回多半是服务端处理慢、锁竞争、下游依赖卡住。分不清这两个你就只能靠重启试试。我一直建议团队里新来的同学做一件事找个下午用最原始的方式手写一次 HTTP 请求别用任何框架。写完那几十行代码你对报文这个词的理解会完全不一样后面再看各种客户端配置项脑子里是有画面的。1.2 为什么 Java 开发者绕不开协议层Java 生态把 HTTP 封装得太好了。注解一加方法一写参数自动绑定返回值自动序列化整个过程行云流水。这种便利的代价是抽象层变厚一旦行为不符合预期你连该看哪儿都不知道。我见过不止一个同学在RequestBody上加了个required false却依然报 400最后发现是前端根本没发请求体而服务端在读流的时候就已经决定要解析了。协议层的知识在几个具体场景里会直接变成生产力。第一个是跨语言协作前端、客户端、服务端三方联调出了分歧谁的实现对、谁的不对只能拿协议说话。第二个是网关与中间件配置Nginx 的client_max_body_size、proxy_read_timeout、proxy_buffering每一个都对应协议里的具体行为不懂协议就只能照着别人的配置抄抄错了也不知道错在哪。第三个是性能优化长连接复用、压缩、缓存协商、分块传输这些都是协议层的能力不是框架的魔法。第四个是排查线上问题抓包工具里那一坨字节能读懂和读不懂是完全两种工作状态。说得再直白一点框架帮你处理了 90% 的常规情况剩下 10% 的异常情况才是你的价值所在而那 10% 几乎全部落在协议层。1.3 新手最容易带偏的五个认知下面这张表是我这些年被问得最多、也是误解最集中的几个点。每一条我都见过有人在实际项目里栽跟头。常见认知实际情况会引发的问题GET 不能带请求体协议没禁止但语义上不建议很多服务端会直接丢弃前端发了 body后端读不到排查半天POST 一定比 GET 安全安全性取决于是否加密传输跟方法无关把敏感数据放 body 却走明文 HTTP状态码 200 就是成功业务失败也常返回 200靠响应体里的 code 区分客户端只看状态码误判成功请求头大小无所谓多数网关对头部总大小有上限常见是 8KB 左右塞了超长 token 直接 431 或 400长连接一定更快长连接有闲置回收和并发占用问题滥用反而拖垮连接池配置不当导致请求排队第五条值得多说两句。长连接的本质是复用 TCP 连接省掉握手开销这在 QPS 高、请求频繁的场景下收益明显。但如果连接池上限设得很小而下游响应又慢请求就会在池子里排队等连接表现出来的现象是偶发超时而且越压测越严重。这时候你去看日志每条请求的处理时间都不长但端到端时间很长差的那部分全耗在等连接上了。提示判断是不是连接池排队导致的慢最直接的办法是看客户端连接池的等待指标比如活跃连接数、等待获取连接的线程数。如果没有这个指标临时把池子调大观察响应时间是否下降也是一种验证手段但别把它当长期方案。2. 报文结构拆解抓一次包胜过读十页文档2.1 请求报文的三段式一个 HTTP 请求报文在字节层面就是三段请求行、头部、空行之后的请求体。请求行长这样POST /api/user/query?fromweb HTTP/1.1依次是方法、请求目标、协议版本。头部是一堆Key: Value每行一个大小写不敏感。然后是一个空行空行之后是请求体长度由Content-Length或者Transfer-Encoding: chunked决定。这个结构看着简单但实操中有几个细节值得记住。请求目标不一定是路径如果经过代理并且方法是 CONNECT或者请求发往源站格式会不一样不过日常开发基本用不到。头部字段名大小写不敏感但值通常敏感比如Content-Type的值里charsetUTF-8的大小写一般无所谓但某些自研服务端可能严格匹配。Content-Length和Transfer-Encoding不能同时出现出现了就有请求走私的风险正规服务端会直接拒绝。还有个容易忽略的点请求行和头部之间有严格的 CRLF 换行不是\n而是\r\n。手写客户端时如果只写\n有些严格实现的服务端会直接解析失败。我第一次手写的时候就栽在这儿服务端返回 400抓包看字节才发现问题。2.2 响应报文的拆解与状态码语义响应报文同样是三段状态行、头部、响应体。状态行是HTTP/1.1 200 OK版本、状态码、原因短语。原因短语是给人看的程序判断只看状态码。状态码的五段划分大家都背过但实际用起来真正需要精确掌握的是这几个200 / 201 / 204成功。201 表示创建成功通常配合Location头204 表示成功但无内容响应体必须为空这点很多接口写错返回了 204 却带了 body某些客户端会报错。301 / 302 / 307 / 308重定向。301 是永久302 是临时307 和 308 的区别在于是否保持原方法。POST 请求遇到 302很多客户端会自动改成 GET这是历史遗留行为如果你的接口依赖重定向后仍用 POST就得用 307。400 / 415 / 422客户端错误三兄弟。400 是请求本身有问题415 是Content-Type不被支持422 是格式对但语义校验不过。这三个的区分度直接决定了联调效率。401 / 403认证与授权。401 是你是谁我不知道403 是我知道你是谁但你不能干这个。混用会让人抓狂。429限流。配合Retry-After头告诉客户端多久后重试这是很实用的一个约定。502 / 503 / 504网关类错误。502 是上游返回了无效响应503 是服务不可用504 是上游超时。这三个在微服务架构下出现频率极高区分清楚能直接定位到是哪一层的问题。注意业务系统里用 200 包一切的做法很常见但至少在网关层和基础设施层要保留真实状态码否则监控和告警就没法基于状态码做聚合出问题时你连错误率上升这个信号都拿不到。2.3 用 Socket 手写一个 HTTP 客户端光说不练没意义下面这段代码用最原始的 Socket 发一个 HTTP/1.1 请求不依赖任何 HTTP 库你能清楚看到报文是怎么拼出来的。import java.io.*; import java.net.Socket; import java.nio.charset.StandardCharsets; public class RawHttpClient { public static void main(String[] args) throws IOException { String host example.internal; int port 80; String path /api/ping; try (Socket socket new Socket(host, port)) { socket.setSoTimeout(3000); // 读取超时单位毫秒 String request GET path HTTP/1.1\r\n Host: host \r\n User-Agent: raw-client/1.0\r\n Accept: application/json\r\n Connection: close\r\n \r\n; // 空行标志头部结束 OutputStream out socket.getOutputStream(); out.write(request.getBytes(StandardCharsets.US_ASCII)); out.flush(); BufferedReader reader new BufferedReader( new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8)); String line; while ((line reader.readLine()) ! null) { System.out.println(line); } } } }这段代码有几个点值得留意。Host头在 HTTP/1.1 里是必须的缺了服务端会返回 400因为虚拟主机要靠它区分站点。Connection: close告诉服务端这次用完就关简化了读取逻辑否则你得自己处理分块编码和连接复用。setSoTimeout设的是读取超时不是连接超时连接超时在new Socket()时通过带超时的构造重载指定。跑通这段代码之后你会对HTTP 建立在 TCP 之上这件事有非常具体的感受。后面再看到各种客户端库的配置项比如connectTimeout、readTimeout、connectionRequestTimeout就能一一对应到这三件事上建连接、读数据、从池子里拿连接。2.4 关键 Header 的实战清单头部字段成百上千日常开发真正高频用到的就那么十几个。下面这张表是按使用场景整理的我把踩过坑的都标了出来。头部字段作用实战要点Host指定目标主机HTTP/1.1 必填手写客户端容易漏Content-Type请求体类型决定服务端怎么解析application/json和application/json;charsetUTF-8在多数框架下等价但个别老框架严格匹配Accept期望的响应类型内容协商用服务端可能据此返回不同格式Content-Length请求体字节长度按字节算不是按字符算中文一个字符占三字节算错会截断Transfer-Encoding分块传输值为chunked时不能同时有 Content-LengthAuthorization认证信息Bearer Token 场景下长度可能很大注意网关的头部大小限制Cookie会话标识数量多时总大小会超限分布式场景更推荐 TokenAccept-Encoding支持的压缩算法服务端压缩能显著减小响应体但客户端必须支持解压Cache-Control缓存策略no-cache和no-store的区别很多人搞混前者可缓存但需验证后者完全禁止X-Request-Id链路追踪强烈建议全链路透传排查问题时它是最好用的线索Content-Length按字节计算这一条值得单独强调。我见过有人用str.length()算长度英文没事中文直接出问题。正确做法是str.getBytes(StandardCharsets.UTF_8).length。更隐蔽的是某些客户端库在你设置了Content-Length之后又自动算一遍两边不一致时服务端读到一半就卡住表现为请求挂起而不是报错排查难度直接翻倍。3. Java 里的 HTTP 客户端选型与参数调优3.1 HttpURLConnection能用但别滥用JDK 自带的HttpURLConnection是很多老项目的默认选择好处是零依赖坏处是 API 设计停留在上个时代。它有几个必须知道的行为默认会缓存响应在某些场景下第二次请求拿到的还是旧数据需要显式setUseCaches(false)默认对 POST 会做重定向转换连接复用依赖于Connection: keep-alive和读取完响应体如果只读了部分流就关闭连接不会被复用反而会被销毁。更麻烦的是它的超时只有两个setConnectTimeout和setReadTimeout没有从连接池获取连接的超时概念因为它的连接池是内部实现不对外暴露。在高并发场景下如果所有连接都在用你的线程会阻塞在建连接这一步而这一步的超时上限是 connectTimeout会造成大面积的请求堆积。HttpURLConnection conn (HttpURLConnection) new URL(http://example.internal/api/ping).openConnection(); conn.setRequestMethod(GET); conn.setConnectTimeout(1000); conn.setReadTimeout(3000); conn.setUseCaches(false); conn.setRequestProperty(Accept, application/json); int code conn.getResponseCode(); try (InputStream in code 400 ? conn.getErrorStream() : conn.getInputStream()) { // 必须读完否则连接无法复用 } conn.disconnect();我的建议是新项目不要再用它维护老代码时知道它的坑在哪就行。3.2 Apache HttpClient 与 OkHttp 的取舍Apache HttpClient 是老牌选择功能全配置项多对连接池、路由、重试的控制粒度很细适合需要精细控制的企业内部系统。它的学习曲线偏陡一个完整的CloseableHttpClient配置能写几十行。OkHttp 更偏现代风格API 简洁默认配置就够用拦截器机制很灵活做日志、签名、加解密都很方便缺点是它原本是移动端场景设计的某些服务端场景需要额外注意配置。两者的核心差异在连接池策略。HttpClient 的PoolingHttpClientConnectionManager可以按路由目标主机设置最大连接数总共一个全局上限OkHttp 的ConnectionPool是按地址维护的参数是最大空闲连接数和空闲存活时间。选型时我的判断依据通常是这三点需要按目标主机精细限流选 HttpClient需要灵活扩展请求拦截选 OkHttp团队已有沉淀就跟着走别为了技术偏好引入第二套。3.3 JDK 11 HttpClient 的异步写法JDK 11 引入的java.net.http.HttpClient算是标准库的正经补课同步异步都支持还能直接用 HTTP/2。日常用同步写法就够HttpClient client HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(2)) .version(HttpClient.Version.HTTP_1_1) // 明确降级避免协商开销 .build(); HttpRequest request HttpRequest.newBuilder() .uri(URI.create(http://example.internal/api/user?fromweb)) .timeout(Duration.ofSeconds(3)) .header(Content-Type, application/json) .POST(HttpRequest.BodyPublishers.ofString({\id\:1001})) .build(); HttpResponseString response client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.statusCode() response.body());这里的version值得说一句。默认是HTTP_2会尝试协议升级如果你的服务端或者中间网关对 h2c 支持不完整升级过程可能带来额外延迟甚至失败。内部系统里显式设成 1.1 是更稳妥的选择除非你确认整条链路都支持 HTTP/2 并且确实需要多路复用带来的收益。timeout设的是整个请求的超时包括建连、发送、等待响应。它和connectTimeout的关系是包含关系所以connectTimeout必须小于timeout否则那个更细粒度的设置根本不会生效。3.4 连接池与超时参数的算法推导参数不能拍脑袋定至少要有推导过程。以连接池为例核心公式是最大连接数 ≈ 峰值 QPS × 平均响应时间秒假设某个下游接口峰值 QPS 是 200平均响应时间 50ms那么瞬时并发约是 200 × 0.05 10 个连接。这是理论最小值。实际配置要考虑毛刺通常乘以 2 到 3配 20 到 30 比较合理。如果配成 5那超过 5 个并发请求就要排队排队时间会直接加到响应时间上形成正反馈——越慢越排队越排队越慢。超时的推导同理。读取超时应该基于 P99 响应时间加上合理余量。比如 P99 是 800ms那读超时设 2 秒是合理的设成 10 秒就意味着下游真出问题时你要等 10 秒才失败这段时间线程一直被占着。连接超时一般设得短1 到 2 秒因为同机房建连失败基本是秒级的等更久没有意义。下面这组配置是我在一个中等规模服务里用过的供参考参数取值推导依据最大连接数50峰值 600 QPS × 平均 60ms ≈ 36留 40% 余量每路由最大连接20避免单一慢下游占满整个池连接超时1000ms同机房建连通常在 10ms 内超 1 秒基本是故障读取超时2000msP99 约 700ms留约 2.8 倍余量获取连接超时500ms拿不到连接就快速失败避免线程堆积空闲连接存活60s小于网关的闲置断开时间防止拿到已被对端关闭的连接最后一行是重点。很多偶发连接重置的问题根源就在这里客户端认为连接还活着实际对端网关已经因为空闲超时把连接关了你复用了这条连接发请求就收到 RST。解决办法是客户端空闲回收时间设得比网关短或者开启连接的存活检测。提示判断是不是空闲连接被对端关闭导致的偶发失败看失败的时间分布。如果集中在低峰期或者请求间隔较长之后基本可以确定是这个原因。4. 服务端视角请求是怎么变成你的 Controller 方法的4.1 从 TCP 字节流到 HttpServletRequest服务端的处理链路从最底下说起。网卡收到数据内核协议栈组装成 TCP 流应用层的容器比如内嵌的 Tomcat在监听的端口上 accept 连接然后由连接器负责解析字节流按 HTTP 协议切分出请求行、头部、请求体封装成一个请求对象。这一步叫协议解析是容器的职责跟你的业务代码没关系。解析完成之后请求被交给 Servlet 容器经过一系列 Filter然后进入 Spring 的DispatcherServlet。它做三件事根据请求路径找到对应的处理器映射调用处理器适配器执行目标方法处理返回值。中间穿插参数解析、数据绑定、类型转换、校验、异常处理等等。你在方法签名上写的RequestParam、PathVariable、RequestBody背后都对应着一个具体的参数解析器。理解这条链路的价值在于当出现 400 的时候你能快速判断是哪一环出的问题。请求在容器解析阶段失败说明报文本身不合法比如头部格式错误、Content-Length与实际不符在参数绑定阶段失败说明报文合法但语义不符合方法签名比如该传 JSON 却传了表单在校验阶段失败说明数据格式没问题但业务规则不满足。这三种 400 的原因完全不同处理方式也完全不同。4.2 Content-Type 决定参数能不能绑上这是我在实际项目里见得最多的问题没有之一。服务端方法上写了RequestBody UserDTO user前端发过来的请求头是Content-Type: application/x-www-form-urlencoded结果就是 415 或者 400前端同学还坚称我传了参数啊。原因很简单RequestBody依赖HttpMessageConverter把请求体反序列化成对象而不同的Content-Type走不同的转换器。application/json走 Jacksonapplication/x-www-form-urlencoded走表单解析两者完全不通用。你发了表单格式JSON 转换器就找不到可用的消息转换器直接抛HttpMediaTypeNotSupportedException。反过来也一样。接口定义成表单接收方法参数直接写字段名不加RequestBody前端却发了 JSON那服务端拿到的就是一堆 null因为它去请求参数里找而参数在请求体里。服务端写法需要的 Content-Type传错时的表现RequestBody 对象application/json415 或 400直接写字段参数application/x-www-form-urlencoded 或 query字段全为 nullRequestParam MultipartFilemultipart/form-data415RequestHeader任意拿不到值返回 null 或 400这里有个实践建议内部接口统一用 JSON接口文档里把Content-Type明确写出来前端生成代码时就不会错。对外接口如果必须兼容表单就在 Controller 里写两个方法映射同一个路径用consumes区分别在一个方法里兼容所有格式那样代码会变得很难维护。4.3 幂等、重试与超时的服务端配合客户端设了重试服务端就必须考虑幂等。这个道理大家都懂但落地时经常漏。举个典型场景客户端读超时设 2 秒服务端某个操作要 3 秒客户端超时后重试服务端此时收到第二个请求第一个还在跑两个请求并发执行同一份业务数据就出问题了。服务端的配合手段有这么几个。第一对写操作引入幂等键客户端生成一个唯一 ID 放在头部服务端用它做去重重复请求直接返回第一次的结果。第二把耗时操作改成异步接口快速返回一个任务 ID客户端轮询查结果从根上避免长事务占用连接。第三合理设置服务端的处理超时让服务端先于客户端超时这样客户端超时的时候服务端已经放弃了不会出现客户端认为失败、服务端实际成功的尴尬局面。关于第三点经验值是服务端超时略小于客户端读超时比如客户端 2000ms服务端 1800ms。这样绝大多数情况下是服务端先超时并返回一个明确的错误客户端拿到的是可解释的响应而不是一个干巴巴的读超时异常。4.4 Keep-Alive 与网关层的隐性损耗HTTP/1.1 默认长连接一个 TCP 连接上可以串行发多个请求。这个机制在浏览器场景下很有效但在服务间调用场景下有几个坑。第一个坑是队头阻塞。同一个连接上请求是串行的前一个响应没回来后一个就得等。如果连接池配得太小或者某个慢请求占着连接不放后面的请求全被堵住。这也是为什么 HTTP/2 要搞多路复用——它把请求拆成帧在一条连接上并发传输从根本上解决了队头阻塞。第二个坑是中间设备的空闲超时。负载均衡、网关、防火墙对空闲连接都有回收策略常见的是 60 秒、300 秒。客户端如果不知道会复用到已经被关闭的连接收到 RST。处理办法前面提过让客户端的空闲回收时间短于链路中最小的那个超时值。第三个坑是连接的半关闭状态。某些情况下连接的一端关闭了写方向但还能读客户端如果没正确处理会一直等一个永远不会来的响应直到应用层超时。这类问题的特征是耗时刚好等于超时时间非常规律看到这种规律性的慢请求优先怀疑连接状态问题。5. 踩坑实录与排查手册5.1 状态码与异常的对应速查表排查问题最快的方式是对号入座。下面这张表是我整理的高频对应关系左边是你看到的现象右边是最可能的原因和第一步该查什么。现象最可能原因第一步排查动作400 且无详细信息报文格式错误、Content-Type 不匹配、必填头缺失抓包看原始请求报文401 反复出现Token 过期、头部未正确传递、时钟偏差检查 Token 有效期和服务端时间403权限不足、IP 白名单、签名校验失败核对账号权限和签名算法404路径错误、网关路由未配置、上下文路径差异对比注册中心的服务路径415Content-Type 与服务端不匹配对齐接口定义的媒体类型429触发限流查限流规则和当前 QPS500服务端未捕获异常查服务端日志的完整堆栈502上游服务返回非法响应或进程挂了查上游服务健康状态504上游超时对比客户端与服务端超时配置连接重置复用了已关闭的连接、对端主动关闭检查空闲连接回收配置偶发读超时连接池排队、下游慢、GC 停顿看连接池指标和下游响应时间分布这张表不是万能的但能覆盖八成以上的日常问题。用它的前提是你能拿到真实的状态码所以第一件事永远是确保日志里打印了状态码和关键响应头别只打印一个异常信息。5.2 连接泄漏与端口耗尽连接泄漏是线上事故的高发区特征是运行一段时间后突然大面积超时重启就恢复过一阵again。根因通常是响应体没读完、流没关闭、连接没归还。具体有几种典型写法。一种是拿到InputStream之后只读了几个字节就关闭连接无法复用被标记为不可用。一种是用了try-finally但 finally 里只关了输入流没关输出流。还有一种是异常路径下直接抛出跳过了关闭逻辑。这三种在代码 review 时很难看出来得靠工具。检测手段有两个方向。应用层看连接池的活跃连接数如果只增不减基本可以确定泄漏。系统层看netstat或者ss输出的连接状态分布如果大量连接停在CLOSE_WAIT说明应用层没主动关闭如果大量TIME_WAIT说明是主动关闭方属于正常现象但数量过多也要关注。# 按状态统计连接数快速定位异常状态堆积 ss -ant | awk NR1 {print $1} | sort | uniq -c | sort -rnCLOSE_WAIT数量持续增长是最危险的信号它意味着对端已经发了 FIN你的应用还没调用 close。这种连接会一直占着文件描述符直到进程重启或者达到上限。注意文件描述符耗尽的表现不一定是报错可能是 accept 失败、日志写不进去、甚至整个进程无响应。如果看到这类莫名其妙什么都干不了的现象先看下进程的 fd 数量。5.3 抓包、日志与复现三步法排查 HTTP 问题的顺序我的习惯是先看日志、再抓包、最后模拟复现这三步能解决绝大多数问题。第一步看日志重点是客户端和服务端两边的日志都要有且能通过一个请求 ID 关联起来。日志里至少要包含请求方法、路径、状态码、耗时、请求 ID。有条件的话把关键响应头也打出来比如Content-Type、Retry-After、X-Request-Id。很多时候只看两边耗时对不上就能判断问题出在中间链路上。第二步抓包用tcpdump在服务器上抓或者本地用抓包工具。抓包的价值在于看到未经任何加工的真实报文这是唯一能确定到底发出去了什么、收到的是什么的手段。# 抓取指定端口上的 HTTP 流量边抓边看 sudo tcpdump -i any -A -s 0 tcp port 8080 and (((ip[2:2] - ((ip[0]0xf)2)) - ((tcp[12]0xf0)2)) ! 0)这条命令里的过滤表达式是关键排除了没有负载的纯 ACK 包让输出干净很多。-A表示以 ASCII 显示内容看明文 HTTP 足够用了HTTPS 就得换思路。第三步模拟复现用curl精确构造请求把怀疑的变量逐个试。curl -v的输出包含了完整的请求和响应头是最高效的验证工具。curl -v -X POST http://example.internal/api/user/query \ -H Content-Type: application/json \ -H X-Request-Id: debug-0001 \ -d {id:1001} \ --max-time 3复现时有个技巧从能通的最小请求开始逐步加回怀疑的头部或参数直到失败出现。这样能精确定位到是哪个因素导致的。反过来从复杂请求删减也行看个人习惯。我一般用加的因为加的过程中每一步都是可验证的。5.4 几个容易被忽略的细节最后补几个零散但很实用的点。关于中文乱码。乱码的本质是编码和解码用了不同的字符集跟 HTTP 本身没关系但 HTTP 是传递环节所以经常背锅。定位方法是确认发送端的字节长度比如一个中文字符在 UTF-8 下是 3 个字节如果在Content-Length里看到的是 1说明客户端用的是单字节编码。确认方法很简单用curl发同样的内容加--data-binary对比服务端收到的字节。关于时间。Date头的格式是 RFC 1123GMT结尾很多服务端会拿它做签名校验或者缓存判断。如果客户端和服务端的系统时间偏差太大会出现请求已过期这类看起来莫名其妙的错误。容器环境里这个问题的发生率比物理机高因为容器迁移后时间同步可能滞后。关于响应体的读取。不管是 OkHttp 还是 HttpClient拿到响应之后都应该确保响应体被完整读取或显式关闭否则连接不会被归还到池里。这块在try-with-resources里比较容易保证但如果你在响应处理过程中抛了异常就得确保close一定会执行。关于网关的请求体大小限制。默认值通常不大上传文件或者提交大 JSON 时会直接返回 413。这个限制在应用层、网关层、负载均衡层可能各有各的配置改的时候要三处一起确认只改一处往往不生效。关于并发压测时的 keep-alive。压测工具默认可能会关闭长连接或者每个线程独占一条连接跟生产环境的实际行为差别很大。压测前一定要确认连接复用策略与实际一致否则压出来的数据和线上对不上。这些细节单拎出来都不复杂但它们组合起来就是为什么我的请求偶尔失败这类问题的全部来源。我个人的体会是HTTP 这门东西该懂的部分其实不多但每一条都要懂到能对应到具体现象的程度光知道定义没用。真正让你在凌晨三点还能淡定定位问题的是那些你亲手抓过的包、亲手复现过的 400、亲手调到过的连接池参数。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑