java bio到nio的演进之路

BIO、NIO介绍

BIO:(Blocking I/O) 同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。
NIO:(New I/O) 同步非阻塞I/O模式,数据的读取写入如果数据还没准备好可以处理别的请求

我们先来看下最传统的BIO模型

public class ServerSocketDemo2 {

    public static void main(String[] args) {
        ServerSocket serverSocket=null;
        try {
            //localhost: 8080
            serverSocket=new ServerSocket(8080);
            while(true) {
                // 核心代码1,监听客户端连接(阻塞1-连接阻塞)
                Socket socket = serverSocket.accept();
                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));//输入流
                // 核心代码2,被阻塞了(阻塞2-流-读取数据阻塞)
                String clientStr = bufferedReader.readLine();
                System.out.println("接收到客户端的信息:" + clientStr);
                bufferedReader.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(serverSocket!=null){
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

我们可以看到阻塞的地方有两个
1、等待连接
2、读取数据
在这个模型下首先要解决的问题是,并发情况下只能一个一个的处理请求,这个我们肯定是不能接受的,于是有了下面的模型

public class ServerSocketDemo {

    static ExecutorService executorService= Executors.newFixedThreadPool(20);

    public static void main(String[] args) {
        ServerSocket serverSocket=null;
        try {
            //localhost: 8080
            serverSocket=new ServerSocket(8080);
            
            while(true) {
                // 核心代码1,监听客户端连接(阻塞1-连接阻塞)
                Socket socket = serverSocket.accept(); 
                System.out.println(socket.getPort());
                executorService.execute(()->{
                    // 异步
                    try {
                        BufferedReader bufferedReader = new BufferedReader(
                                new InputStreamReader(socket.getInputStream()));//输入流
                        // 核心代码2,被阻塞了(阻塞2-流-读取数据阻塞)
                        String clientStr = bufferedReader.readLine(); //读取客户端的一行数据
                        System.out.println("接收到客户端的信息:" + clientStr);
                        
                        bufferedReader.close();
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                });
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(serverSocket!=null){
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

上面的模型我们使用了线程池技术来解决请求只能一个一个请求的问题,可以并发处理多个请求,如tomcat7之前就是采用的线程池技术来解决并发问题,但是面临的瓶颈是线程池的线程数是有限的,当并发过大时采用线程池模型的性能就不够强大了,于是NIO模型诞生了。

public class NIOServerDemo2 {

    private int port = 8080;

    //轮询器 Selector
    private Selector selector;
    //缓冲区 Buffer 等候区
    private ByteBuffer buffer = ByteBuffer.allocate(1024);

    //初始化
    public NIOServerDemo2(int port){
        try {
            this.port = port;
            ServerSocketChannel server = ServerSocketChannel.open();
            // 绑定ip端口
            server.bind(new InetSocketAddress(this.port));
            //BIO 升级版本 NIO,为了兼容BIO,NIO模型默认是采用阻塞式,设置为非阻塞
            server.configureBlocking(false);

            //打开轮训器
            selector = Selector.open();

            // 把ServerSocketChannel注册到轮训器
            server.register(selector, SelectionKey.OP_ACCEPT);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void listen(){
        System.out.println("listen on " + this.port + ".");
        try {
            // 轮询主线程
            while (true){
                // 核心代码1,轮训器,阻塞,直到有事件注册到selector
                selector.select();
                // 每次都拿到所有的号子,判断号子状态
                Set<SelectionKey> keys = selector.selectedKeys();
                Iterator<SelectionKey> iter = keys.iterator();
                //不断地迭代,就叫轮询
                //同步体现在这里,因为每次只能拿一个key,每次只能处理一种状态
                while (iter.hasNext()){
                    SelectionKey key = iter.next();
                    iter.remove();
                    //每一个key代表一种状态
                    //每一个号对应一个业务
                    //核心代码2,数据就绪、数据可读、数据可写 等等等等
                    process(key);
                }
                
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //具体办业务的方法,坐班柜员
    //每一次轮询就是调用一次process方法,而每一次调用,只能干一件事
    //在同一时间点,只能干一件事
    private void process(SelectionKey key) throws IOException {
        //针对于每一种状态给一个反应
        if(key.isAcceptable()){
            System.out.println("就绪了");
            // 状态1:就绪了,就把状态改为可读,下次轮训进来就可以读了
            ServerSocketChannel server = (ServerSocketChannel)key.channel();
            //这个方法体现非阻塞,不管你数据有没有准备好
            //你给我一个状态和反馈
            SocketChannel channel = server.accept();
            //一定一定要记得设置为非阻塞
            channel.configureBlocking(false);
            //当数据准备就绪的时候,将状态改为可读
            key = channel.register(selector,SelectionKey.OP_READ);

        }
        else if(key.isReadable()){
            System.out.println("开始读取数据");
            // 状态2:可读,可以读取数据了,可以选中把号子改成可写,这样就可以实现对话了
            //key.channel 从多路复用器中拿到客户端的引用
            SocketChannel channel = (SocketChannel)key.channel();
            int len = channel.read(buffer);
            if(len > 0){
                buffer.flip();
                String content = new String(buffer.array(),0,len);
                key = channel.register(selector,SelectionKey.OP_WRITE);
                //在key上携带一个附件,一会再写出去
                key.attach(content);
                System.out.println("读取内容:" + content);
            }
        }
        else if(key.isWritable()){
            // 状态3:可以写,那么就可以写给连接进来的客户端发消息了
            SocketChannel channel = (SocketChannel)key.channel();

            String content = (String)key.attachment();
            channel.write(ByteBuffer.wrap(("输出:" + content).getBytes()));
            channel.close();
        }
    }

    public static void main(String[] args) {
        new NIOServerDemo2(8080).listen();
    }
}

客户端给服务端发送数据

public class BIOClient {

	public static void main(String[] args) throws UnknownHostException, IOException {

		//要和谁进行通信,服务器IP、服务器的端口
		//一台机器的端口号是有限
		Socket client = new Socket("localhost", 8080);

		//输出 O  write();
		//不管是客户端还是服务端,都有可能write和read

		OutputStream os = client.getOutputStream();

		//生成一个随机的ID
		String name = UUID.randomUUID().toString();
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("客户端发送数据1:" + name);
		//传说中的101011010
		os.write(("客户端1:"+name).getBytes());
		os.close();
		client.close();

		
	}
	
}

由上可见NIO使用了一个**多路复用器(Selector)**来处理每个请求的连接、写数据、读数据等操作,当一个请求连接进来但是数据没有发送完成时Selector可以去处理别的请求,而不需要等待请求一处理完再去处理其他请求,很好的解决传统BIO模型的问题
NIO相对于BIO解决的问题有:
1、BIO每个线程在获得一个连接时必须阻塞等待读取数据完成才能处理其他请求,而NIO在数据未准备完成时即可处理其他请求
2、BIO未使用线程池时处理请求必须一个一个处理,使用线程池后存在线程开销问题。NIO一个线程即可同时处理多个请求,解决了线程池的问题同时解决了性能问题

BIO和NIO的区别

BIO和NIO区别
1、BIO处理数据是阻塞的(读取数据)
2、NIO处理数据是非阻塞的(数据还不能读取时可以处理别的请求)

这就是BIO称为同步阻塞IO而NIO称为同步非阻塞IO的原因

NIO和AIO的区别

1、NIO是java工作线程在Selector轮询自己检查数据是否准备完成了
2、AIO是由操作系统来通知工作线程数据已经准备完成了
NIO和AIO的同步和异步就体现在是由谁通知数据准备完成

扩展:
1、NIO的代码操作太过于复杂,所以netty出现了,netty是基于nio的封装
2、AIO是基于操作系统来通知的,所以操作系统的性能决定了IO的性能,在Linux系统上,AIO的底层实现仍使用EPOLL,没有很好实现AIO,因此在性能上没有明显的优势3
3、目前的主流仍然是NIO,AIO在linux系统上还是不够成熟,存在一些缺陷

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值