在日常开发中,总是会涉及到对IO相关的操作,而在JAVA中,网络编程包含了BIO
、NIO
、以及AIO
这几种类型,今天这篇文章主要讲解在JAVA BIO的工作模式,以及针对BIO的常见的优化方式。
实现
在较早的开发中,BIO的开发其实还是很简单的,还是以例子的方式加以说明:
/** * 该测试类主要通过bio的方式创建, 接收客户端,并处理消息 * * @author <a href="mailto:xianglj1991@163.com">xianglujun</a> * @since 2022/2/13 13:55 */ public class SocketServerDemo { public static void main(String[] args) { try { // 创建服务端的socket, 并监听在端口9090 ServerSocket serverSocket = new ServerSocket(9090); // 该方法会发生阻塞, 等待客户端链接 while (true) { Socket socket = serverSocket.accept(); System.out.println("客户端链接成功.."); InputStream is = socket.getInputStream(); printMsg(is); } } catch (IOException e) { e.printStackTrace(); } } /** * 从流中读取消息 * * @param is 输入流 */ private static void printMsg(InputStream is) throws IOException { byte[] bytes = new byte[1024]; while (is.read(bytes) != -1) { // 输出消息 String m = new String(bytes); System.out.println("接收到客户端消息: " + m); } } }
在该实例中,使用了最早起的BIO开发方式,主要包含一下几点:
accept()
方法会产生阻塞, 等待有新的客户端链接上来之后, 才会继续向后面执行read(bytes)
: 该方法主要从输入流中读取数据, 直到客户端断开链接为止
在上面的程序中,主要包含了一下几个问题:
- 当有多个客户端链接的时候,会产生阻塞,只有在前一个客户端数据处理完成并断开后,才能够处理下一个链接
- 无法支撑并发的环境
telnet localhost 9090
我们通过telnet命令可以测试,如下图:
这个时候,客户端能够正常的发送数据到服务端,服务端也能够正常的输出。
此时,如果我们再次开启第二个客户端进行链接时,这是就会发现客户端2产生了阻塞,必须要等待第一个客户端处理完成之后才能进行后续的操作。因此,我们针对这种情况进行优化。
优化1
针对以上程序优化的第一个思路就是,在接收客户端请求的时候,客户端之间不受影响, 因此我们对每个客户端的链接,使用单独的线程进行处理。
package com.jdk.test.demo.java.bio; import java.io.IOException; import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket; /** * 该测试类主要通过bio的方式创建, 接收客户端,并处理消息 * * @author <a href="mailto:xianglj1991@163.com">xianglujun</a> * @since 2022/2/13 13:55 */ public class SocketServerDemo2 { public static void main(String[] args) { try { // 创建服务端的socket, 并监听在端口9090 ServerSocket serverSocket = new ServerSocket(9090); // 该方法会发生阻塞, 等待客户端链接 while (true) { Socket socket = serverSocket.accept(); System.out.println("客户端链接成功.."); new Thread(() -> { InputStream is = null; try { is = socket.getInputStream(); printMsg(is); } catch (IOException e) { e.printStackTrace(); } }).start(); } } catch (IOException e) { e.printStackTrace(); } } /** * 从流中读取消息 * * @param is 输入流 */ private static void printMsg(InputStream is) throws IOException { byte[] bytes = new byte[1024]; while (is.read(bytes) != -1) { // 输出消息 String m = new String(bytes); System.out.println("接收到客户端消息: " + m); } } }
可以看到,对于最开始的版本中,在优化1中的实现,主要是引入了线程,针对每个链接都采用单独的线程对数据进行处理, 提高了对链接的处理效率。
我们启动服务端程序,通过telnet
命令测试是否可以同时处理多个请求:
使用客户端2同时链接并发送消息:
最终呈现的结果如下:
因此通过多线程的方式, 能够同时处理多个客户端的请求,并响应多个客户端的请求结果。但是还是具有以下缺点:
- 当客户端链接不断增加的时候,系统中的线程数量会急速增加,导致线程数量不可控
- 当线程数量急剧增加的时候,导致上线问频繁的切换,性能也会有所下降
- 资源使用(内存、CPU)使用的升高
优化2
针对以上存在的问题,我们很自然的能够想到的处理方式就是使用线程池,这样的话,就能够对线程数量的控制。
package com.jdk.test.demo.java.bio; import java.io.IOException; import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * 该测试类主要通过bio的方式创建, 接收客户端,并处理消息 * * @author <a href="mailto:xianglj1991@163.com">xianglujun</a> * @since 2022/2/13 13:55 */ public class SocketServerDemo3 { private static final ThreadPoolExecutor EXECUTOR = new ThreadPoolExecutor(5, 10, 10, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100)); public static void main(String[] args) { try { // 创建服务端的socket, 并监听在端口9090 ServerSocket serverSocket = new ServerSocket(9090); // 该方法会发生阻塞, 等待客户端链接 while (true) { Socket socket = serverSocket.accept(); System.out.println("客户端链接成功.."); EXECUTOR.submit(() -> { InputStream is = null; try { is = socket.getInputStream(); printMsg(is); } catch (IOException e) { e.printStackTrace(); } }); } } catch (IOException e) { e.printStackTrace(); } } /** * 从流中读取消息 * * @param is 输入流 */ private static void printMsg(InputStream is) throws IOException { byte[] bytes = new byte[1024]; while (is.read(bytes) != -1) { // 输出消息 String m = new String(bytes); System.out.println("接收到客户端消息: " + m); } } }
可以通过代码发现,在优化3中的实现,其实也是多线程版本,只是让线程变的可控,至于无限制的创建线程。
但是线程池版本一样有一下缺点:
- 当线程池中线程满了之后,无法再处理后续的客户端请求,导致后续的客户端失败
通过以上实验可以得知,其实BIO的使用场景是比较有限的,它无法在比较高的并发场景中最大的处理更多的客户端请求,因此当我们的系统并发较小时,BIO可以作为一个方法使用。所以在JDK较早版本中,引入了NIO的使用,在下篇文章中将介绍NIO的一些使用。