Netty模型图解超细节(Netty第二步)

动态每日更新算法题,想要学习的可以关注一下一起学习

系列文章目录

第一章 初识NIO(同步非阻塞的I/O模型)(Netty第一步)



一、Netty是什么?

Netty是 一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端。

为什么使用Netty
NIO的主要问题是:

  1. NIO的类库和API繁杂,学习成本高,你需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等。
  2. 需要熟悉Java多线程编程。这是因为NIO编程涉及到Reactor模式,你必须对多线程和网络编程非常熟悉,才能写出高质量的NIO程序。
  3. 臭名昭著的epoll bug。它会导致Selector空轮询,最终导致CPU 100%。直到JDK1.7版本依然没得到根本性的解决。

Netty的优点

  1. API使用简单,学习成本低。
  2. 功能强大,内置了多种解码编码器,支持多种协议。
  3. 性能高,对比其他主流的NIO框架,Netty的性能最优。
  4. 社区活跃,发现BUG会及时修复,迭代版本周期短,不断加入新的功能。

1.Netty的模型图

在这里插入图片描述

传统的阻塞IO模型为了解决高并发和非阻塞的问题,进而产生了Reactor主从模式,而netty就是在此基础上演变而来,如果不了解阻塞模型以及Reactor可以前往顶部跳转之前的文章。

在这里插入图片描述

在这里插入图片描述

需要特别说明的就是不管是BossGroup还是WorkGroup每一个都可以包含无数个NioEventLoop,但是每一个NioEventLoop都会对应一个线程,如果无限制的创建会导致cpu资源耗尽。对于一般业务来说,BossGroup只会创建一个NioEventLoop,因为在BossGroup中它只负责socket的连接并且会不断的阻塞等待下一个连接,所以一个即可。而对于workGroup来说,它是负责大并发的业务处理,包括读写等任务,所以如果不指定数量,默认为cpu的核数×2,这意味着将会启用所有资源。

在这里插入图片描述

2.细节介绍

1.Channel

在这里插入图片描述

在这里插入图片描述

在我的上一篇文章其实讲过Socket和Channel的区别,其实对于这两个是最容易混淆的,我也特地去看了Netty的源码,对于Channel源码给的解释是:
在这里插入图片描述

总结起来就是一个名词:网络socket,可以说这解释说了和没说一样,博主查阅大量资料得以总结在之前的文章,这里也可以再说一遍,socket是网络连接的入口,当然这个入口也可以用来传输信息等,但是原生的socket是阻塞的IO操作,对于Channel来说更像是一种包装后的socket,在netty中用它来传输数据,而socket更像是一种入口。

在这里插入图片描述

2.task任务队列

在这里插入图片描述

task任务队列顾名思义和RabbitMQ一样,用来启动定时任务或者非阻塞业务,目的就是为了在处理复杂业务时,程序不应该阻塞等待业务处理完,而是进行下去,因为Netty本身是能够全双工消息处理,如果在某个地方阻塞那么还是与传统阻塞IO存在同样问题,所以为了解决阻塞等待,于是便有了task。

3.其他异步处理

在这里插入图片描述

与task任务队列不同的是netty中的其他异步处理还包括通道channel的开启关闭,以及callback函数,例如连接开启和关闭,在连接开启后会返回一个Future对象(有可能连接没有完成因为网络等问题),这个对象会记录该连接的所有信息,在返回Future之后,程序会直接向下执行,而不是阻塞等待连接,更像是一种监控。当然业务处理同理。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

4.Selector

在这里插入图片描述

Selector可以理解为一个注册中心,如果说有什么像它,我想nacos可以很好的诠释它,注册后的连接将会分配给NioEventLoop。

5.ChannelHandler

在这里插入图片描述
顾名思义Handler就是一个处理器,它就是用来处理所有的业务操作,包括IO操作等,但是仅负责于此,并不处理连接操作。

6.PipeLine

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

每一个workGroup拿到了由BossGroup分配到的连接,都会分发给一个空闲的NioEventLoop,然后这个空闲的NioEventLoop会将这个连接中的数据放入到PipeLine中,而PipeLine的作用就是处理业务数据,因为PipeLine中包含了很多的Handler。而在示意图中我们看到所有的Handler都是由HandlerContext包裹,这个HandlerContext就是上下文对象,顾名思义就是记录信息,包括当前的pipeline、channel、ip地址以及IO的数据等等
在这里插入图片描述

7.Unpooled

在这里插入图片描述

这是专门操作缓冲区的类,但也不是唯一的,使用平常的ByteBuf也是可以的。

8.入栈和出栈

在这里插入图片描述

对于任意一端包括服务端和客户端,都有入栈和出栈事件,这也是相对而言的。当有数据需要输出,那么经过Handler编码之后输出到Channel中这就是出栈事件,相反如果从channel中读入数据解码这就是入栈事件。

9.编码解码

在这里插入图片描述

如图所示,在网络中传输就是二进制的传输,那么编解码自然少不了。

在这里插入图片描述

解决方案:

使用其他的编解码器
在这里插入图片描述

10.TCP粘包拆包

在这里插入图片描述

由于底层TCP是无法理解上层业务数据,所以在底层是无法保证数据包不被拆分和重组的,所以只能通过上层应用协议栈设计来解决

(1)消息定长,例如每个报文的大小固定长度200字节,不够空位补空格

(2)在包尾增加回车换行符进行分割,例如FTP协议

(3)将消息分为消息头和消息体,消息头中包含表示消息总长度的字段

(4)更复杂的应用层协议。

Handler

public class TimeServerHandler extends ChannelInboundHandlerAdapter {
    private static final Logger log = Logger.getLogger(TimeClientHandler.class.getName());


    private int counter;

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buf = (ByteBuf) msg;
        byte[] req = new byte[buf.readableBytes()];
        buf.readBytes(req);
        String body = new String(req, "UTF-8").substring(0, req.length - System.getProperty("line.separator").length());
        // 每收到一条消息计数器就加1, 理论上应该接收到100条
        System.out.println("The time server receive order: " + body + "; the counter is : "+ (++counter));
        String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ?
                new Date(System.currentTimeMillis()).toString():"BAD ORDER";
        currentTime = currentTime + System.getProperty("line.separator");
        ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
        ctx.writeAndFlush(resp);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.warning("Unexpected exception from downstream: " + cause.getMessage());
        ctx.close();
    }
}

Server

public class TimeServer {

    public static final Logger log = LoggerFactory.getLogger(TimeServer.class);

    public static void main(String[] args) throws Exception {
        new TimeServer().bind();
    }


    public void bind() throws Exception {
        // NIO 线程组
        NioEventLoopGroup bossGroup = new NioEventLoopGroup();
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 1024)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) {
                            socketChannel.pipeline().addLast(new TimeServerHandler());
                        }
                    });
            // 绑定端口,同步等待成功
            ChannelFuture f = bootstrap.bind(NettyConstant.REMOTE_IP, NettyConstant.REMOTE_PORT).sync();
            log.info("Time server[{}] start success", NettyConstant.REMOTE_IP + ": " + NettyConstant.REMOTE_PORT);
            // 等待所有服务端监听端口关闭
            f.channel().closeFuture().sync();
        } finally {
            // 优雅退出,释放线程池资源
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

    }

}

ClientHander

public class TimeClientHandler extends ChannelInboundHandlerAdapter {

    private static final Logger log = Logger.getLogger(TimeClientHandler.class.getName());

    private int counter;
    private byte[] req;

    public TimeClientHandler() {
        req = ("QUERY TIME ORDER" + System.getProperty("line.separator"))
                .getBytes();
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ByteBuf message = null;
        // 循环发送100条消息,每发送一条刷新一次,服务端理论上接收到100条查询时间指令的请求
        for (int i = 0; i < 100; i++) {
            message = Unpooled.buffer(req.length);
            message.writeBytes(req);
            ctx.writeAndFlush(message);
        }
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buf = (ByteBuf) msg;
        byte[] req = new byte[buf.readableBytes()];
        buf.readBytes(req);
        String body = new String(req, "UTF-8");
        // 客户端每接收到服务端一条应答消息之后,计数器就加1,理论上应该有100条服务端日志
        System.out.println("Now is: " + body + "; the current is "+ (++counter));
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.warning("Unexpected exception from downstream: " + cause.getMessage());
        ctx.close();
    }
}

Client

public class TimeClient {

    public static final Logger log = LoggerFactory.getLogger(TimeClient.class);



    public static void main(String[] args) throws Exception {
        new TimeClient().connect(NettyConstant.REMOTE_IP, NettyConstant.REMOTE_PORT);
    }

    public void connect(final String host, final int port) throws Exception {
        // NIO 线程组
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, true)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline().addLast(new TimeClientHandler());
                        }
                    });

            // 发起异步连接操作
            ChannelFuture f = bootstrap.connect(host, port).sync();
            // 等待所有服务端监听端口关闭
            f.channel().closeFuture().sync();
        } finally {
            // 优雅退出,释放线程池资源
            group.shutdownGracefully();
        }

    }

}
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值