netty

878 阅读7分钟

netty

感觉黑马的教程对新手比较好 在线观看www.bilibili.com/video/BV1py… 视频下载yun.itheima.com/course/855.… 视频+资料链接:pan.baidu.com/s/1bC9sPvNd… 提取码:1234

极客的netty 笔记

netty可以做客户端,也可以做服务端的。
image.png

有些bug的产生。解决的思路是,搞不定,就不支持。

netty也屏蔽了很多java nio的细节。

阻塞 非阻塞

他们的区别在于数据的读取、准备的时候,需不需要等。

同步异步
他们的区别是数据是自己过来,还是需要自己去读。
自己去读就是同步,数据读好回调送过来就是异步

image.png

切换模式

更换他的NioEventLoopGroup,改成OioEventLoopGroup。
NioServerSocketChannel 改成 OioServerSocketChannel。
这样就可以了。
他是通过 范型+反射+工厂模式实现的。

Reactor、单线程多线程模式

Reactor模式
image.png

Thread-Per-Connection 模式
image.png

多线程主从模式:
搞几个线程来处理连接。
image.png

image.png

我们将serverSocketChannel绑定到boosGroup。然后到了某个地方就绑定到 childGroup。
就是两种SocketChannel 绑定到两个group。就完成了主从模式

写代码取余选择的时候,可以考虑判断一下,是不是2的幂次方。用 &的计算方式性能更好。

沾包、半包

沾包:发送方每次写入数据 < 套接字缓冲区大小 。接收方读取套接字缓冲区数据不够及时。
半包:发送方写入数据 > 套接字缓冲区大小 ,发送的数据大于协议的 MTU(Maximum Transmission Unit,最大传输单元),必须拆包。

产生原因
收发
一个发送可能被多次接收,多个发送可能被一次接收
传输
一个发送可能占用多个传输包,多个发送可能公用一个传输包

根本原因是:TCP 是流式协议,消息无边界。
但是udp就没有问题,因为UDP 像邮寄的包裹,虽然一次运输多个,但每个包裹都有“界限”,一个一个签收, 所以无粘包、半包问题。

解决方案
image.png

Netty 对三种常用封帧方式的支持
image.png

二次解码:
在项目中,除了可选的的压缩解压缩之外,还需要一层解码,因为一次解码的结果是字节,需要和项目中所使用的对象做转化,方便使用,这层解码器可以称为“二次解 码器”,相应的,对应的编码器是为了将 Java 对象转化成字节流方便存储或传输。
• 一次解码器:ByteToMessageDecoder
• io.netty.buffer.ByteBuf (原始数据流)-> io.netty.buffer.ByteBuf (用户数据)
• 二次解码器:MessageToMessageDecoder_
• io.netty.buffer.ByteBuf (用户数据)-> Java Object_

Protobuf的优势
Protobuf 是一个灵活的、高效的用于序列化数据的协议。
• 相比较 XML 和 JSON 格式,Protobuf 更小、更快、更便捷。
• Protobuf 是跨语言的,并且自带了一个编译器(protoc),只需要用它进行编译,可 以自动生成 Java、python、C++ 等代码,不需要再写其他代码

缺点就是不易阅读。

keepalive 与 Idle 监测

image.png

Idle 监测就是判断对面还存活不。
image.png

在Netty 中开启 TCP keepalive 和 Idle 检测
ch.pipeline().addLast(“idleCheckHandler", new IdleStateHandler(0, 20, 0, TimeUnit.SECONDS));

image.png

内存

内存使用:
• Netty 内存使用技巧 - 减少对像本身大小
用基本类型就不要用包装类
应该定义成类变量的不要定义为实例变量:
一个类 -> 一个类变量
一个实例 -> 一个实例变量
一个类 -> 多个实例
实例越多,浪费越多
• Netty 内存使用技巧 - 对分配内存进行预估
对于已经可以预知固定 size 的 HashMap避免扩容
Netty 根据接受到的数据动态调整(guess)下个要分配的 Buffer 的大小。可参考
io.netty.channel.AdaptiveRecvByteBufAllocator
• Netty 内存使用技巧 - Zero-Copy
使用逻辑组合,代替实际复制:CompositeByteBuf:
io.netty.handler.codec.ByteToMessageDecoder#COMPOSITE_CUMULATOR
使用包装,代替实际复制。
byte[] bytes = data.getBytes();
ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes);
调用 JDK 的 Zero-Copy 接口。
Netty 中也通过在 DefaultFileRegion 中包装了 NIO 的 FileChannel.transferTo() 方法实
现了零拷贝:io.netty.channel.DefaultFileRegion#transferTo
• Netty 内存使用技巧 - 堆外内存
• Netty 内存使用技巧 - 内存池

源码解读Netty 内存使用
怎么从堆外内存切换堆内使用?
• 方法 1:参数设置
io.netty.noPreferDirect = true;
• 方法 2:传入构造参数false
ServerBootstrap serverBootStrap = new ServerBootstrap();
UnpooledByteBufAllocator unpooledByteBufAllocator = new UnpooledByteBufAllocator(false);
serverBootStrap.childOption(ChannelOption.ALLOCATOR, unpooledByteBufAllocator)
• 堆外内存的分配?
ByteBuffer.allocateDirect(initialCapacity)

内存池/非内存池的默认选择及切换方式?
默认选择:安卓平台 -> 非 pooled 实现,其他 -> pooled 实现。
• 参数设置:io.netty.allocator.type = unpooled;
• 显示指定:serverBootStrap.childOption(ChannelOption.ALLOCATOR, UnpooledByteBufAllocator.DEFAULT)
• 内存池实现?
核心要点:有借有还,避免遗忘。

源码流程:

our thread

• 创建 selector
• 创建 server socket channel
• 初始化 server socket channel
• 给 server socket channel 从 boss group 中选择一个 NioEventLoop

boss thread

• 将 server socket channel 注册到选择的 NioEventLoop 的 selector
• 绑定地址启动
• 注册接受连接事件(OP_ACCEPT)到 selector 上

启动服务的本质

Selector selector = sun.nio.ch.SelectorProviderImpl.openSelector()
ServerSocketChannel serverSocketChannel = provider.openServerSocketChannel()
selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
javaChannel().bind(localAddress, config.getBacklog());
selectionKey.interestOps(OP_ACCEPT);

• Selector 是在 new NioEventLoopGroup()(创建一批 NioEventLoop)时创建。
• 第一次 Register 并不是监听 OP_ACCEPT,而是 0:
selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this) 。
• 最终监听 OP_ACCEPT 是通过 bind 完成后的 fireChannelActive() 来触发的。
• NioEventLoop 是通过 Register 操作的执行来完成启动的。
• 类似 ChannelInitializer,一些 Hander 可以设计成一次性的,用完就移除,例如授权。

构建连接

boss thread:
• NioEventLoop 中的 selector 轮询创建连接事件(OP_ACCEPT):
• 创建 socket channel
• 初始化 socket channel 并从 worker group 中选择一个 NioEventLoop

worker thread:
• 将 socket channel 注册到选择的 NioEventLoop 的 selector
• 注册读事件(OP_READ)到 selector 上

接受连接本质:
selector.select()/selectNow()/select(timeoutMillis) 发现 OP_ACCEPT 事件,处理:
• SocketChannel socketChannel = serverSocketChannel.accept()
• selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
• selectionKey.interestOps(OP_READ);

• 创建连接的初始化和注册是通过 pipeline.fireChannelRead 在 ServerBootstrapAcceptor 中完成的。
• 第一次 Register 并不是监听 OP_READ ,而是 0 :
selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this) 。
• 最终监听 OP_READ 是通过“Register”完成后的fireChannelActive
(io.netty.channel.AbstractChannel.AbstractUnsafe#register0中)来触发的
• Worker’s NioEventLoop 是通过 Register 操作执行来启动。
• 接受连接的读操作,不会尝试读取更多次(16次)。

接受数据

1 自适应数据大小的分配器(AdaptiveRecvByteBufAllocator):
发放东西时,拿多大的桶去装?小了不够,大了浪费,所以会自己根据实际装的情况猜一猜下次情况,
从而决定下次带多大的桶。
2 连续读(defaultMaxMessagesPerRead):
发放东西时,假设拿的桶装满了,这个时候,你会觉得可能还有东西发放,所以直接拿个新桶等着装,
而不是回家,直到后面出现没有装上的情况或者装了很多次需要给别人一点机会等原因才停止,回家。

主线 worker thread
• 多路复用器( Selector )接收到 OP_READ 事件
• 处理 OP_READ 事件:NioSocketChannel.NioSocketChannelUnsafe.read()
• 分配一个初始 1024 字节的 byte buffer 来接受数据
• 从 Channel 接受数据到 byte buffer
• 记录实际接受数据大小,调整下次分配 byte buffer 大小
• 触发 pipeline.fireChannelRead(byteBuf) 把读取到的数据传播出去
worker thread
• 判断接受 byte buffer 是否满载而归:是,尝试继续读取直到没有数据或满 16 次;
否,结束本轮读取,等待下次 OP_READ 事件

• 读取数据本质:sun.nio.ch.SocketChannelImpl#read(java.nio.ByteBuffer)
• NioSocketChannel read() 是读数据, NioServerSocketChannel read() 是创建连接
• pipeline.fireChannelReadComplete(); 一次读事件处理完成
pipeline.fireChannelRead(byteBuf); 一次读数据完成,一次读事件处理可能会包含多次读数据操作。
• 为什么最多只尝试读取 16 次?“雨露均沾”
• AdaptiveRecvByteBufAllocator 对 bytebuf 的猜测:放大果断,缩小谨慎(需要连续 2 次判断)

业务处理

触发 pipeline.fireChannelRead(byteBuf) 把读取到的数据传播出去
image.png

发送数据

写数据3种方式
image.png

主线
image.png

• 批量写数据时,如果尝试写的都写进去了,接下来会尝试写更多(maxBytesPerGatheringWrite)。
• 只要有数据要写,且能写,则一直尝试,直到 16 次(writeSpinCount),写 16 次还没有写完,就直
接 schedule 一个 task 来继续写,而不是用注册写事件来触发,更简洁有力。
• 待写数据太多,超过一定的水位线(writeBufferWaterMark.high()),会将可写的标志位改成 false ,
让应用端自己做决定要不要继续写。
• channelHandlerContext.channel().write() :从 TailContext 开始执行;
channelHandlerContext.write() : 从当前的 Context 开始。

断开连接

• 多路复用器(Selector)接收到 OP_READ 事件 :
• 处理 OP_READ 事件:NioSocketChannel.NioSocketChannelUnsafe.read():
• 接受数据
• 判断接受的数据大小是否 < 0 , 如果是,说明是关闭,开始执行关闭:
• 关闭 channel(包含 cancel 多路复用器的 key)。
• 清理消息:不接受新信息,fail 掉所有 queue 中消息。
• 触发 fireChannelInactive 和 fireChannelUnregistered 。

• 关闭连接本质:
• java.nio.channels.spi.AbstractInterruptibleChannel#close
• java.nio.channels.SelectionKey#cancel
• 要点:
• 关闭连接,会触发 OP_READ 方法。读取字节数是 -1 代表关闭。
• 数据读取进行时,强行关闭,触发 IO Exception,进而执行关闭。
• Channel 的关闭包含了 SelectionKey 的 cancel

关闭服务

image.png

• 关闭服务要点:
• 优雅(DEFAULT_SHUTDOWN_QUIET_PERIOD)
• 可控(DEFAULT_SHUTDOWN_TIMEOUT)
• 先不接活,后尽量干完手头的活(先关 boss 后关 worker:不是100%保证)

编写网络应用程序基本步骤

image.png

参数

image.png

第五章: Netty 实战进阶:把“玩具”变成产品.pdf