在日常开发中,总是会涉及到对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的一些使用。




