NIO 总结

IO 基础概念

IO,指的是 input/ouput ,指的是计算机与其他设备的数据交互。而 IO 的模式又分为多种。

 

阻塞与非阻塞:

阻塞:在处理一个请求时,服务器端会分配一个线程,在处理请求过程中并不是全程都会用到 CPU 的,阻塞式就是即使没有用到 CPU,请求也会占用线程 CPU 直到请求全部执行完成。

非阻塞:在请求未用到 CPU 时,当前线程会去执行其他操作,并且会轮询判断该请求是否需要用到 CPU 了,需要用到时再回来处理请求。

 

同步与异步:

同步:指多个线程按既定的流程执行,一段代码一段时间只会有一个线程执行。

异步:在发送请求后,会立刻返回,然后当前线程可以处理其他操作,而返回结果会在执行完成后通过回调函数的形式返回。

 

关于阻塞、非阻塞与同步、异步的联系:

阻塞与非阻塞侧重于 CPU 是否在执行当前操作过程中执行了其他操作,而同步与异步侧重于发送请求后是否立刻返回结果返回。阻塞与非阻塞本质就是属于同步的范畴,因为其都是在等待一次返回的结果。为了更好的解释,改编一下知乎上的一篇回答

阻塞就是打电话给图书馆,在询问书名后,店员寻找,此时你是一直处于等待状态,直到确定有无该书后通知你。

非阻塞就是在等待时你先干其他事,并且每隔一段时间再询问一下查看是否完成(主动寻求结果)。

同步就是电话未挂断,直到通知你结果,此期间你是否干其他事都行,但是不能挂电话(也就是不能获取到结果(同步代码的执行权))。

异步就是店主先通知你说有其他事,先挂电话,等他找到后再通知你。

阻塞与非阻塞都是在等待第一次电话返回的结果,属于用户线程主动获取返回结果,所以其都是属于同步的,而异步是二次电话。

同步阻塞式 IO 就是接通电话后你就一直等待店主那边通知,期间你不做其他事。

同步非阻塞式 IO 就是接通电话你先干其他事,并且每隔一段时间询问店主是否完成。

异步 IO 就是店主先挂电话,随后你忙其他事,店主打来通知电话再来接。由于异步是通过回调返回结果的,所以异步不存在阻塞非阻塞的概念,都是非阻塞的。

 

IO 类型

BIO:同步阻塞式 IO ,效率低下,因为是同步阻塞式的,所以一个线程只能处理一个线程请求,当一个连接内没有IO操作时还是会占用线程,这样会严重影响效率。且数据是以流形式进行传输,在高并发场景下效率极低。方向:输入流,输出流(单向)。适用场景:连接数少且连接时间长的架构。

NIO:同步非阻塞式IO,一个线程可以处理多个连接请求。具体过程是当连接请求传来时,服务端会为其创建通道(Channel,双向),然后由选择器(Selector )进行判断选择需要IO操作的请求分配线程去执行,且数据是以缓冲(Buffer ,多组数据,双向)形式进行传输的。所以NIO是双向的。适用场景:连接数多且连接时间较短。

AIO:异步非阻塞式 IO。由于在 Linux 中其底层也是基于 epoll 实现的,所以其效率并没有比 NIO 高多少,再加上 Linux 对 aio 没有优化好,在一些场景中效率甚至比如 NIO ,所以目前使用的并不多。

 

NIO 三大组件

缓冲区Buffer

本质上就是一个可以读写数据的内存块,可以理解成是一个容器对象,底层包含了一个byte数组用于保存字节数据。也正是因为缓冲区的存在使得NIO有了非阻塞这一特性,因为其他连接在进行IO操作时,可以将当前的IO操作数据暂存在缓冲区,下次再被selector侦测到进行数据传输(从 buffer 微观上看是异步的,但从宏观上总体过程还是同步的)

jdk 实现:

Buffer 本身是一个抽象类,一共有七种实现,分别是 IntBuffer、FloatBuffer、CharBuffer、DoubleBuffer、ShortBuffer、LongBuffer、ByteBuffer(常用),各自对应着各种的类型数组。

buffer及其实现类中有四个重要属性:1、capacity:能装载的最大容量  2、limit:缓冲区的当前最大终点位置,在 buffer 里填写的数据不能超过 limit 规定的值,可变。  3、position:下一个要读(写)元素的索引位置  4、mark:当前位置的标记

常用方法 

红色代表常用

clear():重置 buffer 底层的标记。一般在循环读取 buffer 的数据时调用,如果没有重置,那么在缓冲区容量刚好和读取数据容量一致时position与limit就会相等,那么下一次循环read就会等于0,从而死循环,一次性读完是-1

ByteBuffer

wrap:直接根据数据的字节数组大小来创建一个ByteBuffer

 

通道 Channel

channel 是 buffer 进行传输的通道,与 BIO 的流不同,它是双向的,既可以读,也可以写。在 JDK 中,channel 常见的实现类有 FileChannel(文件读写),DatagramChannel(UDP读写),ServerSocketChannel和SocketChannel(TCP读写)

常用方法,以 FileChannel 举例

注意:在 linux 环境中,执行一此 transferTo 方法就可以完成传输,因为传输数据大小是没有限制的;而在 windows 环境中 transferTo 方法一次性只能发送 8M,所以需要分段多次传输文件。

 

选择器 Selector

selector是用来处理多个客户端的连接,它会检测多个注册的通道上是否有事件发生(多个Channel以事件的方式可以注册到同一个selector上(新连接进来也算是事件)),如果有事件发生,便获取事件然后针对每个事件进行相应的操作,这样就可以达到只用一个单线程去管理多个通道,实现多路复用。

常用方法:

多路复用的原理:

1、服务器端启动时,会创建一个ServerSocketChannel对象(这个对象需要注册到selector),每个客户端首先会创建一个SocketChannel对象,然后通过进行连接。

2、一个selector就管理多个连接,管理方式就是将这些连接生成的SocketChannel注册到selector上。

3、注册后会为这个连接返回一个SelectionKey,会和该Selector关联(集合方式)

4、selector进行监听(通过select方法),返回有事件发生的通道的个数。

5、进一步获取到有事件发生的连接的SelectionKey(通过Selector的selectedKeys方法返回SelectionKey类型的set集合)

6、通过SelectionKey反向获取SocketChannel(通过SelectionKey的channel方法)以及ByteBuffer

7、通过Channel对象进行IO操作

 

SelectionKey的四种事件类型

1、SelectionKey.OP_ACCEPT —— 接收连接进行事件,表示服务器监听到了客户连接,服务器可以接收这个连接了(第一次连接时的事件)

2、SelectionKey.OP_CONNECT —— 连接就绪事件,表示客户与服务器的连接已经建立成功

3、SelectionKey.OP_READ —— 读就绪事件,表示通道中已经有了可读的数据,可以执行读操作了(通道目前有数据,可以进行读操作了)

4、SelectionKey.OP_WRITE —— 写就绪事件,表示已经可以向通道写数据了(通道目前可以用于写操作)

 

SelectionKey 相关方法:

selector.keys():获取当前selector所监听的所有连接对应的SelectionKey集合,

selector.selectedKeys():获取当前selector监听中含有事件的连接对应的SelectionKey集合

 

三者联系

一个线程对应一个 Selector,一个 Selector 管理多个 Channel,一个 Channel 对应一个 Buffer,其中 Channel 与 Buffer 都是双向的,可以高效地进行 IO 操作。

 

零拷贝

首先要明确,零拷贝并不是没有数据拷贝的发生,而是在拷贝过程中没有用到 CPU ,因为 CPU 是程序执行的重要资源,没有占用 CPU,就增强了程序的执行效率。零拷贝是在 Linux 环境下对 NIO IO 过程的一种优化。在 NIO 的 channel 实现类的 transferTo 方法就实现了零拷贝。

传统 IO

在传统 IO 过程中,会经历三次状态切换和四次拷贝

1、线程在用户空间发起 read() 方法,线程从用户态转成内核态

2、DMA将磁盘数据拷贝到内核缓存后,CPU又将数据从内核缓存拷贝到用户缓存,这时线程从内核态又切换为用户态

3、这时候知道了数据要往哪里写,CPU将数据从用户缓存拷贝至socket缓存,线程又从用户态切换为内核态

4、最后DMA将数据从内核缓存拷贝至协议栈,read()调用结束返回,线程又从内核态切换为用户态。

DMA :直接内存拷贝(不经过 CPU)

 

MMAP 优化

通过内存映射,使用mmap()函数将用户空间映射到内核缓冲区,用户空间共享内核空间的数据,所以在拷贝时就减少了上面第二步的cpu拷贝,直接从内核缓存拷贝到socket缓存,但是状态切换还是三次。

 

sendFile优化

Linux2.1引入了sendFile函数,直接摒弃了与用户空间的交互,相比于mmap减少一次状态的切换

 

零拷贝

linux2.4对sendFile函数做了一些修改,将内核缓存直接拷贝到协议栈,从而取消了仅有的一次CPU拷贝(还是有一次基本信息的socket缓存CPU拷贝的,但是消耗很低。)

mmapsendFile区别:

1、mmap适合小数据量读写,sendFile适合大文件传输

2、mmap需要4次上下文切换(这里算上了切换为初始状态),3次数据拷贝;sendFile需要3次上下文切换,最少2次数据拷贝(零拷贝优化)

3、sendFile可以利用DMA方式,减少CPU拷贝,mmap则不能(必须从内核拷贝到Socket缓冲区)

 

案例

1、Put、Get 使用

ByteBuffer 支持类型化的 put 和 get,put 和 get 的顺序、类型必须一致,否则会抛出异常。

public class BufferPutGet {
    public static void main(String[] args) {

        //创建一个Buffer
        ByteBuffer buffer = ByteBuffer.allocate(64);

        //类型化方式放入数据
        buffer.putInt(100);
        buffer.putLong(9);
        buffer.putChar('尚');
        buffer.putShort((short) 4);

        //取出前需要翻转
        buffer.flip();

        System.out.println();

        System.out.println(buffer.getInt());
        System.out.println(buffer.getLong());
        System.out.println(buffer.getInt());        //当数据顺序类型匹配不上时就会抛出异常
        System.out.println(buffer.getShort());

    }
}

 

2、只读 Buffer

可以将普通的 buffer 转成只读 buffer。转成只读 buffer 后再 put 数据就会抛 ReadOnlyBufferException 异常。

public class BufferReadOnly {
    public static void main(String[] args) {

        //创建一个buffer
        ByteBuffer buffer = ByteBuffer.allocate(64);

        for(int i = 0; i < 64; i++) {
            buffer.put((byte)i);
        }

        //读取
        buffer.flip();

        //得到一个只读的Buffer
        ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();
        System.out.println(readOnlyBuffer.getClass());

        //读取
        while (readOnlyBuffer.hasRemaining()) {
            System.out.println(readOnlyBuffer.get());
        }

        readOnlyBuffer.put((byte)100); //ReadOnlyBufferException
    }
}

 

3、文件拷贝

public class NioFileChannelCopy {

    //普通拷贝
    @Test
    public  void test01() throws IOException {
        FileInputStream fileInputStream = new FileInputStream("d://text01.txt");
        FileChannel inputStreamChannel = fileInputStream.getChannel();
        FileOutputStream fileOutputStream = new FileOutputStream("d://text02.txt");
        FileChannel outputStreamChannel = fileOutputStream.getChannel();

        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        while(true){
            byteBuffer.clear();  //重置buffer底层的标记,如果没有重置,那么在缓冲区容量刚好和读取数据容量一致时,position与limit就会相等,那么下一次循环read就会等于0,从而死循环
            int read = inputStreamChannel.read(byteBuffer);
            if(read==-1){
                break ;
            }
            byteBuffer.flip();
            outputStreamChannel.write(byteBuffer);
        }
        fileInputStream.close();
        fileOutputStream.close();
    }

    //通过FileChannel的方法直接拷贝
    @Test
    public void test02() throws IOException {
        FileInputStream fileInputStream = new FileInputStream("d://text01.txt");
        FileChannel inputStreamChannel = fileInputStream.getChannel();
        FileOutputStream fileOutputStream = new FileOutputStream("d://text03.txt");
        FileChannel outputStreamChannel = fileOutputStream.getChannel();
        //进行拷贝
        outputStreamChannel.transferFrom(inputStreamChannel,0,inputStreamChannel.size());

        outputStreamChannel.close();
        inputStreamChannel.close();
        fileOutputStream.close();
        fileInputStream.close();
    }
}

 

4、CS零拷贝测试

// 传统方式传输
public class OldIOClient {

    public static void main(String[] args) throws Exception {
        Socket socket = new Socket("localhost", 7001);

        String fileName = "protoc-3.6.1-win32.zip";
        InputStream inputStream = new FileInputStream(fileName);

        DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());

        byte[] buffer = new byte[4096];
        long readCount;
        long total = 0;

        long startTime = System.currentTimeMillis();

        while ((readCount = inputStream.read(buffer)) >= 0) {
            total += readCount;
            dataOutputStream.write(buffer);
        }

        System.out.println("发送总字节数: " + total + ", 耗时: " + (System.currentTimeMillis() - startTime));

        dataOutputStream.close();
        socket.close();
        inputStream.close();
    }
}


public class OldIOServer {

    public static void main(String[] args) throws Exception {
        ServerSocket serverSocket = new ServerSocket(7001);

        while (true) {
            Socket socket = serverSocket.accept();
            DataInputStream dataInputStream = new DataInputStream(socket.getInputStream());

            try {
                byte[] byteArray = new byte[4096];

                while (true) {
                    int readCount = dataInputStream.read(byteArray, 0, byteArray.length);

                    if (-1 == readCount) {
                        break;
                    }
                }
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }
}

零拷贝:

// 零拷贝
public class NewIOClient {
    public static void main(String[] args) throws IOException {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("localhost",7001));
        String fileName="protoc-3.6.1-win32.zip";

        //得到一个文件channel
        FileChannel fileChannel = new FileInputStream(fileName).getChannel();

        long startTime = System.currentTimeMillis();

        //在linux下一个transferTo方法就可以完成传输
        //但是在windows下一次调用transferTo 只能发送8M,就需要分段传输文件,而且要注意传输的位置
        //transferTo方法底层实现了零拷贝
        long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
        System.out.println("发送的总字节数="+transferCount+"耗时:"+(System.currentTimeMillis()-startTime));

        //关闭通道
        fileChannel.close();
    }
}


public class NewIOServer {
    public static void main(String[] args) throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        ServerSocket serverSocket = serverSocketChannel.socket();
        serverSocket.bind(new InetSocketAddress(7001));

        //创建buffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        while(true){
            SocketChannel socketChannel = serverSocketChannel.accept();

            int readCount=0;
            while(readCount!=-1){
                readCount=socketChannel.read(byteBuffer);
                byteBuffer.rewind();
            }
        }
    }
}

 

5、综合聊天室

客户端

public class GroupChatClient {

    private  SocketChannel socketChannel;
    private static final int PORT=7777;
    private  Selector selector;
    private final String HOST="localhost";
    private  String name;

    public GroupChatClient() {
        try {
            selector = Selector.open();
            socketChannel=SocketChannel.open(new InetSocketAddress(HOST,PORT));
            socketChannel.configureBlocking(false);
            socketChannel.register(selector,SelectionKey.OP_READ);
            name=socketChannel.getLocalAddress().toString();
            System.out.println(name+"is OK ...");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //向服务器发送消息
    public void senMsg(String msg){
        msg=name+"说:"+msg;
        try {
            socketChannel.write(ByteBuffer.wrap(msg.getBytes()));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //读取消息
    public void readMsg(){
        try {
            int selectCount = selector.select();        //阻塞等待连接
            if(selectCount>0){
                Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
                while(keyIterator.hasNext()){
                    SelectionKey selectionKey = keyIterator.next();
                    if(selectionKey.isReadable()){
                        SocketChannel channel = (SocketChannel) selectionKey.channel();
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                        channel.read(byteBuffer);
                        String msg=new String(byteBuffer.array());
                        System.out.println(msg.trim());
                    }
                    keyIterator.remove();
                }
            }else{
//                System.out.println("没有新连接");
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args){
        GroupChatClient groupChatClient = new GroupChatClient();
        Scanner scanner = new Scanner(System.in);
        new Thread() {
            @Override
            public void run() {
                while (true){
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    groupChatClient.readMsg();
                }
            }
        }.start();
        while(true){
            while(scanner.hasNext()){
                String line = scanner.nextLine();
                groupChatClient.senMsg(line);
            }
        }
    }
}

 

服务器端

public class GroupChatServer {

    private Selector selector;
    private ServerSocketChannel listenChannel;
    private static final int PORT=7777;
    public GroupChatServer(){
        try {
            selector=Selector.open();
            listenChannel=ServerSocketChannel.open();
            listenChannel.socket().bind(new InetSocketAddress(PORT));
            listenChannel.configureBlocking(false);
            listenChannel.register(selector,SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void listen() throws IOException {
        while(true){
            if(selector.select(1000)==0){
//                System.out.println("没有事件");
                continue;
            }
            Iterator<SelectionKey> keyIterator= selector.selectedKeys().iterator();   //获取有事件的selectionKeys集合对应的迭代器
            while (keyIterator.hasNext()){
                SelectionKey selectionKey = keyIterator.next();
                if(selectionKey.isAcceptable()){          //如果当前事件是新连接事件
                    SocketChannel socketChannel = listenChannel.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector,SelectionKey.OP_READ);
                    System.out.println(socketChannel.getRemoteAddress()+" 上线 ");
                }

                if(selectionKey.isReadable()){              //事件是read事件,即通道是可读的状态
                    getMsg(selectionKey);
                }

                keyIterator.remove();
            }
        }
    }

    public void getMsg(SelectionKey selectionKey){
        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        try {
            int count = socketChannel.read(byteBuffer);
            if(count>0){        //如果读取的数据量不为0就输出并转发给其他客户端
                String msg=new String(byteBuffer.array());
                System.out.println("from客户端:"+msg);
                sendToOthers(msg,socketChannel);
            }
        } catch (IOException e) {
            try {
                System.out.println(socketChannel.getRemoteAddress()+"离线了");
                //取消注册
                selectionKey.cancel();
                //关闭通道
                socketChannel.close();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
            e.printStackTrace();
        }
    }

    public void sendToOthers(String msg,SocketChannel socketChannel){
        System.out.println("消息转发给客户端线程:"+Thread.currentThread().getName());
        Iterator<SelectionKey> allKeyIterator = selector.keys().iterator();
        while (allKeyIterator.hasNext()){
            SelectionKey key = allKeyIterator.next();
            Channel channel = key.channel();
            if(channel instanceof SocketChannel  &&  channel!=socketChannel){
                SocketChannel channel1=(SocketChannel)channel;
                ByteBuffer byteBuffer = ByteBuffer.wrap(msg.getBytes());
                try {
                    channel1.write(byteBuffer);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    public static void main(String[] args){
        GroupChatServer groupChatServer = new GroupChatServer();
        try {
            groupChatServer.listen();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

 

posted on 2021-06-30 11:42  萌新J  阅读(95)  评论(0编辑  收藏  举报