Channel都是使用Buffer作为数据的载体
- 对于网络编程来说,一个Socket连接就对应一个Channel,信息要从Channel中拿出放入Buffer,才能操作数据。
if (key.isReadable()) {//是否有数据可读 client = (SocketChannel) key.channel(); ByteBuffer readBuffer = ByteBuffer.allocate(1024); int count = client.read(readBuffer);//数据读入Buffer }- Buffer的大小要提前指定,不然和stream有啥区别。
- Buffer则提供了比Stream更高效和可预测的IO,Stream提供了一个能够容纳任意长度数据的假象,但会增加系统开销和频繁的上下文切换。那么Buffer就是有限容量的。
- Buffer将系统开销暴露给看程序员。(直接设置大小)。
- 那么对Netty来说,使用的时候显然没有指定Buffer的大小,那它底层必是下了一番功夫。
AdaptiveRecvByteBufAllocator
-
Netty 的 AdaptiveRecvByteBufAllocator 类是一个动态调整接收缓冲区大小的分配器。它根据之前读取的数据量来调整缓冲区大小,以便更有效地处理不同大小的数据包。以下是 AdaptiveRecvByteBufAllocator 类的源码(基于 Netty 4.1.68.Final 版本)和相关注释:
public class AdaptiveRecvByteBufAllocator extends DefaultMaxMessagesRecvByteBufAllocator { //预期缓冲区大小从1024开始,不会低于64 ,也不会高于65536 。 private final int minimum; private final int initial; private final int maximum; public AdaptiveRecvByteBufAllocator(int minimum, int initial, int maximum) { if (minimum <= 0) { throw new IllegalArgumentException("minimum: " + minimum); } if (initial < minimum) { throw new IllegalArgumentException("initial: " + initial); } if (maximum < initial) { throw new IllegalArgumentException("maximum: " + maximum); } this.minimum = minimum; this.initial = initial; this.maximum = maximum; } @Override protected Handle newHandle() { return new HandleImpl(minimum, initial, maximum); } private static final class HandleImpl extends MaxMessageHandle { private final int minimum; // 最小值 private final int initial;// 初始时 private final int maximum; private int index; private int nextReceiveBufferSize; private boolean decreaseNow; HandleImpl(int minimum, int initial, int maximum) { this.minimum = minimum; this.initial = initial; this.maximum = maximum; nextReceiveBufferSize = initial; } @Override public ByteBuf allocate(ByteBufAllocator alloc) { return alloc.ioBuffer(guess()); } @Override public int guess() { return nextReceiveBufferSize; } @Override protected void observe(int bytesRead) { // 根据读取的字节数调整缓冲区大小 if (bytesRead <= SIZE_TABLE[max(0, index - INDEX_DECREMENT)]) { if (decreaseNow) { index = max(index - INDEX_DECREMENT, 0); nextReceiveBufferSize = SIZE_TABLE[index]; decreaseNow = false; } else { decreaseNow = true; } } else if (bytesRead >= nextReceiveBufferSize) { index = min(index + INDEX_INCREMENT, SIZE_TABLE.length - 1); nextReceiveBufferSize = SIZE_TABLE[index]; decreaseNow = false; } } } } -
AdaptiveRecvByteBufAllocator 类的工作原理如下:
-
构造函数接收三个参数:minimum(最小缓冲区大小)、initial(初始缓冲区大小)和 maximum(最大缓冲区大小)。这些参数用于限制动态调整的范围。
-
newHandle() 方法创建一个新的 HandleImpl 实例,该实例包含了调整缓冲区大小的逻辑。
-
HandleImpl 类中的 observe() 方法根据读取的字节数(bytesRead)来调整缓冲区大小。如果读取的字节数小于当前索引减去 INDEX_DECREMENT 对应的 SIZE_TABLE 值,那么缓冲区大小将减小。如果读取的字节数大于或等于当前缓冲区大小,那么缓冲区大小将增加。
-
缓冲区大小的调整是通过修改 index 变量来实现的。index 变量表示 SIZE_TABLE 数组的索引,SIZE_TABLE 数组包含了预定义的缓冲区大小值。通过增加或减小 index,可以在 SIZE_TABLE 中选择不同的缓冲区大小。
- 这种动态调整缓冲区大小的方法可以帮助 Netty 更有效地处理不同大小的数据包,从而提高性能。
-
-
该类还会决定使用DirectBuffer还是HeapBuffer:
Netty中的Channel
描述一下Netty中的Channel概念
-
Channel是Netty中的一个核心组件,它代表一个网络套接字或能够执行I/O操作(如读、写、连接和绑定)的组件。
-
Channel为用户提供以下信息和功能:
- Channel的当前状态(如:是否打开?是否已连接?)
- Channel的配置参数(如:接收缓冲区大小)
// 获取一个Channel实例 Channel channel = ... // 获取Channel的配置参数 ChannelConfig config = channel.config(); // 设置接收缓冲区大小为1024 config.setRecvByteBufAllocator(new FixedRecvByteBufAllocator(1024));- Channel支持的I/O操作(如:读、写、连接和(bind)绑定)
- ChannelPipeline 处理与Channel关联的所有 IO 事件和请求。
public void initChannel(SocketChannel ch) throws Exception { ChannelPipeline p = ch.pipeline(); p.addLast(new MyServerHandler()); // 添加一个自定义的处理器到 ChannelPipeline 中, // 处理IO 事件和请求 } -
Netty中的所有I/O操作都是异步的,这意味着任何I/O调用都会立即返回,而不保证在调用结束时请求的I/O操作已经完成。相反,将会获得一个ChannelFuture实例,当请求的I/O操作成功、失败或取消时,它会通知您。
Channel channel = ...; // 获取一个Channel实例 // 创建一个写入数据的ByteBuf ByteBuf data = Unpooled.copiedBuffer("Hello, world!", Charset.defaultCharset()); // 异步写入数据到Channel中,返回一个ChannelFuture实例 ChannelFuture future = channel.writeAndFlush(data);//立即返回 // 添加一个监听器来处理异步操作结果 future.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) {//完成后得到通知,回调该方法 if (future.isSuccess()) { // 异步操作成功,处理逻辑... System.out.println("写入数据成功"); } else { // 异步操作失败,处理逻辑... System.out.println("写入数据失败:" + future.cause().getMessage()); } } }); // 继续执行其他逻辑,不会被阻塞 System.out.println("执行其他操作"); -
Channel具有层次结构,一个Channel可以根据创建方式具有父级。例如,由ServerSocketChannel接受的SocketChannel将返回ServerSocketChannel作为其父级。
// 创建一个ServerSocketChannel实例 ServerSocketChannel serverChannel = ServerSocketChannel.open(); // 绑定端口并开始监听连接 serverChannel.socket().bind(new InetSocketAddress(8080)); serverChannel.configureBlocking(false); while (true) { // 接受连接并获取SocketChannel实例 SocketChannel socketChannel = serverChannel.accept();//ServerChannel会生出SocketChannel if (socketChannel != null) { // 获取SocketChannel的父级ServerSocketChannel实例 ServerSocketChannel parentChannel = (ServerSocketChannel) socketChannel .provider() .openServerSocketChannel() .provider() .retrieveSocketChannel(serverChannel.socket()); System.out.println("SocketChannel's parent channel: " + parentChannel); // ...其他操作 } // ...其他操作 } -
某些传输可能会暴露特定于传输的附加操作。可以将Channel向下转型为子类型以调用此类操作。例如,在旧的I/O数据报传输中,多播加入/离开操作由DatagramChannel提供。
// 假设已经获取了一个Channel实例 Channel channel = ...; // 将Channel向下转型为(UDP)DatagramChannel,以便进行多播加入操作 if (channel instanceof DatagramChannel) { DatagramChannel datagramChannel = (DatagramChannel) channel; InetAddress groupAddress = InetAddress.getByName("224.0.0.1"); // 多播组地址 NetworkInterface ni = NetworkInterface.getByName("eth0"); // 网络接口名 datagramChannel.joinGroup(groupAddress, ni).sync(); // 加入多播组,并等待操作完成 } else { // 如果不是DatagramChannel,则忽略多播加入操作 System.out.println("该Channel不支持多播加入操作"); }- TCP传输可能不支持多播操作,但UDP传输可能支持。因此,如果您要使用特定于传输的操作,您需要将Channel向下转换为其实际类型(如DatagramChannel)以调用这些操作。(不是直接将TCP转成了UDP)
-
在完成Channel操作后,务必调用close()或close(ChannelPromise)以释放所有资源。这确保以适当的方式释放所有资源,例如文件句柄。