BIO-NIO-AIO
一、前言
Java支持的3种网络编程IO模式,分别为:BIO、NIO、AIO
BIO与NIO是同步的,AIO是异步的,AIO是对NIO的封装;
同步与异步主要体现在accept与read方法;
二、BIO
英文全称:Blocking IO;
同步阻塞模型,一个客户端连接对应一个处理线程;
2.1)缺点:
- IO代码里read操作是阻塞操作,如果链接不做数据读写操作会导致线程阻塞,浪费资源;
- 如果线程很多,会导致服务器线程太多,压力太大;
2.2)应用场景:
使用于连接数比较小且固定的架构,这种方式对服务器资源要求比较高,但程序简单容易理解; 例如传统的Socket、文件操作等;
2.3)注意:
1)serverSocket.accept()就是一个阻塞方法;一直等待客户端的连接;
2)socket.getInputStream().read(bytes)也是一个阻塞发放,一直等待客户端发送信息;
一旦发送阻塞,后面的逻辑都不会运行,也不会接受新的客户端链接,所以在服务端应该使用多线程的方式进行处理,但是又不可避免多线程过多而对系统造成一定的问题,因此高并发下不可用;
服务端示例代码:
1 public class SocketServer { 2 public static void main(String[] values) throws IOException { 3 ServerSocket serverSocket = new ServerSocket(9000); 4 while (true) { 5 System.out.println("等待链接"); 6 /* 7 * 等待客户端链接 8 * 这里在客户端没有链接的时候会一直阻塞,直到有客户端建立了链接才会进行下一步 9 * */ 10 Socket socket = serverSocket.accept(); 11 System.out.println("客户端连接上了"); 12 new Thread(() -> { 13 try { 14 handler(socket); 15 } catch (Exception e) { 16 e.printStackTrace(); 17 } 18 }); 19 } 20 } 21 22 private static void handler(Socket socket) throws IOException { 23 System.out.println(Thread.currentThread().getId()); 24 byte[] bytes = new byte[1024]; 25 /* 26 * 这里会一直阻塞,一直要等到客户端发送消息后才会进行下一步 27 * */ 28 int read = socket.getInputStream().read(bytes); 29 System.out.println("读取完毕"); 30 if (read != -1) { 31 System.out.println("接受到的数据:" + new String(bytes, 0, read)); 32 System.out.println("thread id=" + Thread.currentThread().getId()); 33 34 } 35 //回执消息 36 socket.getOutputStream().write("Hi~~~~~~~~~~~~~".getBytes()); 37 socket.getOutputStream().flush(); 38 } 39 }
客户端示例代码:
public class SocketClient { public static void main(String[] values) throws IOException { Socket socket = new Socket("127.0.0.1", 9000); //发送数据 socket.getOutputStream().write("123".getBytes()); socket.getOutputStream().flush(); System.out.println("发送数据结束"); byte[] bytes = new byte[1024]; //接收信息 socket.getInputStream().read(bytes); System.out.println("接收到的信息:" + new String(bytes)); socket.close(); } }
三、NIO
英文全称:Non Blocking IO
Redis就是一个NIO的经典应用;
同步非阻塞,服务器端实现模式为一个咸亨可以处理多个请求,客户端发送的连接请求都会注册到多路服用器(selector)上,多路服用器轮询到连接有IO请求就进行处理;
IO多路复用一般用的Linux API(select,poll,epoll)来实现,他们区别如下:
| select | poll | epoll(jdk 1.5及以上) | |
| 操作方式 | 遍历 | 遍历 | 回调 |
| 底层实现 | 数组 | 链表 | 哈希表 |
| IO效率 |
每次调用都进行线性遍历, 时间复杂度:O(n) |
每次调用都进行线性遍历, 时间复杂度:O(n) |
事件通知方式,每当有IO事件就绪,系统注册的回调函数就会备调用, 时间复杂度O(1) |
| 最大连接 | 有上限 | 无上限 | 无上限 |
3.1)NIO适用场景:
适用于链接数目多且连接比较短的架构,比如:聊天服务器,弹幕系统,服务器之间的通讯,编程比较复杂;是JDK1.4开始支持的;
3.2)NIO三大组件:
Channel:通道,类似于流,每个Channel对应一个buffer缓冲区;
Buffer:缓冲区,其实就是一个数组;
Selector:选择器;
基本概念:
1)channel注册到selector上,由selector根据Channel读写事件的发生将其交由某个空闲的线程处理;
2)selector可以对应一个或多个线程;
3)NIO的Buffer与channel都是即可以读也可以写;
服务端示例代码:
1 public class NioServer { 2 3 // /** 4 // * 属性描述:这里可以使用线程的方式进行性能的提升 5 // * @date : 2019/12/18 0018 下午 6:03 6 // */ 7 // public static ExecutorService pool = Executors.newFixedThreadPool(10); 8 9 public static void main(String[] values) throws IOException { 10 //创建一个通道,通道建立成功表示服务已经建立成功,后面需要对服务进行配置; 11 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); 12 //必须配置为非阻塞,这样才能往Selector上注册,否则会报异常,selector模式本身就是非阻塞模式 13 serverSocketChannel.configureBlocking(false); 14 //绑定一个监听的端口 15 serverSocketChannel.socket().bind(new InetSocketAddress(9000)); 16 //创建选择器selector 17 Selector selector = Selector.open(); 18 /* 19 * 将ServerSocketChannel注册到Selector上,并且Selector对客户端Accept链接明感; 20 * 这里会得到SelectionKey 21 * */ 22 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); 23 24 while (true) { 25 System.out.println("等待事件被触发"); 26 /* 27 * 轮询监听channel里的Key,Select是阻塞的,accept也是阻塞的; 28 * 在客户端没有链接的时候这里是会一直阻塞的; 29 * 一旦发生阻塞,这里就会变成非阻塞,执行后的代码; 30 * */ 31 selector.select(); 32 /* 33 * 接受到数据后就会进入下一步 34 * */ 35 System.out.println("事件被触发"); 36 /* 37 * 有客户端请求,被轮询监听到; 38 * selector.selectedKeys()中保存了当前链接的事件信息; 39 * 如果有多个客户端发生链接、写等事件,都会在这里体现; 40 * */ 41 Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator(); 42 while (keyIterator.hasNext()) { 43 SelectionKey selectionKey = keyIterator.next(); 44 //删除本次已经处理的Key,防止下次select被重复处理; 45 keyIterator.remove(); 46 //这里可以放到线程池里执行,增加并发处理量 47 handle(selectionKey); 48 } 49 } 50 } 51 52 private static void handle(SelectionKey selectionKey) throws IOException { 53 //根据事件进行不同的处理 54 if (selectionKey.isAcceptable()) { 55 //链接事件处理 56 System.out.println("有客户端链接成功"); 57 ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel(); 58 /* 59 * NIO非阻塞体现:此处accept方法是阻塞的,但是这里因为发送了链接时间,所以这个方法会马上执行万,不会阻塞; 60 * 处理完成链接请求不会继续等待客户端发送的数据; 61 * 注意:这里是通过serverSocketChannel.accept()服务端的API获取到了客户端链接的SocketChannel, 62 * 成服务端的Channel与客户端的Channel是同一个东西,就是通道 63 * */ 64 final SocketChannel accept = serverSocketChannel.accept(); 65 //配置非阻塞 66 accept.configureBlocking(false); 67 //通过Selector监听Channel时对读事件明感; 68 accept.register(selectionKey.selector(), SelectionKey.OP_READ); 69 } else if (selectionKey.isReadable()) { 70 System.out.println("有客户端的数据读写发送了"); 71 SocketChannel socketChannel = (SocketChannel) selectionKey.channel(); 72 ByteBuffer buffer = ByteBuffer.allocate(1024); 73 //NIO阻塞体现:首先reda方法不会阻塞,其次这种事件响应模式,当调用到read方法的时候肯定发生了客户端发送数据的事件 74 int len = socketChannel.read(buffer); 75 if (len != -1) { 76 System.out.println("读取到客户端发送的数据:" + new String(buffer.array(), 0, len)); 77 } 78 ByteBuffer bufferToWrite = ByteBuffer.wrap("Hello".getBytes()); 79 socketChannel.write(bufferToWrite); 80 selectionKey.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE); 81 socketChannel.close(); 82 } 83 } 84 85 }
客户端示例代码:
public class NioClient { /** * 属性描述:通道管理器 * @date : 2019/12/17 0017 下午 8:06 */ private Selector selector; public static void main(String[] values) throws IOException { NioClient nioClient = new NioClient(); nioClient.initClient("127.0.0.1", 9000); nioClient.connect(); } /** * 功能描述:采用轮询的方式进行监听,监听Selector上是否有需要处理的事件 * @date : 2019/12/17 0017 下午 8:08 */ private void connect() throws IOException { //轮询访问selector while (true){ /* * 选择一组可以进行I/O操作的事件,放在selector中客户端的方法不会被阻塞 * 这里与服务端的方法不一样,当至少一个通道被选中时,selector的wakeup方法会被调用, * 方法返回,对于客户端来说,通道时一直被选中的 * */ selector.select(); Iterator<SelectionKey> selectionKeys = this.selector.selectedKeys().iterator(); while (selectionKeys.hasNext()){ SelectionKey key = selectionKeys.next(); selectionKeys.remove(); if(key.isConnectable()){ SocketChannel channel = (SocketChannel) key.channel(); //如果正在链接,则完成链接; if(channel.isConnectionPending()){ channel.finishConnect(); } //设置为非阻塞 channel.configureBlocking(false); ByteBuffer buffer = ByteBuffer.wrap("Hello.........".getBytes()); channel.write(buffer); channel.register(this.selector, SelectionKey.OP_READ); }else if(key.isReadable()){ read(key); } } } } /** * 功能描述:读取信息的处理 * @date : 2019/12/17 0017 下午 8:16 */ private void read(SelectionKey key) throws IOException { SocketChannel channel = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate(512); int len = channel.read(buffer); if(len!=-1){ System.out.println("客户端收到消息:" + new String(buffer.array(), 0, len)); } } /** * 功能描述:获取一个Socket通道,并对该通道做一些初始化工作 * @date : 2019/12/17 0017 下午 8:07 */ private void initClient(String ip, int port) throws IOException { //获取通道 SocketChannel channel = SocketChannel.open(); //设置为非阻塞 channel.configureBlocking(false); //获取通道管理器 this.selector = Selector.open(); /* * 客户端链接服务器,其实方法执行并没有实现链接,需要在listen方法中调用 * 用channel.finishConnect()才能完成链接 * */ channel.connect(new InetSocketAddress(ip, port)); //将通道管理器和该通道绑定,并未该通道注册SelectionKey.OP_CONNECT事件 channel.register(selector, SelectionKey.OP_CONNECT); } }
3.3)NIO处理流程:

服务端:
1)创建服务,指定监听端口及模式,这里注意,链接模式必须为非阻塞模式,否则报错;
2)创建选择器Selector;
3)先注册一个链接事件,这里开始就应该是一个链接事件,后面好进行后续处理;
4)创建循环,不断的通过select()方法监听事件,这个API是阻塞的,当客户端创建链接或者写入数据的时候都会被触发;
如果客户端没有任何事情发生,这里阻塞是正常的,不会对程序造成任何影响,有活干活没事自己一边凉快去;
5)通过选择器的selectedKeys方法获取当前所有的事件描述对象SelectionKey;
6)为了避免重复处理可以将集合中正在处理的SelectionKey移除;
7)通过不同的事件类型进行处理;
8)如果当前事件为”连接“(isAcceptable)类型,可以通过key对象(SelectionKey)可以获取到对应事件的channel,这里的SocketChannel不管是客户端还是服务端,其实都是同一个东西;
这里需要注意,操作服务端通过selectionKey.channel()拿到的就是服务端Channel(ServerSocketChannel),操作客户端通过selectionKey.channel()拿到的是客户端的SocketChannel;通过强转是可以进行转换的;
类型为注册的时候拿到的是服务端Channel(ServerSocketChannel),对客户端信息的读写使用的是客户端Channel(SocketChannel);
ServerSocketChannel.accept方法本身是阻塞的,但是NIO中可以调用此API方法的时候必定是注册事件发生了,所以这里的阻塞可以忽略不计;注意,如果放错地方,比如放到读数据里就会发生阻塞;
SocketChannel.read方法本身是阻塞的,但是在NIO中可以调用此方法的时候必定是写入事件发生了,所以这里的阻塞可以忽略不急;注意,如果放错地方,比如放到注册事件里就会发生阻塞;
9)这里可以进行其他的操作,比如链接成功后再次通过SocketChannel的register方法注册当前的读事件;
10)在写入或读取处理完成后,设置再次处理的感知事件注册,将SocketChannel关闭;
3.4)NIO在开发中存在的问题:
NIO在实际的开发中代码多而容易出错,所以在正常使用的时候,我们一般是采用Netty的方式;
已经链接的处理还没有处理完成的时候,新的链接会被搁置,达到一定数量的时候会拒绝链接;
如果链接处理使用多线程,那么会面对更多的线程并发问题的处理;
ByteBuffer设计很XXXX,使用很迷惑,非常反人类;中间包括了很多读写转换,非常容易出BUG,所以现在一般都是使用Netty来开发;

浙公网安备 33010602011771号