BIO 与 NIO 初探

/ BIONIONetty / 没有评论 / 3767浏览

BIO 与 NIO初探

BIO示例代码

@Test
public void testBIO() throws IOException {
  ServerSocket serverSocket = new ServerSocket(8080);
  while (true){
    //Thread block 1
    //Listens for a connection to be made to this socket and accepts it.
    //The method blocks until a connection is made.
    Socket conn = serverSocket.accept();
    service.submit((Callable<String>)()->{
      byte []request = new byte[1024];
      //Thread block 2
      // This method blocks until input data is available, 
      // end of file is detected, or an exception is thrown.
      conn.getInputStream().read(request);
      System.out.println(new String(request));
      return null;
    });
  }
}

简要分析:

NIO 示例代码

    /**
     * 此段代码只做示例用,功能并不完整,也没有考虑有所要处理的情况
     * @throws IOException
     */
    @Test
    public void testNIO() throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //显示的设置不使用BIO而是使用NIO模式
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress(8080));
        System.out.println("NIO 服务启动");
        //JDK 底层操作系统,多路复用,事件机制,操作系统会告诉JVM(selector和操作系统底层的多路复用机制打交道)
        Selector selector = Selector.open();
        //主线程循环检测操作系统是否有新的连接
        while (true) {
            //获取新链接
            SocketChannel socketChannel = serverSocketChannel.accept();//非阻塞,不管有没有都要返回,没有返回null
            if(socketChannel == null){
                continue;
            }
            //显示的设置不使用BIO而是使用NIO模式
            socketChannel.configureBlocking(false);
            //通过事件机制,当有数据过来的时候,再去处理,让 selector 帮我们去监控OP_READ事件,或者叫观察者模式(有数据传输)
            socketChannel.register(selector, SelectionKey.OP_READ);

            //查询事件
            selector.select();
            Set<SelectionKey> eventKeys = selector.selectedKeys();
            //循环遍历事件
            Iterator<SelectionKey> iterator = eventKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey event = iterator.next();
                //SocketChannel channel = (SocketChannel)event.channel();//可以从时间知道是哪个连接

                if (event.isReadable()) {//一个连接有数据过来了
                    service.submit((Callable<String>) () -> {
                        //读写数据,处理请求,返回响应
                        //ByteBuffer byteBuffer1 = ByteBuffer.allocateDirect(1024);//堆外内存,直接映射
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);//封装的数组,对外内存
                        socketChannel.read(byteBuffer);
                        //转为读取模式
                        byteBuffer.flip();
                        System.out.println(new String(byteBuffer.array()));
                        //      socketChannel.write()
                        return null;
                    });
                }
            }
        }
    }

主要对比

NIO 框架

为什么要使用开源框架?

这个问题几乎可以当做废话,框架肯定要比一些原生的API封装了更多地功能,重复造轮子在追求效率的情况并不是明智之举。那么先来说说NIO有什么缺点吧:

  1. NIO的类库和API还是有点复杂,比如Buffer的使用
  2. Selector编写复杂,如果对某个事件注册后,业务代码过于耦合
  3. 需要了解很多多线程的知识,熟悉网络编程
  4. 面对断连重连、保丢失、粘包等,处理复杂
  5. NIO存在BUG,根据网上言论说是selector空轮训导致CPU飙升,具体有兴趣的可以看看JDK的官网

那么有了这些问题,就急需一些大牛们开发通用框架来方便劳苦大众了。最受欢迎NIO框架就是MINA和Netty了。

MINA VS Netty

  1. MINA和Netty的主要贡献者都是同一个人——Trustin lee,韩国Line公司的。
  2. MINA于2006年开发,到14、15年左右,基本停止维护
  3. Nety开始于2009年,目前仍由苹果公司的norman maurer在主要维护。
  4. Norman Maurer是《Netty in Action》一书的作者

讲了一大堆的废话之后,总结来说就是——Netty有前途,学它准没错。

Netty介绍

按照定义来说,Netty是一个异步、事件驱动的用来做高性能、高可靠性的网络应用框架。主要的优点有:

  1. 框架设计优雅,底层模型随意切换适应不同的网络协议要求
  2. 提供很多标准的协议、安全、编码解码的支持
  3. 解决了很多NIO不易用的问题
  4. 社区更为活跃,在很多开源框架中使用,如Dubbo、RocketMQ、Spark、Spring-data-redis等

主要支持的功能或者特性有:

2020328112844-netty

  1. 底层核心有:Zero-Copy-Capable Buffer,非常易用的灵拷贝Buffer(这个内容很有意思,稍后专门来说);统一的API;标准可扩展的时间模型
  2. 传输方面的支持有:管道通信、Http隧道、TCP与UDP
  3. 协议方面的支持有:基于原始文本和二进制的协议;解压缩;大文件传输;流媒体传输;protobuf编解码;安全认证;http和websocket

Netty服务器小例子

基于Netty的服务器编程可以看做是Reactor模型:

202032811311-Reactor

即包含一个接收连接的线程池(也有可能是单个线程,boss线程池)以及一个处理连接的线程池(worker线程池)。boss负责接收连接,并进行IO监听;worker负责后续的处理。为了便于理解Netty,直接看看代码:

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

import java.net.InetSocketAddress;
import java.nio.charset.Charset;

public class NettyNioServer {
    public void serve(int port) throws InterruptedException {
        final ByteBuf buffer = Unpooled.unreleasableBuffer(Unpooled.copiedBuffer("Hi\r\n", Charset.forName("UTF-8")));
		// 第一步,创建线程池
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try{
	        // 第二步,创建启动类
            ServerBootstrap b = new ServerBootstrap();
            // 第三步,配置各组件
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .localAddress(new InetSocketAddress(port))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                                @Override
                                public void channelActive(ChannelHandlerContext ctx) throws Exception {
                                    ctx.writeAndFlush(buffer.duplicate()).addListener(ChannelFutureListener.CLOSE);
                                }
                            });
                        }
                    });
            // 第四步,开启监听
            ChannelFuture f = b.bind().sync();
            f.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully().sync();
            workerGroup.shutdownGracefully().sync();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        NettyNioServer server = new NettyNioServer();
        server.serve(5555);
    }
}

代码非常少,而且想要换成阻塞IO,只需要替换Channel里面的工厂类即可:

public class NettyOioServer {
    public void serve(int port) throws InterruptedException {
        final ByteBuf buf = Unpooled.unreleasableBuffer(Unpooled.copiedBuffer("Hi\r\b", Charset.forName("UTF-8")));

        EventLoopGroup bossGroup = new OioEventLoopGroup(1);
        EventLoopGroup workerGroup = new OioEventLoopGroup();

        try{
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)//配置boss和worker
                    .channel(OioServerSocketChannel.class) // 使用阻塞的SocketChannel
         ....

总结

NIO在接收请求方式上,无疑是要高效于BIO,原因并非是不阻塞,我认为NIO一样是阻塞的,只是方式不同,先来的有效请求先处理,先阻塞时间短的。此时间可用于等待等待时间长的。

在处理请求上,NIO和BIO并没有什么不同,主要看线程池规划是否和理。NIO相对BIO在密集型计算的模型下,可以用更少的线程,甚至单线程