IO线程模型

文章目录

  • IO线程模型
    • 一、BIO
      • 1、概念
      • 2、Demo
        • 2.1、Demo1.0
        • 2.2、Demo2.0
        • 2.3、小结
    • 二、NIO
      • 1、概念
      • 2、Demo
        • 2.1、Demo1.0
        • 2.2、Demo2.0

IO线程模型

一、BIO

1、概念

       BIO 全称 Block-IO 是一种**同步且阻塞**的通信模式。是一个比较传统的通信方式,模式简单,使用方便。但并发处理能力低,通信耗时,依赖网速。

       同步: 可以理解为干这件事中间,不能干其他事
       阻塞: 可以理解为有事把游戏暂停了,干完事了再来继续游戏

概念不好理解,直接上Demo

2、Demo

2.1、Demo1.0

public class SocketServer {
    private static void handler(Socket clientSocket) throws Exception {
            byte[] bytes = new byte[1024];
            System.out.println("准备read。。。");
            int read = clientSocket.getInputStream().read(bytes);
            System.out.println("read完毕!");
            if(read != -1){
                System.out.println("接收到客户端的数据:" + new String(bytes, 0, read));
            }
    }

    public static void main(String[] args) throws Exception {
        ServerSocket serverSocket = new ServerSocket(8001);
        while(true){
            System.out.println("等待连接。。。");
            // 阻塞住了
            Socket clientSocket = serverSocket.accept();

            System.out.println("有客户端连接了。。。");

            handler(clientSocket);
        }
    }
}

       先理解一下这段代码里面的 Socket clientSocket = serverSocket.accept(); int read = clientSocket.getInputStream().read(bytes);,这两端代码都是阻塞的,也就是当执行到这里的时候,就会卡住了,暂时不会执行下面的东西了

启动的时候,这里控制台输出完等待连接以后,就会卡住了

IO线程模型_第1张图片

然后这里使用一个Telnet的东西,百度一下即可
IO线程模型_第2张图片
使用Telnet搭建一个客户端连接到上面的服务端中

IO线程模型_第3张图片

我们这个时候再开一个Telnet客户端连接到上面的服务端

IO线程模型_第4张图片

这时候再第一个Telnet中随便摁下键盘,你会发现控制台输出

IO线程模型_第5张图片

我觉的这里有两点:

  • 一是当你两个Telnet连接的时候,只有第一个Telnet先显示连接,另一个Telnet没有显示,同一时间只能处理一件事(这感觉是同步
  • 二是第一个Telnet接收完之后又显示出来了第二个Telnet的连接信息,说明第二个Telnet没有被抛弃,等到第一个搞完了再轮到它(这应该就是阻塞

现在应该能体验到这种方式比较局限,它适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4 以前的唯一选择,但程序直观简单易理解。

2.2、Demo2.0

稍微改进一些

public class SocketServer {

    private static void handler(Socket clientSocket) throws Exception {
            byte[] bytes = new byte[1024];
            System.out.println("准备read。。。");
            int read = clientSocket.getInputStream().read(bytes);
            System.out.println("read完毕!");
            if(read != -1){
                System.out.println("接收到客户端的数据:" + new String(bytes, 0, read));
            }
    }

    public static void main(String[] args) throws Exception {
        ServerSocket serverSocket = new ServerSocket(8001);
        while(true){
            System.out.println("等待连接。。。");
            // 阻塞住了
            Socket clientSocket = serverSocket.accept();

            System.out.println("有客户端连接了。。。");

            new Thread(() -> {
                try {
                    handler(clientSocket);
                }catch (Exception e){
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

修改的地方也就是多开了线程去连接下一个客户端,也就是每连接一个客户端就会新开一个线程(也就是打破了阻塞),使用两个Telnet试一试
IO线程模型_第6张图片

并且当你在两个Telnet嗯东西的时候,控制台也有反应

2.3、小结

两种方式都是BIO:

  1. 第一种就是在服务端处理完第一个客户端的所有事件之前,无法为其他服务端提供服务
  2. 第二种弥补了第一种的缺点,但是会产生大量空闲线程,徒增压力,浪费资源

这样通过通过多线程的方式,确实可以解决一些问题,但是还是会带来一些新的问题,所以要寻求更好的解决方法

二、NIO

1、概念

      Java NIO,全程 Non-Block IO ,是 Java SE 1.4 版以后,针对网络传输效能优化的新功能。是一种非阻塞同步的通信模式。

2、Demo

2.1、Demo1.0

public class NioServer {
    // 保存客户端连接
    static List<SocketChannel> channelList = new ArrayList<>();

    public static void main(String[] args) throws IOException {
        // 创建NIO ServerSocketChannel,与BIO的serverSocket类似
        ServerSocketChannel serverSocket = ServerSocketChannel.open();
        serverSocket.socket().bind(new InetSocketAddress(8001));

        // 设置ServerSocketChannel为非阻塞
        serverSocket.configureBlocking(false);
        System.out.println("服务启动成功");

        while (true) {
            // 非阻塞模式accept方法不会阻塞,否则会阻塞
            // NIO的非阻塞是由操作系统内部实现的,底层调用了linux内核的accept函数
            SocketChannel socketChannel = serverSocket.accept();

            if (socketChannel != null) { // 如果有客户端进行连接
                System.out.println("连接成功");
                // 设置SocketChannel为非阻塞
                socketChannel.configureBlocking(false);
                // 保存客户端连接在List中
                channelList.add(socketChannel);
            }

            // 遍历连接进行数据读取 10w - 1000 读写事件
            Iterator<SocketChannel> iterator = channelList.iterator();

            while (iterator.hasNext()) {
                SocketChannel sc = iterator.next();
                ByteBuffer byteBuffer = ByteBuffer.allocate(128);
                // 非阻塞模式read方法不会阻塞,否则会阻塞
                int len = sc.read(byteBuffer);
                // 如果有数据,把数据打印出来
                if (len > 0) {
                    System.out.println(Thread.currentThread().getName() + " 接收到消息:" + new String(byteBuffer.array()));
                } else if (len == -1) { // 如果客户端断开,把socket从集合中去掉
                    iterator.remove();
                    System.out.println("客户端断开连接");
                }
            }
        }
    }
}

自己看懂就可以,看不懂,看我下面

IO线程模型_第7张图片

怎么去理解这个非阻塞是什么意思呢?你Debug启动一下项目然后,在SocketChannel socketChannel = serverSocket.accept();打一个断点,然后点下面那个按钮,你就会发现它会一直循环,这就是非阻塞了

IO线程模型_第8张图片
然后当我们连接一个客户端的时候

IO线程模型_第9张图片
IO线程模型_第10张图片

这里就可以看出来,accept后,通过不断的轮询channelist中的连接,有则打印出来,没有就继续accept,没有中间阻塞的情况,这里也没有使用多线程,也就是说用一个线程,完成了BIO那里开多个线程完成的事情

这里可以同时开几个Telnet,然后Debug启动服务端,进行一下联调,差不多就能理解点了

这里会发现还有优化的空间,如果我们这里连接了10w个客户端,但是只有1w个客户端有真正的事件发生,我们的关注点应该在那1w个上面,如果我们每次都要去遍历这10w个客户端的话,很头疼的

2.2、Demo2.0

这个就是解决了上面的那个问题,使用了多路复用器

public class NioSelectorServer {
    public static void main(String[] args) throws IOException {
        int OP_ACCEPT = 1 << 4;
        System.out.println(OP_ACCEPT);

        // 创建NIO ServerSocketChannel
        ServerSocketChannel serverSocket = ServerSocketChannel.open();
        serverSocket.socket().bind(new InetSocketAddress(8001));

        // 设置ServerSocketChannel为非阻塞
        serverSocket.configureBlocking(false);

        // 打开Selector处理Channel,即创建epoll
        Selector selector = Selector.open();

        // 把ServerSocketChannel注册到selector上,并且selector对客户端accept连接操作感兴趣
        SelectionKey selectionKey = serverSocket.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("服务启动成功");

        while (true) {
            // 阻塞等待需要处理的事件发生 已注册事件发生后,会执行后面逻辑
            selector.select();

            // 获取selector中注册的全部事件的 SelectionKey 实例
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();

            // 遍历SelectionKey对事件进行处理
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                // 如果是OP_ACCEPT事件,则进行连接获取和事件注册
                if (key.isAcceptable()) {
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
                    SocketChannel socketChannel = server.accept();
                    socketChannel.configureBlocking(false);
                    // 这里只注册了读事件,如果需要给客户端发送数据可以注册写事件
                    SelectionKey selKey = socketChannel.register(selector, SelectionKey.OP_READ);
                    System.out.println("客户端连接成功");
                } else if (key.isReadable()) {  // 如果是OP_READ事件,则进行读取和打印
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(128);
                    int len = socketChannel.read(byteBuffer);
                    // 如果有数据,把数据打印出来
                    if (len > 0) {
                        System.out.println(Thread.currentThread().getName() +  "接收到消息:" + new String(byteBuffer.array()));
                    } else if (len == -1) { // 如果客户端断开连接,关闭Socket
                        System.out.println("客户端断开连接");
                        socketChannel.close();
                    }
                }
                //从事件集合里删除本次处理的key,防止下次select重复处理
                iterator.remove();
            }
        }
    }
}

这个为了解决上面那个问题,加入了多路复用,为不让他做一些无用的循环遍历,抛弃了channellist集合,把连接都注册到多路复用器里面

IO线程模型_第11张图片

我大致理解的就是这样子的,可能不太周到

这样子处理的话,如果连接了10w个连接,当有事件的连接过来的时候,就会去处理该连接,而不会全局的循环,也就避免了时间上的消耗

还有一个AIO,之后遇到了再总结吧!

你可能感兴趣的:(分享,java,jvm,算法)