代码分析Java中的BIO与NIO

开发环境

OS:Win10(需要开启telnet服务,或使用第三方远程工具)

Java版本:8

BIO

概念

BIO(Block IO),即同步阻塞IO,特点为当客户端发起请求后,在服务端未处理完该请求之前,客户端将一直等待服务端的响应。而服务端在此时也专注于该请求的处理,无法处理其它客户端的请求。

示例

java 复制代码
package com.mlyzr.bio;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Calendar;

/**
 * @author 勿忘初心
 * @since 2023-08-07-23:59
 * 同步阻塞IO示例
 */
public class BioServer {

    public static void main(String[] args) throws IOException {
        // 监听本地8096端口
        ServerSocket serverSocket = new ServerSocket(8096);
        while(true){
            System.out.println("等待客户端连接...");
            // 接收客户端请求,阻塞
            Socket clientSocket = serverSocket.accept();
            System.out.println("监听到客户端连接");
            // 使用NIO处理请求信息
            handler(clientSocket);
        }
    }

    public static void handler(Socket clientSocket) throws IOException {
        byte[] bytes = new byte[1024];
        System.out.println("准备读取");
        int read = clientSocket.getInputStream().read(bytes);
        System.out.println("读取完毕");
        if(read != -1){
            System.out.println("接收到来自客户端的数据 "+new String(bytes,0,read,StandardCharsets.UTF_8));
        }
    }
}

在IDEA运行上述代码后,将在控制台看如下输出

此时先打开第一个客户端,这里使用telnet连接,具体操作为使用快捷键 Win+R 打开命令行,输入cmd,输入telnet localhost 8096(代码中绑定的端口)后回车,按下 ctrl+] (ctrl+ 右括号)即可进入telnet的交互模式,可向服务端发送信息。(telnet命令报错请使用自行搜索开启telnet服务,或使用其它工具如mobxterm等)

注意:cmd工具的telnet模式下无法发送中文字符,会出现乱码现象,如需发送中文字符请使用MobaXterm等第三方工具进行测试。

客户端

当客户端成功连接后,服务端控制台输出将由等待状态变为读取状态,等待当前客户端发送消息。

此时再新建一个客户端连接,服务端控制台输出依旧不会发生任何改变, 因为第一个客户端的资源处理还没有结束。

在第一个客户端中使用send命令向服务端发送消息(不熟悉telnet命令的话可以输入help查看)可以看到服务端已经接收到发送的 client1 字符串后输出,并且之前的第二个客户端的连接目前可以被处理,同样进入准备读取的状态。

使用第二个客户端发送消息,此时服务端正常输出。

优化

不难发现上述操作中存在的问题,服务端一次只能处理一个客户端的请求,其余的客户端只能等待,当某一个客户端请求的资源处理耗时较长时,对于其它的客户端使用是非常糟糕的,这里可以通过使用多线程方式来进行优化。

java 复制代码
package com.mlyzr.bio;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

/**
 * @author 勿忘初心
 * @since 2023-08-07-23:59
 * 同步阻塞IO示例
 */
public class BioServer {

    public static void main(String[] args) throws IOException {
        // 监听本地8096端口
        ServerSocket serverSocket = new ServerSocket(8096);
        while(true){
            System.out.println("等待客户端连接...");
            // 接收客户端请求,阻塞
            Socket clientSocket = serverSocket.accept();
            System.out.println("监听到客户端连接");

            // 使用多线程来解决系统处理资源的能力
            new Thread(new Runnable(){
                @Override
                public void run() {
                    try {
                        // NIO处理请求资源
                        handler(clientSocket);
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
            }
            ).start();
        }


    }

    public static void handler(Socket clientSocket) throws IOException {
        byte[] bytes = new byte[1024];
        System.out.println("准备读取");
        int read = clientSocket.getInputStream().read(bytes);
        System.out.println("读取完毕");
        if(read != -1){
            System.out.println("接收到来自客户端的数据 "+new String(bytes,0,read,StandardCharsets.UTF_8));
        }
    }
}

使用多线程后,可以看到服务端能够同时监听多个客户端的请求。

总结

使用改进后的代码通过测试可以发现服务端具备了同时处理多个客户端的能力,但同时仍然存在一些问题:

1.当请求非常大时,服务端将因为线程太多无法处理而导致崩溃。

2.即使使用了线程池,当线程池占满后服务将于单线程处理无异。

3.客户端只连接不发送数据,将一直占用服务端资源造成资源浪费。

NIO

概念

同步非阻塞IO,特点为当客户端发起请求后,在此期间客户端可以做其它的操作,但需要主动轮询服务端的处理结果,而且服务端也无需专注于当前请求的处理,也可以处理其他请求。

示例

java 复制代码
package com.mlyzr.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.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * @author 勿忘初心
 * @since 2023-08-08-15:28
 * 同步非阻塞IO示例代码
 */
public class NioServer {

    public static void main(String[] args) throws IOException {
        // 保存客户端的Channel集合
        List<SocketChannel> channelList = new ArrayList<>();
        // 创建NIO的ServerSocketChannel,与BIO的ServerSocket类似
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.socket().bind(new InetSocketAddress(8096));
        // 设置channel为非阻塞
        serverChannel.configureBlocking(false);
        System.out.println("等待客户端连接");

        while(true){
            // 非阻塞模式的accept方法不会阻塞
            SocketChannel socketChannel = serverChannel.accept();
            // 客户端是否连接
            if(socketChannel != null){
                System.out.println("监听到客户端连接");
                socketChannel.configureBlocking(false);
                // 将客户端连接保存到list中
                channelList.add(socketChannel);
            }

            Iterator<SocketChannel> iterator = channelList.iterator();
            while(iterator.hasNext()){
                SocketChannel sc = iterator.next();
                ByteBuffer byteBuffer = ByteBuffer.allocate(128);
                int read = sc.read(byteBuffer);
                // 若存在数据,则将数据打印出来
                if(read > 0){
                    System.out.println("接收到客户端数据"+new String(byteBuffer.array(),0,read,StandardCharsets.UTF_8));
                }
                // 无数据则说明客户端已断开
                if(read == -1){
                    iterator.remove();
                    System.out.println("客户端已断开连接");
                }
            }
        }

    }
}

客户端

运行实例代码后,可以同时打开多个客户端,客户端连接将被加入集合遍历处理,无需等待,服务端此时可以处理多个请求,此时服务端使用的是单线程。

优化

虽然上述代码解决了BIO中遗留的阻塞问题,但多个请求连接而不发送数据占用资源的情况仍然存在,如当请求数据量巨大时,需要通过遍历集合的方式寻找存在需要处理数据的客户端时,时间的损耗仍然非常大。因此使用多路复用的方式来解决无效遍历的问题。

java 复制代码
package com.mlyzr.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 * @author 勿忘初心
 * @since 2023-08-08-15:28
 * 同步非阻塞IO示例代码
 */
public class NioServer {

    public static void main(String[] args) throws IOException {
        // 创建NIO的ServerSocketChannel,与BIO的ServerSocket类似
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.socket().bind(new InetSocketAddress(8096));
        // 设置channel为非阻塞
        serverChannel.configureBlocking(false);
        // 引入多路复用,提高对Channel的处理能力, 即epoll
        Selector selector = Selector.open();
        // 将ServerSocketChannel注册到selector上,即服务端接收到连接请求时,将连接事件进行注册
        SelectionKey selectionKey = serverChannel.register(selector,SelectionKey.OP_ACCEPT);
        System.out.println("等待客户端连接");

        while(true){
            // 判断是否有注册的事件,无事件将阻塞
            selector.select();

            // 获取所有已经注册的事件实例
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> keyIterator = selectionKeys.iterator();

            while(keyIterator.hasNext()){
                SelectionKey selectKey = keyIterator.next();
                // 若为连接事件,则获取该实例
                if(selectKey.isAcceptable()){
                    ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectKey.channel();
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    // 设置为非阻塞
                    socketChannel.configureBlocking(false);
                    // 注册读事件,若需要发送消息给客户端可以注册写事件
                    SelectionKey key = socketChannel.register(selector,SelectionKey.OP_READ);
                    System.out.println("监听到客户端连接");
                }
                if (selectKey.isReadable()){
                    SocketChannel socketChannel = (SocketChannel) selectKey.channel();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(128);
                    int read = socketChannel.read(byteBuffer);
                    // 若存在数据,则将数据打印出来
                    if(read > 0){
                        System.out.println("接收到客户端数据"+new String(byteBuffer.array(),0,read,StandardCharsets.UTF_8));
                    }
                    // 无数据则说明客户端已断开
                    if(read == -1){
                        System.out.println("客户端已断开连接");
                        socketChannel.close();
                    }
                }
                // 从事件集合中删除本次处理的key,防止重复处理
                keyIterator.remove();
            }

        }

    }
}

总结

当使用多路复用后,会监听到客户端的连接事件,并为当前的连接注册读事件,当客户端发送数据时,会被再次监听,此时会进入读操作的事件处理中,将打印接收到的数据,同时每一次的事件使用后将会被移除,防止事件重复,从而解决了无效遍历的问题。

相关推荐
张张张3126 分钟前
4.2学习总结 Java:list系列集合
java·学习
KATA~9 分钟前
解决MyBatis-Plus枚举映射错误:No enum constant问题
java·数据库·mybatis
xyliiiiiL25 分钟前
一文总结常见项目排查
java·服务器·数据库
shaoing26 分钟前
MySQL 错误 报错:Table ‘performance_schema.session_variables’ Doesn’t Exist
java·开发语言·数据库
腥臭腐朽的日子熠熠生辉1 小时前
解决maven失效问题(现象:maven中只有jdk的工具包,没有springboot的包)
java·spring boot·maven
ejinxian1 小时前
Spring AI Alibaba 快速开发生成式 Java AI 应用
java·人工智能·spring
杉之1 小时前
SpringBlade 数据库字段的自动填充
java·笔记·学习·spring·tomcat
圈圈编码2 小时前
Spring Task 定时任务
java·前端·spring
俏布斯2 小时前
算法日常记录
java·算法·leetcode
27669582922 小时前
美团民宿 mtgsig 小程序 mtgsig1.2 分析
java·python·小程序·美团·mtgsig·mtgsig1.2·美团民宿