2、高性能 Netty

该栏目讲解NIO、Netty组件、Netty参数调优、粘包与半包解决方案、聊天室



Netty入门

1、简介

  • 概述:Netty 是一个异步的基于事件驱动的网络应用框架
  • 特点
    • 高并发
    • 传输快
    • 封装好

2、入门案例

  • 服务端
// 服务端
public class ServerDemo {

    public static void main(String[] args) {
        new ServerBootstrap()
                // 添加BossEventLoop、WorkerEventLoop,类似于Selector + 线程池
                .group(new NioEventLoopGroup())
                // 添加服务端Socket实现
                .channel(NioServerSocketChannel.class)
                // 添加消息处理器
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel channel) {
                        // 将输入进来的消息转换为string
                        channel.pipeline().addLast(new StringDecoder());
                        // 将消息进行业务处理
                        channel.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
                            @Override
                            protected void channelRead0(ChannelHandlerContext ctx, String msg) {
                                System.out.println(msg);
                            }
                        });
                    }
                })
                // 绑定端口
                .bind(8888);
    }
    
}
  • 客户端
// 客户端
public class ClientDemo {

    public static void main(String[] args) throws InterruptedException {
        new Bootstrap()
                // 添加NioEventLoopGroup
                .group(new NioEventLoopGroup())
                // 添加客户端socket
                .channel(SocketChannel.class)
                // 添加处理器
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel channel) {
                        // 将消息进行编码
                        channel.pipeline().addLast(new StringEncoder());
                    }
                })
                // 连接服务端
                .connect("127.0.0.1", 8888)
                // 同步等待
                .sync()
                // 获取channel
                .channel()
                // 发送数据
                .writeAndFlush("Hello Netty!");
    }
    
}

Netty 组件

1、事件驱动

1.1 EventLoop

  • 概述:EventLoop 本质是一个单线程执行器(内部维护了一个 Selector),里面有 run 方法处理 Channel 上源源不断的 I/O 事件
  • 继承关系
    • 继承了 ScheduledExecutorService 线程池类
    • 继承了 OrderedEventExecutor,说明每个 EventExecutor 是一个单独的线程,可以执行 Runnable 任务

1.2 EventLoopGroup

  • 概述EventLoopGroup 是一组 EventLoop,Channel 一般会调用 EventLoopGroup 的 register 方法来绑定其中一个 EventLoop,后续这个 Channel 上的 I/O 事件都由此 EventLoop 来处理(保证了 I/O 事件处理时的线程安全)
  • 继承关系:继承 EventExecutorGroup
public class EventLoopDemo {

    public static void main(String[] args) {

        // 创建事件循环组
        DefaultEventLoopGroup group = new DefaultEventLoopGroup(2);
        // 获取一个事件循环对象
        final EventLoop eventLoop = group.next();
        System.out.println(eventLoop);

        // 创建事件循环组
        NioEventLoopGroup workers = new NioEventLoopGroup(2);
        // 执行普通任务
        workers.execute(() -> {
            System.out.println("normal task...");
        });
        // 执行定时任务
        workers.scheduleAtFixedRate(() -> {
            System.out.println("running...");
        }, 0, 2, TimeUnit.SECONDS);

        // 优雅关闭:首先切换EventLoopGroup到关闭状态从而拒绝新的任务的加入,
        // 然后在任务队列的任务都处理完成后,停止线程的运行
        workers.shutdownGracefully();
    }

}

2、通道

2.1 Channel

  • 概述:连接了网络套接字或能够进行 I/O 操作的组件
  • 常用方法
    • close():关闭 channel
    • closeFuture():处理 channel 关闭后的事件
    • pipeline():添加处理器
    • write():数据写入
    • writeAndFlush():数据写入并刷新

2.2 ChannelFuture

  • 概述:异步通知的 Channel 类
  • 常用方法
    • sync():同步等待连接建立完成
    • channel():获取Channel对象

2.3 CloseFuture

  • 概述:用于 channel 关闭后的处理
  • 常用方法
    • sync():同步等待连接关闭完成
    • addListener():异步关闭连接
public class ChannelDemo {

    public static void main(String[] args) throws InterruptedException {
        final NioEventLoopGroup group = new NioEventLoopGroup();
        final ChannelFuture channelFuture = new Bootstrap()
                .group(group)
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<Channel>() {
                    @Override
                    protected void initChannel(Channel channel) throws Exception {
                        channel.pipeline().addLast(new StringEncoder());
                    }
                })
                .connect("localhost", 8888);
        // 同步等待连接建立完成,并获取channel对象
        final Channel channel = channelFuture.sync().channel();

        // 关闭连接
        channel.close();

        // 异步处理关闭连接后
        final ChannelFuture closeFuture = channel.closeFuture();
        closeFuture.addListener((ChannelFutureListener) channelFuture1 -> {
            System.out.println("处理关闭之后的操作");
            group.shutdownGracefully();
        });

        // 同步关闭连接
        // closeFuture.sync();
        // System.out.println("处理关闭之后的操作");
    }
}

3、Future & Promise

3.1 JDK Future

  • 概述:只能同步等待任务结束才能得到结果
  • 相关方法
方法名称功能描述
cancel取消任务
isCanceled任务是否取消
isDone任务是否完成,不能区分成功失败
get获取任务结果,阻塞等待

3.2 Netty Future

  • 概述:可以同步等待任务结束得到结果,也可以异步方式得到结果,但都是要等任务结束
  • 相关方法
方法名称功能描述
getNow获取任务结果,非阻塞,还未产生结果时返回null
await等待任务结束,如果任务失败,不会抛异常,而是通过 isSuccess 判断
sync等待任务结束,如果任务失败,抛出异常
isSuccess判断任务是否成功
cause获取失败信息,非阻塞,如果没有失败,返回 null
addLinstener添加回调,异步接收结果

3.3 Promise

  • 概述:不仅有 Netty Future 的功能,而且脱离了任务独立存在,只作为两个线程间传递结果的容器
  • 相关方法
方法名称功能描述
setSuccess设置成功结果
setFailure设置失败结果

3.4 案例

public class PromiseDemo {

    public static void main(String[] args) throws Exception {
        syncPromise();
        asyncPromise();
        failurePromise();
    }

    /**
     * 同步获取结果
     */
    private static void syncPromise() throws Exception {
        DefaultEventLoop eventLoop = new DefaultEventLoop();
        DefaultPromise<Integer> promise = new DefaultPromise<>(eventLoop);

        eventLoop.execute(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 返回成功结果
            promise.setSuccess(10);
            // 返回失败结果
            RuntimeException e = new RuntimeException("Error...");
            promise.setFailure(e);
        });

        // 同步非阻塞获取结果,此时线程还没处理完,所以获取为null
        System.out.println("同步非阻塞结果:" + promise.getNow());
        // 同步阻塞获取结果
        System.out.println("同步阻塞结果:" + promise.get());
    }

    /**
     * 异步处理结果
     */
    private static void asyncPromise() {
        DefaultEventLoop eventLoop = new DefaultEventLoop();
        DefaultPromise<Integer> promise = new DefaultPromise<>(eventLoop);

        eventLoop.execute(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            promise.setSuccess(10);
        });

        // 异步等待结果
        promise.addListener(future -> System.out.println("异步结果:" + promise.getNow()));
    }

    /**
     * 返回失败结果
     */
    private static void failurePromise() throws InterruptedException {
        DefaultEventLoop eventLoop = new DefaultEventLoop();
        DefaultPromise<Integer> promise = new DefaultPromise<>(eventLoop);

        eventLoop.execute(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 返回失败结果
            RuntimeException e = new RuntimeException("Error...");
            promise.setFailure(e);
        });

        // 等待结果,有异常时不会像sync和get直接抛出
        promise.await();
        System.out.println(promise.isSuccess() ? promise.getNow() : promise.cause());
    }

}

4、Handler & Pipline

4.1 ChannelHandler

  • 概述:用来处理 Channel 上的各种事件,分为入站、出站两种。
  • 类型
    • 入站处理器:ChannelInboundHandlerAdapter 的子类,主要用来读取客户端数据
    • 出站处理器:ChannelOutboundHandlerAdapter 的子类,主要对写回结果进行加工
  • 执行顺序:入站处理器是按照添加的顺序执行的,而出站处理器是按照添加的逆序执行的

4.2 ChannelPipeline

  • 概述:由 ChannelHandlerContext(包装了 ChannelHandler)组成的双向链表
  • 常用方法
    • context.fireChannelRead():调用下一个入站处理器
    • context.write():从当前节点找上一个出站处理器
    • context.channel().write():从尾部开始触发后续出站处理器
      Chanler与Pipline

5、ByteBuf

  • 概述:是对字节的封装

  • 结构
    ByteBuf结构

  • 创建对象

    • ByteBufAllocator.DEFAULT.buffer(size):创建ByteBuf
    • ByteBufAllocator.DEFAULT.heapBuffer(size):创建池化基于堆的 ByteBuf
    • ByteBufAllocator.DEFAULT.directBuffer(size):创建池化基于直接内存的 ByteBuf
    • ByteBufAllocator.DEFAULT.compositeBuffer():创建零拷贝的 ByteBuf
    • Unpooled.wrappedBuffer(byteBuf):使用Unpooled工具类创建零拷贝的 ByteBuf
  • 常用方法

    • 写入数据:writeInt(int value)
    • 读取数据:readByte()
    • 释放:retain() & release()
    • 切片:slice():对原始ByteBuf进行切片成多个 ByteBuf,还是使用原始 ByteBuf 的内存,切片后的ByteBuf 维护独立的 read,write 指针
    • 复制:duplicate():取了原始 ByteBuf 所有内容,并且没有 max capacity 的限制,也是与原始 ByteBuf 使用同一块底层内存,只是读写指针是独立的
    • 复制:copy():会将底层内存数据进行深拷贝,因此无论读写,都与原始 ByteBuf 无关
  • 扩容规则

    • 如何写入后数据大小未超过 512,则选择下一个 16 的整数倍,例如写入后大小为 12 ,则扩容后capacity是 16
    • 如果写入后数据大小超过 512,则选择下一个2 ^ n,例如写入后大小为 513,则扩容后 capacity 是2 ^ 10 = 1024(2 ^ 9=512 已经不够了)
    • 扩容不能超过 max capacity 会报错
  • 释放规则

    • 采用了引用计数法来控制回收内存,每个 ByteBuf 都实现了 ReferenceCounted接口
    • 每个 ByteBuf 对象的初始计数为 1
    • 调用 release 方法计数减1,如果计数为 0,ByteBuf 内存被回收
    • 调用 retain 方法计数加1,表示调用者没用完之前,其它 handler 即使调用了 release 也不会造成回收
    • 当计数为 0 时,底层内存会被回收,这时即使 ByteBuf 对象还在,其各个方法均无法正常使用
  • 池化

    • 概述:池化的最大意义在于可以重用 ByteBuf,没有池化,则每次都得创建新的 ByteBuf 实例
    • 开启池化功能:-Dio.netty.allocator.type={unpooled|pooled}
public class ByteBufDemo {

    public static void main(String[] args) {
        /* 创建对象 */
        // 创建byteBuf对象
        ByteBuf buffer1 = ByteBufAllocator.DEFAULT.buffer();
        // 创建池化基于堆的byteBuf对象
        ByteBuf buffer2 = ByteBufAllocator.DEFAULT.heapBuffer();
        // 创建池化基于直接内存的byteBuf对象
        ByteBuf buffer3 = ByteBufAllocator.DEFAULT.directBuffer();
        // 工具类创建byteBuf对象
        ByteBuf buffer4 = Unpooled.wrappedBuffer(buffer1);
        // 创建具有零拷贝的byteBuf对象
        CompositeByteBuf compositeByteBuf = ByteBufAllocator.DEFAULT.compositeBuffer();

        /* 读写方法 */
        // 写入
        buffer2.writeBytes(new byte[]{1, 2, 4, 5, 6});
        // 读取
        byte value1 = buffer2.readByte();
        System.out.println(value1);

        /* 释放内存相关的方法 */
        // 对记数+1
        buffer3.retain();
        // 对记数-1
        buffer3.release();

        /* 零拷贝相关的方法 */
        // 复制
        final ByteBuf buffer6 = buffer4.slice(0, 5);
        System.out.println(buffer6);

        // 复制
        final ByteBuf buffer7 = buffer4.duplicate();
        System.out.println(buffer7);

        // 复制
        final ByteBuf buffer8 = buffer4.copy();
        System.out.println(buffer8);

        // 合并多个buffer
        final CompositeByteBuf byteBuf = compositeByteBuf.addComponents(buffer1, buffer2);
        System.out.println(byteBuf);
    }

}

粘包与半包

1、粘包

  • 现象:发送abc def,接收abcdef
  • 原因
    • 应用层:接收方 ByteBuf 设置太大(Netty 默认1024)
    • 滑动窗口:假设发送方 256 bytes 表示一个完整报文,但由于接收方处理不及时且窗口大小足够大,这 256 bytes 字节就会缓冲在接收方的滑动窗口中,当滑动窗口中缓冲了多个报文就会粘包
    • Nagle 算法:会造成粘包

2、半包

  • 现象:发送 abcdef,接收 abc def
  • 原因
    • 应用层:接收方 ByteBuf 小于实际发送数据量
    • 滑动窗口:假设接收方的窗口只剩了 128 bytes,发送方的报文大小是 256 bytes,这时放不下了,只能先发送前 128 bytes,等待 ack 后才能发送剩余部分,这就造成了半包
  • MSS 限制:当发送的数据超过 MSS 限制后,会将数据切分发送,就会造成半包

3、解决方案

  • 短链接:发一个包建立一次连接,这样连接建立到连接断开之间就是消息的边界,缺点效率太低
  • 固定长度:每一条消息采用固定长度,缺点浪费空间
  • 分隔符:每一条消息采用分隔符,例如 \n,缺点需要转义
  • 预设长度:每一条消息分为head和body,head中包含body的长度

协议设计

1、简介

  • 原因:TCP/IP 中消息传输基于流的方式,没有边界。协议的目的就是划定消息的边界,制定通信双方要共同遵守的通信规则
  • 要素
    • 魔数,用来在第一时间判定是否是无效数据包
    • 版本号,可以支持协议的升级
    • 序列化算法,消息正文到底采用哪种序列化反序列化方式,可以由此扩展,例如:json、protobuf、hessian、jdk
    • 指令类型,是登录、注册、单聊、群聊… 跟业务相关
    • 请求序号,为了双工通信,提供异步能力
    • 正文长度
    • 消息正文

2、编解码器

/**
 * 消息编解码器
 */
@ChannelHandler.Sharable
@Slf4j
public class MessageCodec extends MessageToMessageCodec<ByteBuf, Message> {

    @Override
    protected void encode(ChannelHandlerContext ctx, Message message, List<Object> outs) 
    		throws IOException {
        ByteBuf buffer = ctx.alloc().buffer();
        // 魔数
        buffer.writeBytes(new byte[]{1, 2, 3, 4});
        // 版本
        buffer.writeByte(1);
        // 序列化类型
        buffer.writeByte(1);
        // 消息类型
        buffer.writeByte(message.getMessageType());
        // 序列号
        buffer.writeInt(message.getSequenceId());
        // 保留
        buffer.writeByte(0xff);
        // 序列化消息体
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(message);
        final byte[] msgBytes = baos.toByteArray();
        // 长度
        buffer.writeInt(msgBytes.length);
        // 消息体
        buffer.writeBytes(msgBytes);
        // 发送消息
        outs.add(buffer);
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> outs) 
    			throws Exception {
        int magicNum = buffer.readInt();
        byte version = buffer.readByte();
        byte serializeType = buffer.readByte();
        int messageType = buffer.readByte();
        int sequenceId = buffer.readInt();
        buffer.readByte();
        final int length = buffer.readInt();
        byte[] msgBytes = new byte[length];
        buffer.readBytes(msgBytes, 0, length);
        // 反序列化消息体
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(msgBytes));
        Message message = (Message) ois.readObject();
        outs.add(message);
    }

}

聊天室

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值