Netty是为了解决网络编程的复杂性和提供易于使用、高性能和可扩展的框架而开发的。它通过提供一组可重用的组件来处理网络通信的低级细节,例如套接字管理、线程和缓冲,简化了开发网络应用程序的过程。这使开发人员可以专注于应用程序逻辑而不是网络编程的复杂性。此外,Netty支持各种协议和传输机制,使其成为构建各种网络应用程序的多功能选择。
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操作本身是同步的。
- 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
selectedKeys = selector.selectedKeys(); - Iterator
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(); // 关闭客户端连接
- }
- }
- 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
() { - @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
() { - @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 不看重 Windows 上的使用,在 Linux 系统上,AIO 的底层实现仍使用 EPOLL(后续会讲),没有很好实现 AIO,因此在性能上没有明显的优势,而且被 JDK 封装了一层不容易深度优化。
Java的I/O模型是在Java编程语言层面的抽象,而Linux的I/O模型是操作系统内核层面的实现。因此,虽然它们有一些相似之处,但并不是完全相同的概念。
同步和异步:同步和异步是针对应用程序和内核的交互而言的,同步指的是用户进程触发IO 操作并等待或者轮询的去查看IO 操作是否就绪,而异步是指用户进程触发IO 操作以后便开始做自己的事情,而当IO 操作已经完成的时候会得到IO 完成的通知。
阻塞和非阻塞:阻塞和非阻塞是针对于进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式,说白了是一种读取或者写入操作方法的实现方式,阻塞方式下读取或者写入函数将一直等待,而非阻塞方式下,读取或者写入方法会立即返回一个状态值。
阻塞式IO
非阻塞式IO
IO多路复用
信号驱动
异步IO
前面四种IO模型实际上都属于同步IO,只有最后一种是真正的异步IO,因为无论是多路复用IO还是信号驱动模型,IO操作的第2个阶段都会引起用户线程阻塞,也就是内核进行数据拷贝的过程都会让用户线程阻塞。
select
、poll
和epoll
都是多路复用的实现机制,它们用于管理多个I/O通道的并发事件。这些机制在操作系统中允许应用程序监视多个文件描述符,以确定哪些文件描述符已准备好进行I/O操作,从而避免了频繁的轮询。
select
它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。内核需要将消息传递到用户空间,都需要内核拷贝。
poll
poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的。
epoll基于操作系统支持的I/O通知机制
epoll通过内核和用户空间共享一块内存来实现的。
1.水平触发(LT):基于管道轮询的机制。管道中有100k的数据,只读了60k,中断了之后,还是能收到这个管道有数据的通知的。后续还是能轮询到这个管道有数据。
2.边缘触发(ET):基于管道状态改变的机制。只处理状改变的管道。管道中有100k的数据,只读了60k,中断了,是读不了剩下的40k。只有新数据进入这个管道时,这个管道状态再次变成“有数据”的状态,才会发送通知。
epoll 工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件描述符的阻塞读/写操作把处理多个文件描述符的任务饿死。
epoll_create 开辟空间获得文件描述符
epoll_ctl 添加socket文件描述符到空间内
epoll_wait 获取有事件的socket
I/O(Input/Output,输入/输出)和用户态与内核态之间存在密切的关系,特别是在操作系统中。用户态和内核态是操作系统中的两个不同特权级别,它们用于管理和保护计算机系统的资源。以下是关于I/O、用户态和内核态之间的关系的重要信息:
I/O操作涉及用户态和内核态:
用户态和内核态的切换:
内核态的I/O处理:
异步I/O和用户态I/O:
I/O操作涉及用户态和内核态之间的切换,因为操作系统内核必须管理和控制I/O设备。这个切换是操作系统的核心功能之一,用于确保计算机系统的稳定性、安全性和性能。不同的I/O模型可以影响用户态和内核态之间的切换方式和频率。说到内核态切换,下面不得不介绍的就是零拷贝。
Netty与零拷贝(Zero-Copy)之间有密切的关系,因为Netty是一个网络应用框架,专门设计用于高性能的网络通信,而零拷贝是一项技术,可以用于提高数据传输的效率,特别是在网络通信中。
以下是Netty与零拷贝的关系和如何在Netty中利用零拷贝技术的一些重要信息:
Netty的高性能特性:Netty被设计为高性能的网络应用框架,它旨在处理大量并发连接和高吞吐量的网络通信。为了实现这一目标,Netty采用了多种性能优化技术,其中之一就是零拷贝。
零拷贝是一种优化技术,旨在减少数据在内存之间的复制次数。传统的数据传输通常涉及将数据从一个缓冲区复制到另一个缓冲区,这会引入额外的CPU和内存开销。零拷贝技术通过操作系统或硬件支持,允许数据在不复制的情况下从一个地方传输到另一个地方,从而提高了数据传输的效率。
`ByteBuf`是Netty的自定义缓冲区类型,它支持零拷贝和引用计数等特性。在Netty中,`ByteBuf`可以在数据传输时直接暴露底层数据,而不需要进行数据复制,从而减少了CPU和内存开销。
read:将数据从磁盘通过DMA读取到内核缓存区中,在拷贝到用户缓冲区
write: 先将数据写入到socket缓冲区中,经过DMA写入网卡设备
4次切换,4次拷贝
1.虚拟内存空间可以远远大于物理内存空间
2.多个虚拟内存可以指向同一个物理地址
正是多个虚拟内存可以指向同一个物理地址,可以把内核空间和用户空间的虚拟地址映射到同一个物理地址,这样的话,就可以减少IO的数据拷贝次数。用户态可以直接访问内核态的数据。
4次切换,3次拷贝
sendfile表示在两个文件描述符之间传输数据,它是在操作系统内核中操作的,避免了数据从内核缓冲区和用户缓冲区之间的拷贝操作。
2次切换,3次拷贝
linux2.4版本后,对sendfile做了优化升级,引入SG-DMA技术,其实就是对DMA拷贝加入了scatter/gather操作,它可以直接从内核空间缓冲区中将数据读取到网卡,这样的话还可以省去CPU拷贝。
2次切换,2次拷贝,CPU全程不参与数据搬运
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
() { - @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();
- }
- }
- }
使用直接内存需要谨慎管理,以避免内存泄漏和其他问题。