NIO


1、NIO概念

1.1 Unix定义了五种 I/O 模型

  • 阻塞 I/O
  • 非阻塞 I/O
  • I/O 复用
  • 信号驱动 I/O
  • 异步 I/O

1.1.1 阻塞(Block)和非阻塞(NonBlock)

阻塞和非阻塞是进程在访问数据的时候,数据是否准备就绪的一种处理方式:
阻塞:需要等待缓冲区中的数据准备好过后才处理其他的事情,否則一直等待在那里。
非阻塞:进程访问数据缓冲区的时候,如果数据没有准备好则直接返回,不会等待。

1.1.2 同步与异步

一个进程的IO调用步骤大致如下:

1、进程向操作系统请求数据

2、操作系统把外部数据加载到内核的缓冲区中

3、操作系统把内核的缓冲区拷贝到进程的缓冲区

4、进程获得数据完成自己的功能

当操作系统在把外部数据放到进程缓冲区的这段时间(即上述的第二,三步),如果应用进程是挂起等待的,那么就是同步IO,反之,就是异步IO

Java中IO分为:
BIO (Blocking IO)
NIO (Non-blocking IO/New IO)
AIO (Asynchronous IO/NIO2)

1.2 NIO模型图

image-20200730202046654

2、 NIO与传统IO的区别

image-20200730202205711

IO NIO
面向流 面向缓冲
阻塞IO 非阻塞IO
selector

2.1 面向流与缓冲

​ IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性

2.2 阻塞与非阻塞

​ Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。

2.3 选择器(Selector)

​ Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。

3、NIO构成组件

Java NIO Channel通道和流非常相似,主要有以下几点区别:
a.通道可以读也可以写,多功能高速通道,流一般来说是单向的(只能读或者写,如输入流与输出流);
b.通道可以异步读写;
c.通道总是基于缓冲区Buffer来读写;
正如上面提到的,我们可以从通道中读取数据,写入到buffer,也可以中buffer内读数据,写入到通道中;下面有个示意图:

image-20200730202755347

Channel的实现:

  • FileChannel

  • DatagramChannel

  • SocketChannel

  • ServerSocketChannel

FileChannel用于文件的数据读写。

DatagramChannel用于UDP的数据读写。

SocketChannel用于TCP的数据读写。

ServerSocketChannel允许我们监听TCP链接请求,每个请求会创建会一个SocketChannel.

Buffer用于和Channel交互。我们从channel中读取数据到buffers里,从buffer把数据写入到channel,Buffer本质上就是一块内存区,可以用来写入数据,并在稍后读取出来。这块内存被NIO Buffer包裹起来,对外提供一系列的读写方便开发的接口。

3.1 Buffer基本用法

利用Buffer读写数据,通常遵循四个步骤:

  1. 把数据写入buffer;
  2. 调用flip;
  3. 从Buffer中读取数据;
  4. 调用buffer.clear()或者buffer.compact()

当写入数据到buffer中时,buffer会记录已经写入的数据大小。当需要读数据时,通过flip()方法把buffer从写模式调整为读模式;在读模式下,可以读取所有已经写入的数据。

当读取完数据后,需要清空buffer,以满足后续写入操作。清空buffer有两种方式:调用clear()或compact()方法。clear会清空整个buffer,compact则只清空已读取的数据,未被读取的数据会被移动到buffer的开始位置,写入位置则近跟着未读数据之后。

Buffer有三个属性:
容量,位置,上限(capacity, position , limit)
buffer缓冲区实质上就是一块内存,用于写入数据,也供后续再次读取数据。这块内存被NIO Buffer管理,并提供一系列的方法用于更简单的操作这块内存。

3.2 Java NIO Selector

​ Selector是Java NIO中的一个组件,用于检查一个或多个NIO Channel的状态是否处于可读、可写。因此此可以实现单线程管理多个channels,也就是可以管理多个网络连接。

1.创建
要使用必须先创建一个:
创建一个selector可以通过selector.open()方法

2.注册
如果要监测channel,必须先把channel注册到Selector上,这个操作使用channel.register(),

3.监测
我们关注的channel状态,有四种基础类型可供监听:(观察客人发来的需求)
Connect, Accept, Read ,Write 通过selectedKeys(),可以获取所有channel的相关信息,包括channel本身,以及channel的状态等信息,知道了channel的状态,我们就可以对channel进行相应的操作。

4、基于NIO实现的多人聊天

4.1 服务端

public class NIOSelectorDemo {

    public static void main(String[] args) throws IOException {
        //创建ServerSocketChannel
        ServerSocketChannel server = ServerSocketChannel.open();
        //绑定9090端口
        server.bind(new InetSocketAddress(9090));
        //设置为非阻塞
        server.configureBlocking(false);    

        //创建Selector
        Selector selector = Selector.open();    
        //当serversocket有连接请求时,监控这个事件
        server.register(selector, SelectionKey.OP_ACCEPT);    
        //创建SocketChannel的ArrayList用于管理客户端与服务端的连接
        ArrayList<SocketChannel> clients = new ArrayList<SocketChannel>();

        while(true)
        &#123;
            //阻塞,当至少有一个channel上有事件发生时,返回
            //selector.select()返回了,意味着channel发生可处理事件
            selector.select();    

            //获得可处理的事件selectionKeys(包含目标channel)
            Set<SelectionKey> keys = selector.selectedKeys();    
            Iterator<SelectionKey> it = keys.iterator();
            while( it.hasNext() )
            &#123;
                SelectionKey key = it.next();
                it.remove();

                //表示可接收请求
                if(key.isAcceptable())
                &#123;
                    ServerSocketChannel socket = (ServerSocketChannel)key.channel();
                    //接收到客户端请求时生成SocketChannel对象,用于和客户的数据传输
                    SocketChannel client = socket.accept();
                    //设置为非阻塞
                    client.configureBlocking(false);
                    //注册进Selector
                    client.register(selector, SelectionKey.OP_READ);
                    //向客户端发送消息“hello”
                    ByteBuffer buf = ByteBuffer.wrap("hello".getBytes());
                    client.write(buf);
                    //将新客户端添加到客户列表里
                    clients.add(client);    
                &#125;
                else if(key.isReadable())
                &#123;
                    SocketChannel client = (SocketChannel)key.channel();
                    //接收来自客户端的数据,组装成msg
                    ByteBuffer buf = ByteBuffer.allocate(1024);
                    client.read(buf);
                    buf.flip();
                    String msg = client.getRemoteAddress()+"==>"+new String(buf.array(),0,buf.limit());

                    //将msg发送给所有客户端
                    buf.clear();
                    buf.put(msg.getBytes());
                    buf.flip();
                    for(int i = 0; i < clients.size(); i++)
                    &#123;
                        SocketChannel c = clients.get(i);
                        c.write(buf);
                        buf.rewind();
                    &#125;
                &#125;
            &#125;
        &#125;

    &#125;

&#125;

4.2 客户端

4.2.1 读线程

class ReadThread implements Runnable
&#123;

    private SocketChannel socket = null;

    public ReadThread(SocketChannel socket) &#123;
        // TODO Auto-generated constructor stub
        this.socket = socket;
    &#125;

    @Override
    public void run() &#123;
        // TODO Auto-generated method stub
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        while(true)
        &#123;
            try &#123;
                int ret = socket.read(buffer);
                if(ret == -1)
                    break;

                buffer.flip();
                System.out.println(new String(buffer.array(),0,buffer.limit()));

                buffer.clear();
            &#125; catch (IOException e) &#123;
                // TODO Auto-generated catch block
                e.printStackTrace();
                break;
            &#125;
        &#125;
    &#125;

&#125;

4.2.2 写线程

class WriteThread implements Runnable
&#123;

    private SocketChannel socket = null;

    public WriteThread(SocketChannel socket) &#123;
        // TODO Auto-generated constructor stub
        this.socket = socket;
    &#125;

    @Override
    public void run() &#123;
        // TODO Auto-generated method stub
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        String msg = "";
        Scanner scanner = new Scanner(System.in);

        while(true)
        &#123;
            msg = scanner.nextLine();
            buffer.clear();
            buffer.put(msg.getBytes());
            buffer.flip();
            try &#123;
                socket.write(buffer);
            &#125; catch (IOException e) &#123;
                // TODO Auto-generated catch block
                e.printStackTrace();
                break;
            &#125;
        &#125;
    &#125;

&#125;

文章作者: kilig
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 kilig !
 上一篇
AOP技术(1) AOP技术(1)
1、AOP介绍​ AOP(Aspect-Oriented Programming,面向方面编程),可以说是OOP(Object-Oriented Programing,面向对象编程)的补充和完善。OOP引入封装、继承和多态性等
2020-07-31
下一篇 
网络编程 网络编程
​ Java语言作为最流行的网络编程语言,提供了强大的网络编程功能。 ​ 使用Java语言可以编写底层的网络通信程序,这是通过java.net包中提供的InetAddress类、URL类、Socket类以及Se
  目录