本文共 8899 字,大约阅读时间需要 29 分钟。
上篇文章讲述了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的演变过程说清楚。
通道表示打开到 IO 设备(例如:文件、套接字)的连接。可以理解为客户端 – 服务端,之间建立起的路。起到了数据读写的作用,但是Channel是不能直接往两端写的,选要一个缓冲区Buffer。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210531205912569.png 对于以前我们BIO,它的数据读写时通过数据流的形式,比如输入流、输出流。 但是NIO他是基于通道和缓冲区去读写数据的。有什么优势呢?(此NIO暂时不带seletor)Channel组件的实现一共分为以下几种:
代码简单演示下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));
缓冲区是一块可以写入数据,然后可以从中读取数据的内存,本质上就是一个数组。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。
个人觉得Buffer是NIO相对核心的组件。
刚才说了Channel是不能够直接读写客户端和服务器端数据的,中间需要有一个buffer缓冲区存储数据,让Channel读写。 从数据类型上区分NIO Buffer主要分为以下几类:这些Buffer覆盖了你能通过IO发送的基本数据类型:byte、short、int、long、float、double和char。
说一下Buffer数组结构和相关的一些方法: (网上找了个图)// 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);
ByteBuffer buffer = ByteBuffer.allocateDirect(10);
![在这里插 同时 具体讲述如下:
普通的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 { Listclients = 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/