暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

BIO & NIO

爱做菜的程序猿 2021-09-08
305

BIO&NIO

java存在的Socket,以下是它API的类:

  • BIO

    • ServerSocket

    • Socket

  • NIO

    • ServerSocketChannel

    • SocketChannel

BIO

案例

模拟一段BIO服务器端进行Socket编程:

public class BIOServer {
   public static void start(int port) throws IOException {
       //1、建立socket连接
       ServerSocket serverSocket = new ServerSocket();
       //2、绑定和监听
       serverSocket.bind(new InetSocketAddress(port),2);
       //支持连接的端口号和连接数
       //3、起线程跑连接,或者用线程池。
       while (true){
           final Socket clientSocket = serverSocket.accept();
           System.out.println("accept");//标识接收到了请求
           new Thread(()->{
               try {
                   BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
                   PrintWriter out = new PrintWriter(clientSocket.getOutputStream(),true);
                   String line = in.readLine();//阻塞
                   while(line !=null){
                       out.println(line);
                       out.flush();
                       line=in.readLine();
                  }
                   clientSocket.close();
              } catch (IOException e) {
                   e.printStackTrace();
              }

          });
      }
  }

   public static void main(String[] args) throws IOException {
       start(2020);
  }
}

启动cmd然后和我们的程序建立连接:

telnet localhost 2020

然后执行cmd命令发现成功:

阻塞分析

红色部分代表了阻塞,下面解释一下阻塞的原因:

  • accept:阻塞是因为网络传输的问题等

  • read&parse:需要解决字节边界问题,需要截取每个请求需要的字节。当收不到请求字节了,就要切换线程。当发送的请求断断续续的,每次接收一点就要切换一次后,阻塞就会非常严重。

  • write:接收方有缓存大小,类似于窗口大小,当接收到足够的数据包才会写出去。例如,需要数据包为1-5,但只接受到了2-5,那我们会一直堵塞,直到得到数据包1。

tcp也有自己的一套流量控制和拥塞控制算法,所以tcp天生拥有背压能力,能够很天然的适应流量弹性的变化。

缺点

上下文切换:在read&parse过程中,我们需要去切割字节,获取到对应的请求,但如果此时网络不畅,那么我们接收到的封包就会断断续续的,过了一段时间后就会切换下个线程。

eg:如果A线程需要的数据是0-100,我的线程是A,B,C。刚开始A线程接收到50个数据,接下去因为网络原因无法接受到了,所以切换到了B或者C线程。这时候如果又来了10个A所需的数据又要切换到A接收,这样非常浪费时间。

总结:可以发现,当我们的连接数多了,我们传统的blocking的服务器模型,效率就很低了。为了解决BIO的上下文切换存在的问题,我们引出了NIO

NIO

核心目标:减少线程数,减少上下文切换问题。

解决办法:将accept、read&parse、write放入更为底层的部分,放入到一个线程中,由一个线程去统筹其他的线程来执行这些操作。比如说,当网络断断续续后,这个线程可以通知正在read过程中被阻塞的线程别阻塞了,切换到其他的Socket线程进行读。

下面是NIO的模型,采用操作系统事件的机制,我们将统筹所有连接的线程称之为Selector:

模型解释

负责多个Socket连接,当Socket的状态发生变化了,都会通知Selector。Selector会对所有的连接进行轮询(定时任务),做对应事件的事情,所以不会涉及到任何的浪费。

Selector API

  • channel.register(selector) :注册监听

  • while(true)+ select():轮询事件

  • selectedKeys():获得selectionKey对象,表示channel的注册信息

  • SelectionKey.attach()对selectionkey:关联任何对象

  • isReadable()/ isAcceptable()/ isWritable():判断事件类型

  • 事件类型:OP_ACCEPT/OP_READ/OP_WRITE/OP_CONNECT

整体步骤

  • 把想要被Selector监听的ServerSocket注册到channel上

  • 无限轮询,然后去查看Socket的状态

  • 一旦轮询到需要的对象,使用selectedKeys去获取对象

  • 根据事件类型(ACCEPT、READ、WRITE、CONNECT),根据这四种状态去进行相应的操作。

案例

举例说明下步骤,一共七步走,

  • 第一步:创建一个信道

  • 第二步:设置是否阻塞并设置端口号,这里要用NIO肯定是非阻塞的

  • 第三步:同BIN过程,绑定套接字地址,这里可以绑定多个,只要在后面加上.bin即可

  • 第四步:创建selector并绑定事件

  • 第五步:进行轮询,查看是否有注册过channel的状态得到了满足

  • 第六步:从selector中得到集合,但也有可能Socket状态都没改变,集合为空

  • 第七步:进入事件处理三步走

    • 从信道中获取连接

    • 同BIO过程,对其进行accept

    • 设置连接非阻塞,并且转换连接的状态

public class NIOServer {
   public static void start(int port) throws IOException {
       //1、创建一个信道
       ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
       //2、设置是否阻塞并设置端口号,这里要用NIO肯定是非阻塞的
       serverSocketChannel.configureBlocking(false);
       InetSocketAddress address = new InetSocketAddress(port);
       //3、同BIN过程,绑定套接字地址,这里可以绑定多个,只要在后面加上.bin即可
       serverSocketChannel.bind(address);
       //4、创建selector并绑定事件
       Selector selector = Selector.open();
       serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
       while(true){ //这里如果只请求一次不会出错
           //5、进行轮询,查看是否有注册过channel的状态得到了满足
           //但是这块底层会有一些bug,因为非阻塞,所以while会空转
           selector.select();
           //6、从selector中得到集合,但也有可能Socket状态都没改变,集合为空
           Set<SelectionKey> readyKeys = selector.selectedKeys();
           Iterator<SelectionKey> it = readyKeys.iterator();
           //7、进入事件处理三步走
           while (it.hasNext()){ //进入事件处理三步走
               SelectionKey key = it.next();
               if (key.isAcceptable()){
                   //(1)、从信道中获取连接
                   ServerSocketChannel server = (ServerSocketChannel) key.channel();
                   //(2)、同BIO过程,对其进行accept
                   SocketChannel socket = server.accept();
                   System.out.println("Accept !");
                   //(3)、设置连接非阻塞,并且转换连接的状态
                   socket.configureBlocking(false);
                   socket.register(selector,SelectionKey.OP_READ);//将其从accept转换成read
                   System.out.println("经历了一次状态转换过程");
              }
               if (key.isReadable()) {
                   //(1)、从信道中获取连接
                   SocketChannel socket = (SocketChannel) key.channel();
                   //(2)创建字节流,接受传入的流
                   final ByteBuffer buffer =ByteBuffer.allocate(64);
                   final  int bytesRead =socket.read(buffer);//读取流
                   if (bytesRead>0){
                       buffer.flip();//翻转缓冲区,理解成刷新缓存
                       int ret =socket.write(buffer);
                       if (ret<=0){
                           socket.register(selector,SelectionKey.OP_WRITE);
                      }
                       buffer.clear();
                  } else  if (bytesRead<0){
                       key.cancel();
                       socket.close();
                       System.out.println("Client close");
                  }
              }
               it.remove();
          }
      }
  }
   public static void main(String[] args) throws IOException {
       start(2020);
  }
}

测试一下,运行程序,打开cmd连接端口号:

telnet localhost 2020

查看结果:

这里会存在一个问题,因为NIO非阻塞,所以当没接收到Socket连接的时候会存在空转问题,空转就是while(true)就会一直执行,直到吧内存占满。

缺点

  • 空转问题(会报错):因为NIO是非阻塞的,所以当没有请求后,也不会阻塞。此时while(true)就会一直执行,直到吧内存占满。

  • 代码不好复用,如果我需要实现某一功能并没有BIO那么容易,也不好抽取出来成单独模块。

    虽然NIO有缺点,但NIO的非阻塞和单线程处理Socket对效率的提升而言非常大,所以我们还是得用。往后我们就引入了Reactor模式,即响应式编程


文章转载自爱做菜的程序猿,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论