Netty学习
1.Netty介绍
Netty 是由 JBOSS 提供的一个 Java 开源框架。Netty 提供异步的、基于事件驱动的网络应用框架,用以快速开发高性能、高可靠性的网络IO程序,是目前最流行的 NIO 框架。 Elasticsearch、Dubbo 框架内部都采用了 Netty。
Netty 主要基于主从Reactor多线程模型做了一定的 改进,其中主从Reactor多线程模型有多个Reactor
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/96b19ed7556a70b2db63ee75f3f426a0.webp?x-image-process=image/format,png)
2.Netty好处
1.设计优雅:适用于各种传输类型的统一 API 阻塞和非阻塞 Socket;基于灵活且可扩展的事件模型,可以清晰地分离关注点;高度可定制的线程模型 - 单线程,一个或多个线程池
2.高性能、吞吐量更高:延迟更低;减少资源消耗;最小化不必要的内存复制
3.安全:完整的 SSL/TLS 和 StartTLS 支持
3.Netty组件
1. channel 可以理解为数据的通道
2. msg 理解为流动的数据,最开始输入是 ByteBuf,但经过 pipeline 中的各个 handler 加工,会变成其它类型对象,最后输出又变成 ByteBuf
3. handler 可以理解为数据的处理工序
1) 工序有多道,合在一起就是 pipeline(传递途径),pipeline 负责发布事件(读、读取完成…)传播给每个 handler,handler 对自己感兴趣的事件进行处理(重写了相应事件处理方法)
2) handler 分 Inbound 和 Outbound 两类
Inbound 入站:会依次调用
Outbound 出站:倒序依次调用
4.eventLoop 可以理解为处理数据的工人
1)eventLoop 可以管理多个 channel 的 io 操作,并且一旦 eventLoop 负责了某个 channel,就会将其与channel进行绑定,以后该 channel 中的 io 操作都由该 eventLoop 负责
2)eventLoop 既可以执行 io 操作,也可以进行任务处理,每个 eventLoop 有自己的任务队列,队列里可以堆放多个 channel 的待处理任务,任务分为普通任务、定时任务
3)eventLoop 按照 pipeline 顺序,依次按照 handler 的规划(代码)处理数据,可以为每个 handler 指定不同的 eventLoop
组件关系
Channel, ChannelPipeline, ChannelHandler, ChannelHandlerContext 关系
1.channel 和 pipeline 一一对应的
channel 和 pipeline 一一对应的。创建channel时,同时创建pipeline。共生攻灭
pipeline是channel的属性
2.pipeline 串起的其实是ctx,而不是handler。(ChannelHandlerContext)
ctx是pipeline的属性(pipeline中双向链表的头元素、中间元素、尾元素)
不过,被串起的ctx中有个属性是handler。所以说:pipeline串起ctx,其实也就是 pipeline串起了handler
@ChannelHandler.Sharable注解
1.容易误解的地方,只要加上@ChannelHandler.Sharable注解,他在整个生命周期中就是以单例的形式存在,其实不然
2.Netty并没有那么智能,也就是说handler的单例需要自己实现
pipeline.addLast方法中只有checkMultiplicity(handler);
很明显,判断handler是不是共享的,然后是不是首次添加,不满足其一,直接抛异常
例子:添加一个StatusHandler,目的为了记录同时在线的设备数量
@ChannelHandler.Sharable
public class StatusHandler extends ChannelInboundHandlerAdapter {
private volatile static int count = 0;
private Object lock = new Object();
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
synchronized (lock) {
count++;
}
System.out.println(getClass().toGenericString() + ":" + this);
super.channelActive(ctx);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
synchronized (lock) {
count--;
}
super.channelInactive(ctx);
}
public static int getCount() {
return count;
}
}
public class InitHandler extends ChannelInitializer<NioSocketChannel> {
private StatusHandler statusHandler = new StatusHandler();
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
System.out.println(this);
nioSocketChannel.pipeline()
.addLast(new TimeoutHandler(5,1000,1000))
.addLast(new MessageHandler())
.addLast(statusHandler)
.addLast(new WriteHandler<String>());
}
}
4.常用方法
1.Channel常用方法
1.close() 可以用来关闭Channel
2.closeFuture() 用来处理 Channel 的关闭
sync 方法作用是同步等待 Channel 关闭
而 addListener 方法是异步等待 Channel 关闭
3.pipeline() 方法用于添加处理器
4.write() 方法将数据写入
因为缓冲机制,数据被写入到 Channel 中以后,不会立即被发送
只有当缓冲满了或者调用了flush()方法后,才会将数据通过 Channel 发送出去
5.writeAndFlush() 方法将数据写入并立即发送(刷出)
2.Future & Promise
netty 中的 Future 与 jdk 中的 Future 同名,但是是两个接口
netty 的 Future 继承自 jdk 的 Future,而 Promise 又对 netty Future 进行了扩展(作为两个线程间传递结果的容器)
参考下图
功能/名称 | jdk Future | netty Future | Promise |
---|
cancel | 取消任务 | - | - |
isCanceled | 任务是否取消 | - | - |
isDone | 任务是否完成,不能区分成功失败 | - | - |
get | 获取任务结果,阻塞等待 | - | - |
getNow | - | 获取任务结果,非阻塞,还未产生结果时返回 null | - |
await | - | 等待任务结束,如果任务失败,不会抛异常,而是通过 isSuccess 判断 | - |
sync | - | 等待任务结束,如果任务失败,抛出异常 | - |
isSuccess | - | 判断任务是否成功 | - |
cause | - | 获取失败信息,非阻塞,如果没有失败,返回null | - |
addListener | - | 添加回调,异步接收结果 | - |
setSuccess | - | - | 设置成功结果 |
setFailure | - | - | 设置失败结果 |
3. Pipeline
addFirst 添加到第一个handler
addLast 在队尾添加一个handler
addBefore 在某一个handler之前添加一个handler
addAfter 在某一个handler之后添加一个handler
remove 移除某一个handler
5.###ByteBuf###
ByteBuf通过ByteBufAllocator选择allocator并调用对应的buffer()方法来创建的,默认使用直接内存作为ByteBuf,容量为256个字节,可以指定初始容量的大小
当ByteBuf的容量无法容纳所有数据时,ByteBuf会进行扩容操作
如果在handler中创建ByteBuf,建议使用ChannelHandlerContext ctx.alloc().buffer()来创建
1.直接内存和堆内存
直接内存创建和销毁的代价昂贵,但读写性能高(少一次内存复制),适合配合池化功能一起用
直接内存对 GC 压力小,因为这部分内存不受 JVM 垃圾回收的管理,但也要注意及时主动释放
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(16);
ByteBuf buffer = ByteBufAllocator.DEFAULT.heapBuffer(16);
ByteBuf buffer = ByteBufAllocator.DEFAULT.directBuffer(16);
2.池化与非池化
池化的最大意义在于可以重用 ByteBuf,采用了与jemalloc类似的内存分配算法提升分配效率;高并发时,池化功能更节约内存,减少内存溢出的可能
1.最大容量与当前容量
在构造ByteBuf时,可传入两个参数,分别代表初始容量和最大容量,若未传入第二个参数(最大容量),最大容量默认为Integer.MAX_VALUE
当ByteBuf容量无法容纳所有数据时,会进行扩容操作,若超出最大容量,会抛出java.lang.IndexOutOfBoundsException异常
2.ByteBuf分别由读指针和写指针两个指针控制(参考下图)
3.扩容(当ByteBuf中的容量无法容纳写入的数据时,会进行扩容操作)
写入后数据超过容量大小,则进行2的N次方进行扩容
扩容不能超过 maxCapacity,否则会抛出java.lang.IndexOutOfBoundsException异常
4.读写操作
a.读取
1).readShort()、readByte()等等read开头的方法
也可以使用readBytes读取多个byte
byte[] bytes = new byte[10];
buf.readBytes(encrypt);
2).重复读取
需要调用buffer.markReaderIndex()对读指针进行标记,并通过buffer.resetReaderIndex()将读指针恢复到mark标记的位置
3). get开头的一系列方法,这些方法不会改变读指针的位置,也可以重复读取,区别在于buffer的清除
b.写入
1)writeByte(int value)、writeShort(int value)、writeBytes(byte[] src)等方法
2)set开头的一系列方法,也可以写入数据,但不会改变写指针位置
5.内存释放
1.引用计数法来控制回收内存,每个 ByteBuf 都实现了 ReferenceCounted 接口,retain 方法计数加 1,release 方法计数减 1,如果计数为 0,ByteBuf 内存被回收
2.释放规则:谁是最后使用者,谁负责 release
入站:如果不调用 ctx.fireChannelRead(msg) 向后传递,必须 release
出站:消息最终都会转为 ByteBuf 输出,一直向前传,由HeadContext flush后release
异常处理原则:有时候不清楚 ByteBuf 被引用了多少次,但又必须彻底释放,可以循环调用 release 直到返回 true
3.常用的ByteBuf
UnpooledHeapByteBuf 使用的是 JVM 内存,只需等 GC 回收内存即可
UnpooledDirectByteBuf 使用的就是直接内存了,需要特殊的方法来回收内存
PooledByteBuf 和它的子类使用了池化机制,需要更复杂的规则来回收内存
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/b0a3615629ec5e1117b13664f18e3374.png)
6.Netty支持的序列化协议
1.序列化(编码)是将对象序列化为二进制形式(字节数组),主要用于网络传输、数据持久化等;而反序列化(解码)则是将从网络、磁盘等读取的字节数组还原成原始对象,主要用于网络传输对象的解码,以便完成远程调用
2.XML方式
3.JSON方式、Fastjson
4.Thrift 不仅是序列化协议,还是一个RPC框架。使用场景:不仅是序列化协议,还是一个RPC框架
5.Avro,Hadoop的一个子项目,解决了JSON的冗长和没有IDL的问题
6.Protobuf
1.性能要求在100ms以上的服务,基于XML的SOAP协议值得考虑
2.基于Web browser的Ajax,以及Mobile app与服务端之间的通讯,JSON协议是首选
3.当对性能和简洁性有极高要求的场景,Protobuf,Thrift,Avro之间具有一定的竞争关系
4.对于T级别的数据的持久化应用场景,Protobuf和Avro是首要选择
7.Netty的粘包和半包问题
1.什么是粘包半包问题
TCP是以流的方式来处理数据,一个完整的包可能会被TCP拆分成多个包进行发送,也可能把小的封装成一个大的数据包发送
2.解决方式
1)四种解码器
LineBasedFrameDecoder 换行符
DelimiterBaseFrameDecoder 分隔符
FixdLengthFrameDecoder 定长
StringDecoder 信息转化成字符串
2)自定义解码器(举例说明,代码逻辑为拿到ByteBuf然后根据起始帧+数据长度帧做判断,非起始帧追加到list中,并且依据数据长度帧截取)
public class MessageHandler extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
final int minFrameLength = 2;
if (in.readableBytes() < minFrameLength) {
return;
}
in.markReaderIndex();
short header = in.readUnsignedByte();
log.info("帧头,header:{}",header);
short length = in.readUnsignedByte();
in.resetReaderIndex();
if (in.readableBytes() < length+4) {
return;
}
ByteBuf frame = in.readRetainedSlice(length+4);
out.add(frame);
}
}
8.Netty应用场景
1.物联网对接硬件服务
2.游戏行业,地图见通讯
3.实时聊天系统,例如各大公司中的官网联系客服实时聊天的系统
4.RPC框架通信层框架
自定义开发RPC框架核心流程(选择Netty作为通信框架+Spring配置Bean注解等+选择合适高效的消息编解码器+服务的发布与订阅)
1. 服务消费方(client)调用以本地调用方式调用服务;
2. client stub 接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;
3. client stub 找到服务地址,并将消息发送到服务端;
4. server stub 收到消息后进行解码;
5. server stub 根据解码结果调用本地的服务;
6. 本地服务执行并将结果返回给 server stub;
7. server stub 将返回结果打包成消息并发送至消费方;
8. client stub 接收到消息,并进行解码;
9. 服务消费方得到最终结果。
RPC 的目标就是要 2~8 这些步骤都封装起来,让用户对这些细节透明。
9.Netty Server demo
public class NettyServer {
public void bind(boolean tls, String filePath, String password, int port) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap sb = new ServerBootstrap();
SSLContext sslContext = null;
if (tls) {
sslContext = SSLContextFactory.getSslContext(filePath, password);
}
final SSLContext finalContext = sslContext;
sb.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 5000)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childOption(ChannelOption.TCP_NODELAY, true)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nsc) throws Exception {
if (finalContext != null) {
SSLEngine sslEngine = finalContext.createSSLEngine();
sslEngine.setUseClientMode(false);
nsc.pipeline().addLast("ssl", new SslHandler(sslEngine));
}
nsc.pipeline()
.addLast("halfPack", new MessageHandler())
.addLast("log", new LoggingHandler(LogLevel.ERROR))
.addLast("idle", new IdleStateHandler(1000, 0, 0))
.addLast("heatBeat", new HeartBeatServerHandler())
.addLast("fromClient", new ServerInHandler())
.addLast("toClient", new ServerOutHandler())
;
}
});
ChannelFuturefuture = sb.bind(port).sync();
if (future.isSuccess()) {
log.info("nettyServer 服务端启动成功,port:{}",port);
} else {
log.info("nettyServer 服务端启动失败");
future.cause().printStackTrace();
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
future.channel().closeFuture().sync();
}
}
public class NettyChannelManager {
private static final Map<String, Channel> CHANNEL_POOL = new ConcurrentHashMap<>();
private static final Map<Channel, String> KEY_POOL = new ConcurrentHashMap<>();
public static void add(String key, Channel channel) {
CHANNEL_POOL.put(key, channel);
KEY_POOL.put(channel, key);
}
public static void remove(String key) {
Channel channel = CHANNEL_POOL.get(key);
if (channel == null) {
return;
}
CHANNEL_POOL.remove(key);
KEY_POOL.remove(channel);
}
public static void removeAndClose(String key) {
Channel channel = CHANNEL_POOL.get(key);
remove(key);
if (channel != null) {
try {
channel.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void removeAndClose(Channel channel) {
String key = KEY_POOL.get(channel);
removeAndClose(key);
}
public static Channel getChannel(String key) {
return CHANNEL_POOL.get(key);
}
public static String getKey(Channel channel) {
return KEY_POOL.get(channel);
}
public static boolean hasKey(String key) {
return CHANNEL_POOL.containsKey(key);
}
public static boolean hasChannel(Channel channel) {
return KEY_POOL.containsKey(channel);
}
}