🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
很多人说BIO不好,会“block”,但到底什么是IO的Block呢?考虑下面两种情况: * 用系统调用`read`从socket里读取一段数据 * 用系统调用`read`从一个磁盘文件读取一段数据到内存 如果你的直觉告诉你,这两种都算“Block”,那么很遗憾,你的理解与[Linux](https://so.csdn.net/so/search?from=pc_blog_highlight&q=Linux)不同。Linux认为: * 对于第一种情况,算作block,因为Linux无法知道网络上对方是否会发数据。如果没数据发过来,对于调用`read`的程序来说,就只能“等”。 * 对于第二种情况,**不算做block**。 是的,对于磁盘文件IO,Linux总是不视作Block。 你可能会说,这不科学啊,磁盘读写偶尔也会因为硬件而卡壳啊,怎么能不算Block呢?但实际就是不算。 > 一个解释是,**所谓“Block”是指操作系统可以预见这个Block会发生才会主动Block**。例如当读取TCP连接的数据时,如果发现Socket buffer里没有数据就可以确定定对方还没有发过来,于是Block;而对于普通磁盘文件的读写,也许磁盘运作期间会抖动,会短暂暂停,但是操作系统无法预见这种情况,只能视作不会Block,照样执行。 基于这个基本的设定,在讨论IO时,一定要严格区分网络IO和磁盘文件IO。NIO和后文讲到的IO多路复用只对网络IO有意义。 > 严格的说,O\_NONBLOCK和IO多路复用,对标准输入输出描述符、管道和FIFO也都是有效的。但本文侧重于讨论高性能网络服务器下各种IO的含义和关系,所以本文做了简化,只提及网络IO和磁盘文件IO两种情况。 # 1. 为什么需要IO模型 1.cpu基于内存计算(内存存取速度远高于外部设备),所以在进行数据读取时,首先将数据从外部设备读取到内存,这个拷贝过程可能比较耗时,这时cpu应该空闲等待,还是处理其他请求,这时io模型需要解决的事情。 ## 1.2 网络IO 对于一个网络I/O通信过程,比如网络数据读取,会涉及两个对象: * 调用这个I/O操作的用户线程 * 操作系统内核 一个进程的地址空间分为用户空间和内核空间,用户线程不能直接访问内核空间。 当用户线程发起I/O操作后(Selector发出的select调用就是一个I/O操作),网络数据读取操作会经历两个步骤: 1. 用户线程等待内核将数据从网卡拷贝到内核空间 2. 内核将数据从内核空间拷贝到用户空间 有人会好奇,内核数据从内核空间拷贝到用户空间,这样会不会有点浪费? 毕竟实际上只有一块内存,能否直接把内存地址指向用户空间可以读取? Linux中有个叫mmap的系统调用,可以将磁盘文件映射到内存,省去了内核和用户空间的拷贝,但不支持网络通信场景!各种I/O模型的区别就是这两个步骤的方式不一样。 # 2. 同步阻塞I/O **因为IO,阻塞线程** **用户线程发起read调用后就阻塞了**,**让出CPU**。内核等待网卡数据到来,把数据从网卡拷贝到内核空间,接着把数据拷贝到用户空间,再把用户线程叫醒。 ![](https://img.kancloud.cn/43/9e/439e641ca3d7a606dc88729e1b276803_640x549.png) # 3. 同步非阻塞IO 用户进程主动发起read调用,这是个系统调用,CPU由用户态切换到内核态,执行内核代码。 内核发现该socket上的数据已到内核空间,将用户线程挂起,然后把数据从内核空间拷贝到用户空间,再唤醒用户线程,read调用返回。 **用户线程不断发起read调用(此时没有阻塞,上边那个调用read直接阻塞了)**,数据没到内核空间时,每次都返回失败,直到数据到了内核空间,这次read调用后,在等待数据从内核空间拷贝到用户空间这段时间里,线程还是阻塞的,等数据到了用户空间再把线程叫醒。![](https://img.kancloud.cn/f8/b6/f8b6ae5b0823c22f360b61d5c11be331_640x561.png) # 4. I/O多路复用 用户线程的读取操作分成两步: * **线程先发起select调用**,问内核:数据准备好了吗? * 等内核把数据准备好了,**用户线程再发起read调用** * 在等待数据从内核空间拷贝到用户空间这段时间里,线程还是阻塞的 为什么叫I/O多路复用? 因为一次select调用可以向内核查多个数据通道(Channel)的状态。 ![](https://img.kancloud.cn/7c/15/7c159c14a7cfb3e0720b3f521626455f_640x565.png) # 5. 异步I/O 用户线程发起read调用的同时注册一个回调函数,read立即返回,等内核将数据准备好后,再调用指定的回调函数完成处理。在这个过程中,用户线程一直没有阻塞。 ![](https://img.kancloud.cn/e8/ea/e8eaad8656f2cb652445cdb4ae0e8e97_640x578.png) # 6. Java nio ## 6.1 NIOClient ``` public class NIOClient { /*标识数字*/ private static int flag = 0; /*缓冲区大小*/ private static int BLOCK = 4096; /*接受数据缓冲区*/ private static ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK); /*发送数据缓冲区*/ private static ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK); /*服务器端地址*/ private final static InetSocketAddress SERVER_ADDRESS = new InetSocketAddress( "localhost", 8888); public static void main(String[] args) throws IOException { // TODO Auto-generated method stub // 打开socket通道 SocketChannel socketChannel = SocketChannel.open(); // 设置为非阻塞方式 socketChannel.configureBlocking(false); // 打开选择器 Selector selector = Selector.open(); // 注册连接服务端socket动作 socketChannel.register(selector, SelectionKey.OP_CONNECT); // 连接 socketChannel.connect(SERVER_ADDRESS); // 分配缓冲区大小内存 Set<SelectionKey> selectionKeys; Iterator<SelectionKey> iterator; SelectionKey selectionKey; SocketChannel client; String receiveText; String sendText; int count=0; while (true) { //选择一组键,其相应的通道已为 I/O 操作准备就绪。 //此方法执行处于阻塞模式的选择操作。 selector.select(); //返回此选择器的已选择键集。 selectionKeys = selector.selectedKeys(); //System.out.println(selectionKeys.size()); iterator = selectionKeys.iterator(); while (iterator.hasNext()) { selectionKey = iterator.next(); if (selectionKey.isConnectable()) { System.out.println("client connect"); client = (SocketChannel) selectionKey.channel(); // 判断此通道上是否正在进行连接操作。 // 完成套接字通道的连接过程。 if (client.isConnectionPending()) { client.finishConnect(); System.out.println("完成连接!"); sendbuffer.clear(); sendbuffer.put("Hello,Server".getBytes()); sendbuffer.flip(); client.write(sendbuffer); } client.register(selector, SelectionKey.OP_READ); } else if (selectionKey.isReadable()) { client = (SocketChannel) selectionKey.channel(); //将缓冲区清空以备下次读取 receivebuffer.clear(); //读取服务器发送来的数据到缓冲区中 count=client.read(receivebuffer); if(count>0){ receiveText = new String( receivebuffer.array(),0,count); System.out.println("客户端接受服务器端数据--:"+receiveText); client.register(selector, SelectionKey.OP_WRITE); } } else if (selectionKey.isWritable()) { sendbuffer.clear(); client = (SocketChannel) selectionKey.channel(); sendText = "message from client--" + (flag++); sendbuffer.put(sendText.getBytes()); //将缓冲区各标志复位,因为向里面put了数据标志被改变要想从中读取数据发向服务器,就要复位 sendbuffer.flip(); client.write(sendbuffer); System.out.println("客户端向服务器端发送数据--:"+sendText); client.register(selector, SelectionKey.OP_READ); } } selectionKeys.clear(); } } } ``` ## 6.2 NIOServer ``` public class NIOServer { /*标识数字*/ private int flag = 0; /*缓冲区大小*/ private int BLOCK = 4096; /*接受数据缓冲区*/ private ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK); /*发送数据缓冲区*/ private ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK); private Selector selector; public NIOServer(int port) throws IOException { /** * 以下的所有说明均已linux系统底层进行说明: * nio 的底层实现是 epoll 模式,采用多路复用技术,对nio的代码进行深入分析,结合epoll的底层实现 * 进行详细的说明 * 1.linux网络编程是两个进程之间的通信,跨集群合网络 * 2.开启一个socket线程,在linux系统上任何操作均以文件句柄数表示,默认情况下 * 一个线程可以打开1024个句柄,也就说最多同时支持1024个网络连接请求。阿里云默认打开65535个文件 * 句柄,通常情况下,1G内存最多可以打开10w个句柄数 * * */ // 打开服务器套接字通道 // 底层: 在linux上面开启socket服务,启动一个线程。绑定ip地址和端口号 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 服务器配置为非阻塞 serverSocketChannel.configureBlocking(false); // 检索与此通道关联的服务器套接字 ServerSocket serverSocket = serverSocketChannel.socket(); // 进行服务的绑定 serverSocket.bind(new InetSocketAddress(port)); // 通过open()方法找到Selector // 底层: 开启epoll,为当前socket服务创建epoll服务,epoll_create selector = Selector.open(); // 注册到selector,等待连接 /** * 底层: * 1.将当前的epoll,服务器地址,端口号绑定,如果有连接请求,直接添加到epoll中,epoll的底层是红黑树, * 可以快速的实现连接的查找和状态更新。如果有新的连接过来,直接存放到epoll中。如果有连接过期,中断, * 会从epoll中删除。 * 2.通过epoll_ctl添加到epoll的同时,会注册一个回调函数给内核,当网卡有数据来的时候,会通知内核,内核 * 调用回调函数,将当前内核数据的事件状态添加到list链表中 */ serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); System.out.println("Server Start----8888:"); } // 监听 private void listen() throws IOException { while (true) { // 选择一组键,并且相应的通道已经打开 /** * epoll底层维护一个链表,rdlist,基于事件驱动模式,当网卡有数据请求过来,会发起硬件中断,通知内核已经有来了。内核调用 * 回调函数,将当前的事件添加到rdlist中,将当前可用的rdlist列表发送给用户态,用户去遍历rdlist中的事件,进行处理 */ selector.select(); // 返回此选择器的已选择键集。 Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = selectionKeys.iterator(); while (iterator.hasNext()) { SelectionKey selectionKey = iterator.next(); // 获得当前epoll的rdlist复制到用户态,遍历,同事删除当前rdlist中的事件 iterator.remove(); handleKey(selectionKey); } } } // 处理请求 private void handleKey(SelectionKey selectionKey) throws IOException { // 接受请求 ServerSocketChannel server = null; SocketChannel client = null; String receiveText; String sendText; int count=0; // 测试此键的通道是否已准备好接受新的套接字连接。 if (selectionKey.isAcceptable()) { // 返回为之创建此键的通道。 server = (ServerSocketChannel) selectionKey.channel(); // 接受到此通道套接字的连接。 // 此方法返回的套接字通道(如果有)将处于阻塞模式。 client = server.accept(); // 配置为非阻塞 client.configureBlocking(false); // 注册到selector,等待连接 client.register(selector, SelectionKey.OP_READ); } else if (selectionKey.isReadable()) { // 返回为之创建此键的通道。 client = (SocketChannel) selectionKey.channel(); //将缓冲区清空以备下次读取 receivebuffer.clear(); //读取服务器发送来的数据到缓冲区中 count = client.read(receivebuffer); if (count > 0) { receiveText = new String( receivebuffer.array(),0,count); System.out.println("服务器端接受客户端数据--:"+receiveText); client.register(selector, SelectionKey.OP_WRITE); } } else if (selectionKey.isWritable()) { //将缓冲区清空以备下次写入 sendbuffer.clear(); // 返回为之创建此键的通道。 client = (SocketChannel) selectionKey.channel(); sendText="message from server--" + flag++; //向缓冲区中输入数据 sendbuffer.put(sendText.getBytes()); //将缓冲区各标志复位,因为向里面put了数据标志被改变要想从中读取数据发向服务器,就要复位 sendbuffer.flip(); //输出到通道 client.write(sendbuffer); System.out.println("服务器端向客户端发送数据--:"+sendText); client.register(selector, SelectionKey.OP_READ); } } /** * @param args * @throws IOException */ public static void main(String[] args) throws IOException { // TODO Auto-generated method stub int port = 8888; NIOServer server = new NIOServer(port); server.listen(); } } ```