NIO基础(一)

三大组件

Channel:双向的数据的传输通道

  • FileChannel:文件传输通道
  • DatagramChannel:数据报传输通道,UDP开发
  • SocketChannel:TCP数据传输通道,服务器客户端都可以用
  • ServerSocketChannel:TCP数据传输通道,服务器端

Buffer:内存缓冲区,暂存Channel的数据

  • ByteBuffer:存储字节数据到缓冲区
  • ShortBuffer:存储字符串数据到缓冲区
  • CharBuffer:存储字符数据到缓冲区
  • IntBuffer:存储整数数据到缓冲区
  • LongBuffer:存储长整型数据到缓冲区
  • DoubleBuffer:存储小数到缓冲区
  • FloatBuffer:存储小数到缓冲区

Selector:配合一个线程来管理多个Channel。获得这些Channel上发生的事件。

ByteBuffer

public class TestDemo {
    public static void main(String[] args) {
        try (FileChannel channel = new FileInputStream("mytest.txt").getChannel()) {
        	//创建一个容量是10的ByteBuffer
            ByteBuffer byteBuffer = ByteBuffer.allocate(10);
            //因为ByteBuffer 容量是10,FileChannel中内容大小不确定,所以要重复读取数据
            while (true) {
            	//向ByteBuffer 中写数据
                int len = channel.read(byteBuffer);
                System.out.println("读取到的字节数:" + len);
                if (len == -1) {
                    break;
                }
                //切换读模式
                byteBuffer.flip();
                while (byteBuffer.hasRemaining()){
                	//get方法从当前位置position读取数据
                    System.out.println("实际字节:" + byteBuffer.get());
                }
                byteBuffer.clear();
            }
        } catch (IOException e) {
        }
    }
}

Buffer使用步骤

  • 向buffer写入数据。例如调用channel.read(buffer),buffer.put()
  • 调用flip切换至读模式
  • 从buffer读取数据,例如调用channel.write(buffer),buffer.get()
  • 调用clear()或compact()切换至写模式。clear()方法会清空整个缓冲区。compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面
  • 重复上边的步骤

ByteBuffer继承Buffer,有三个重要属性

	//当前读写指针
    private int position = 0;
    //读写限制
    private int limit;
    //Buffer容量
    private int capacity;
  • 写模式下,position是写入位置,limit等于容量
  • flip动作发生后,position重置为0,limit重置为position
  • clear动作发生后,重新进入写模式,position从0开始,limit等于容量
  • compact动作发生,会把未读取的部分向前压缩,然后切换为写模式,position则在未读完数据后边开始。
  • mark方法将标记position的位置,调用reset的时候,会重置position到标记的位置

字符串转换

ByteBuffer hello = StandardCharsets.UTF_8.encode("hello");

String s = StandardCharsets.UTF_8.decode(hello).toString();

分配字节缓冲区有两个方法
allocateDirect和allocate,从源码可以看出来创建的ByteBuffer不同

ByteBuffer byteBuffer = ByteBuffer.allocate(10);
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(10);

    public static ByteBuffer allocateDirect(int capacity) {
    	//系统内存,读写效率高,因为少了一次拷贝。不会受GC影响
    	//分配效率低,可能会造成内存泄漏
        return new DirectByteBuffer(capacity);
    }

    public static ByteBuffer allocate(int capacity) {
        if (capacity < 0)
            throw new IllegalArgumentException();
            //返回一个堆内存ByteBuffer
            //读写效率低,因为在堆中,会受到GC影响
        return new HeapByteBuffer(capacity, capacity);
    }

向Buffer中写数据:

  • 从Channel写到Buffer。

    channel.read(buffer)

  • 通过Buffer的put()方法写到Buffer里。

    buffer.put(17)

flip()方法:
flip方法将Buffer从写模式切换到读模式。
调用flip()方法会将position设回0,并将limit设置成之前position的值。

从Buffer中读取数据:

  • 从Buffer读取数据到Channel。

    channel.write(buffer);

  • 使用get()方法从Buffer中读取数据。

    byte aByte = buf.get();

clear()与compact()方法:
clear()方法,position将被设回0,limit被设置成 capacity的值
compact()方法将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后面。limit属性依然像clear()方法一样,设置成capacity。

mark()与reset()方法:
调用Buffer.mark()方法,可以标记Buffer中的一个特定position。之后可以通过调用Buffer.reset()方法恢复到这个position。

equals()与compareTo()方法:
equals()方法当满足下列条件时,表示两个Buffer相等:

  • 有相同的类型(byte、char、int等)。
  • Buffer中剩余的byte、char等的个数相等。
  • Buffer中所有剩余的byte、char等都相同。

compareTo()方法比较两个Buffer的剩余元素,满足下列条件,则认为一个Buffer“小于”另一个Buffer:

  • 第一个不相等的元素小于另一个Buffer中对应的元素 。
  • 所有元素都相等,但第一个Buffer比另一个先耗尽(第一个Buffer的元素个数比另一个少)。
黏包,半包

黏包:多条数据合并为一条
半包:一条数据多次读完

public class TestDemo {
    public static void main(String[] args) {
        /*
        例如三条原始数据
        hello,world\n
        I'm zhangsan\n
        How are you?\n
        现在接受的时候变成了两个byteBuffer(黏包,半包)
        hello,world\nI'm zhangsan\nHo
        w are you?\n
        要求按照\n恢复城原始样子
         */
        ByteBuffer source = ByteBuffer.allocate(32);
        source.put("hello,world\\nI'm zhangsan\\nHo".getBytes());
        source.put("w are you?\\n".getBytes());
    }

    private static void split(ByteBuffer source){
        //切换读模式
        source.flip();
        for (int i = 0; i < source.limit(); i++) {
            //找到 \n 所在位置
            if (source.get(i) == '\n'){
                // 计算新创建的ByteBuffer需要的空间大小
                int length = i + 1 - source.position();
                ByteBuffer buffer = ByteBuffer.allocate(length);
                for (int j = 0; j < length; j++) {
                    //写入数据
                    buffer.put(source.get());
                }
            }
        }
        source.compact();
    }
}

Scatter/Gather(分散/聚集)

分散(scatter): 在读操作时将读取的数据写入多个buffer中

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body   = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { header, body };
channel.read(bufferArray);

read()方法按照buffer在数组中的顺序将从channel中读取的数据写入到buffer,当一个buffer被写满后,channel紧接着向另一个buffer中写。

聚集(gather): 将多个buffer的数据写入同一个Channel

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body   = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { header, body };
channel.write(bufferArray);

write()方法会按照buffer在数组中的顺序,将数据写入到channel,只有position和limit之间的数据才会被写入。

通道之间的数据传输

如果两个通道中有一个是FileChannel,可以直接将数据从一个channel传输到另外一个channel。
FileChannel不能切换到非阻塞模式
transferFrom(),transferTo()

RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel      fromChannel = fromFile.getChannel();

RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel      toChannel = toFile.getChannel();

long position = 0;
long count = fromChannel.size();

toChannel.transferFrom(position, count, fromChannel);
//fromChannel.transferTo(position, count, toChannel);

Selector

创建Selector

Selector selector = Selector.open();
  • FileChannel:一个连接到文件的通道。
    通过使用一个InputStream、OutputStream或RandomAccessFile来获取一个FileChannel实例。
    FileChannel无法设置为非阻塞模式。

    RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
    FileChannel inChannel = aFile.getChannel();
    
  • SocketChannel:连接到TCP网络套接字的通道。

    • 打开一个SocketChannel并连接到互联网上的某台服务器。
    SocketChannel socketChannel = SocketChannel.open();
    socketChannel.connect(new InetSocketAddress("http://myweb.com", 80));
    
    • 一个新连接到达ServerSocketChannel时,会创建一个SocketChannel。
  • ServerSocketChannel :监听新进来的TCP连接的通道。

    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    //如果设置非阻塞模式,要判断SocketChannel是否为null
    //serverSocketChannel.configureBlocking(false);
    serverSocketChannel.socket().bind(new InetSocketAddress(8088));
    while(true){
    	SocketChannel socketChannel = serverSocketChannel.accept();
    }
    
  • DatagramChannel:一个能收发UDP包的通道。

    DatagramChannel channel = DatagramChannel.open();
    channel.socket().bind(new InetSocketAddress(8888));
    //接受数据
    ByteBuffer receive = ByteBuffer.allocate(48);
    receive.clear();
    channel.receive(receive);
    //发送数据
    ByteBuffer send= ByteBuffer.allocate(48);
    send.clear();
    send.put(newData.getBytes());
    send.flip();
    int bytesSent = channel.send(send, new InetSocketAddress("myweb.com", 80));	
    

    DatagramChannel的connect()方法只是让他从特定的地址收发数据,像传统通道一样使用,但是数据传送方面没有任何保障

向Selector注册通道

//设置channel非阻塞模式
channel.configureBlocking(false);
//将channel注册到selector
SelectionKey key = channel.register(selector,SelectionKey.OP_READ);

与Selector一起使用时,Channel必须处于非阻塞模式下

Selector监听Channel时,可以监听四种不同类型的事件:

  • Connect:客户端建立连接触发
  • Accept:连接请求时触发
  • Read:可读事件
  • Write:可写事件

通过Selector选择通道

  • int select():阻塞到至少有一个通道在你注册的事件上就绪了
  • int select(long timeout):和select()一样,除了最长会阻塞timeout毫秒(参数)。
  • int selectNow():不会阻塞,不管什么通道就绪都立刻返回

返回的int值表示有多少通道已经就绪
select()方法会返回读事件已经就绪的那些通道

wakeUp()唤醒
select()阻塞到至少有一个通道在你注册的事件上就绪了。
如果一直没有事件发生,就会一直阻塞。
wakeUp()可以将阻塞在select()方法上的线程会立马返回。

拿到Selector中所有的注册到该Selector的通道

Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();

Selector不会自己从已选择键集中移除SelectionKey实例。必须在处理完通道时自己移除

Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();

while(keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();
    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.
    } else if (key.isConnectable()) {
        // a connection was established with a remote server.
    } else if (key.isReadable()) {
        // a channel is ready for reading
    } else if (key.isWritable()) {
        // a channel is ready for writing
    }
	//客户端如果 强制断开 连接,需要将key从selector集合中移除
	//所以异常处理时,应该调用keyIterator.cancel();移除
	//如果正常断开,channel.read(buffer)返回值是-1,也要调用keyIterator.cancel();移除
    keyIterator.remove();
}

Path

Path接口在java.nio.file包下,和java.io.File有相似性
创建Path实例:Paths.get()

//绝对路径
Path path = Paths.get("c:\\data\\myfile.txt");

//创建相对路径Path, .表示当前目录  ..表示父类目录
//指向d:\data\projects文件夹
Path projects = Paths.get("d:\\data", "projects");
//指向 d:\data\projects\a-project\myfile.txt 文件
Path file = Paths.get("d:\\data", "projects\\a-project\\myfile.txt");

标准化路径:
Path.normalize(),将路径执行真正的目录地址,去掉其中的...

零拷贝

传统的IO将一个文件通过socket写出

  • java本身并不具备IO读写能力,
  • 要从java程序的用户态切换到内核态,去调用操作系统的都能力,将数据读入内核缓冲区
  • 再从内核态切换回用户态,将数据从内核缓冲区读入用户缓冲区
  • 之后再从用户缓冲区写入socket缓冲区
  • 最后向网卡写入数据,从用户态切换至内核态,调用操作系统的写能力

真正读写操作是操作系统来完成的。用户态与内核态切换了3次,数据拷贝了4次

ByteBuffer.allocateDirect()使用的就是操作系统内存

NIO和IO的主要区别

  • IO是面向流的,没有被缓存在任何地方。
    NIO是面向缓冲区的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。
  • IO的各种流是阻塞的。
    NIO的非阻塞模式。
  • NIO的选择器允许一个单独的线程来监视多个输入通道。
    IO没有选择器
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值