仅针对网络IO的介绍,要搞懂IO和NIO的大致原理,我们要介绍两个概念:
一.【总体流程】
1. server创建一个socket,并进行端口的绑定,设定监听端口,这时候就可以接受client的网络链接请求了。
这时候我们有两种选择,用户的进程/线程是阻塞 等待连接还是一直主动询问操作系统,接口有没有链接进来。
ServerSocketChannel sc = ServerSocketChannel.open();
sc.bind(new InetSocketAddress(PORT));
sc.configureBlocking(false);//可以阻塞/非阻塞
1.1 【等待连接】即 我们常说的BIO,我们线程阻塞,挂起,并等待操作系统的唤醒。
1.2 【主动询问】即 NIO(非阻塞IO,不是java NIO,java的NIO其实指的是IOM-多路复用),不断的询问操作系统,是否有链接进来。
2.client 发起链接,数据通过网卡触发CPU的终端,CPU根据socket(源IP,源端口,目标IP,目标端口),定位到具体的进程。
3.一方面通过触发CPU的中断,进行排队,一方面网络数据写入内核缓冲区。
4.CPU唤醒对应的进程(如果是阻塞的话,非阻塞,则通过一直询问内核来获取)。
5.进程进行CPU分片的排队,轮到进程A时候,进行数据的读取。
6.触发进行数据读取,把对应的需要读取的数据,从内核缓冲区,读到进程缓冲区之后,进行读取。
那单个 简单的网络IO流程如上,那就引发具体的问题:
1. BIO 线程进行阻塞,时效性肯定是更高的,但线程阻塞无法做其他事情了,吞吐量就会小很多了。
2. NIO 如果有1000个链接,读写 每次询问操作系统,会触发上下文切换,同样会极大的影响性能。
二.【细分IO】
接下来就详细讨论下各个IO的具体情况,涉及的是 大纲图 6的流程,即触发 流程6的时候,用户的进程是在做什么的。
开始介绍6流程时候,我们先了解现有计算机结构。目前网卡数据的写入一般由DMA进行完成的,因此网络的IP包不会触发CPU的上下文切换。
2.1【BIO】
1 public static final Integer PORT = 8080; 2 3 public static void main(String[] args) throws Exception{ 4 ServerSocket serverSocket = new ServerSocket(PORT); 5 System.out.println("-----1. 等待连接-------"); 6 7 while (true) { 8 Socket socket = serverSocket.accept();//阻塞 9 System.out.println("-----2. 已连接,客户端--" + socket.getRemoteSocketAddress() + "-----"); 10 InputStream inputStream = socket.getInputStream(); 11 BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));//阻塞 12 System.out.println("----客户端信息:" + reader.readLine()); 13 inputStream.close(); 14 socket.close(); 15 } 16 }
以上是BIO的Demo
1.首先我们看到,Socket绑定端口之后,accept获取链接的时候是阻塞方式的。
2.通过字节流方式读取网络数据也是阻塞的。即我们通过inputStream获取字节流的时候,如果无数据的时候读取不到,代码是不会往下执行的。
如下图所示:即,我们代码触发read网络IO流时候,会等待网卡写入数据到内核缓冲区的数据(①阶段),等缓冲区数据够了(比如 等待一个完整的socket数据包),进行复制到用户的缓冲区(②阶段),复制完成之后,用户线程即可往下执行代码,进行数据的读取了。
BIO只是网络IO的一种作业模型,有对应的优缺点如下,但是目前不常用,一般再本地的IO上使用,性能不下于NIO,特别是JDK1.5之后有优化过。
BIO的优点:
2.2【BIO】
1 public static final Integer PORT = 8080; 2 3 public static final LinkedList<SocketChannel> clients = new LinkedList<>(); 4 public static void main(String[] args) throws Exception{ 5 ServerSocketChannel sc = ServerSocketChannel.open(); 6 sc.bind(new InetSocketAddress(PORT)); 7 sc.configureBlocking(false);//可以阻塞/非阻塞 8 System.out.println("-----1. 等待连接-------"); 9 while (true) { 10 Thread.sleep(1000); 11 SocketChannel client = sc.accept();//非阻塞 12 //处理链接 13 if (client == null) { 14 System.out.println("-----1.1 未有连接-------"); 15 } else { 16 client.configureBlocking(false);//可以阻塞/非阻塞 17 System.out.println("-----2. 已连接,客户端--" + client.socket().getRemoteSocketAddress() + "-----"); 18 clients.add(client); 19 } 20 21 for(SocketChannel temp:clients){ 22 ByteBuffer buffer = ByteBuffer.allocateDirect(4096); 23 int num = temp.read(buffer); 24 while (num <= 0){ 25 Thread.sleep(1000); 26 System.out.println("----2.1 未有消息-------"); 27 num = temp.read(buffer);//非阻塞 28 } 29 30 buffer.flip(); 31 byte[] msg = new byte[buffer.limit()]; 32 buffer.get(msg); 33 System.out.println("----客户端信息:"+new String(msg)); 34 clients.remove(temp); 35 buffer.clear(); 36 temp.close(); 37 } 38 39 } 40 41 }
以上是NIO的Demo(注:是Non Blocking IO,不是JAVA说的 NewIO)
1.首先我们看到,Socket绑定端口之后,accept获取链接的时候是非阻塞方式的。会一直循环,并不会卡住代码流程。
2.获取连接之后,设置客户端也是非阻塞的,由于为了测试方便,因此都在一个循环流程里面。刚兴趣的可以自己跑一跑这个demo,看看场景。
如下图所示:即,我们代码触发read网络IO流时候,会一直以上代码的while里面循环,会等待网卡写入数据到内核缓冲区的数据(①阶段),等缓冲区数据够了(比如 等待一个完整的socket数据包),进行复制到用户的缓冲区(②阶段),复制完成之后,获取到数据(即 num > 0),然后进行数据的读取了。
NIO的特点如下: 应用程序的线程需要不断的进行 I/O 系统调用,轮询数据是否已经准备好,如果没有准备好,继续轮询,直到完成系统调用为止。
1 public static final Integer PORT = 8080; 2 3 public static void main(String[] args) throws Exception{ 4 5 //SelectorProvider.provider().openServerSocketChannel(); 6 //sun.nio.ch.DefaultSelectorProvider.create() 7 //这边windows是 WindowsSelectorProvider() 8 ServerSocketChannel sc = ServerSocketChannel.open(); 9 sc.bind(new InetSocketAddress(PORT)); 10 sc.configureBlocking(false);//为什么要设置非阻塞,因为再我们调用多路复用器selector询问这个是否有链接进来时候,期望serverSocket是立即答复。 11 12 //多路复用器 13 Selector selector = Selector.open(); 14 System.out.println("-----1. 等待连接-------"); 15 while (true){ 16 Thread.sleep(1000); 17 SocketChannel client = sc.accept();//非阻塞 18 //处理链接 19 if (client == null) { 20 System.out.println("-----1.1 未有连接-------"); 21 } else { 22 //把sc注册进去,仅注册读事件 23 client.configureBlocking(false); 24 ByteBuffer buffer = ByteBuffer.allocateDirect(4096); 25 client.register(selector, SelectionKey.OP_READ,buffer); 26 System.out.println("-----2. 已连接,客户端--" + client.socket().getRemoteSocketAddress() + "-----"); 27 } 28 29 //这里为什么要阻塞 30 while (selector.select(1000) > 0){ 31 //调用多路复用器获取 32 System.out.println("-----3 调用多路复用器-------"); 33 Set<SelectionKey> selectionKeySet = selector.selectedKeys(); //从多路复用器,取出有效的链接(仅注册read事件),调用多路复用器这个是阻塞的,等待多路复用器返回链接 34 35 Iterator<SelectionKey> iterator = selectionKeySet.iterator(); 36 while(iterator.hasNext()){ 37 SelectionKey key = iterator.next(); 38 iterator.remove(); 39 SocketChannel temp = (SocketChannel)key.channel(); 40 ByteBuffer buffer = ByteBuffer.allocateDirect(4096); 41 int num = temp.read(buffer);//为什么要设置非阻塞,因为再我们调用多路复用器selector询问这个是否有链接进来时候,期望serverSocket是立即答复。 42 43 //循环等待有消息 44 while (num <= 0){ 45 Thread.sleep(1000); 46 System.out.println("----2.1 未有消息-------"); 47 num = temp.read(buffer);//非阻塞 48 } 49 50 buffer.flip(); 51 byte[] msg = new byte[buffer.limit()]; 52 buffer.get(msg); 53 System.out.println("----客户端信息"+temp.socket().getPort()+":"+new String(msg)); 54 buffer.clear(); 55 } 56 } 57 58 } 59 }
以上是IOM的Demo(注:就是JAVA说的 NewIO)
1.首先我们看到,跟之前IO不同的地方在于 。
Selector selector = Selector.open();
client.register(selector, SelectionKey.OP_READ,buffer);
2.打开Selector,即多路复用器,然后把我们关心的事件注册到多路复用器上,这里为了区分,我仅仅把读事件注册进去,这个即告诉操作系统,如果发现该端口的链接存在读事件返回回来即可。
3.selector.select() 我们再调用多路复用器询问的时候,如果存在读事件时候,即返回对应的key,我们循环Key,进行读。
如下图所示:即,类似BIO,因为由操作系统帮我们去校验链接的状态了,不用一个个链接循环去询问(一个个循环触发上下文切换,非常耗时),同样会等待网卡写入数据到内核缓冲区的数据(①阶段),等缓冲区数据够了(比如 等待一个完整的socket数据包),进行复制到用户的缓冲区(②阶段),复制完成之后,获取到数据,然后进行数据的读取了。由于有IO多路复用器已经帮我们过滤了,获取到的都是具备完善的条件的(即 可读/可写),因此通过阻塞进行数据的读,时效性更高。
多路复用IO的优点: