博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
网络编程--OS层级NIO的Channel和Buffer
阅读量:2048 次
发布时间:2019-04-28

本文共 8899 字,大约阅读时间需要 29 分钟。

网络编程–OS层级NIO的Channel和Buffer

上篇文章讲述了BIO同步阻塞模型,大家都知道正是因为阻塞的原因,针对多连接,必须要用多线程去处理每一个连接,形成了每连接对应每线程的线程。由此,在多连接的场景下,过多的线程会线程内存浪费以及CPU的调度消耗。

于是内核需要升级,将accept 和 read调用能设置成非阻塞的。服务器端建立连接后,内核程序会把该线程返回的文件描述符fd打上非阻塞标记NONBLOCKING。先看下抓包效果

在这里插入图片描述
在这里插入图片描述
由上面我们可以看出来,操作系统内核升级之后,确实提供了,不阻塞的方式。
既然这样的话我们就可以通过一个线程去处理多个连接了,大致的实现模式是这样的:
在这里插入图片描述
通过一次while去接受连接以及处理连接读写,因为accept和recv都不阻塞了。

以上是C程序大致的伪代码模型,下面说说具体java中怎么实现让设置accept和recv不阻塞,也就是打上NONBLOCKING标记。

java 对于socketchannel提供socketChannel.configureBlocking(false)的方法,去实现非阻塞。看下源码:

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
通过源码也是可以清晰的看出来,最后就是通过外部方法configureBlocking的调用,去实现的非阻塞设置,参数 FileDescriptor 文件描述符,就是对指定的文件描述符是否标记为非阻塞状态,和上面上述的相对应。

在jdk1.4之后引入了新的IO–NEW NIO,其中包括Channel、Buffer、Selector三大组件。今天主要讲解下Channel和Buffer,以及实现通讯。因为Selector主要是对多路复用器的封装,留在我们后面讲解,正好把BIO的演变过程说清楚。

Channel 通道

通道表示打开到 IO 设备(例如:文件、套接字)的连接。可以理解为客户端 – 服务端,之间建立起的路。起到了数据读写的作用,但是Channel是不能直接往两端写的,选要一个缓冲区Buffer。

![在这里插入图片描述](https://img-blog.csdnimg.cn/20210531205912569.png在这里插入图片描述
对于以前我们BIO,它的数据读写时通过数据流的形式,比如输入流、输出流。
但是NIO他是基于通道和缓冲区去读写数据的。有什么优势呢?(此NIO暂时不带seletor)

  1. Channel是双向的既可以从缓冲区读,也往缓冲区可以写。(虽然通道是双向的。但输入流的通道只能用于读取数据到缓冲区,输出流的通道用于把缓冲区数据写入通道。)
  2. Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方;而NIO时基于通道和缓冲区的,可以前后移动缓冲区数据,灵活性很强
  3. Buffer中还提供了一一种文件映射mmap技术,减少了一次文件从用户缓冲区到内核缓冲区的copy过程。(下面到Buffer时候会说到)
  4. 非阻塞IO(Non Blocking IO)

Channel组件的实现一共分为以下几种:

  1. FileChannel(file) – 主要针对文件操作的通道,上面说到文件映射内存区,也只有它才能创建。而FileChannel是不能设置为非阻塞的,因为它没有继承AbstractSelectableChannel
  2. DatagramChannel(UDP) – 通过UDP读写网络中的数据,用的少
  3. SocketChannel(TCP) — TCP网络通讯客户端通道,可以设置为非阻塞
  4. ServerSocketChannel(TCP) – TCP网络通讯服务器端通道,可以设置为非阻塞

代码简单演示下FileChannel的使用:

public static void main(String[] args) throws IOException {
// 先拿到一个文件流,输入流 FileInputStream fileInputStream = new FileInputStream("F:\\ceshi.txt"); // 得到输入流对应的读通道 FileChannel fileChannel =fileInputStream.getChannel(); // 申请一个缓冲区 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); // 将通道数据读取到缓冲区 fileChannel.read(byteBuffer); // 读取缓冲区数据 System.out.println(new String(byteBuffer.array())); // 切换模式 byteBuffer.flip(); // 将读取到的数据写入文件 // 拿到有个文件输出流 FileOutputStream outputStream = new FileOutputStream("F:\\ceshiout.txt"); // 创建写通道 FileChannel fileChannelos =outputStream.getChannel(); // 通道数据写入缓存区 fileChannelos.write(byteBuffer); }

代码简单演示下ServerSocketChannel的创建和非阻塞设置(结合buffer下面会演示详细的):

// 1. 创建套接字通道        ServerSocketChannel ss = ServerSocketChannel.open();        // 2.将该通道设置为非阻塞模式 --> 源码父类AbstractSelectableChannel 中 configureBlocking 实现了非阻塞设置        ss.configureBlocking(true);        // 3.绑定连接端口        ss.bind(new InetSocketAddress("127.0.0.1",9999));

Buffer缓冲区

缓冲区是一块可以写入数据,然后可以从中读取数据的内存,本质上就是一个数组。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。

个人觉得Buffer是NIO相对核心的组件。

刚才说了Channel是不能够直接读写客户端和服务器端数据的,中间需要有一个buffer缓冲区存储数据,让Channel读写。
在这里插入图片描述
从数据类型上区分NIO Buffer主要分为以下几类:

  1. ByteBuffer (常用)
  2. CharBuffer
  3. DoubleBuffer
  4. FloatBuffer
  5. IntBuffer
  6. LongBuffer
  7. ShortBuffer

这些Buffer覆盖了你能通过IO发送的基本数据类型:byte、short、int、long、float、double和char。

说一下Buffer数组结构和相关的一些方法:
在这里插入图片描述(网上找了个图)

  1. capacity
    作为一个内存块,Buffer有个固定的最大值,就是capacity。Buffer只能写capacity个byte、long、char等类型。一旦Buffer满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据
  2. position
    当写数据到Buffer中时,position表示当前的位置。初始的position值为0。当一个byte、long等数据写到Buffer后,position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity – 1。当读取数据时,也是从某个特定位置读。当将Buffer从写模式切换到读模式,position会被重置为0。当从Buffer的position处读取数据时,position向前移动到下一个可读的位置。
  3. limit
    在写模式下,Buffer的limit表示最多能往Buffer里写多少数据。写模式下,limit等于capacity。当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。
  4. clear
    清空缓冲区并返回对缓冲区的应用,其实是将位置设置到0,不会删除缓冲区数据,下次数据进来会更新。
  5. hasReamining()
    判断缓冲区理是否还有元素
  6. mark()
    对缓冲区设置标记
  7. remaining()
    返回position和limit位置之间的元素个数
  8. reset
    将位置position 转到以前设置的mark所在的位置
  9. flip
    为缓冲区的界限设置为当前位置,并将当前位置重置为0。(切换模式)
    以上方法的使用数组的变化网上很多文章,大家可以去看看,我这边就不做分析了,简单放下代码示例看看:
// 1. 分配一个缓冲区大小为10的字节数组        ByteBuffer buffer = ByteBuffer.allocate(10);        System.out.println(buffer.position()); // 返回当前指针位置  0        System.out.println(buffer.limit()); //    返回缓冲区最大限制 10        System.out.println(buffer.capacity()); // 返回缓冲区容量大小 10        // 向缓冲区添加元素        buffer.put("hello".getBytes());        System.out.println(buffer.position()); // 返回当前指针位置  0        System.out.println(buffer.limit()); //    返回缓冲区最大限制 10        System.out.println(buffer.capacity()); // 返回缓冲区容量大小 10        // 读取缓冲区数据        buffer.flip(); // 为缓冲区的界限设置为当前位置,并将当前位置重置为0(切换读模式)

Buffer站在内存分配的角度分为:

1.HeapByteBuffer : 堆内缓冲区。(就是buffer缓冲区分配在堆内–用户空间)
ByteBuffer buffer = ByteBuffer.allocate(10);
在这里插入图片描述
在这里插入图片描述

  1. DirectByteBuffer :对外缓冲区。 (就是buffer缓冲区分配在堆外–用户空间)
    ByteBuffer buffer = ByteBuffer.allocateDirect(10);![在这里插在这里插入图片描述
    同时
    在这里插入图片描述
  2. MappedByteBuffer 文件映射缓冲区(核心),它也属于对外内存。基于mmap+write方式实现。
    在这里插入图片描述
    好处什么?
    用户要想获取磁盘信息,必须系统调用进行一次用户态和内核态的切换,将文件从磁盘通过DMA复制到内核缓冲区,再copy到用户缓存区。而mmap系统调用就是创建了一个用户和内核缓冲区共享的内存空间,就免去了一次拷贝的过程。

具体讲述如下:

普通的IO要想从硬盘获取文件信息,然后再输出到网卡的过程是这样的。
在这里插入图片描述
经历了4次copy,其中2次是DMA复制,2次是CPU复制。

在NIO中零拷贝通过**fileChannel.map()**创建一个MappedByteBuffer缓冲区,底层通过mmap+write方式实现,文件映射之后是这样的:

在这里插入图片描述
从以上的4次copy 编程了3次copy,读写性能大大提高了,用户端直接读写磁盘(当然了中间还有个DMA),不需要频繁耗费性能去系统调用切换内核态copy文件。(CPU拷贝也是很消耗CPU性能的工作)

- MMAP 有哪些注意事项?

MMAP 使用时必须实现指定好内存映射的大小,mmap 在 Java 中一次只能映射 1.5~2G 的文件内存,其中RocketMQ中限制了单文件1G来避免这个问题 MMAP 可以通过 force() 来手动控制,但控制不好也会有大麻烦 MMAP 的回收问题,当MappedByteBuffer 不再需要时,可以手动释放占用的虚拟内存,但使用方式非常的麻烦。

fileChannel.map之后会立刻获得一个 1.5G 的文件,但此时文件的内容全部是 0(字节 0),之后对内存中 MappedByteBuffer 做的任何操作,都会被最终映射到文件之中。

以上是NIO中零拷贝实现原理。通过fileChannel.map()创建共享空间,系统调用mmap技术实现。

现在补充下DMA知识

直接内存访问 DMA (Direct Memory Access)
DMA允许外设设备和内存存储器之间直接进行IO数据传输,其过程不需要CPU的参与。
在这里插入图片描述
现在代码看下NIO中MappedByteBuffer的用法:

//MappedByteBuffer 便是MMAP的操作类(获得一个 1.5G 的文件)MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 1.5 * 1024 * 1024 * 1024);// writebyte[] data = new byte[4];int position = 8;mappedByteBuffer.put(data)
//复制图片,利用直接缓存区    public void test() throws Exception{
FileChannel inChannel = FileChannel.open(Paths.get("D:\\1.jpg"), StandardOpenOption.READ); FileChannel outChannel = FileChannel.open(Paths.get("D:\\2.jpg"), StandardOpenOption.READ,StandardOpenOption.WRITE,StandardOpenOption.CREATE); outChannel.transferFrom(inChannel,0, inChannel.size()); inChannel.close(); outChannel.close(); }

最后给上网络非阻塞IO代码实现(不用多路复用器)

服务器端:

public static void main(String[] args) throws IOException {
List
clients = new ArrayList
(); // 1. 创建套接字通道 ServerSocketChannel ss = ServerSocketChannel.open(); // 2.将该通道设置为非阻塞模式 --> 源码父类AbstractSelectableChannel 中 configureBlocking 实现了非阻塞设置 ss.configureBlocking(true); // 3.绑定连接端口 ss.bind(new InetSocketAddress("127.0.0.1",9999)); // 4.接受客户端连接 while (true){
SocketChannel channel = ss.accept(); // 非阻塞// System.out.println("===============接受连接=================="+channel); if(channel==null){
}else{
// 设置客户端通道为非阻塞 channel.configureBlocking(false); System.out.println(channel.socket().getPort()); // 将一个个连接加入到集合中 clients.add(channel); } // 遍历集合对一个个连接,看是否有消息过来 for(SocketChannel socketChannel : clients){
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024); if(socketChannel==null){
continue; } // 创建一个缓冲区 int num = socketChannel.read(byteBuffer); // 非阻塞// System.out.println("===============服务连接数据=================="+num); if(num>0){
byteBuffer.flip(); byte[] bytes = new byte[byteBuffer.limit()]; byteBuffer.get(bytes); System.out.println(new String(bytes)); byteBuffer.clear(); } } } }

客户端:

public static void main(String[] args) throws IOException {
// 1. 创建客户端套接字通道,且设置非阻塞 SocketChannel socketChannel = SocketChannel.open(); socketChannel.configureBlocking(false); // 2. 连接服务器端 socketChannel.connect(new InetSocketAddress("127.0.0.1", 9999)); System.out.println(socketChannel); if (socketChannel.finishConnect()){
System.out.println("链接成功"); // 3. 发送信息到服务器端 // 创建缓冲区 ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024); // 向缓冲区写入数据 byteBuffer.put("你好啊".getBytes()); byteBuffer.flip(); // 缓冲区写入通道 socketChannel.write(byteBuffer); socketChannel.close(); }else {
System.out.println("没连上"); } }

总结:

NIO中通过channel和buffer的实现,可以把accept和read设置为非阻塞,这样可以规避BIO模型中多线程CPU消耗和线程浪费问题。

但是同时我们也发现了其中的弊端:

如果有10000个连接进来,虽然不阻塞了,但是每次我都要对着10000个连接进行全面的遍历,去获取有事件的连接数据,可能只有1 2个连接有消息事件,但是却要全部遍历了。而且大家都清楚,每一次遍历读取数据都是一次系统调用,所以这就产生了很多无意义的性能消耗,浪费事件和资源。

解决思想:

如果有10000个连接进来了,我每次不去遍历所有连接,而是去遍历有消息事件的连接就行了哈。
这就是多路复用器的设计思想,下篇介绍!

转载地址:http://gjhof.baihongyu.com/

你可能感兴趣的文章
Leetcode C++《热题 Hot 100-24》5.最长回文子串
查看>>
Leetcode C++《热题 Hot 100-25》11.盛最多水的容器
查看>>
Leetcode C++《热题 Hot 100-26》15.三数之和
查看>>
Leetcode C++《热题 Hot 100-27》17.电话号码的字母组合
查看>>
Leetcode C++《热题 Hot 100-28》19.删除链表的倒数第N个节点
查看>>
Leetcode C++《热题 Hot 100-29》22.括号生成
查看>>
Leetcode C++《热题 Hot 100-30》31.下一个排列
查看>>
Leetcode C++《热题 Hot 100-40》64.最小路径和
查看>>
Leetcode C++《热题 Hot 100-41》75.颜色分类
查看>>
Leetcode C++《热题 Hot 100-42》78.子集
查看>>
Leetcode C++《热题 Hot 100-43》94.二叉树的中序遍历
查看>>
Leetcode C++ 《第175场周赛-1 》5332.检查整数及其两倍数是否存在
查看>>
Leetcode C++ 《第175场周赛-2 》5333.制造字母异位词的最小步骤数
查看>>
Leetcode C++ 《第175场周赛-3》1348. 推文计数
查看>>
Leetcode C++《热题 Hot 100-44》102.二叉树的层次遍历
查看>>
Leetcode C++《热题 Hot 100-45》338.比特位计数
查看>>
读书摘要系列之《kubernetes权威指南·第四版》第一章:kubernetes入门
查看>>
Leetcode C++《热题 Hot 100-46》739.每日温度
查看>>
Leetcode C++《热题 Hot 100-47》236.二叉树的最近公共祖先
查看>>
Leetcode C++《热题 Hot 100-48》406.根据身高重建队列
查看>>