Java BIO 基本介绍
- Java BIO 就是传统的java io 编程,其相关的类和接口在 java.io
- BIO(blocking I/O) : 同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户连接服务器)。
- BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,程序简单易理解
Java BIO 工作机制
采用 BIO 通信模型 的服务端,通常由一个独立的 Acceptor 线程负责监听客户端的连接。我们一般通过在while(true) 循环中服务端会调用 ServerSocket的accept() 方法等待接收客户端的连接的方式监听请求,一旦接收到一个连接请求,就可以建立通信套接字Socket,并为这个通信套接字创建一个新的客户端线程处理这条 Socket 链路。如下图所示。

但是在池化技术大行其道的今天,线程的创建和销毁时非常浪费资源的,使用线程池将线程利用起来重复使用,线程池可以设置等待队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。那么上面的传统 BIO 工作机制图就变成了如下图所示的演进。

说了这么多很多刚入门的小伙伴依旧很懵逼,看不懂。下面我举一个生活中的例子:
传统的 BIO 模型工作机制 -:在生活中饭店一般都有大门口接待服务员(类似于 Acceptor 线程 )接待所有进入饭店的客人,当有一个客人进来吃饭后,饭店就招聘一个服务员(类似于创建线程)专门接待这桌客人,这个服务员在此期间不能做其他事情,当客人走后饭店就把这个服务员辞退(类似于销毁线程),有点类似于同生共死,当然世界上没有那家饭店愿意做这种事情,太不现实;
线程池技术优化后的 BIO 模型工作机制:饭店都会有固定的多个服务员,当有一个客人进来吃饭后,就指定某个服务员专门接待这桌客人(类似于从线程池中取出一个线程),服务员在此期间也不能做其他时间,但是不同的是,当客人走后就可以让这个服务员休息一会(类似于将线程放回线程池等待下次使用),等待下一桌客人的到来。
Java BIO 应用实例
1、代码实现思路
- 使用线程池替代单个线程,每建立一个客户端连接就从线程池中取出一个线程
- serverSocket的accept函数监听客户端连接,如果没有客户端连接,程序将阻塞在accept函数上
- 创建handler函数,处理Socket链路数据,接收来自客户端发送的请求,并打印在控制台
2、服务端代码实现
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @Description: 基于BIO通信模型的TimeServer
* @Author: hh
* @date 2020/10/14
*/
public class TimeServer {
//1、创建一个线程池,如果有客户端连接就创建一个线程
private static final ExecutorService threadPool = new ThreadPoolExecutor(
10,
10,
120L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100));
public static void main(String[] args) {
//1、指定监听端口
int port = 9999;
try (ServerSocket serverSocket = new ServerSocket(port);) {
System.out.println("服务端启动,监听端口: " + port);
while (true) {
//2、创建一个无限循环监听客户端连接,如果没有客户端接入,则主线程将阻塞在 accept() 函数上
System.out.println("等待连接。。。");
Socket socket = serverSocket.accept();
System.out.println("连接到一个客户端,主机地址:" + socket.getInetAddress().getHostAddress());
threadPool.execute(() -> {
handler(socket);
});
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Socket链路处理器
* @param socket
*/
private static void handler(Socket socket) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
System.out.println(Thread.currentThread().getName() + " - 接收来自客户端信息");
String body = null;
//3、循环读取客户端发送信息
while (true) {
if ((body = reader.readLine()) == null) {
break;
}
//4、输出客户端发送信息
System.out.println("服务端接收来自客户端信息:" + body);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
3、客户端发起请求
1、启动 TimeServer,控制台输出如下:

2、如果是win10系统电脑,使用 win+R命令打开运行窗口,输入cmd命令

3、点击"确定",进入命令行窗口,输入"telnet 127.0.0.1 9999",TimeServer控制台输出如下

4、ctrl + ] 命令回显内容,回车,进入编辑状态,输入任何内容,TimeServer控制台将会输出相应内容

BIO 模型问题分析
- 每个请求都需要创建独立的线程,与对应的客户端进行数据 Read,业务处理,数据 Write 。
- 当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大。
- 连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费