Socket 编程
问题
什么是 Socket?Java 中 BIO、NIO、AIO 有什么区别?
答案
Socket 通信模型
BIO vs NIO vs AIO
| 模型 | 全称 | 线程模型 | 适用场景 |
|---|---|---|---|
| BIO | Blocking I/O | 一个连接一个线程 | 连接少、固定 |
| NIO | Non-blocking I/O | 一个线程管多个连接 | 高并发、短请求 |
| AIO | Async I/O | 回调通知 | 连接多、操作重 |
BIO(同步阻塞)
BIO - 一个连接一个线程
ServerSocket server = new ServerSocket(8080);
while (true) {
Socket socket = server.accept(); // 阻塞等待连接
new Thread(() -> {
InputStream in = socket.getInputStream();
byte[] buf = new byte[1024];
int len = in.read(buf); // 阻塞等待数据
// 处理请求...
}).start();
}
BIO 的问题
每个连接需要一个线程,10000 个连接需要 10000 个线程。线程资源昂贵(默认 1MB 栈空间),大量线程切换的 CPU 开销也很大。
NIO(同步非阻塞 + 多路复用)
NIO - Selector 多路复用
Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select(); // 阻塞等待就绪事件
Set<SelectionKey> keys = selector.selectedKeys();
for (SelectionKey key : keys) {
if (key.isAcceptable()) {
// 新连接
SocketChannel client = serverChannel.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
// 有数据可读
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
client.read(buffer);
// 处理请求...
}
}
keys.clear();
}
NIO 三大核心:
- Channel:双向通道(替代 Stream)
- Buffer:缓冲区(数据读写的中转站)
- Selector:多路复用器(一个线程监听多个 Channel 的事件)
Netty
实际项目中很少直接用 NIO(API 复杂、有 Bug),通常用 Netty 框架。
Netty 服务端
EventLoopGroup bossGroup = new NioEventLoopGroup(1); // 接收连接
EventLoopGroup workerGroup = new NioEventLoopGroup(); // 处理 IO
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
ctx.writeAndFlush("收到: " + msg);
}
});
}
});
bootstrap.bind(8080).sync();
常见面试问题
Q1: select、poll、epoll 的区别?
答案:
| 维度 | select | poll | epoll |
|---|---|---|---|
| 最大连接数 | 1024 (FD_SETSIZE) | 无限制 | 无限制 |
| 数据结构 | 位图 | 链表 | 红黑树 + 就绪链表 |
| 遍历方式 | 每次全遍历 | 每次全遍历 | 回调通知就绪的 fd |
| 性能 | O(n) | O(n) | O(1) 事件通知 |
| 内核拷贝 | 每次 fd 复制到内核 | 每次 fd 复制到内核 | mmap 共享内存 |
Java NIO 的 Selector 在 Linux 上底层使用 epoll。
Q2: Netty 的线程模型?
答案:
Netty 使用 Reactor 主从多线程模型:
- Boss Group(1 个线程):负责接收新连接(accept)
- Worker Group(N 个线程):负责 IO 读写和业务处理
每个 Channel 绑定到固定的 EventLoop(线程),避免线程竞争。
Q3: 什么是零拷贝?
答案:
传统 IO 需要 4 次数据拷贝(磁盘→内核→用户→内核→网卡)。零拷贝通过减少拷贝次数提升性能:
- mmap:内核缓冲区和用户空间共享,减少一次拷贝
- sendfile:数据直接从内核缓冲区到网卡,绕过用户空间
Kafka 的高吞吐就依赖 sendfile 零拷贝。Netty 使用 FileChannel.transferTo() 实现零拷贝。
详见 IO 与 NIO。