这份附录不解释机制,只做索引:把全卷 45 节里反复出现的八组类,按「签名、作用、易错点、最小示例」压成表。机制在正文对应章节,这里只保证你查到一个方法时,知道它归哪一组、参数是什么、最容易在哪翻车。
签名不是凭记忆写的:下面的方法列表来自 javap 输出的 java.net.Socket、java.nio.channels.SocketChannel 等类,示例代码在 javac --release 25 下编译通过。凡是 JDK 25 里有、但本卷没用的方法(比如 Socket.setPerformancePreferences),这里不列。
怎么用这份速查
三条约定:
- 只列本卷真正用到的成员。看到某个方法不在表里,先回正文确认这一卷是否讲过它,别在附录里找不存在的承诺。
- 「易错点」列写的是会静默出错的地方:不抛异常、但结果不对。会直接抛异常的情况集中在最后一节的异常对照表。
- 示例是片段,不是完整程序;变量名统一用
socket、server、channel、buffer,和linkd代码保持一致。
分组顺序按数据流走:地址 → 阻塞连接 → 阻塞监听 → 非阻塞连接 → 非阻塞监听 → 事件 → 缓冲区 → 文件。
InetAddress:解析、回环与绑定地址
| 成员 | 作用 | 易错点 |
|---|---|---|
static InetAddress getByName(String host) | 解析主机名或 IP 字面量 | 传入主机名时会走 DNS,可能阻塞数秒;只想要字面量时用 ofLiteral |
static InetAddress[] getAllByName(String host) | 返回全部解析结果 | 只取 [0] 在双栈机器上常拿到 IPv6,对 IPv4-only 服务直接连不上 |
static InetAddress getByAddress(String host, byte[] addr) | 用 4 或 16 字节构造,不做解析 | addr 长度不是 4/16 抛 UnknownHostException,错误信息只说「address length」 |
static InetAddress ofLiteral(String ip) | 只接受字面量(JDK 22+) | 传主机名抛 IllegalArgumentException,这是它的设计目的 |
boolean isLoopbackAddress() | 判断 127.0.0.0/8 或 ::1 | 判断「是不是本机」不能只用它,还要看绑定地址是否 0.0.0.0 |
boolean isAnyLocalAddress() | 判断通配地址 0.0.0.0 / :: | 绑定到通配地址后,getLocalAddress() 返回的就是它,日志里看不到真实网卡地址 |
String getHostAddress() | 返回 IP 文本 | 不做反向解析,打印日志优先用它 |
String getHostName() | 返回主机名,可能触发反向解析 | 在事件循环里调用它会阻塞,日志里别用 |
最小示例,确认绑定地址与回环判定:
InetAddress loopback = InetAddress.getByName("127.0.0.1");
System.out.println(loopback.getHostAddress()); // 127.0.0.1
System.out.println(loopback.isLoopbackAddress()); // true
InetAddress bound = new InetSocketAddress(9090).getAddress();
System.out.println(bound.isAnyLocalAddress()); // true,端口构造器用的是通配地址
第二段是 linkd 里最常见的地址错误来源:new InetSocketAddress(9090) 等价于绑定 0.0.0.0:9090,对本机之外的网卡也开放。要做只监听回环的实验环境,必须显式写 new InetSocketAddress("127.0.0.1", 9090)。
NetworkInterface.getNetworkInterfaces() 返回的是 Enumeration,在没有可用接口的容器里可能是 null 而不是空枚举,遍历前要判空。
Socket:客户端连接与读写
| 成员 | 作用 | 易错点 |
|---|---|---|
Socket() + connect(SocketAddress, int timeout) | 分离构造与连接,可设连接超时 | 超时只覆盖建连;连上之后的读不受它约束 |
setSoTimeout(int ms) | 设置 read 的阻塞上限 | 只影响读,不影响写;写超时在 JDK API 里不存在 |
setTcpNoDelay(boolean) | 关闭 Nagle 合并 | 打开后小包立即发,包数变多;吞吐型场景不一定更快 |
setKeepAlive(boolean) | 打开 TCP keepalive 探测 | 探测间隔由内核决定(macOS 默认以小时计),不能当应用层心跳 |
setSoLinger(boolean, int) | 控制 close 行为 | (true, 0) 让 close 直接发 RST,未发完的数据被丢弃 |
shutdownOutput() | 发 FIN,只关写方向 | 关闭后仍可 read;很多「半关闭」实现漏掉了这一点 |
InputStream read(byte[]) | 读字节 | 返回 -1 才是对端关闭;返回 0 只在长度为 0 的数组上出现 |
getChannel() | 取关联的 SocketChannel | 由 new Socket() 创建的 socket 返回 null,只有 SocketChannel.socket() 才有值 |
最小示例,一次请求加一次读:
try (Socket socket = new Socket()) {
socket.connect(new InetSocketAddress("127.0.0.1", 9090), 1000);
socket.setSoTimeout(2000);
socket.setTcpNoDelay(true);
OutputStream out = socket.getOutputStream();
out.write("GET /healthz HTTP/1.0\r\n\r\n".getBytes(StandardCharsets.US_ASCII));
out.flush();
InputStream in = socket.getInputStream();
byte[] buf = new byte[1024];
int n = in.read(buf); // 超时抛 SocketTimeoutException
System.out.println("read=" + n);
}
setSoTimeout 必须在 read 之前调用,且对同一个 socket 上的每次 read 都生效;如果业务需要「整体请求 3 秒」,单靠它做不到,得自己算剩余时间,正文 3.2 节给了写法。
getInputStream() 返回的是 SocketInputStream,它的 read 在 available()==0 时会阻塞,不要用 available() 判断「有没有完整消息」——它只说明内核缓冲里现在有多少字节。
ServerSocket:绑定、backlog 与 accept
| 成员 | 作用 | 易错点 |
|---|---|---|
ServerSocket() + bind(SocketAddress, int backlog) | 分离构造与绑定,可设 backlog | backlog 是提示值,内核会截断;不是「最大并发连接数」 |
setReuseAddress(boolean) | 允许重用 TIME_WAIT 端口 | 必须在 bind 之前调用,bind 之后设置无效 |
setSoTimeout(int ms) | 设置 accept 的阻塞上限 | 超时抛 SocketTimeoutException,服务器通常要捕获后继续循环 |
accept() | 取出一个已完成握手的连接 | close() 会让阻塞中的 accept 抛 SocketException: Socket closed |
getLocalPort() | 取实际监听端口 | 构造时传 0 才有意义,这是拿随机端口的唯一办法 |
setReceiveBufferSize(int) | 设置接受的 socket 的接收缓冲 | 要在 bind 前设置才对 accept 出来的连接生效 |
最小示例,带超时的 accept 循环:
try (ServerSocket server = new ServerSocket()) {
server.setReuseAddress(true);
server.bind(new InetSocketAddress("127.0.0.1", 9090), 128);
server.setSoTimeout(5000);
while (true) {
try (Socket c = server.accept()) {
c.getInputStream().read();
} catch (SocketTimeoutException e) {
// 五秒没有新连接,回到循环继续等
}
}
}
accept 返回的 Socket 继承监听 socket 的部分选项,但不继承 SO_TIMEOUT;服务端要限制单连接读时间,得在 accept 之后单独 setSoTimeout。正文 3.1 节用 nc 验证过 backlog 的行为:连接排满后,新连接在内核里等待,而不是立刻被拒绝。
SocketChannel:非阻塞连接与部分读写
| 成员 | 作用 | 易错点 |
|---|---|---|
static SocketChannel open() | 打开一个未连接的 channel | 默认是阻塞模式,非阻塞必须显式 configureBlocking(false) |
boolean connect(SocketAddress) | 发起连接 | 非阻塞下返回 false 表示「进行中」,不是失败 |
boolean finishConnect() | 完成进行中的连接 | 未就绪时返回 false;连接失败抛 IOException,不是返回 false |
int read(ByteBuffer) | 读入缓冲区 | 非阻塞返回 0 表示「这次没有数据」,-1 才是 EOF |
int write(ByteBuffer) | 写出缓冲区 | 可能只写一部分,必须用 while (buf.hasRemaining()) |
shutdownOutput() | 半关闭写方向 | 之后 read 仍可用,write 抛 ClosedChannelException |
configureBlocking(false) | 切换非阻塞 | 已注册到 Selector 后再调用抛 IllegalBlockingModeException |
最小示例,非阻塞连接加循环写:
try (SocketChannel ch = SocketChannel.open()) {
ch.configureBlocking(false);
boolean done = ch.connect(new InetSocketAddress("127.0.0.1", 9090));
while (!done) {
done = ch.finishConnect(); // 真实事件循环里等 OP_CONNECT,这里只是示意
}
ch.setOption(StandardSocketOptions.TCP_NODELAY, true);
ByteBuffer out = ByteBuffer.wrap("ping".getBytes(StandardCharsets.UTF_8));
while (out.hasRemaining()) {
ch.write(out);
}
}
connect 返回 true 时说明本机直接连上了(回环常见),此时不需要 finishConnect;返回 false 才要等就绪。把这两种情况混在一起写成「无条件 finishConnect」在回环上能跑,换到真实网络就会抛 NoConnectionPendingException。
非阻塞 channel 上不要依赖 SO_TIMEOUT:它属于阻塞语义,非阻塞读根本不会等。超时要在选择层做,正文 14.2 节有完整写法。
ServerSocketChannel:非阻塞监听
| 成员 | 作用 | 易错点 |
|---|---|---|
static ServerSocketChannel open() | 打开监听 channel | 同样是阻塞模式起步 |
bind(SocketAddress, int) | 绑定地址与 backlog | 返回 this,可以链式调用 |
accept() | 取出连接 | 非阻塞且无连接时返回 null,不是抛异常 |
configureBlocking(false) | 切非阻塞 | 注册 OP_ACCEPT 前必须切,否则 register 抛异常 |
socket() | 取底层 ServerSocket | 用来设置 SO_REUSEADDR 等选项,但不要在非阻塞 channel 上调 accept |
最小示例,注册监听:
try (ServerSocketChannel ssc = ServerSocketChannel.open()) {
ssc.configureBlocking(false);
ssc.bind(new InetSocketAddress("127.0.0.1", 9090), 128);
try (Selector selector = Selector.open()) {
ssc.register(selector, SelectionKey.OP_ACCEPT);
// 之后在 select 循环里 accept
}
}
null 和异常的区别是这一组最容易记反的地方:非阻塞 accept 没连接返回 null,channel 已关闭才抛 ClosedChannelException。把 null 当异常处理,会在空闲时刷错误日志;把异常当 null 处理,会在关闭时死循环。
Selector 与 SelectionKey:就绪事件
| 成员 | 作用 | 易错点 |
|---|---|---|
static Selector open() | 创建选择器 | macOS 上实现是 sun.nio.ch.KQueueSelectorProvider,不是 epoll |
int select() | 阻塞到至少一个键就绪 | 没有任何注册时可能永久阻塞 |
int select(long timeout) | 带超时的等待 | 返回 0 表示超时,不是出错 |
int selectNow() | 非阻塞轮询 | 空转时 CPU 占用高,只适合收尾 |
Set<SelectionKey> selectedKeys() | 就绪键集合 | 处理完必须 remove(),否则下一轮重复处理 |
Selector wakeup() | 唤醒阻塞中的 select | 用于跨线程投递任务,调用后 select 立即返回 |
SelectionKey interestOps(int) | 修改关注的事件 | 事件就绪后不会自动清零,OP_WRITE 几乎总是就绪,写完要手动取消 |
void cancel() | 取消注册 | 真正注销发生在下一次 select 之后 |
最小示例,事件分发骨架:
try (Selector selector = Selector.open()) {
ssc.register(selector, SelectionKey.OP_ACCEPT); // ssc 来自上一节的监听 channel
while (true) {
selector.select(1000);
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
it.remove(); // 先移除,再处理
if (!key.isValid()) {
continue;
}
if (key.isAcceptable()) {
SocketChannel ch = ((ServerSocketChannel) key.channel()).accept();
ch.configureBlocking(false);
ch.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
} else if (key.isReadable()) {
// 读处理,见上一节
}
}
}
}
Selector 从 JDK 11 起还有 select(Consumer<SelectionKey>, long) 重载,把「取键 + 处理」合成一次调用,省掉手动 remove。本卷统一用显式迭代,因为要展示 remove 这个必须记住的动作。
本机确认选择器实现:
java -cp . Prov
sun.nio.ch.KQueueSelectorProvider
这是本机输出。它说明 macOS 上 Selector 走 kqueue;写「Selector 就是 epoll」在 macOS 上既不准确,也会误导后面的平台差异判断。
ByteBuffer:三值与读写切换
| 成员 | 作用 | 易错点 |
|---|---|---|
static ByteBuffer allocate(int) | 堆内缓冲区 | 每次分配都清零,热路径上应考虑池化 |
static ByteBuffer allocateDirect(int) | 堆外缓冲区 | 分配比堆内贵一个量级,回收靠 Cleaner,不受 -Xmx 限制 |
Buffer flip() | 写模式转读模式:limit=position; position=0 | 不是「反转内容」,内容一个字节都没动 |
Buffer clear() | 重置为写模式:position=0; limit=capacity | 不清数据,只是把 limit 放开 |
ByteBuffer compact() | 丢掉已读部分,剩余字节前移 | 只在读模式用;用完仍是写模式,不需要再 flip |
Buffer rewind() | position=0,limit 不变 | 重读同一段,不改变可读长度 |
ByteBuffer order(ByteOrder) | 设置字节序 | 默认大端;getInt 前设置,设置后对已有数据同样生效 |
ByteBuffer duplicate() | 复制缓冲区状态,共享内容 | 两个 buffer 改的是同一块内存,调试时容易以为互不影响 |
boolean hasArray() | 是否有可访问的数组 | direct buffer 返回 false,调 array() 抛 UnsupportedOperationException |
本机跑一段状态观察:
static String state(ByteBuffer b) {
return "pos=" + b.position() + " lim=" + b.limit()
+ " cap=" + b.capacity() + " rem=" + b.remaining();
}
ByteBuffer b = ByteBuffer.allocate(8);
System.out.println(state(b)); // allocate
b.putInt(0x01020304);
System.out.println(state(b)); // putInt
b.flip();
System.out.println(state(b)); // flip
System.out.println("0x" + Integer.toHexString(b.getInt()));
b.clear();
System.out.println(state(b)); // clear
allocate pos=0 lim=8 cap=8 rem=8
putInt pos=4 lim=8 cap=8 rem=4
flip pos=0 lim=4 cap=8 rem=4
0x1020304
clear pos=0 lim=8 cap=8 rem=8
这是本机输出。注意 getInt 打印成 0x1020304,前导零被 toHexString 省掉,值仍是 0x01020304;把这类输出贴进文档时补零,否则读者会以为字节序错了。
compact 的语义单独记一句:读了一半想接着写,用 compact;读完了想从头写,用 clear。用错不会报错,只会让下一批数据写进错误位置。
FileChannel:读写、定位与传输
| 成员 | 作用 | 易错点 |
|---|---|---|
static FileChannel open(Path, OpenOption...) | 打开文件通道 | 写模式要显式给 WRITE/CREATE,默认只读 |
int read(ByteBuffer) | 顺序读 | 读到末尾返回 -1,非阻塞文件通道不存在返回 0 的情况 |
int write(ByteBuffer) | 顺序写 | 不保证一次写完,要检查 hasRemaining() |
long position() / position(long) | 读写位置 | 位置由 channel 自己维护,和 buffer 的 position 是两回事 |
long transferTo(long, long, WritableByteChannel) | 把文件区间写给目标通道 | 返回值可能小于请求长度,必须循环推进 |
MappedByteBuffer map(MapMode, long, long) | 映射文件区间 | 单次映射大小受 Integer.MAX_VALUE 限制 |
void force(boolean) | 刷盘 | 只保证到设备,不保证到远端副本 |
FileLock lock() | 加文件锁 | 同进程内重复加锁抛 OverlappingFileLockException |
最小示例,循环 transferTo:
long pos = 0;
long size = in.size();
while (pos < size) {
long moved = in.transferTo(pos, size - pos, out);
if (moved == 0) {
break; // 目标通道暂时写不动,交给上层重试
}
pos += moved;
}
transferTo 不改变源 channel 的 position,区间完全由参数决定;这一点和 read 不同,混用「先 position(0) 再 transferTo」的写法会让人误以为位置生效了。返回值小于请求长度不是错误,正文 5.3 节给了平台差异和测量方法。
这些 API 落在 linkd 的哪些类上
| API 组 | linkd 里的位置 | 用途 |
|---|---|---|
InetAddress / InetSocketAddress | LinkdMain、配置解析 | 绑定地址、端口校验 |
Socket / ServerSocket | 第 3 章阻塞 echo 版 | 先跑通语义,再换 NIO |
SocketChannel | Connection.channel() | 非阻塞读写与半关闭 |
ServerSocketChannel | EventLoop.register 的监听端 | 接受连接并注册事件 |
Selector / SelectionKey | EventLoop.run() | 就绪分发、interestOps 调整 |
ByteBuffer | BufferPool.acquire/release、FrameDecoder.decode | 缓冲复用与成帧 |
FileChannel | 静态文件发送路径 | transferTo 与 map |
这张表也是排查「这个类到底该用什么 API」的入口:症状在连接层,先看第 3、7 章;症状在缓冲层,先看第 5、7 章。
异常与返回值对照表
| 现象 | 常见原因 | 下一步 |
|---|---|---|
ConnectException: Connection refused | 端口没人监听,或监听在别的地址 | lsof -nP -iTCP:端口 确认 |
SocketTimeoutException: Connect timed out | 建连超时,路由或防火墙丢包 | 抓包看 SYN 是否有回包 |
SocketTimeoutException: Read timed out | SO_TIMEOUT 到期,对端没发够数据 | 区分「慢」与「半包」 |
SocketException: Socket closed | 另一线程关了 socket,阻塞调用被打断 | 关闭与读要串行化 |
SocketException: Connection reset | 对端发了 RST | 检查对端是否 setSoLinger(true, 0) |
read 返回 0(非阻塞) | 这次没有数据 | 回到 select,不是 EOF |
read 返回 -1 | 对端 shutdownOutput 或关闭 | 关闭本端,清理注册 |
IllegalBlockingModeException | 已注册后再切阻塞模式 | 先 configureBlocking(false) 再 register |
NoConnectionPendingException | 没发起连接就调 finishConnect | 只在 connect 返回 false 后调用 |
UnsupportedOperationException(array()) | 对 direct buffer 取数组 | 改用 get(byte[]) |
版本与边界
本附录对应 Temurin 25.0.5+8。InetAddress.ofLiteral 是 JDK 22 才加入的成员,在 JDK 17 上编译会失败;Selector.select(Consumer, long) 自 JDK 11 起就有。本卷其他 API 在 JDK 17 之后基本稳定。
没有列进来的东西:DatagramSocket/DatagramChannel(正文 2.2 节按场景讲,不常查)、SSLEngine(10.2 节,状态机比方法签名重要)、AsynchronousSocketChannel(本卷只在 1.2 节对照模型,不展开)。需要这些的,回正文对应小节,别指望附录给全。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。