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

网络编程–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);
// write
byte[] 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<SocketChannel>  clients = new ArrayList<SocketChannel>();
        // 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个连接进来了,我每次不去遍历所有连接,而是去遍历有消息事件的连接就行了哈。
这就是多路复用器的设计思想,下篇介绍!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Survivor001

你可以相信我,如果你愿意的话

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值