第二章、新一代非阻塞IO--NIO

一、NIO是什么

在Netty in Action中有这么一段解释

New or non-blocking?

The N in NIO is typically thought to mean non-blocking rather than new.NIO has been
around for so long now that nobody calls it new IO anymore. Most people refer to it as nonblocking IO

新的还是非阻塞的?
NIO中的N更多的意思是非阻塞,而不是新的。NIO已经诞生很久了,已经没有人叫他新的IO了。更多的人更倾向与非阻塞IO。

二、NIO架构模式与优缺点

NIO的诞生,就是为了处理BIO在等待客户端连接以及等待接收数据时的阻塞,并且不用为每一个客户端
去创建线程,而是从线程池中获取线程资源来处理客户端任务,这样可以使用较少的线程来处理业务。
那么NIO时如何去解决这个问题的? 我们从NIO的架构模式来看

在这里插入图片描述
客户端可以注册到server中的selector中, selector会循环去判断那些客户端发生了读、写事件,然后从线程池中获取资源去处理这些事件,处理完之后将线程放回线程池,等待下一次的读、写事件。这样就不会造成阻塞,相对于bio有了进一步的系统性能提升。

三、NIO组件

NIO的组件主要包括一下三种:

- channel

每一个客户端都可以认为是一个channel,客户端的读写数据都是通过这个channel进行操作的

- buffer

channel读写数据是基于buffer。也就是说数据是从buffer入到channel中的,同样,buffer也可以接受channel中的数据用于服务端去处理

- selector

监听channel的事件,这些事件主要包括连接以及其他操作。

1、selector

  1. NIO使用非阻塞Io的方式,使用一个线程来处理多个客户端的连接,其中,selector就是用来处理客户端的事件的
  2. 当客户端连接时,selector在内部维护一个set集合去管理这些通道,当服务器监听到通道(channel)发生了事件的时候,会将发生事件的通道(channel)收集起来,然后供程序去获取处理。
  3. selector也是使用一个循环的方式去获取发生事件的通道,不过在获取通道的时候,我们可以指定阻塞时间,这样就可以在没有事件的时候去做其他的事情(不过一般都是循环去获取有事件的通道)。

2、channel

  1. 一个客户端的连接,也就是一个通道----即channel。
  2. 传统的io是单项的。比如FileInputStream,只能处理文件的输入,FIleOutPutStream用来输出文件。但是channel是双向的。也就是既可以从channel中读取数据到服务端,也可以写入数据到channel,从而送达客户端。
  3. 在NIO中,FileChannel用于文件的操作,DatagramChannel用于UDP数据的操作,ServerSocketChannel与SocketChannel用于TCP数据的操作

3、buffer

  1. buffer是可以存储数据的内存块,表现形式为一个数组
  2. buffer中有特定的字段用来记录读数据的索引、写数据的索引以及数据的容量等等
  3. buffer是NIO通讯必须的组件,它与channel进行数据的传输,从而达到客户端与服务端的信息的传递

channel与buffer的使用方式

    public static void main(String[] args) throws Exception{
		//获取文件的输入流
        FileInputStream fileInputStream = new FileInputStream("d:\\1.txt");
        //获取channel
        FileChannel channel = fileInputStream.getChannel();
        //创建一个buffer对象,可存储100字节
        ByteBuffer allocate = ByteBuffer.allocate(100);
        //将通道中的数据写入到buffer中
        channel.read(allocate);
        //将buffer进行反转,这样才能去读取数据
        allocate.flip();
        //将读取到的数据打印到控制台
        System.out.println(new String(allocate.array()));
		//别忘记关闭资源
        channel.close();
        fileInputStream.close();

		  
//        String str = "hello nio";
//        获取到文件的输出流
//        FileOutputStream fileOutputStream = new FileOutputStream("d:\\1.txt");
//        从输出流中获取通道		
//        FileChannel channel = fileOutputStream.getChannel();
//        将字符串写入到buffer中
//        ByteBuffer allocate = ByteBuffer.allocate(100);
//        allocate.put(str.getBytes());
//        将buffer进行反转
//        allocate.flip();
//        将buffer中的数据写入通道中
//        channel.write(allocate);
//        channel.close();
//        fileOutputStream.close();
    }

上面我们在读buffer的时候,会涉及到flip()这个方法,源码是这样去写的

public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
    }

由于写数据会造成position的后移,buffer使用position去记录当前读/写的位置。所以,在写完数据之后,需要读数据的时候,需要将position置为0,而能读取到的最大索引位置为上一次写入的索引position----即limit,这样就可以从写状态切换回读状态。

四、NIO使用方法

我们模拟一个客户端以及服务端的聊天系统

服务端代码

public static void main(String[] args) throws Exception{
	//创建服务端socketChannel
    ServerSocketChannel listenerChannel = ServerSocketChannel.open();
    //设置为非阻塞的
    listenerChannel.configureBlocking(false);
    //绑定端口
    listenerChannel.bind(new InetSocketAddress(2222));
    
	//创建selector
	Selector selector = Selector.open();
	//将通道绑定到selector上
	listenerChannel.register(selector, SelectionKey.OP_ACCEPT);

	//循环去处理发生事件的通道(包括连接、写、读)
	while(true){
		//获取发生事件的通道的数量
		Integer readChannelCount = selector.select();
		if(readChannelCount  > 0){
			//获取发生事件的通道
			Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()){
                SelectionKey key = iterator.next();
                //是连接事件,则获取客户端的channel,并且注册到selector上
                //如果是连接事件,则这个key里面的通道一定是我们上面创建的ServerSocketChannel
                if(key.isAcceptable()){
                    SocketChannel socketChannel = listenerChannel.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ);
                    System.out.println(socketChannel.getRemoteAddress() + "上线了");
                }
                //读就绪,则将channel中的数据打印到控制台
                if(key.isReadable()){
                    //获取通道
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    //创建buffer来接收数据
		            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
		            //将通道中的数据写入buffer
		            int read = socketChannel.read(byteBuffer);
		            if(read > 0){
		               String message = new String(byteBuffer.array());
		               System.out.println("from 客户端 :" + message);
		               //转发信息给其他客户端
				       System.out.println("服务器转发消息...");
				       ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());
				       //获取所有的通道
				       Iterator<SelectionKey> iterator = selector.keys().iterator();
				       while (iterator.hasNext()){
				           SelectionKey selectionKey = iterator.next();
				           //排除服务器以及自己
				           if(selectionKey == key || selectionKey.channel() instanceof ServerSocketChannel)
				                System.out.println("服务器");
				                continue;
				            }
				            SocketChannel channel =(SocketChannel) selectionKey.channel();
				            try {
				            	//将信息转发给通道
				                channel.write(buffer);
				            } catch (IOException e) {
				                e.printStackTrace();
				            }
				        }
            		}
                }
                //删除 防止重复处理
                iterator.remove();
            }
		}else{
			//do another things...
		}
	}
}

客户端代码

public static void main(String[] args) throws Exception {
	
	//创建selector
	Selector selector = Selector.open();

    SocketChannel = socketChannel = SocketChannel.open(new InetSocketAddress(host, port));
	//设置非阻塞的
    socketChannel.configureBlocking(false);
    //将通道注册到selector中
    socketChannel.register(selector, SelectionKey.OP_READ);
    System.out.println("客户端" + socketChannel.getLocalAddress() + " is ok");

	//开启一个异步线程,去接受服务端发送的信息
    new Thread(() ->{
        while (true){
            try {
            	//这里跟server端基本一样
                int readCount= selector.select();
		        if(readCount> 0){
		            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
		            while (iterator.hasNext()){
		                SelectionKey selectionKey = iterator.next();
		                SocketChannel channel = (SocketChannel) selectionKey.channel();
		                if(selectionKey.isReadable()){
		                    ByteBuffer buffer = ByteBuffer.allocate(1024);
		                    channel.read(buffer);
		                    System.out.println("接受到消息" + new String(buffer.array()));
		                }
		            }
		        }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }).start();
    //从控制台输入信息,发送给服务器
    Scanner scanner = new Scanner(System.in);
    while (scanner.hasNextLine()){
        message = socketChannel.getLocalAddress() + "说:" + message;
        socketChannel.write(ByteBuffer.wrap(message.getBytes()));
    }
}

测试结果如下

运行服务端,启动两个客户端

服务端结果如下
在这里插入图片描述

客户端结果如下
客户端1

客户端2

使用客户端52006发送消息
在这里插入图片描述

查看服务端与另外一个客户端接收到的结果
在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值