读完这篇,你能做什么
能发送有超时与状态检查的 HTTP 请求,并按失败层次决定后续动作。
学习理由、准备要求与知识点
为什么学它
调用外部服务需要分清连接、响应与重试后果,不能只判断请求是否抛异常。
先读这些内容
知识点清单
- Socket 字节流
- 应用消息边界
- 连接与请求超时
HttpClient复用- HTTP 状态检查
- DNS
/TLS/网络异常 - 同步与异步发送
- 有副作用操作重试
运行前先确认Java 17;源码在本机 Microsoft OpenJDK 17.0.20.1 src.zip 核验,在线链接为上游 jdk-17.0.20-ga。深入模块可在掌握基础后回读。
01先弄懂它是什么
用 Socket 读网络数据时,拿到的是连续字节;ServerSocket 负责监听并接收连接。发送方写了两次,接收方不一定正好读两次,所以协议要规定长度、分隔符或其他消息边界。即使已经连上,也不保证下次读取就有完整消息;连接提前关闭时可能只收到一半,程序需要发现并报告。
直接调用 HTTP 接口时,Java17 的 HttpClient 提供同步、异步请求和连接复用。HttpRequest 写明地址、方法、请求头和正文,HttpResponse 返回状态码、头和内容。收到 400 或 500 等状态码,不一定会抛 Java 异常;请求发出以后仍要检查状态和正文,再决定业务上算成功还是失败。
构造简单公开请求时可使用 HttpRequest.newBuilder(URI.create("https://example.com/")).timeout(Duration.ofSeconds(5)).GET().build(),再由 HttpClient 发送。
这里是构造片段,需要导入对应类型,不包含凭证。Java17 HttpClient 不是 AutoCloseable,通常复用客户端;若正文采用输入流处理器,则应按响应体的资源契约及时关闭。
文中这几个词,先认一认
- 消息分帧
- 规定连续字节中一条消息从哪里开始、到哪里结束,例如先写长度再写正文。
- 连接超时
- 尝试建立网络连接最多允许等多久,超出后按失败处理。
- 请求超时
- HTTP 请求等待响应等阶段的时间限制;正文若以流读取,还要单独核对处理器的行为。
- 响应体处理器
- 决定响应正文按字符串、字节、文件或输入流等方式交给调用者的处理规则。
02看看它怎么工作
连不上服务器,与连上后一直等不到数据,是两种等待。Socket.connect 的超时限制前一种,setSoTimeout 限制读取时阻塞多久;HTTP 客户端的连接超时,也不能包办整次请求和正文处理。
改用异步 API,只是让调用方先拿到一个“结果以后完成”的对象,并没有免去线程、连接等资源的占用,取消操作也有成本。仍应给整个调用设定期限,并分别观察查地址、连接、TLS、等待响应和读正文用了多久。
收到一串字节,怎样知道一条消息到哪里结束?下面的教学协议先放四个字节说明正文长度,再放正文;长度按“大端”顺序存放,也就是较高位的字节在前,并按无符号数解释,不把最高位当作负号。这个小模型帮助你分清只收到半条消息、连续收到多条消息和长度越界。HTTP 已有自己的消息含义和各版本格式,应交给成熟客户端处理,不能把这个自定格式当作 HTTP。
对不可信长度先验证再分配,避免把四字节字段直接转成负数组长度或巨额内存申请。帧长度零可以合法表示空消息,但仍消耗头部字节,因此解析继续前进。图把头部完整性、长度上限和正文完整性分开,任何一项不满足都应返回明确失败。
正在加载流程图…
查看流程图源码(Mermaid)
flowchart TD
A["接收连续字节"] --> B{"是否至少有四字节头部"}
B -->|"否且非空"| C["截断错误"]
B -->|"是"| D["按大端读取无符号长度"]
D --> E{"长度在协议上限内"}
E -->|"否"| F["拒绝长度"]
E -->|"是"| G{"剩余正文是否完整"}
G -->|"否"| C
G -->|"是"| H["提取一帧并继续解析后续字节"]按顺序看,每一步做了什么
- 选择 Socket 协议还是成熟 HTTP 客户端。
- 明确消息边界和最大长度。
- 分别设置连接、请求与正文处理预算。
- 处理状态码、异常和提前结束。
- 复用适当客户端并关闭正文资源。
03对照实现代码
Microsoft OpenJDK 17.0.20.1;java.net.http/jdk/internal/net/http/HttpRequestBuilderImpl.java · jdk.internal.net.http.HttpRequestBuilderImpl#checkURI(URI)
以下为原创 Java 17 教学实现,不是原样 JDK 源码;只保留文中明确讨论的机制,省略优化、兼容及内部接口。
import java.net.URI;
public final class SourceModel {
public static URI checked(URI uri) {
if (uri == null) throw new NullPointerException("uri");
String scheme = uri.getScheme();
if (!"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme))
throw new IllegalArgumentException("scheme");
if (uri.getHost() == null) throw new IllegalArgumentException("host");
// 额外教学策略:不接受在 URI 用户信息部分嵌入身份数据。
if (uri.getRawUserInfo() != null) throw new IllegalArgumentException("userinfo");
return uri;
}
}真实 HttpRequestBuilderImpl.checkURI 检查 URI 是否有合适主机,以及方案是否为 http 或 https。模型使用公开 URI 方法表达这些主要检查,并额外拒绝 userinfo;这一额外规则是教学输入约束,不声称真实 JDK 方法完全同样处理。它不执行 DNS 或连接测试。
URI 语法正确不代表目标可达,主机字段存在也不表示证书有效或请求已授权。构建阶段主要拒绝不适用输入,后续网络与应用验证仍要分别进行。把输入校验成功当作网络调用成功,会混淆多个独立失败阶段。
请求对象将方法、头和正文描述交给客户端执行,构建器不是通用浏览器导航器。重定向、代理和认证等行为需要按客户端配置和协议核对,不能让任意外部 URL 自动继承应用凭证。示例只构造公开无凭证目标,重点是明确 URI 与请求生命周期。
该源码位于 JDK 内部 HTTP 实现包,只用于阅读定位。应用应使用公开 HttpRequest 与 HttpClient API,不导入内部构建器或绕过模块导出限制。实现类名和内部路径可能随版本变化,版本化引用帮助学习机制,不应成为业务编译依赖。
代码先调用谁,再调用谁
HttpRequest.newBuilder接收 URI- 内部构建器调用
checkURI - 检查方案与主机等结构
- 构建不可变请求描述
HttpClient后续负责发送并交付响应
04动手写一个例子
解析四字节长度前缀的完整字节快照
输入一段已收集字节,解析零个或多个教学帧:四字节大端无符号长度加正文。长度不得超过 maxFrame,头部或正文不完整则抛 IOException,允许零长度正文,不修改输入。
- 输入 1
长度1+A,长度2+BC- 输出 1
两帧A与BC
- 输入 2
空字节数组- 输出 2
零帧
- 输入 3
长度3但只有两字节正文- 输出 3
截断错误
用 ByteBuffer 显式设置大端顺序,读取头部 int 后转换为 unsigned long,避免高位为一的长度变成负数。先与 maxFrame 比较,确认可接受后再转成数组长度。读取正文前还要比较剩余字节数,防止把截断输入当成完整数据。
循环持续到没有剩余字节。若还有一到三字节,说明存在不完整头部;若恰好一帧结束则正常完成。每轮至少消费四字节头部,即使正文长度为零也会推进位置,因此不会出现空帧导致的无限循环。
本题输入是完整快照,截断立即失败。真实网络逐块到达时,未完整未必等于永久错误,需要保留部分头部和正文状态,等更多数据或确认连接结束后再判断。把快照解析与增量解析分开,能够让每个算法的输入前提清楚可测。
为什么这样写是对的每轮开始时,缓冲位置之前的字节已被完整且唯一地分成合法帧,当前位置是下一帧头部或输入末尾。
每个头部读取一次,每个正文复制一次;长度检查发生在分配前。整个快照已经在输入内存中,因此这不是固定内存的实时网络解码器。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
展开收起代码 · Main.java · 完整可运行程序
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
public class Main {
static List<byte[]> decode(byte[] wire, int maxFrame) throws IOException {
if (wire == null || maxFrame < 0) throw new IllegalArgumentException("input");
ByteBuffer input = ByteBuffer.wrap(wire).order(ByteOrder.BIG_ENDIAN);
List<byte[]> frames = new ArrayList<>();
while (input.hasRemaining()) {
if (input.remaining() < 4) throw new EOFException("header");
long length = Integer.toUnsignedLong(input.getInt());
// 无符号长度先验上限,不能直接作为有符号数组长度。
if (length > maxFrame) throw new IOException("frame limit");
if (length > input.remaining()) throw new EOFException("body");
byte[] body = new byte[(int)length];
input.get(body);
frames.add(body); // 每个正文独立复制;字节数组本身仍可修改。
}
return List.copyOf(frames);
}
static byte[] frame(String text) {
byte[] body = text.getBytes(StandardCharsets.UTF_8);
return ByteBuffer.allocate(4+body.length).putInt(body.length).put(body).array();
}
static void reject(byte[] wire, int max) {
try { decode(wire,max); } catch (IOException e) { return; }
throw new AssertionError("must reject");
}
public static void main(String[] args) throws Exception {
ByteArrayOutputStream joined = new ByteArrayOutputStream();
joined.writeBytes(frame("A")); joined.writeBytes(frame("BC"));
List<String> texts = decode(joined.toByteArray(),10).stream()
.map(b -> new String(b,StandardCharsets.UTF_8)).toList();
if (!texts.equals(List.of("A","BC"))) throw new AssertionError(texts);
// 零帧与一条空帧是不同结果,分别验证。
if (!decode(new byte[0],10).isEmpty()) throw new AssertionError();
if (decode(frame(""),0).size() != 1) throw new AssertionError();
reject(new byte[]{0,0},10);
reject(new byte[]{0,0,0,3,1,2},10);
reject(new byte[]{-1,-1,-1,-1},100);
reject(frame("ABC"),2);
System.out.println("PASS 7");
}
}这些特殊情况也要试一试
- 空输入代表零帧。
- 零长度正文仍是一条帧。
- 高位长度按无符号值处理并受上限约束。
- 残缺头部和正文分别报告截断。
进阶选读 · 可选深入:增量解码与背压
场景连接持续到达碎片字节,不能先保存完整流再解析。
做法维护读头部与读正文两个状态,跨读取保留位置,限制待处理总字节和帧队列容量;连接结束时再判断残缺状态。
复杂度累计处理 O(n),内存由最大帧与队列上限决定。
好处与代价状态更多且需要取消、超时和背压策略,不能只把快照解析循环套在每次 read 外面。
05项目里怎么用
连续调用同一批接口时,通常复用 HTTP 客户端,让它管理和复用连接,不必每次重新创建。接收正文也要选合适方式:小文本可用 ofString,它会把正文收进内存再组成字符串;大内容可考虑边收边处理,但仍要限制大小,并按所用接口的约定及时消费和关闭响应流。收到文本不等于调用成功,还要分别检查 HTTP 状态、Java 异常和正文中的业务错误。
重试前先判断请求可能做到哪一步。连不上,和已经发送却等不到响应,含义不同;后者可能已在服务端完成修改。写操作应有固定操作编号、避免重复效果的处理或结果查询,并限制整组尝试的总时间,不能每次重试都重新获得完整等待额度。
TLS 验证与目标地址管理应保持客户端公开安全默认能力。遇到证书或主机名错误应核对目标、信任配置和证书状态,而不是直接关闭校验。日志记录可包含阶段、状态和请求标识,避免输出认证头、完整敏感正文或嵌入 URI 的身份数据。
容易踩的坑
- 把一次 read 当成一条完整消息。
- 未校验长度就按网络输入分配数组。
- 连接超时被误当作整个请求和正文超时。
- HTTP错误状态码未检查,直接当业务成功。
几种做法,怎么选
表格有四列,窄屏可左右滑动查看代价和适用条件。
| 方案 | 优势 | 代价与限制 | 适用条件 |
|---|---|---|---|
| Socket 自定义协议 | 完全控制消息格式与交互 | 需自己管理分帧、超时与兼容 | 确有专用传输协议需求 |
同步 HttpClient | 调用链清楚且易于处理结果 | 等待期间占用调用线程 | 普通服务调用与可控并发 |
异步 HttpClient | 可组合并发请求完成阶段 | 取消、异常和资源管理更复杂 | 有明确并发与延迟需求的调用 |
06检验是否读懂了
先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。
先用自己的话解释
01TCP 为什么会出现半包或多条连读?
展开参考答案与推导
它提供连续字节流,读取批次由缓冲、网络与调度决定,不保留应用每次写入的边界。应用需要长度、分隔符或成熟协议来重建消息,而不是根据 read 次数判断。
02HTTP四百状态是否一定抛 Java异常?
展开参考答案与推导
通常仍是一个有效 HTTP 响应,客户端交付状态和正文供应用处理。网络或协议失败与应用返回错误状态是不同情况,调用者必须检查状态及业务约定。
03为什么先把长度转换成无符号 long?
展开参考答案与推导
四字节协议字段可能把最高位作为数值位,直接 int 会得到负数。转换后再做上限检查,才能明确拒绝过大长度而不是触发意外负数组长度或错误分支。
换个情况,还会处理吗
01客户端超时后服务端却创建了订单,为什么?
展开参考答案与推导
超时表示客户端在预算内没有获得所需结果,不证明服务端未执行。重试需要幂等标识或查询处理结果,不能把传输失败简单解释为业务完全没发生。
02小响应正常,大响应内存明显增加,先看哪里?
展开参考答案与推导
检查正文处理器是否一次性缓冲为字符串或字节数组,以及是否有内容上限。需要时改成受控流式消费并关闭资源,不能仅调大堆内存而忽略不受限输入。
03自定义解析器每块都报截断,问题可能是什么?
展开参考答案与推导
把每次网络读取块当成完整快照处理了。增量接收应保留未完成头部和正文,等待后续字节;只有确认输入结束或超过期限后,残缺状态才成为最终失败。
写代码试一试
01验证多个空帧不会卡住
- 输入 / 条件
连续两个零长度头部- 输出 / 预期
两条空帧
提示 每轮至少消耗四字节。
展开参考答案与推导
拼接两个 frame("") 并断言结果大小为二,每个正文长度为零。它检验循环进展由头部消费保证,而不是错误依赖正文长度必须为正。
02为公开 GET 构造请求
- 输入 / 条件
https://example.com/- 输出 / 预期
GET请求描述
提示 构造不等于发送。
展开参考答案与推导
使用 HttpRequest.newBuilder(URI.create(...)).timeout(Duration.ofSeconds(5)).GET().build(),导入 URI、Duration 与 HttpRequest。这个片段只创建描述,发送还需客户端及响应处理,不应把构造成功称为联网成功。
03增加输入隔离测试
- 输入 / 条件
解析一帧A后修改原wire正文- 输出 / 预期
结果仍为A
提示 正文由新数组保存。
展开参考答案与推导
解析后把原字节数组对应正文改成其它字节,再断言返回正文未变。结果列表不可修改不等于里面 byte[] 不可修改,测试与文档都要分别说明这两个层次。
读完之后,独立完成
课后作业:自己做一遍
本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
能发送有超时与状态检查的 HTTP 请求,并按失败层次决定后续动作。
本篇重点加练
重点练消息完整性与超时范围,避免接收一段字节就提前认定一次调用完成。
- 应用消息边界
- 连接与请求超时
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01原理问答
发送端依次写入 [A,B] 与 [C,D,E] 两组字节。为接收端设计三种可能的读取分块,说明消息协议还需要提供哪些信息才能判定完整请求。
- 提交内容
- 分块图与协议需求清单。
- 怎样算完成
- 应用消息与读取调用分开
- 包含未完整到达的情况
练到的知识点Socket 字节流 · 应用消息边界
- 作业 02设计评审
个人学习助手每次请求都创建
HttpClient,连接预算 1 秒、整体调用预算 3 秒。设计复用方式和超时观测点,列出执行过程中需要额外限定的资源。- 提交内容
- 客户端生命周期图与超时合同。
- 怎样算完成
- 连接阶段与请求阶段分开
- 复用边界与配置相符
练到的知识点连接与请求超时 · HttpClient 复用
综合应用
- 作业 03排错实验
用本地可控 HTTP 服务返回 200、404、500,并模拟延迟响应;记录
HttpClient的状态结果与异常路径。DNS/TLS故障仅设计测试方案,不必访问外部网站。- 提交内容
- 本地实验、自写状态与超时测试、实际记录。
- 怎样算完成
- HTTP 失败和传输失败分别分类
- 实验有退出与清理机制
练到的知识点HTTP 状态检查 · DNS/TLS/网络异常
- 作业 04面试场景
异步提交订单后调用方取消等待,随后再次提交。根据“未收到结果”这一事实列出服务器可能处于的状态,规定客户端随后可以怎样查询结果。
- 提交内容
- 状态时间线和重试/查询政策。
- 怎样算完成
- 取消等待与撤销副作用分开
- 不同失败阶段有不同证据需求
练到的知识点同步与异步发送 · 有副作用操作重试
- 作业 05编程实践
实现增量长度前缀帧解析器,输入以任意小块提供:头部可被拆成四次,两个帧可同时到达,载荷长度最多 1024;明确结束时残缺帧的处理。
- 提交内容
- Java 实现、自写所有切分位置及边界测试、运行证据。
- 怎样算完成
- 解析不依赖输入分块
- 长度与缓冲资源有界
- 未完成和非法输入区分
练到的知识点Socket 字节流 · 应用消息边界
- 作业 06编程实践
实现内存分块接收模型:消息以换行结束,单条最长 64 字节,测试输入可中途停顿;注入虚拟时间并定义整体接收预算,不建立公网连接。
- 提交内容
- Java 模型、自写分块/超长/超时/多消息测试与运行证据。
- 怎样算完成
- 消息完成条件可核验
- 超时起算与重置规则明确
练到的知识点应用消息边界 · 连接与请求超时
- 作业 07设计评审
协议允许消息正文含换行,同时还要支持空正文。比较可选消息边界格式,给出双方误解同一字节序列的歧义检查。
- 提交内容
- 协议候选和代表性字节样本。
- 怎样算完成
- 所有合法正文都能表示
- 长度限制与非法输入合同明确
练到的知识点应用消息边界
- 作业 08面试场景
连接很快建立,服务却每隔 800ms 返回一个字节。产品要求调用最多等待 3 秒,列出需验证的超时类型和客户端测量点。
- 提交内容
- 阶段时间线与预算验收。
- 怎样算完成
- 局部活动与整体完成分开
- 测试有确定终止条件
练到的知识点连接与请求超时
进阶挑战 · 掌握前置后选做
- 作业 09设计评审
学完并发队列后:批量抓取一百个本地模拟端点,最多四个同时执行。设计超时、取消、部分失败和结果顺序合同,并说明如何检验实际并发上限。
- 提交内容
- 调度设计、资源预算与验收矩阵。
- 怎样算完成
- 并发数量有测量证据
- 失败不造成无界任务或资源泄露
练到的知识点连接与请求超时 · HttpClient 复用 · 同步与异步发送 · 有副作用操作重试
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 10思维拓展 · 条件变化
短请求协议变成长时间流式生成,用户希望随时取消但保留已收到的完整片段。重新设计完成、超时、取消和片段边界之间的关系。
- 提交内容
- 流式状态图与事件序列测试规格。
- 怎样算完成
- 部分结果合同明确
- 超时政策对应新的业务时长
练到的知识点应用消息边界 · 连接与请求超时
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读参考文档英文
Java 17 API:HttpClient (新标签页)↗
查阅连接超时、同步/异步发送与客户端复用,区分网络异常和 HTTP 响应。
- 实践参考文档英文
Java 17 API:Socket (新标签页)↗
阅读连接与读取超时、流关闭和套接字生命周期。
- 深入规范英文
IETF RFC 9110:HTTP 语义 (新标签页)↗
阅读方法、状态码、表示元数据和幂等性;结合真实请求记录理解。
- 深入参考文档英文
OpenJDK 17u jdk.internal.net.http.HttpRequestBuilderImpl (新标签页)↗
定位内部
HttpRequestBuilderImpl.checkURI的方案与主机检查,应用仅依赖公开 HTTP API。 - 深入参考文档英文
Java17 HttpRequest API (新标签页)↗
查阅请求构建、方法、正文和超时契约。