《Java 网络编程基础》附录 A Socket 与 NIO API 速查

《Java 网络编程基础》附录 A:把 InetAddress、Socket、ServerSocket、SocketChannel、ServerSocketChannel、Selector、ByteBuffer、FileChannel 八组 API 压成速查表,逐条给签名、作用、易错点与最小示例,按 JDK 25 核对,并附 linkd 类映射与异常对照表。

这份附录不解释机制,只做索引:把全卷 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)分离构造与绑定,可设 backlogbacklog 是提示值,内核会截断;不是「最大并发连接数」
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 / InetSocketAddressLinkdMain、配置解析绑定地址、端口校验
Socket / ServerSocket第 3 章阻塞 echo 版先跑通语义,再换 NIO
SocketChannelConnection.channel()非阻塞读写与半关闭
ServerSocketChannelEventLoop.register 的监听端接受连接并注册事件
Selector / SelectionKeyEventLoop.run()就绪分发、interestOps 调整
ByteBufferBufferPool.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 outSO_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 节对照模型,不展开)。需要这些的,回正文对应小节,别指望附录给全。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 《Netty 高性能网络编程》15.3 linkd-v2 验收与复盘
  2. 《Netty 高性能网络编程》15.2 限流、超时与降级
  3. 《Netty 高性能网络编程》15.1 网关路由与转发