SocketChannel网络编程
SocketChannel 和 ServerSocketChannel 是 NIO 中用于 TCP 网络通信的核心类。它们既可以阻塞使用,也可以配置为非阻塞并配合 Selector 使用。
# 1. ServerSocketChannel
服务端使用 ServerSocketChannel 监听端口。
ServerSocketChannel server = ServerSocketChannel.open();
server.bind(new InetSocketAddress(8080));
server.configureBlocking(false);
非阻塞模式下,accept() 如果没有新连接会立即返回 null。
SocketChannel client = server.accept();
if (client != null) {
client.configureBlocking(false);
}
# 2. SocketChannel
客户端使用 SocketChannel 连接服务端。
SocketChannel client = SocketChannel.open();
client.configureBlocking(false);
client.connect(new InetSocketAddress("localhost", 8080));
while (!client.finishConnect()) {
// wait or do something else
}
非阻塞连接可能不会立即完成,需要调用 finishConnect()。
# 3. 读取数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len = channel.read(buffer);
if (len == -1) {
channel.close();
} else if (len > 0) {
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
buffer.clear();
}
返回值含义:
> 0:读取到的字节数。0:非阻塞模式下当前没有数据。-1:对端关闭连接。
# 4. 写入数据
ByteBuffer buffer = StandardCharsets.UTF_8.encode("hello");
while (buffer.hasRemaining()) {
channel.write(buffer);
}
阻塞模式下,上面通常可以完成写入。非阻塞模式下,write 可能返回 0,也可能只写入部分数据。真实框架需要维护待写队列,不能简单自旋。
# 5. TCP 粘包和拆包
TCP 是字节流协议,不保留应用层消息边界。
一次 write 不等于对端一次 read。
可能出现:
- 粘包:多条消息被一次读到。
- 拆包:一条消息被分多次读到。
解决方式需要应用层协议设计:
- 固定长度消息。
- 分隔符协议,如行协议。
- 长度字段 + 消息体。
- TLV 格式。
例如长度字段协议:
4字节消息长度 + N字节消息体
# 6. 半包写出
非阻塞写入时,不能假设一次写完:
int written = channel.write(buffer);
if (buffer.hasRemaining()) {
// 保存 buffer,等待下次 OP_WRITE 继续写
}
这也是直接使用原生 NIO 编写网络框架复杂的原因之一。
# 7. 生产建议
- 业务系统不要轻易手写复杂 NIO 网络框架。
- 高性能 TCP 服务优先考虑 Netty。
- IO 线程只做轻量读写和编解码,耗时业务交给业务线程池。
- 协议必须明确消息边界。
- 对连接数、读写缓冲、空闲超时、异常关闭做完整治理。
# 专家实践清单
| 问题 | 处理方式 | 风险 |
|---|---|---|
| 连接建立 | 非阻塞 connect + finishConnect | 误判连接完成 |
| 半包粘包 | 协议定义长度或分隔符 | 消息边界混乱 |
| 写不完 | 维护待发送队列 | 数据丢失 |
| 连接空闲 | 心跳和超时关闭 | 死连接占资源 |
| 业务耗时 | 派发到业务线程池 | IO 线程被阻塞 |
SocketChannel 生命周期:
open
▼
configureBlocking(false)
▼
connect / accept
▼
register Selector
▼
read / write
▼
close
# Tips 快问快答
Q:SocketChannel 主要用于什么?
A:NIO TCP 客户端或已连接 Socket 的非阻塞读写。
Q:非阻塞 connect 后为什么要 finishConnect?
A:连接过程可能尚未完成,需要在可连接事件中确认。
Q:TCP 为什么会有粘包拆包? A:TCP 是字节流协议,不保留应用层消息边界。
Q:如何解决消息边界问题? A:常用固定长度、长度字段、分隔符或更完整的协议编码。
Q:write 返回 0 代表什么?
A:当前无法写出更多数据,需要稍后再写。
Q:为什么 IO 线程不能做慢业务? A:一个 IO 线程可能管理很多连接,阻塞会影响所有连接。
Q:生产中会直接手写 SocketChannel 吗? A:较少,复杂 TCP 服务通常使用 Netty 等成熟框架。
Q:SocketChannel 最常见的坑是什么? A:忽略半包、半写、异常关闭和连接状态管理。
# 8. 总结
SocketChannel 是理解非阻塞网络编程的核心类。它让一个线程处理多个连接成为可能,但也把粘包拆包、半包写出、状态管理等复杂性暴露给开发者。生产环境通常使用成熟框架封装这些细节。