Netty学习

Netty学习

1.Netty介绍

	Netty 是由 JBOSS 提供的一个 Java 开源框架。Netty 提供异步的、基于事件驱动的网络应用框架,用以快速开发高性能、高可靠性的网络IO程序,是目前最流行的 NIO 框架。 Elasticsearch、Dubbo 框架内部都采用了 Netty。
	Netty 主要基于主从Reactor多线程模型做了一定的 改进,其中主从Reactor多线程模型有多个Reactor

在这里插入图片描述

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 对自己感兴趣的事件进行处理(重写了相应事件处理方法)
		##pipeline 中有多个 handler,处理时会依次调用其中的 handler
		##例:入栈1-2-3 + 出栈4-5-6 则处理顺序为123654.
		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() 方法将数据写入并立即发送(刷出)
########ps1:
#ctx的writeAndFlush是从当前handler直接发出这个消息,而channel的writeAndFlush是从整个pipline最后一个outhandler发出
#例:入栈1-2-3 + 出栈4-5-6 channel从6-5-4处理,ctx从当前handler处理

##########ps2: channel.writeAndFlush() 立即发出交给outhandler发出
#ctx.fireChannelRead(msg)把当前handler处理后的数据传输给下一个handler

2.Future & Promise

netty 中的 Future 与 jdk 中的 Future 同名,但是是两个接口
netty 的 Future 继承自 jdk 的 Future,而 Promise 又对 netty Future 进行了扩展(作为两个线程间传递结果的容器)
参考下图
功能/名称jdk Futurenetty FuturePromise
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.直接内存和堆内存//(可以使用Unpooled工具类创建各种ByteBuf)
    直接内存创建和销毁的代价昂贵,但读写性能高(少一次内存复制),适合配合池化功能一起用
	直接内存对 GC 压力小,因为这部分内存不受 JVM 垃圾回收的管理,但也要注意及时主动释放
	//基于直接内存的ByteBuf
	ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(16);
	//创建池化基于堆的 ByteBuf
	ByteBuf buffer = ByteBufAllocator.DEFAULT.heapBuffer(16);
	//创建池化基于直接内存的 ByteBuf
	ByteBuf buffer = ByteBufAllocator.DEFAULT.directBuffer(16);
2.池化与非池化
    池化的最大意义在于可以重用 ByteBuf,采用了与jemalloc类似的内存分配算法提升分配效率;高并发时,池化功能更节约内存,减少内存溢出的可能
组成///
1.最大容量与当前容量
	在构造ByteBuf时,可传入两个参数,分别代表初始容量和最大容量,若未传入第二个参数(最大容量),最大容量默认为Integer.MAX_VALUE
	当ByteBuf容量无法容纳所有数据时,会进行扩容操作,若超出最大容量,会抛出java.lang.IndexOutOfBoundsException异常
2.ByteBuf分别由读指针和写指针两个指针控制(参考下图)
3.扩容(ByteBuf中的容量无法容纳写入的数据时,会进行扩容操作)
	写入后数据超过容量大小,则进行2N次方进行扩容
    扩容不能超过 maxCapacity,否则会抛出java.lang.IndexOutOfBoundsException异常
4.读写操作 //ps:byte 1字节、short 2字节、int 4字节、long 8字节、float 4字节、double 8字节、char 2字节、boolean 1字节
    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,如果计数为 0ByteBuf 内存被回收
    2.释放规则:谁是最后使用者,谁负责 release
    	入站:如果不调用 ctx.fireChannelRead(msg) 向后传递,必须 release
        出站:消息最终都会转为 ByteBuf 输出,一直向前传,由HeadContext flush后release
        异常处理原则:有时候不清楚 ByteBuf 被引用了多少次,但又必须彻底释放,可以循环调用 release 直到返回 true
    3.常用的ByteBuf
	UnpooledHeapByteBuf 使用的是 JVM 内存,只需等 GC 回收内存即可
	UnpooledDirectByteBuf 使用的就是直接内存了,需要特殊的方法来回收内存
	PooledByteBuf 和它的子类使用了池化机制,需要更复杂的规则来回收内存

在这里插入图片描述

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; // 最小长度为2,因为有可能长度域本身也有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对象添加到out列表中
            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

//netty服务类,另启动一个线程去启动nettyServer
public class NettyServer {
    public void bind(boolean tls, String filePath, String password, int port) throws Exception {
        //bossGroup就是parentGroup,是负责处理TCP/IP连接的
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        //workerGroup就是childGroup,是负责处理Channel(通道)的I/O事件
        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)
                //使用NIO作为服务器通道实现
                .channel(NioServerSocketChannel.class)
                //初始化服务端可连接队列,指定了队列的大小5000
                //bossGroup最大保持连接数
                .option(ChannelOption.SO_BACKLOG, 5000)
                //保持长连接 workerGroup
                .childOption(ChannelOption.SO_KEEPALIVE, true)
                .childOption(ChannelOption.TCP_NODELAY, true)
                // 绑定客户端连接时候触发操作
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel nsc) throws Exception {
                        //ssl安全处理类
                        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())
                                //登录成功之后处理的方法使用ServerInHandler类来处理接收到的消息
                                .addLast("fromClient", new ServerInHandler())
                                //使用ServerOutHandler类来转发出消息
                                .addLast("toClient", new ServerOutHandler())
                        ;

                    }
                });
        //绑定监听端口,调用sync同步阻塞方法等待绑定操作完
        ChannelFuturefuture = sb.bind(port).sync();

        if (future.isSuccess()) {
            log.info("nettyServer 服务端启动成功,port:{}",port);
        } else {
            log.info("nettyServer 服务端启动失败");
            future.cause().printStackTrace();
            bossGroup.shutdownGracefully(); //关闭线程组
            workerGroup.shutdownGracefully();
        }
        //成功绑定到端口之后,给channel增加一个 管道关闭的监听器并同步阻塞,直到channel关闭,线程才会往下执行,结束进程。
        future.channel().closeFuture().sync();
    }

}
///保存channel的类
public class NettyChannelManager {
    /**
     * 保存连接 Channel 的地方
     */
    private static final Map<String, Channel> CHANNEL_POOL = new ConcurrentHashMap<>();
    private static final Map<Channel, String> KEY_POOL = new ConcurrentHashMap<>();

    /**
     * 添加 Channel
     *
     * @param key
     */
    public static void add(String key, Channel channel) {
        CHANNEL_POOL.put(key, channel);
        KEY_POOL.put(channel, key);
    }

    /**
     * 删除 Channel
     *
     * @param 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);
    }

    /**
     * 删除并同步关闭连接
     *
     * @param key
     */
    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);
    }

    /**
     * 获得 Channel
     *
     * @param key
     * @return String
     */
    public static Channel getChannel(String key) {
        return CHANNEL_POOL.get(key);
    }

    /**
     * 获得 key
     *
     * @param channel
     * @return Channel
     */
    public static String getKey(Channel channel) {
        return KEY_POOL.get(channel);
    }

    /**
     * 判断是否存在key
     * @author lanni
     * @date 2020/9/16 10:10
     * @param key
     * @return boolean
     **/
    public static boolean hasKey(String key) {
        return CHANNEL_POOL.containsKey(key);
    }

    /**
     * 判断是否存在channel
     * @author lanni
     * @date 2020/10/12 9:34
     * @param channel
     * @return boolean
     **/
    public static boolean hasChannel(Channel channel) {
        return KEY_POOL.containsKey(channel);
    }

}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

没有什么是应该

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值