为什么要有Netty
Netty是为了解决网络编程的复杂性和提供易于使用、高性能和可扩展的框架而开发的。它通过提供一组可重用的组件来处理网络通信的低级细节,例如套接字管理、线程和缓冲,简化了开发网络应用程序的过程。这使开发人员可以专注于应用程序逻辑而不是网络编程的复杂性。此外,Netty支持各种协议和传输机制,使其成为构建各种网络应用程序的多功能选择。
Java中的IO模型
Netty是一个Java编写的网络IO库,Netty在其底层仍然使用Java I/O库,如java.nio
包。它使用了Java NIO(New I/O)的一些特性,例如非阻塞通道(Channel)、选择器(Selector)等,以实现高性能的网络通信。
三种通信模型
-
BIO (Blocking I/O): 同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。
-
NIO (New I/O): NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架。NIO提供了与传统BIO模型中的
Socket
和ServerSocket
相对应的SocketChannel
和ServerSocketChannel
两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。 -
AIO (Asynchronous I/O): AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。
NIO
示例代码
- 服务端
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class NIOServer {
public static void main(String[] args) throws IOException {
// 创建ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
serverSocketChannel.configureBlocking(false); // 设置为非阻塞模式
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Server is listening on port 8080...");
while (true) {
int readyChannels = selector.select();
if (readyChannels == 0) {
continue;
}
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
SocketChannel clientChannel = serverSocketChannel.accept();
clientChannel.configureBlocking(false);
clientChannel.register(selector, SelectionKey.OP_READ);
System.out.println("Accepted connection from " + clientChannel.getRemoteAddress());
} else if (key.isReadable()) {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = clientChannel.read(buffer);
if (bytesRead == -1) {
clientChannel.close();
System.out.println("Client disconnected.");
} else if (bytesRead > 0) {
buffer.flip();
while (buffer.hasRemaining()) {
clientChannel.write(buffer); // 回显客户端发送的数据
}
buffer.clear();
}
}
keyIterator.remove();
}
}
}
}
- 客户端
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class NIOClient {
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost", 8080));
String message = "Hello, NIO Server!";
ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());
socketChannel.write(buffer); // 发送消息给服务器
ByteBuffer responseBuffer = ByteBuffer.allocate(1024);
int bytesRead = socketChannel.read(responseBuffer); // 读取服务器的响应
if (bytesRead != -1) {
responseBuffer.flip();
byte[] bytes = new byte[responseBuffer.remaining()];
responseBuffer.get(bytes);
String response = new String(bytes);
System.out.println("Received from server: " + response);
}
socketChannel.close(); // 关闭客户端连接
}
}
AIO
示例代码
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousChannelGroup;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.Executors;
public class AIOTimeServer {
public static void main(String[] args) throws IOException {
int port = 8080;
AsynchronousChannelGroup group = AsynchronousChannelGroup.withThreadPool(Executors.newFixedThreadPool(10));
final AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open(group);
serverChannel.bind(new InetSocketAddress(port));
System.out.println("Server is listening on port " + port);
serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
@Override
public void completed(AsynchronousSocketChannel clientChannel, Void attachment) {
serverChannel.accept(null, this); // 接受下一个连接
String response = "Current time: " + System.currentTimeMillis();
ByteBuffer buffer = ByteBuffer.wrap(response.getBytes());
clientChannel.write(buffer, null, new CompletionHandler<Integer, Void>() {
@Override
public void completed(Integer result, Void attachment) {
try {
clientChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, Void attachment) {
exc.printStackTrace();
try {
clientChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
@Override
public void failed(Throwable exc, Void attachment) {
exc.printStackTrace();
}
});
try {
group.awaitTermination(Long.MAX_VALUE, java.util.concurrent.TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
为什么Netty依旧使用NIO的API?
Netty 不看重 Windows 上的使用,在 Linux 系统上,AIO 的底层实现仍使用 EPOLL(后续会讲),没有很好实现 AIO,因此在性能上没有明显的优势,而且被 JDK 封装了一层不容易深度优化。
Linux的IO模型
Java的I/O模型是在Java编程语言层面的抽象,而Linux的I/O模型是操作系统内核层面的实现。因此,虽然它们有一些相似之处,但并不是完全相同的概念。
LINUX五种IO模型
同步和异步:同步和异步是针对应用程序和内核的交互而言的,同步指的是用户进程触发IO 操作并等待或者轮询的去查看IO 操作是否就绪,而异步是指用户进程触发IO 操作以后便开始做自己的事情,而当IO 操作已经完成的时候会得到IO 完成的通知。
阻塞和非阻塞:阻塞和非阻塞是针对于进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式,说白了是一种读取或者写入操作方法的实现方式,阻塞方式下读取或者写入函数将一直等待,而非阻塞方式下,读取或者写入方法会立即返回一个状态值。
-
阻塞式IO
-
非阻塞式IO
-
IO多路复用
-
信号驱动
-
异步IO
前面四种IO模型实际上都属于同步IO,只有最后一种是真正的异步IO,因为无论是多路复用IO还是信号驱动模型,IO操作的第2个阶段都会引起用户线程阻塞,也就是内核进行数据拷贝的过程都会让用户线程阻塞。
从图中可以看出用户态与内核态的切换,那么什么是用户/内核态呢?这也是下面的内容。
用户态与内核态
I/O(Input/Output,输入/输出)和用户态与内核态之间存在密切的关系,特别是在操作系统中。用户态和内核态是操作系统中的两个不同特权级别,它们用于管理和保护计算机系统的资源。以下是关于I/O、用户态和内核态之间的关系的重要信息:
-
I/O操作涉及用户态和内核态:
- I/O操作包括从应用程序到外部设备(如磁盘、网络、键盘、显示器等)的数据传输。这些操作通常涉及到用户态和内核态之间的切换。
- 当应用程序需要执行I/O操作时,它会调用操作系统提供的I/O系统调用(例如,读取文件或发送数据包)。这些系统调用是在用户态执行的。
- 操作系统内核负责管理系统资源和I/O设备,因此在执行I/O操作时,控制必须从用户态切换到内核态,以便内核可以直接访问硬件资源。
-
用户态和内核态的切换:
- 用户态和内核态是不同的CPU执行模式。在用户态下,应用程序只能访问有限的资源,而在内核态下,操作系统内核可以访问系统的全部资源。
- 当应用程序需要执行需要特权访问的操作,例如执行系统调用或访问设备驱动程序,它必须通过软中断或异常将控制权切换到内核态。这个切换是由操作系统的内核来处理的。
- 用户态到内核态的切换会涉及一些开销,因为需要保存和恢复CPU寄存器、切换堆栈等操作。因此,频繁的切换会影响性能。
-
内核态的I/O处理:
- 一旦控制切换到内核态,操作系统内核就可以执行I/O操作。它会管理设备驱动程序、缓冲区、中断处理等细节,以确保数据的正确传输。
- 内核还会维护I/O请求队列,以有效地处理多个I/O请求。
-
异步I/O和用户态I/O:
- 异步I/O是一种I/O模型,允许应用程序继续执行其他任务,而不必等待I/O操作完成。在这种情况下,通常使用异步I/O库来管理I/O操作,而不需要频繁地切换用户态和内核态。
- 异步I/O通常通过回调函数或事件通知机制来处理I/O完成事件。
I/O操作涉及用户态和内核态之间的切换,因为操作系统内核必须管理和控制I/O设备。这个切换是操作系统的核心功能之一,用于确保计算机系统的稳定性、安全性和性能。不同的I/O模型可以影响用户态和内核态之间的切换方式和频率。说到内核态切换,下面不得不介绍的就是零拷贝。
Zero Copy(零拷贝)
Netty与零拷贝(Zero-Copy)之间有密切的关系,因为Netty是一个网络应用框架,专门设计用于高性能的网络通信,而零拷贝是一项技术,可以用于提高数据传输的效率,特别是在网络通信中。
以下是Netty与零拷贝的关系和如何在Netty中利用零拷贝技术的一些重要信息:
Netty的高性能特性:Netty被设计为高性能的网络应用框架,它旨在处理大量并发连接和高吞吐量的网络通信。为了实现这一目标,Netty采用了多种性能优化技术,其中之一就是零拷贝。
零拷贝是一种优化技术,旨在减少数据在内存之间的复制次数。传统的数据传输通常涉及将数据从一个缓冲区复制到另一个缓冲区,这会引入额外的CPU和内存开销。零拷贝技术通过操作系统或硬件支持,允许数据在不复制的情况下从一个地方传输到另一个地方,从而提高了数据传输的效率。
`ByteBuf`是Netty的自定义缓冲区类型,它支持零拷贝和引用计数等特性。在Netty中,`ByteBuf`可以在数据传输时直接暴露底层数据,而不需要进行数据复制,从而减少了CPU和内存开销。
零拷贝的实现
传统IO
read:将数据从磁盘通过DMA读取到内核缓存区中,在拷贝到用户缓冲区
write: 先将数据写入到socket缓冲区中,经过DMA写入网卡设备
4次切换,4次拷贝
虚拟内存 mmap
1.虚拟内存空间可以远远大于物理内存空间
2.多个虚拟内存可以指向同一个物理地址
正是多个虚拟内存可以指向同一个物理地址,可以把内核空间和用户空间的虚拟地址映射到同一个物理地址,这样的话,就可以减少IO的数据拷贝次数。用户态可以直接访问内核态的数据。
4次切换,3次拷贝
sendFile
sendfile表示在两个文件描述符之间传输数据,它是在操作系统内核中操作的,避免了数据从内核缓冲区和用户缓冲区之间的拷贝操作。
2次切换,3次拷贝
sendfile + DMA scatter/gather实现的零拷贝
linux2.4版本后,对sendfile做了优化升级,引入SG-DMA技术,其实就是对DMA拷贝加入了scatter/gather操作,它可以直接从内核空间缓冲区中将数据读取到网卡,这样的话还可以省去CPU拷贝。
2次切换,2次拷贝,CPU全程不参与数据搬运
直接内存 Direct ByteBuf
Netty通常使用直接内存(Direct Memory)来提高性能。直接内存是一种特殊的内存分配方式,不同于Java堆内存。
-
ByteBuf与直接内存:Netty中的
ByteBuf
是一个用于处理字节数据的缓冲区抽象。ByteBuf
可以使用直接内存分配,这称为"Direct ByteBuf"。直接内存分配意味着ByteBuf
中的数据存储在堆外内存,而不是在Java堆中。 -
减少内存复制:在进行网络数据传输时,数据通常需要从应用程序的缓冲区复制到操作系统内核缓冲区,然后再从内核缓冲区复制到网络适配器。使用直接内存,可以在这些步骤中减少或消除数据复制,提高了性能。
-
零拷贝:直接内存可以与零拷贝相结合,使数据可以在应用程序和操作系统之间进行高效的传输。
-
缓冲区池:Netty通常使用池化的
ByteBuf
来管理直接内存的分配和释放。这种方式可以避免频繁地分配和释放直接内存,提高了内存管理的效率。 -
内存管理控制:Netty提供了一些工具和机制,帮助开发者有效地管理直接内存,包括手动释放、自动回收等。
示例代码
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
public class NettyDirectMemoryExample {
public static void main(String[] args) throws InterruptedException {
// 创建两个EventLoopGroup,一个用于接受客户端连接,一个用于处理客户端请求
NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// 创建ServerBootstrap
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new EchoServerHandler());
}
});
// 绑定端口并启动服务器
serverBootstrap.bind(8080).sync().channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
// 自定义ChannelHandler处理客户端消息
static class EchoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf in = (ByteBuf) msg;
ByteBuf out = Unpooled.directBuffer(); // 创建Direct ByteBuf
try {
out.writeBytes(in); // 将接收到的数据写入Direct ByteBuf
ctx.write(out); // 写入回应数据到客户端
ctx.flush();
} finally {
in.release(); // 释放接收缓冲区
out.release(); // 释放Direct ByteBuf
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
}
使用直接内存需要谨慎管理,以避免内存泄漏和其他问题。