一、什么是IO?
-
要说BIO之前,还得说一说什么是IO。记得自己在某一次面试过程中,面试官问自己:
Q:如果一个http请求在线上环境执行的非常慢,你会如何定位问题?
A:而我却挠了挠脑袋,支支吾吾的说了半天。
最终在面试结束之前的你有什么要问我的吗?环节,我主动向面试官请教了这个问题,其中,面试官说的最多的就是IO这两个字。
那IO到底是什么呢?
概念:
我们都知道unix世界里、一切皆文件、而文件是什么呢?文件就是一串二进制流而已、不管socket、还是FIFO、管道、终端、对我们来说、一切都是文件、一切都是流、在信息交换的过程中、我们都是对这些流进行数据的收发操作、简称为I/O操作(input and output)、往流中读出数据、系统调用read、写入数据、系统调用write。
简单来说就是:涉及到数据交互的地方就是存在IO,只不过有些数据是从网络中获取的,有些是从硬盘中获取的。
二、第一次显示的执行IO操作
- 我们什么时候会直接的去做一些IO操作呢?在大学期间,我们一定做过一些利用Java提供的api完成一些文件的拷贝操作。毫无疑问,这是我们第一次直面IO,这个时候我们才知道,原来所谓的一串二进制数组就能表示成一个文件。直到后来我们使用socket完成了聊天室的功能,能发送各种表情、图片等等等等。这个时候,我们才知道,在计算机中,原来二进制能表示出来一切东西(后来学计算机基础时,才彻底明白计算机中一切皆0和1)。
- 使用Java的输入输出流的案例就不过多总结了(虽然当初对什么时候用输入流什么时候用输出流傻傻分不清楚!😆),这里详细讲一下我们当初使用Java的socket编写的聊天程序,也就是我们说的BIO。
三、认识BIO(Blocking IO)单线程版本
-
BIO,顾名思义,它是一个阻塞式IO,那它到底是哪里阻塞呢?我们看下面的demo:
public static void startCommonServer() throws IOException { ServerSocket socket = new ServerSocket(8800); while (true) { try{ System.out.println("服务器启动,等待连接, 第一次阻塞"); Socket clientSocket = socket.accept(); System.out.println("客户端连接成功: " + clientSocket); InputStream inputStream = clientSocket.getInputStream(); // 1 byte[] bytes = new byte[1024]; // 2 int length; // 3 // read 方法在读取完数据时或者抛出异常前,此方法一直阻塞 // 所以无法将"Hi avengerEug"写入到socket,进而客户端收不到消息 System.out.println("连接成功,准备读数据, 第二次阻塞"); // 4 while ((length = inputStream.read(bytes)) != -1) { // 5 System.out.println(length); // 6 System.out.println(new String(bytes, 0, length)); // 7 } System.out.println("数据读取成功,继续等待下一个客户端连接"); // 8 } catch (IOException e) { e.printStackTrace(); } finally { // 关闭input流代码省略 } } }
大家可以做如下两个实验,并参照下执行结果是否与我列出来的相同:
序号 实验 实验结果 原因 1 debug模式开启服务端应用程序,在数字 1
所在行添加断点,此时无任何客户端连接服务器一次都没打印出来 因为在accept方法阻塞主了,此时无法继续往下执行。当有客户端连接上时,此时会解除accept的阻塞,会继续往下执行 2 debug模式开启服务端应用程序,在数字 1
所在行添加断点,此时分别开两个客户端连接。可以观察下,在不放开断点的情况下客户端连接成功 这句话一共打印了几遍?只打印了一遍,当断点放开后,还是只打印了一遍 因为第一个客户端已经跟服务端建立连接了,服务端一直在等客户端给他写数据,阻塞在那里了。导致while循环目前只执行了一次,无法去处理后面连接到服务器的客户端。那什么时候才能处理后面连接到服务器的客户端呢?非常抱歉,只要第一个客户端不退出连接,服务器无法处理后面的客户端,在此案例中,BIO性能是非常低的,只能服务一个客户端 经过上面的实验,我们能够发现这种BIO的写法,只能跟一个客户端打交道。那要如何优化它呢?解决问题当然是从根源着手呀,通过上面的分析,我们知道了此种模式的BIO一共有两个阻塞的地方,分别为accept和read方法。这是jdk底层api的阻塞,我们无法进行修改,那咱们就来规避它!BIO accept方法的阻塞是因为防止while循环一直在那里空转,并且有客户端连接了再进行处理,这体现了懒汉式的思想,因此,这反而算它的一个优点。于是,我们直接把矛头指向read方法。我们在上述的实验2中得出了一个结论:只要第一个客户端不退出连接,服务器无法处理后面的客户端,Java不是有线程么,每次连接一个客户端,我们就开启一个新线程去处理它呀,将上述的数字1-数字8之间的代码放在一个线程中去执行,这样就不会影响服务器处理其他的客户端了呀。因此,有了如下BIO的多线程版本。
四、BIO多线程版本
-
有了上述的分析,我们可以很简单的使用线程来解决BIO单线程的问题,详见如下代码:
public static void startCommonServer() throws IOException { ServerSocket socket = new ServerSocket(8800); while (true) { try{ System.out.println("服务器启动,等待连接, 第一次阻塞"); Socket clientSocket = socket.accept(); System.out.println("客户端连接成功: " + clientSocket); new Thread(() -> { // 1 InputStream inputStream = null; try { inputStream = clientSocket.getInputStream(); byte[] bytes = new byte[1024]; int length; System.out.println("连接成功,准备读数据, 第二次阻塞"); while ((length = inputStream.read(bytes)) != -1) { System.out.println(length); System.out.println(new String(bytes, 0, length)); } System.out.println("数据读取成功,继续等待下一个客户端连接"); } catch (IOException e) { e.printStackTrace(); } }).start(); } catch (IOException e) { e.printStackTrace(); } finally { // 关闭input流代码省略 } } }
同上,我们紧接着做上述的两个实验:
序号 实验 实验结果 原因 1 debug模式开启服务端应用程序,在数字 1
所在行添加断点,此时无任何客户端连接服务器一次都没打印出来 因为在accept方法阻塞主了,此时无法继续往下执行。当有客户端连接上时,此时会解除accept的阻塞,会继续往下执行 2 debug模式开启服务端应用程序,在数字 1
所在行添加断点,此时分别开两个客户端连接。可以观察下,在不放开断点的情况下客户端连接成功 这句话一共打印了几遍?只打印了一遍,当断点放开后,能打印两遍了! 此时因为咱们把read方法的阻塞交由其他线程去执行了,并没有影响主线程的执行流程。因此服务器可以同时处理多个客户端的链接,但read方法还是阻塞了,只不过是位于不同的线程中而已。 经过上面的这种实验,多线程版本的BIO好像不阻塞了,服务器也能同时处理多个客户端了。是的,这样的BIO是能同时处理多个客户端了,但是,它只能处理一些已知数量的连接,并发性低。若我们同时有10万个客户端同时请求过来(有些客户端还仅仅是连接,不发送任何数据给服务器,白白的占用一个线程),那你的服务器就要同时开10万个线程去处理这些请求。一般的服务器是部署在linux系统中的。在linux系统中,每开一个线程其实底层就是开启一个文件,这涉及到linux中一个叫句柄的东西(需要恶补相关知识点),它是很难同时在一台linux系统中开启10万个线程的。虽然,我们可以用扩容来分发请求数可以解决这个问题,但线程其实是一个很宝贵的资源的,仅仅的10万请求就要进行扩容操作,这是非常不可取的。(除非你裤兜里的钱多,不在乎~😆),而且,每次重新开启一个线程也会耗费很多资源的,这块虽然可以用线程池来代替,还是那句话,就算使用了线程池,但它总归是有一个容量的。说了那么多,总而言之就是:BIO的多线程版本,只能适用于已知的并发数的系统,处理能力有限。
五、总结
- BIO是学习IO的入门知识点,也是我们编写聊天室的hello world。它的单线程版本性能非常鸡肋,只能供学习使用。而它的多线程版本虽然能同时处理多个客户端了,但它的处理并发能力是有限的,适用于并发数量有限的项目。
- 下篇文档总结主题:认识NIO,从ByteBuffer开始
- 如果你觉得我的文章有用的话,欢迎点赞、收藏和关注。😆
- I’m a slow walker, but I never walk backwards