2020-06-01一 NIO

一. NIO

定义

NIO是面向缓冲区的流, 我们将数据和缓冲区通过一根管道连接起来,然后我们对缓冲区中的数据进行操作了

NIO是双向的流, 也就是说,这个缓冲区既可以存储又可以输出

NIO是非阻塞的, 通道建立之后,就会自动的读或取了,这就意味着一个线程可以管理多个流通道

NIO在解析数据的时候非常麻烦, 但适用于高并发小流量的场景,如聊天服务器

线程越多, 浪费的资源就越多

多线程为什么浪费资源? 

NIO的作用

避免多线程的开销

可以模拟出多线程的处理方式 (通道的数据时间有间隔的)

二. Buffer(缓冲区)

定义

因为NIO主要就是对缓冲区进行操作,所以,这个至关重要

分类

除了boolean外的基本数据类型,都提供了对应的缓冲区

ByteBuffer , CharBuffer, ShortBuffer, IntBuffer, LongBuffer, FloatBuffer, DoubleBuffer

常用的就是ByteBuffer , CharBuffer

重要属性

capacity : 缓冲区容量, 表示缓冲区中最大存储数据的容量, 一旦声明,不能改变

limit :  界限, 表示缓冲区中可以操作数据的大小(limit和limit后的数据不能进行读写)

position : 位置, 表示缓冲区中正在操作数据的位置

mark : 标记, 可以标记position的位置 ,可以使用reset()方法,将position回到标记位置

常用方法

allocate(int capacity) : 指定缓冲区的大小

put() : 存储数据

get() : 获取数据

flip() : 切换成输出模式

rewind() : 切换回输出模式的初始化位置,重复读

clear() : 清空缓冲区, 所有标记回到最初状态, 其中的数据并没有被清空,只是处于"被遗忘"状态

mark() : 标记position的位置

reset() : 将position回到标记的位置

演示

publicstaticvoidmain(String[]args)throwsException{

    //创建指定容量的字节缓冲区

    ByteBufferbuffer=ByteBuffer.allocate(10);


    System.out.println(buffer.position());

    System.out.println(buffer.limit());

    System.out.println(buffer.capacity());


    System.out.println("..........put.........");

    //添加

    buffer.put("abc".getBytes());

    System.out.println(buffer.position());

    System.out.println(buffer.limit());

    System.out.println(buffer.capacity());


    //切换成输出模式 position位置归0 limit移动到position原来的位置

    System.out.println("..........flip.........");

    buffer.flip();

    System.out.println(buffer.position());

    System.out.println(buffer.limit());

    System.out.println(buffer.capacity());


    //获取当前position位置的值 position位置+1

    System.out.println("..........get.........");

    byteb=buffer.get();

    System.out.println(b);

    System.out.println(buffer.position());

    System.out.println(buffer.limit());

    System.out.println(buffer.capacity());


    //切换回输出模式的初始化位置,重复读

    System.out.println("..........get.........");

    buffer.rewind();

    System.out.println(buffer.position());

    System.out.println(buffer.limit());

    System.out.println(buffer.capacity());


    //清空缓冲区,一切还原,为再次写入做准备

    System.out.println("..........get.........");

    buffer.clear();

    System.out.println(buffer.position());

    System.out.println(buffer.limit());

    System.out.println(buffer.capacity());


    //表示postion的位置  使用reset()将position的位置回归到mark标记位置

    System.out.println("..........mark()和reset().........");

    buffer.put("abc".getBytes());//添加三个字节

    buffer.mark();

    buffer.put("df".getBytes());

    System.out.println(buffer.position());

    System.out.println(buffer.limit());

    System.out.println(buffer.capacity());

    buffer.reset();

    System.out.println(buffer.position());

    System.out.println(buffer.limit());

    System.out.println(buffer.capacity());

}

三. Channel(通道)

定义

用于读取、写入、映射和操作文件的通道,可以将程序和数据实体建立连接

java的流都提供了获取通道的方法

Channel是双向的,既可以读又可以写,而流是单向的

Channel可以进行异步的读写

对Channel的读写必须通过Buffer对象

常用方法

read(Buffer b) : 将数据写入到缓冲区

write(Buffer b) : 从缓冲区输出数据

四. FileChannel(不推荐使用)

定义

用于读取、写入、映射和操作文件的通道

将数据读取存储到缓冲区, 也可以将缓冲区的数据写入到本地

这个类无法直接关联到文件,必须通过IO的流进行获取, 预留的方法,但是还没有启用

FileChannel是阻塞的

演示

publicstaticvoidmain(String[]args)throwsException{

    ByteBufferbf=ByteBuffer.allocate(1024);

    //从字节数据流中获取文本FileChannel

    FileInputStreamfis=newFileInputStream("d:\\骑在银龙的背上.mp3");

    FileChannelfcr=fis.getChannel();


    //从字节输出流中获取文本FileChannel

    FileOutputStreamfos=newFileOutputStream("d:\\音乐.mp3");

    FileChannelfcw=fos.getChannel();

    while(fcr.read(bf)!=-1){

        bf.flip();

        fcw.write(bf);

        bf.clear();

    }

    fcr.close();

    fcw.close();

    fis.close();

    fos.close();

    //快速复制

    //fcr.transferTo(0, fcr.size(), fcw);

}

五. DatagramChannel

定义

针对面向数据报套接字的可选择通道

操作UDP的NIO流

演示

接收端

publicstaticvoidmain(String[]args)throwsException{

    //获取DatagramChannel

    DatagramChannelchannel=DatagramChannel.open();

    //创建socket

    channel.bind(newInetSocketAddress(9999));

    //创建缓冲区

    ByteBufferbuf=ByteBuffer.allocate(100);


    Scannerscanner=newScanner(System.in);

    while(true){

        //清空缓冲区,准备接收数据

        buf.clear();

        //接收网络数据

        channel.receive(buf);

        System.out.println(newString(buf.array(),0,buf.position()));



        Stringstr=scanner.nextLine();

        //情况缓冲区,准备存入数据

        buf.clear();

        buf.put(str.getBytes());

        //将缓冲区切换成输出模式

        buf.flip();

        //发送数据

        channel.send(buf,newInetSocketAddress("127.0.0.1",6666));

    }

}

发送端

publicstaticvoidmain(String[]args)throwsException{

    //创建获取DatagramChannel

    DatagramChannelchannel=DatagramChannel.open();

    //创建socket

    channel.socket().bind(newInetSocketAddress(6666));;


    ByteBufferbuffer=ByteBuffer.allocate(1024);

    buffer.put("我爱你".getBytes());


    SocketAddresssocket=newInetSocketAddress("127.0.0.1",9999);


    Scannerscanner=newScanner(System.in);

    while(true){


        buffer.clear();


        Stringstr=scanner.nextLine();

        //将数据装入缓冲区

        buffer.put(str.getBytes());

        //将缓冲区切换为输出模式

        buffer.flip();

        channel.send(buffer,socket);

        //清空缓冲区,为接受数据做准备

        buffer.clear();

        //接收数据

        channel.receive(buffer);

        System.out.println(newString(buffer.array(),0,buffer.position()));

    }

}

六. SocketChannel和ServerSocketChannel

定义

对应着TCP协议

打开一个SocketChannel并连接到互联网上的某台服务器

一个新连接到达ServerSocketChannel时,会创建一个SocketChannel

用法和Socket,ServerSocket完全一致

演示

客户端

publicstaticvoidmain(String[]args)throwsException{

    //打开通道

    SocketChannelchannel=SocketChannel.open();

    //建立连接

    channel.connect(newInetSocketAddress("127.0.0.1",9999));

    //设置缓冲区

    ByteBufferbuffer=ByteBuffer.allocate(1024);


    buffer.put("I LOVE YOU".getBytes());

    //将缓冲区设置为输出模式

    buffer.flip();

    channel.write(buffer);

}

服务端

publicstaticvoidmain(String[]args)throwsException{

    //打开通道

    ServerSocketChannelchannel=ServerSocketChannel.open();

    //建立服务端

    channel.socket().bind(newInetSocketAddress(9999));

    //获取socket

    SocketChannelsocket=channel.accept();


    ByteBufferbf=ByteBuffer.allocate(1024);

    //接收数据

    socket.read(bf);


    System.out.println(newString(bf.array(),0,bf.position()));

}

测试题

编程实现一个可以相互聊天的客户端和服务端

七. Selector

定义

Selector是一个通道管理器

我们知道,NIO具有非阻塞的能力, 可以在一个线程内同时执行多个操作, 节省了线程间切换的开销

但是, 当启动非阻塞的时候,输入和输出方法就完全独立运行了, 这可能导致读的时候对面还没有把信息发送过来, 写的时候,对方还没有完全准备好

所有, 我们使用Selector类对通道进行管理,当某个操作准备好了之后, Selector会提醒我们,这时,我们就可以进行操作了

Selector监视的状态分类

SelectionKey.OP_CONNECT  连接准备就绪

SelectionKey.OP_ACCEPT  客户端已经连接

SelectionKey.OP_READ  要读的数据已经准备好

SelectionKey.OP_WRITE  可以进行写入了

常用方法

select() 获取所有已经准备好的通道,仅仅是这次的,上一次调用这个方法获取的通道不算

selectedKeys() 获取上一次select()方法获取到的通道

编码步骤

publicstaticvoidmain(String[]args)throwsException{

    //打开通道

    SocketChannelchannel=SocketChannel.open();

    //建立连接

    channel.connect(newInetSocketAddress("127.0.0.1",9999));

    //将当前通道设置为非阻塞

    channel.configureBlocking(false);

    //获取通道选择器

    Selectorselector=Selector.open();

    //将通道注册进通道选择器中,这里设置通道选择器需要监视的状态是"可读取"

    channel.register(selector,SelectionKey.OP_READ);


    //往服务端发送一条数据

    ByteBufferbs=ByteBuffer.allocate(1024);

    bs.put("我爱你".getBytes());

    bs.flip();

    channel.write(bs);


    //控制循环,时刻检测通道选择器

    while(true){

        //查看通道选择监视的状态时候有通道符合要求了

        //select 方法获取所有符合状态的通道

        if(selector.select()>0){

            //遍历符合状态的通道

            for(SelectionKeykey:selector.selectedKeys()) {

                //判断当前通道是否可读

                if(key.isReadable()) {

                    //读取内容

                    ByteBufferbuffer=ByteBuffer.allocate(1024);

                    SocketChannelsocket=(SocketChannel)key.channel();

                    intlen=socket.read(buffer);

                    System.out.println(len);

                    System.out.println(newString(buffer.array(),0,buffer.position()));

                    //改变通道的需要监视的状态

                    //key.interestOps(SelectionKey.OP_READ);

                }

                //将键从已经选择的集合中去除

                //这个里获取到的通道都是上一次select()方法已经执行到的,如果不去除的话,下一次调用select()方法就无法获取到了

                selector.selectedKeys().remove(key);

            }

        }

    }

}

总结:

原始tcp的问题

如果不使用多线程, 会造成第一个连接阻塞第二个连接

如果使用多线程, 会增加系统的开销(会有很多的资源浪费在管理,监测线程上)

分析问题

通过分析我们发现, 尽量不要使用多线程, 但是不是使用多线程又不行, read阻塞着下一个连接

所以,我们设法将程序中的阻塞方法设置为非阻塞 , 就可以解决 上一个连接阻挡下一个连接的情况

但是, 问题是数据无法正确的读取到了, 使用通道管理器管理通道

方法是可以设置为非阻塞

通道管理器帮助我们管理通道

NIO编程的优点

不需要使用多线程, 减少了线程的开销

可以模拟多线程的运行方式 (其实没有办法真的达到多线成的平均时间的效果)

NIO的缺点

其实没有办法真的达到多线成的平均时间的效果

不适用于大流量的场景 , 只适用于小流量高并发的场景

Buffer

缓冲区, 有两种模式: 输入和输出模式

capacity limit  postion mark

flip() clear()

数据假死, 被遗忘状态

作业

第一题

利用NIO流上传图片到服务器

扩展题

思考,如何使用NIO流来实现群聊

你可能感兴趣的:(2020-06-01一 NIO)