初步认识nio

nio

阻塞和非阻塞,同步和异步

阻塞和非阻塞

1.阻塞的意思是资源未就位,线程就一直等待就位,例如socket的读写操作,没有数据就一直卡在哪里。或者是我等待《庆余年》更新,但是现在电视还没有开始,那我就盯着电视,这期间我就不干别的了,对于我来说,我就是阻塞状态,这期间我不能做别的事情。
2.如果我一会打开爱奇艺看一下,没有播放,我写点bug,然后过几分钟我再看一下,对于我来说,我就是非阻塞的了。
3.但是这还是太依赖于我这个线程,而《庆余年》这个资源确实是太懒了。那么我就订阅一下,让他通知我,我就坐着等通知,开播了就给我发个短信,对于《庆余年》这个资源和我这个线程来说,就叫做异步阻塞了,我还是什么都干不了。
4.如果我写我的bug,打游戏,等电视开播了就通知我,就是异步非阻塞。我和他互不干扰。
1:同步阻塞,2:同步非阻塞,3:异步阻塞,4:异步非阻塞。
可以看到其实阻塞或者非阻塞是针对线程的,同步或者异步是针对资源的,而异步其实是需要回调或者叫做通知的。

nio

nio就是一个同步非阻塞,例如SocketChannel的read或者write,主要的进步就是如果对于socket套接字如果没有可读,可写的资源,就立刻返回一个错误的信息,我去干别的,一会再来看看,先去干别的。但是一个socket只能针对一个
###nio的内容(channel,buffer,selector)

buffer

buffer就是一个容器,用来装数据,nio数据的读写其实都实在buffer里来操作的,buffer里有三个重要的变量,position,limit, capacity,读写和时候会一直用到他们三个,下边的图介绍了他么的关系。
在这里插入图片描述
在设置读模式的时候,position就是这个容器的开始,limit是当前读取的位置,capacity是容器的大小,一般就是最大。从图中可以看出,表示buffer里有五个数字,0,1,2,3,4,limit表示一共有5个可以读的数据,position从0开始读。
在写模式的时候position就是当前写的位置,limit和capacity一样,都是表示容器最大可以写到哪里。如图所示,已经写了0,1,2,那么接下来就从索引3开始写,position就是3。

在nio中buffer是一个抽象类。平时我们用的是他的实现类,用到的大概有8种,

  • ByteBuffer
  • CharBuffer
  • LongBuffer
  • IntBuffer
  • DoubleBuffer
  • FloatBuffer
  • ShortBuffer
  • MappedByteBuffer
    前7中是我们平时常用的八种基本数据类型的7中,没有boolean的buffer,而MappedByteBuffer有些特别,是用来专门读取文件的。接下来会有几个小demo依次讲解怎么用。
    buffer的通用方法基本操作的都是position,limit,capacity,所以接下来我们讲解一下他们的常用方法,然后在写代码。

从最基本的开始

        ByteBuffer buffer = ByteBuffer.allocate(256);
        buffer.limit();
        buffer.position();
        buffer.capacity();

从语法上就能看出来他们的返回值就是前边说的三大金刚。

写转读-flip()方法

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

看源码和最开始的图片对比一下可以发现,其实他是把buffer从写状态变成了读模式。写和读模式的转化其实就是position和limit的转化。
从buffer中获取数据-get()方法
从buffer中读取数据可以直接调用buffer的get方法,这样就会position++,然后返回上一个读取的信息。当然position要小于等于limit。
重新读-rewind()
把position设置成0,其他的不变。
剩余未读数 - remaining()
返回limit和position的差值。

public final int remaining() {
        return limit - position;
    }

是否有可读数据hasRemain()

public final boolean hasRemaining() {
        return position < limit;
    }

重新开始写 clear()

   public final Buffer clear() {
        position = 0;
        limit = capacity;
        return this;
    }

position设置成0,limit = capacity,只是恢复到了写最开始的状态,并没有清除数据,但是可以知道是从哪里写了。
保留部分数据 compact()
如果我们读取了上图中的0,1,2,想要保留3和4,就需要用你compact,他会保留3,4,防止到buffer的最开始。源码如下

    public ByteBuffer compact() {

        int pos = position();
        int lim = limit();
        assert (pos <= lim);
        int rem = (pos <= lim ? lim - pos : 0);

        unsafe.copyMemory(ix(pos), ix(0), (long)rem << 0);
        position(rem);
        limit(capacity());
        discardMark();
        return this;
    }

int rem = (pos <= lim ? lim - pos : 0);这一步就是要计算要保留的数据的大小。copyMemory我不知道底层这么实现的,但是看方法名大概就知道了,然后重新这是三大金刚。
比较大小 compareTo
比较里的元素是不是一样。看源码如下:

 public int compareTo(ByteBuffer that) {
        int n = this.position() + Math.min(this.remaining(), that.remaining());
        for (int i = this.position(), j = that.position(); i < n; i++, j++) {
            int cmp = compare(this.get(i), that.get(j));
            if (cmp != 0)
                return cmp;
        }
        return this.remaining() - that.remaining();
    }

一眼就能看出他们是循环比较大小的,如果没有必要,就不要比较了。
当然还有最特殊的MappedByteBuffer,可以看下班的channel部分,会专门说为什么要有他。
#####代码实例
读写转化

ByteBuffer buffer = ByteBuffer.allocate(256);

        System.out.println("put ----------");
        buffer.put((byte) 1).put((byte) 2).put((byte) 3);
        System.out.println(buffer.position());
        System.out.println(buffer.limit());
        System.out.println(buffer.capacity());

        System.out.println("flip ----------");
        buffer.flip();
        System.out.println(buffer.position());
        System.out.println(buffer.limit());
        System.out.println(buffer.capacity());

        System.out.println("get ----------");
        byte b = buffer.get();
        System.out.println("读取数据"  + b);
        System.out.println(buffer.position());
        System.out.println(buffer.limit());
        System.out.println(buffer.capacity());

        System.out.println("compact ----------");
        buffer.compact();
        System.out.println(buffer.position());
        System.out.println(buffer.limit());
        System.out.println(buffer.capacity());

        System.out.println("rewind ----------");
        buffer.rewind();
        System.out.println(buffer.position());
        System.out.println(buffer.limit());
        System.out.println(buffer.capacity());
        System.out.println("clear ----------");
        buffer.clear();
        System.out.println(buffer.position());
        System.out.println(buffer.limit());
        System.out.println(buffer.capacity());

具体可以看一下执行结果。

channel

在这里插入图片描述
channel包含FileChannek,DatagramChannel,SocketChannel,ServerSocketChannel.

  • FIleChannel可以读取文件的数据
  • DatagramChannel读取UDP的数据
  • SocketChannel读取tcp的数据
  • ServerSocketChanne监听网络,对于每一个新进来的链接都会创建一个SocketChannel
    他就是一个管道,用来进行数据的传输,至于数据的操作,其实是在buffer里的。下边就是范例,表示读取数据的代码。
    FileChannel
try {
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            RandomAccessFile file = new RandomAccessFile("/Users/menghaibin/Desktop/test.sql","r");
            FileChannel channel = file.getChannel();
            //获取数据到到buffer
            int read = channel.read(buffer);
            while (read != -1){

                //如果数据没有读完,则打印数据
                while (buffer.hasRemaining()){
                    System.out.print((char)buffer.get());
                }
                
                //将position设置成0,limit=capacity,表示可以重新写入
                buffer.clear();
                //接着读取数据到buffer
                read = channel.read(buffer);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

DatagramChannel
UDP是一个无连接的协议,所以不能像其它通道那样读取和写入,他是发送和接受数据包。接受数据是receive方法来接收数据。发送数据是send方法。链接到制定地址为connect,发送消息和接受消息代码如下:
客户端

package com.mhb.nio;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.util.Scanner;

public class TestDatagradChannelSend {

    public static void main(String[] args) throws IOException {
        final DatagramChannel channel = DatagramChannel.open();

        new Thread(new Runnable() {
            @Override
            public void run() {
                ByteBuffer buffer = ByteBuffer.allocate(1024);

                byte[] bytes = null;
                while (true){
                    buffer.clear();
                    SocketAddress socketAddress = null;
                    try {
                        socketAddress =  channel.receive(buffer);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    if (socketAddress != null){
                        int position = buffer.position();
                        bytes = new byte[position];
                        buffer.flip();

                        for (int i = 0; i < position; i++) {
                            bytes[i] = buffer.get();
                        }
                    }

                    try {
                        System.out.println("receive remote " + socketAddress.toString() + ":" + new String(bytes, "UTF-8"));
                    } catch (UnsupportedEncodingException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

        while (true){
            Scanner sc = new Scanner(System.in);
            String next = sc.next();
            try {
                sendMessage(channel, next);
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }

    public static void sendMessage(DatagramChannel channel, String mes) throws IOException {
        if (mes == null || mes.isEmpty()) {
            return;
        }
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        buffer.clear();
        buffer.put(mes.getBytes("UTF-8"));
        buffer.flip();
        System.out.println("send msg:" + mes);
        int send = channel.send(buffer, new InetSocketAddress("localhost",8989));
    }

}

服务端

package com.mhb.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;

public class TestDatagramChannelReceive {

    public static void main(String[] args) throws IOException {
        DatagramChannel channel = DatagramChannel.open();
        channel.bind(new InetSocketAddress(8989));
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        byte[] b;
        while (true){
            buffer.clear();
            SocketAddress socketAddress = channel.receive(buffer);
            if (socketAddress != null){
                int position = buffer.position();
                b = new byte[position];
                buffer.flip();
                for (int i = 0; i < position; i++) {
                    b[i] = buffer.get();
                }
                System.out.println("receive remote" + socketAddress.toString() + ":" + new String(b, "UTF-8"));
                sendReback(socketAddress, channel);
            }
        }
    }

    public static void sendReback(SocketAddress socketAddress, DatagramChannel channel) throws IOException {
        String message ="我收到了消息";
        ByteBuffer allocate = ByteBuffer.allocate(1024);
        allocate.put(message.getBytes());
        allocate.flip();
        channel.send(allocate, socketAddress);
    }
}

ServerSocketChannel
ServerSocketChannel用open来创建一个channel,然后通过socket().bind方法绑定端口,accept()来坚挺请求,通过configureBlocking来设置是否阻塞

  public static void main(String[] args) throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.socket().bind(new InetSocketAddress(8080));
        serverSocketChannel.configureBlocking(false);
    }

selector

selector就是java中的多路复用器,用来检测nio中的channle是否可读,可写状态。也可以管理网络连接
在这里插入图片描述
如上图,他能用更少的线程完成更多的事情,而不是像传统的socket,如果要多个连接,我们只能使用多线程来创建channel,channel是多个的,但是管理还是靠一个selector来创建的。
判断selector注册了那些时间,可以用is***方法来判断,例如isAcceptable, isConnectable, isReadable, isWritable方法,示例如下:

 public static void main(String[] args) throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.socket().bind(new InetSocketAddress(8080));
        serverSocketChannel.configureBlocking(false);

        Selector selector = Selector.open();
        SelectionKey register = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println(SelectionKey.OP_ACCEPT);
        int i = register.interestOps();
        boolean acceptable = register.isAcceptable();
        register.isConnectable();
        System.out.println(i);

    }

总结

简单来说,nio用到的就是buffer,channel,selector,buffer就是我们就是数组,channel用来传输数据,selector就是一个容器,用来关注channel,有关注的channel,就可以把channel的数据buffer做交换处理了

发布了176 篇原创文章 · 获赞 84 · 访问量 44万+

猜你喜欢

转载自blog.csdn.net/lovemenghaibin/article/details/103968061