初识NIO

anlan

共 12752字,需浏览 26分钟

 · 2023-06-20

有谁跟我一样,本以为没了疫情之后,IT行业会回暖,但事实却是今年更卷了,跳槽非常难,在这种背景下,要么你技术很牛逼,要么你学历很吊,但巧了,笔者正好是渣本加菜鸡,为了能被晚淘汰两年,现在下决心多学点东西。 从现在开始,我准备学习NIO相关知识,因为我这块是一片空白,如果你也不会,那就一起学,如果你会,那你教教我好不好?

一、BIO

什么是BIO?其实就是初学java的时候老师讲的I/O,比如InputStream、OutputStream等很多stream,它们都在java.io包下:

152931d93e9b099b8c38bec806d4a74a.webp

BIO的全称叫Blocking IO,翻译过来就是阻塞IO,为了让你们有个直观的认识,先看段程序:

      
        package com.example.io;
      
      
        
          
import java.io.IOException; import java.net.ServerSocket; import java.net.Socket;
public class SocketServer { public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(9000);         while (true) { System.out.println("等待连接..."); Socket socket = serverSocket.accept(); System.out.println("有客户端连接了..."); handle(socket); } }
private static void handle(Socket socket) throws IOException { byte[] bytes = new byte[1024]; System.out.println("准备read...");         int read = socket.getInputStream().read(bytes); System.out.println("read完毕...");         if (read != -1) { System.out.println("接收到客户端的数据: " + new String(bytes, 0, read)); } socket.getOutputStream().flush(); } }

这段程序就是java基础中的网络编程,它其实就是基于BIO开发的。

1.1、启动服务端

在第15行打个断点,运行程序

0bbca8a3748e98f26d4f5d58a271716a.webp

但你会发现程序并没有运行到第15行,当然肯定也不会卡在第12行,那就是卡在第13行了,BIO的B就是这个意思,它会阻塞住。

1.2、客户端通过telnet连接服务端 0034c8d2b66c94bee14fe63661038034.webp

回车后你会发现,程序走了

5c866e148039e8e4cced4bf8b58a361b.webp1.3、通过客户端发送数据

上面的调试到了handle方法,意思就是要处理这个请求,在23行打个断点,放开请求

223e2a44499c7c10071be00aab6de409.webp

你会发现又卡住了,很明显这次卡在了第22行,这行代码的意思是在等待客户端发送数据,因为刚才的telnet只是连接上了客户端,并没有发送数据,接下来通过telnet发送数据,方法是按下ctrl+]

b5bbfdcd0aa0a0a11e37bf81eae973a7.webp

如果对telnet不熟悉的话,可以通过help命令查看一下:

5ec24c5e3eb7363e8346ab2133634e1f.webp

接下来通过send发送一条数据:send xiaoP

db7a1363fe2591c6fd87b0ce469d72ec.webp

回车后你会发现程序走了

34d5827a90772ab61b83489d4447edff.webp

断点放开后,服务端成功收到了客户端发送的内容:

01a0806e9beaac9f555e14b632129f52.webp

以上就是简单的BIO的演示过程,可以看到,accept和read方法都是阻塞的,其实不仅阻塞,也是同步的,把断点都去掉,然后运行程序

f3fc393a6d70cbe1b67995e5ba78d81b.webp

然后打开两个客户端

df72ad8ad4cce1ae506b1b26c0b88fcf.webp

然后在左边的窗口按ctrl+],再在右边的窗口按ctrl+]

594fb4ae138742bc3d156f96a2831d58.webp

然后在右边的窗口先send数据

12ef4f20021cdabd4961bcae50236882.webp

可以看到,我右边已经回车发送了,但是服务端并没有打印xiaoP2,

7338201645eb2ed51ab662556a82e5f2.webp

这时候我在左边的窗口回车

ee7634a4391b6c480ecd47aa9ad3cfbb.webp

这时候控制台打印了

45188d50ebda2b5c9878bd64ef427181.webp

可以看到,服务端在处理请求的时候是串行的,也就是单线程,必须处理完第一个请求才会处理第二个,这样的话就会有一个严重的问题,假如说第一个请求因为某些原因迟迟未处理,那后面的请求也都会被阻塞,那既然它是单线程,我们可以不可以通过多线程来处理,一个线程处理一个请求,这样就不会被阻塞了?稍微改下程序

      
        public static void main(String[] args) throws IOException {
      
      
                ServerSocket serverSocket = new ServerSocket(9000);
      
      
                while (true) {
      
      
                    System.out.println("等待连接...");
      
      
                    Socket socket = serverSocket.accept();
      
      
                    System.out.println("有客户端连接了...");
      
      
                    new Thread(new Runnable() {
      
      
                        @Override
      
      
                        public void run() {
      
      
                            try {
      
      
                                handle(socket);
      
      
                            } catch (IOException e) {
      
      
                                e.printStackTrace();
      
      
                            }
      
      
                        }
      
      
                    }).start();
      
      
                }
      
      
            }
      
    

重新跑起来,然后重复上面的步骤,你就会发现xiaoP2打印出来了。

但是这样还是会有问题,这样的程序处理几个请求可以,实际开发中,如果一次来几十万甚至上百万个请求,你能开几十万个线程吗?这种cpu不存在吧?那你可能会说,用线程池呢?那假如你线程池开了300个线程,一次处理300个请求,等你处理完几十万个请求,客户端那边早超时了,这还不是最致命的,上面演示了,如果一个客户端连接了服务端但是不急着发送数据,它会一直占用着线程资源不释放。

总结下BIO的缺点:

1、需要开大量的线程处理请求

2、存在阻塞问题

3、线程切换造成的开销

4、连接但不发送数据,会占用线程不释放

所以BIO的使用场景是很少的,除非你的项目需要的连接数很少。

那有没有办法解决这种问题,肯定是有的,其实在jdk1.4之后,引入了nio,其中n表示none blocking,非阻塞。 e1a02b39ba689ec0210f07ef4b6fca42.webp二、初识NIO
2.1、最简单的NIO程序

我们先用nio的API写一个简单的程序并启动它

      
        package com.example.nio;
      
      
        
          
import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.ArrayList; import java.util.Iterator; import java.util.List;
public class NioServer {
/** * 存放SocketChannel */ private static List<SocketChannel> list = new ArrayList<>();
public static void main(String[] args) throws IOException { //nio中叫ServerSocketChannel,bio中叫ServerSocket ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); //绑定到9000端口 serverSocketChannel.socket().bind(new InetSocketAddress(9000)); //设置为非阻塞 serverSocketChannel.configureBlocking(false); System.out.println("服务启动成功!"); while (true) { //如果上面的configureBlocking设置为true,那这行代码和bio中的accept是一样的,都是阻塞的 SocketChannel socketChannel = serverSocketChannel.accept(); if (socketChannel != null) { System.out.println("连接成功!"); //设置为非阻塞 socketChannel.configureBlocking(false); list.add(socketChannel); } //遍历连接进行数据读取 Iterator<SocketChannel> iterator = list.iterator(); while (iterator.hasNext()) { SocketChannel next = iterator.next(); ByteBuffer byteBuffer = ByteBuffer.allocate(128); int len = next.read(byteBuffer); //如果有数据,把数据打印出来 if(len > 0) { System.out.println("接收到数据:" + new String(byteBuffer.array())); } else if (len == -1) { //如果客户端断开,把socket从集合中移除 iterator.remove(); System.out.println("客户端断开连接"); } } } } }
513e2754f8e2039ab4f37045aa4b027c.webp2.2、通过客户端连接

还是和bio中的调试一样,开两个客户端,先连接第一个客户端,但是先在第二个客户端中发送数据

209a3825027225b07fba805b02c32fe7.webp909e8ba78796fbe02976de7ffc582450.webp

可以看到,没有bio那种阻塞的问题了,不管你开再多的连接,都可以处理。为什么?最本质的区别就是accept和read方法不再是阻塞的了,这样的话主线程其实一直在做循环,不断的在找客户端连接,不断的在读数据(如果客户端发送有的话)。你是不是觉得这样的程序就没问题了?怎么可能,试想一下,假如有1万个连接,但是只有一个连接发数据了,那为了读取这一个连接的数据,每次都要循环一遍这一万个连接?合适吗?那如何优化?优化的目的很明确,就是我们只需要找到有数据的那个连接就行了,具体怎么做?nio这么强大,它肯定已经做好了,这个东西叫多路复用器,也就是大名鼎鼎的selector。

2.3、通过selector改良程序
      
        package com.example.nio;
      
      
        
          
import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.*; import java.util.Iterator; import java.util.Set;
public class NioSelectorServer {
public static void main(String[] args) throws IOException { //nio中叫ServerSocketChannel,bio中叫ServerSocket ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); //绑定到9000端口 serverSocketChannel.socket().bind(new InetSocketAddress(9000)); //设置为非阻塞 serverSocketChannel.configureBlocking(false);
/** * 打开selector处理channel,即创建epoll */ Selector selector = Selector.open(); //将serverSocketChannel注册到selector上,并且selector对客户端的accept连接操作感兴趣 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务启动成功!");
while (true) { //阻塞等待需要处理的事件发生,这里是阻塞的,有事件的话才会往下执行 selector.select(); //获取selector中注册的全部事件的SelectionKey实例 Set selectionKeys = selector.selectedKeys(); Iterator iterator = selectionKeys.iterator(); //遍历selectionKey对事件进行处理 while (iterator.hasNext()) { SelectionKey next = iterator.next(); //如果是OP_ACCEPT事件,则进行连接获取和事件注册 if (next.isAcceptable()) { ServerSocketChannel channel = (ServerSocketChannel) next.channel(); SocketChannel socketChannel = channel.accept(); socketChannel.configureBlocking(false); //这里只注册了读事件,如果需要给客户端发送数据,可以注册写事件 socketChannel.register(selector, SelectionKey.OP_READ); System.out.println("客户端连接成功!"); } else if (next.isReadable()) { //如果是OP_READ事件,则进行读取和打印 SocketChannel channel = (SocketChannel) next.channel(); ByteBuffer byteBuffer = ByteBuffer.allocate(128); int len = channel.read(byteBuffer); //如果有数据,把数据打印出来 if (len > 0) { System.out.println("接收到数据:" + new String(byteBuffer.array())); } else if(len == -1) { //如果客户端断开连接,关闭Socket System.out.println("客户端断开连接"); channel.close(); } } //从事件集合里删除本次处理的key,防止下次selector重复处理 iterator.remove(); } } } }

启动程序,观察

b10c764dc848a67c8ac72d71b9eae2d3.webp

可以看到,程序阻塞在了第31行,selector解决了程序一直空跑的情况,这时候打开一个客户端,运行telnet localhost 9000,会发现程序往下执行了,telnet相当于一个accept事件,往下debug会进入next.isAcceptable判断中,放开断点,你会发现程序又阻塞在31行,这时候发送数据

29c80c199c2bb2b70c9b52f3f2b7e7a4.webpdf9cb10c89a87b9c09a63d5a5e31ed17.webp

程序又往下走了

3696b75d97f8860c59f465304503cdb9.webp

放开断点,这时候会进入到next.isReadable中,因为客户端给服务端发数据,对服务端来说是一个读事件

d6114b8eb2780d284e4c96029c10cd8b.webp

放开断点,让程序运行

ea6c9d0141f5ef9220dd9f3248a41dbe.webp

打印了数据,并且程序继续阻塞在selector.select这行代码。这是不是就实现了我们想要的功能,有数据的时候才会处理,那原理是什么?看一张图

878fd270b7c41946902862afe6c0fc0a.webp

对照着代码看这一张图

1、服务端启动的时候,注册到selector,监听accept事件,返回selectionKey 2、客户端连接服务端的时候,会触发客户端的accept监听,这时候很关键啊,服务端也就是ServerSocketChannel调用accept方法后会返回一个SocketChannel,这玩意和客户端是一一对应的 3、然后将SocketChannel注册到selector,监听read事件,同样也会返回selectionKey,客户端向服务端发数据,站在服务端的角度来看就是read,它要read客户端发来的数据 4、客户端向服务端发送数据,触发read监听,通过SocketChannel将数据读取出来
注:selectionKey其实就是对应的ServerSocketChannel或者SocketChannel,通过selectionKey可以拿到ServerSocketChannel或SocketChannel 以上就是nio运行的大概流程。
那底层是怎么实现的?下篇文章见!
浏览 7
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报