NIO 与 Netty

NIO

什么是 NIO

NIO全称Non Blocking IO,同步非阻塞IO框架。服务器实现模式为一个线程可以处理多个请求(连接),客户端发送的连接请求都会注册到多路复用器selector上,多路复用器轮询到连接有IO请求就进行处理

为什么用 NIO

常见的网络服务中,如果每一个客户端都维持一个与登陆服务器的连接。那么服务器将维护多个和客户端的连接以出来和客户端的contnect 、read、write ,特别是对于长链接的服务,有多少个c端,就需要在s端维护同等的IO连接。这对服务器来说是一个很大的开销(著名的C10K问题)

NIO 怎么玩

Reactor模式的单线程版-NIO服务端

public static void main(String[] args) throws IOException {
    ServerSocketChannel socketChannel = ServerSocketChannel.open();
    // 绑定端口
    socketChannel.bind(new InetSocketAddress(8080));
    // 声明异步
    socketChannel.configureBlocking(false);
    // 声明多路复用器,创建 linux下的epoll文件描述对象
    Selector selector = Selector.open();
    // 注册连接事件
    socketChannel.register(selector,SelectionKey.OP_ACCEPT);
    while (true){
        // 通过操作系统的epoll,选择触发事件的selectionKey 加入set集合中
        selector.select();
        final Set<SelectionKey> selectionKeys = selector.selectedKeys();
        final Iterator<SelectionKey> iterator = selectionKeys.iterator();
        // 遍历触发事件的selectionKey,selectionKey中包含 socketChannel
        while (iterator.hasNext()){
            final SelectionKey next = iterator.next();
            // 判断事件类型
            if(next.isAcceptable()){
                // 如果是连接事件,给对应的SocketChannel注册读取事件
                final ServerSocketChannel channel = (ServerSocketChannel) next.channel();
                final SocketChannel accept = channel.accept();
                accept.configureBlocking(false);
                accept.register(selector,SelectionKey.OP_READ);
            }else if (next.isReadable()){
                // 如果是读取事件,直接读取
                final SocketChannel channel = (SocketChannel) next.channel();
                ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                final int read = channel.read(byteBuffer);
                System.out.println(new String(byteBuffer.array(),0,read));
            }
        }
        // 移除set集合中已经处理的socketChannel,避免二次处理
        iterator.remove();
    }
}
复制代码

如何运行并测试,该NIO服务端呢?

Windows 环境下,可以通过命令行工具的 telnet 命令,连接服务端并发送数据;
telnet localhost 8080 连接服务端
ctrl+] 打开发送页面,并通过 send 命令发送消息到服务端

image.png

NIO 是如何接受socket请求并读取数据?

在NIO服务端启动时,会通过Selector selector = Selector.open()创建一个linux epoll/windows select实例对象,然后再selector.select()时,将事件、channel注册到epoll中并等待事件发生,如果事件发生会从操作系统内部的就绪事件列表中rdList【当操作系统感知到事件发生时,会将事件加入到就绪事件列表中rdList】,选择对应的事件加入到SelectionKey中,然后进行处理。

NIO解决了BIO什么痛点,还有什么缺陷

  • NIO 解决了
    • BIO 线程阻塞,读写阻塞,线程等待时间过长问题
    • 客户端连接数过多导致的服务端CPU过高,线程浪费过多问题(C10K)
  • NIO 还有以下缺陷
    • 没有从本质上解决C10K问题【如果存在10万连接,同时读写数据,单selector,NIO的循环也需要很久】
    • selector.select(),存在空轮询bug,当select()方法没有获取事件,也有可能不阻塞,直接向下执行,且一次出现后续会一直出现。
    • 代码编写复杂,需要大量异常IO处理

Netty

Netty 是基于NIO的,运用Reactor模型设计的高性能、异步事件驱动的网络应用框架。

Netty 解决了 NIO 的哪些痛点

  • 解决了selector.select()的 空轮询bug
  • 简化了开发,netty内部已经对断线重连、 网络闪断、心跳处理、半包读写、 网络拥塞和异常流等进行了处理。
  • netty内部初始化多个selector,从而避免C10K问题

Netty 怎么玩

Netty 服务端

public class NettyService {
    public static int port = 8080;
    public static void main(String[] args) {
        // 创建接受accept事件的selector线程组
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        // 创建处理read/write等业务处理事件的selector线程组
        EventLoopGroup workerGroup = new NioEventLoopGroup(8);
        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        // 业务处理handler
                        socketChannel.pipeline().addLast(new DefaultChannelInboundHandlerAdapter());
                    }
                })
                .option(ChannelOption.SO_BACKLOG, 128)
                .childOption(ChannelOption.SO_KEEPALIVE, true);
        try {
            ChannelFuture future = bootstrap.bind(port).sync();
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
}
复制代码

服务端业务处理handler

public class DefaultChannelInboundHandlerAdapter extends ChannelInboundHandlerAdapter {
    protected static ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
    @Override
    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
        System.out.println("客户端上线:"+ ctx.channel().remoteAddress());
        channels.add(ctx.channel());
    }

    @Override
    public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
        System.out.println("客户端下线:"+ ctx.channel().remoteAddress());
        channels.remove(ctx.channel());
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf byteBuf = (ByteBuf) msg;
        String rtn = "客户端:"+ ctx.channel().remoteAddress()+":"+ byteBuf.toString(CharsetUtil.UTF_8);
        channels.forEach(channel -> channel.writeAndFlush(Unpooled.wrappedBuffer(rtn.getBytes(StandardCharsets.UTF_8))));
    }
}
复制代码

Netty 客户端

public class NettyClient {
    public static void main(String[] args) {
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(workerGroup)
                .channel(NioSocketChannel.class)
                .option(ChannelOption.SO_KEEPALIVE, true)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    public void initChannel(SocketChannel ch) throws Exception {
                        ch.pipeline().addLast(new StringEncoder()).addLast(new StringDecoder())
                                .addLast(new DefaultClientHandlerAdapter());
                    }
                });
        try {
            ChannelFuture connect = bootstrap.connect("127.0.0.1", 8080).sync();
            Channel channel = connect.channel();
            System.out.println("===="+channel.localAddress()+"====");
            Scanner scanner = new Scanner(System.in);
            while (scanner.hasNext()){
                String next = scanner.next();
                channel.writeAndFlush(next);
            }
            channel.closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            workerGroup.shutdownGracefully();
        }
    }
}
复制代码

客户端业务处理handler

public class DefaultClientHandlerAdapter extends ChannelInboundHandlerAdapter{
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("客户端接受到消息");
        System.out.println(msg.toString());
    }

    @Override
    public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
        System.out.println("客户端写入消息");
    }
}
复制代码

如何运行并测试,以上netty代码

先运行服务段main方法,然后运行客户端main方法
就可以通过客户端控制台输入内容发送到服务端

Netty 的线程模型

在网上看到一张图片,感觉非常到位!(服务端)

image.png

  • 根据 new NioEventLoopGroup() 的源码,不难发现NioEventLoopGroup其实是一个线程组对象,内部的线程池对象包含 selector 和 taskQueue;
  • bossGroup 负责accept事件,workerGroup负责读写事件
  • NioEventLoop 其实是一个线程
  • pipeline 是处理事件的业务管道
  • ChannelHandler 是业务管道中的具体的处理类,以客户端应用程序为例,如果事件的运动方向是从客户端到服务端的,那么我们称这些事件为出站的,即客户端发送给服务端的数据会通过pipeline中的一系列ChannelOutboundHandler(ChannelOutboundHandler调用是从tail到head方向逐个调用每个handler的逻辑),并被这些Handler处理,反之则称为入站的,入站只调用pipeline里的ChannelInboundHandler逻辑(ChannelInboundHandler调用是从head到tail方向逐个调用每个handler的逻辑)。

image.png

Netty 粘包拆包

TCP 做为传输层协议,并不理解上层业务数据的具体含义,它只会根据TCP 缓冲区的大小,进行数据包的划分。比如缓存区的大小是10k,你发送了12k的数据,TCP会将这个12K数据,拆分成10K+2K【拆包】,先发送10k,然后等下一个数据,取8K与上一个数据的2K组成10K【粘包】再发送。

Netty 如何解决

  1. 消息定长度,传输的消息大小固定长度,如,每次发送100字节,不足的补空字符
  2. 每次消息末尾添加特殊字符,然后再消息接受端进行分割
  3. 发送消息的同时发送长度,先发送长度,然后再发送消息。消息接收端,接受到消息长度,然后等待下一个数据包到达,获取指定长度。

Netty提供了多个解码器,可以进行分包的操作,如下:

  • LineBasedFrameDecoder (回车换行分包)
  • DelimiterBasedFrameDecoder(特殊分隔符分包)
  • FixedLengthFrameDecoder(固定长度报文来分包)

Netty 断线自动重连实现

在客户端实现再客户端连接服务端时,添加监听器,监听连接状态,如果连接失败,再次重连
代码实现:

ChannelFuture cf = bootstrap.connect(host, port);
cf.addListener(new ChannelFutureListener() {
    @Override
    public void operationComplete(ChannelFuture future) throws Exception {
        if (!future.isSuccess()) {
            //重连交给后端线程执行
            future.channel().eventLoop().schedule(() -> {
                try {
                    connect();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, 3000, TimeUnit.MILLISECONDS);
        }
    }
});
//对通道关闭进行监听
cf.channel().closeFuture().sync();
复制代码

Netty 怎么解决 selector.select() 的空轮询 bug

Netty 创建了一个int selectCnt = 0,在执行select时,selectCnt+1,如果select获取到的事件数量大于0时,selectCnt 设置为 0,当selectCnt大于设置的阈值512【io.netty.selectorAutoRebuildThreshold 可以自己设置】时,Netty会新建一个selector,并将出现bug的selector上的注册事件重新注册到新建的selector上,同时关闭出bug的selector。

源码:

    protected void run() {
        int selectCnt = 0;
        for (;;) {
            try {
                int strategy;
                try {
                    strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
                    switch (strategy) {
                    case SelectStrategy.CONTINUE:
                        continue;
                    case SelectStrategy.BUSY_WAIT:
                    case SelectStrategy.SELECT:
                        // 省略部分为select的一些获取逻辑
                    default:
                    }
                } catch (IOException e) {
                    rebuildSelector0();
                    selectCnt = 0;
                    handleLoopException(e);
                    continue;
                }
                selectCnt++;
                cancelledKeys = 0;
                needsToSelectAgain = false;
                final int ioRatio = this.ioRatio;
                boolean ranTasks;
				// 省略部分不相干代码
                if (ranTasks || strategy > 0) {
                    if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS && logger.isDebugEnabled()) {
                        logger.debug("Selector.select() returned prematurely {} times in a row for Selector {}.",
                                selectCnt - 1, selector);
                    }
                    selectCnt = 0;
                } else if (unexpectedSelectorWakeup(selectCnt)) { // 这行代码是关键!
                    selectCnt = 0;
                }
            } catch (CancelledKeyException e) {
                // Harmless exception - log anyway
                if (logger.isDebugEnabled()) {
                    logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector {} - JDK bug?",
                            selector, e);
                }
            } catch (Error e) {
				// 省略异常处理代码
            }
        }
    }
复制代码

unexpectedSelectorWakeup方法:

private static final int SELECTOR_AUTO_REBUILD_THRESHOLD 
        = SystemPropertyUtil.getInt("io.netty.selectorAutoRebuildThreshold", 512);

private boolean unexpectedSelectorWakeup(int selectCnt) {
    if (Thread.interrupted()) {
        return true;
    }
    if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
            selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
        rebuildSelector();
        return true;
    }
    return false;
}


作者:疯狂小周
链接:https://juejin.cn/post/6956516753752981535
来源:掘金
 

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值