一文带你搞懂什么是BIO
作者:写bug的黑猫
BIO
BIO英文全名是 blocking IO,也叫做 阻塞IO,是最容易理解、最容易实现的IO工作方式。
1.1、什么是阻塞IO(BIO)
当我们在谈论阻塞IO(Blocking IO)时,我们指的是一种输入输出方式,其中线程正在进行IO操作时会被阻塞(即暂停运行),直到IO 操作完成。这种阻塞是同步的,也就是说线程会等待IO操作完成后再继续执行后续的任务。
在阻塞IO中,当一个线程调用IO操作(如读取或写入数据)时,如果没有数据可用或无法立即完成IO操作,线程会被挂起,直到满足操作条件。这种挂起意味着线程无法执行其他任务,因为它一直等待IO操作完成。
举个例子说明,假设一个线程负责从网络套接字读取数据,Socket就是网络套接字。当线程调用读取数据的方法时,如果没有数据可用,线程将被阻塞,直到有数据可读为止。在此期间,线程无法执行其他任务,他会一直等待直到数据到达或IO操作超时。
阻塞IO的特点是简单易懂,但也存在一些问题。其他一个问题是当有多个IO操作需要处理时,每个操作都会阻塞对应的线程,导致线程的浪费。在高并发或大规模的应用程序中,这可能会导致性能的下降,因为线程的创建和上下文切换会带来额外的开销。
为了解决阻塞IO的性能问题,Java引入了非阻塞IO(Non-blocking IO)模型,例如NIO(New IO)和NIO.2。这些模型使用了事件驱动的方式,通过单个线程处理多个IO操作。当一个IO操作无法立即完成时,线程不会被阻塞,而是继续执行其他任务,等待IO操作完成后再处理。这种方式能更有效地利用系统资源,并提高并发处理能力。
综上所述,阻塞IO是一种简单易懂的IO操作方式,但在高并发或大规模应用程序中可能存在性能问题。了解阻塞IO的概念可以帮助我们理解其他IO模型的工作原理和优势。
1.2、BIO工作原理
当一个客户端请求到达服务器时,服务器会为该请求创建一个新的线程。这个线程将负责处理该请求的所有IO操作,包括读取请求数据、处理请求、发送响应等。这种一对一的线程模型在简单的应用场景下可能是可行的,但当并发请求增加时,线程的数量也会相应增加。
大量线程的创建和管理开销较大,会消耗大量的系统资源,包括内存和CPU。每个线程都需要占用一定的内存空间,并且线程之间的切换也需要消耗CPU资源。如果并发请求非常高,线程的数量可能会过多,导致系统资源不足,甚至引发性能下降、系统崩溃等问题。
BIO的优点是通俗易懂,适用于一些处理少量应发请求的简单服务器,比如:单线程服务器、简单的客户端-服务器通信等。对于高并发、大规模的网络应该程序,BIO模型可能无法满足要求,这会使得线程创建和管理开销太大。
1.3、BIO服务器
当谈到编写一个BIO(Blocking I/O)程序时,我们可以创建一个简单的服务器,它能够接受客户端连接请求并处理这些请求。下面是一个使用java socket编写的BIO服务器简单示例:
import java.io.*; import java.net.ServerSocket; import java.net.Socket; public class BioServer { public static void main(String[] args) { int port = 8080; // 服务器监听的端口号 try { ServerSocket serverSocket = new ServerSocket(port); System.out.println("服务器启动,监听端口:" + port); while (true) { // 等待客户端连接 Socket socket = serverSocket.accept(); System.out.println("客户端连接成功,地址:" + socket.getInetAddress() + ":" + socket.getPort()); // 创建线程处理客户端请求 new Thread(() -> { try { // 获取输入流和输出流 BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); PrintWriter writer = new PrintWriter(socket.getOutputStream()); // 读取客户端发送的数据 String request = reader.readLine(); System.out.println("接收到客户端数据:" + request); // 处理请求并返回响应 String response = "Hello, client!"; writer.println(response); writer.flush(); System.out.println("发送响应给客户端:" + response); // 关闭连接 socket.close(); System.out.println("客户端连接关闭"); } catch (IOException e) { e.printStackTrace(); } }).start(); } } catch (IOException e) { e.printStackTrace(); } } }
这个示例程序创建了一个ServerSocket来监听指定端口(这里使用8080)。在主循环中,通过调用accept()
方法等待客户端的连接请求,没有连接的时候,程序会一直阻塞在这里,直到收到客户端连接请求。一旦客户端连接成功,程序会创建一个新的线程来处理该客户端的请求。
在处理线程中,我们获取与客户端连接的输入流和输出流,使用BufferedReader
来读取客户端发送的数据,并使用PrintWriter
来进行客户端发送响应。这里处理逻辑非常简单,及仅仅返回一个固定的字符作为响应。
当请求处理结束后,关闭与客户端的连接,并继续等待下一个客户端的连接。
需要注意,这个示例程序是单线程的。当有新的客户端连接时,都会创建一个新的线程来处理请求。这种方式仅适用于简单的应用场景,在高并发下,会创建大量的线程,导致性能下降。
1.4、BIO客户端
如果想发送消息到上面的BIO服务器,我们可以使用一个简单的Socket客户端来连接服务器并发送请求。以下是一个示例代码:
import java.io.*; import java.net.Socket; public class BioClient { public static void main(String[] args) { String serverAddress = "localhost"; // 服务器地址 int serverPort = 8080; // 服务器端口号 try { // 连接服务器 Socket socket = new Socket(serverAddress, serverPort); System.out.println("连接服务器成功"); // 获取输入流和输出流 BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); PrintWriter writer = new PrintWriter(socket.getOutputStream()); // 发送请求 String request = "Hello, server!"; writer.println(request); writer.flush(); System.out.println("发送请求给服务器:" + request); // 接收响应 String response = reader.readLine(); System.out.println("接收到服务器响应:" + response); // 关闭连接 socket.close(); System.out.println("连接关闭"); } catch (IOException e) { e.printStackTrace(); } }
在这个示例程序中,BIO客户端与服务器的通信过程如下:
- 创建一个Socket对象,指定服务器地址和端口号。
- 通过调用Socket对象的getInputStream()方法和getOutputStream()方法,分别获取与服务器连接的输入流和输出流。输入流用于接收服务器的响应,输出流用于向服务器发送请求。
- 构造一个请求消息,例如Hello, server!。
- 使用输出流的println()方法将请求消息发送给服务器,并调用flush()方法确保消息被立即发送到服务器。
- 使用输入流的readLine()方法来读取服务器发送的响应消息。
- 打印接收到的服务器响应消息。
- 关闭与服务器的连接,调用Socket对象的close()方法。
你可以在该示例程序中修改请求消息和服务器地址、端口号,以适应你的实际情况。
运行实例
先启动服务器,进行端口监听,再启动客户端,连接到指定服务器地址的端口
#BioServer输出台显示
服务器启动,监听端口:8080
客户端连接成功,地址:/127.0.0.1:60038
接收到客户端数据:Hello, server!
发送响应给客户端:Hello, client!
客户端连接关闭
#BioClient输出台显示
连接服务器成功
发送请求给服务器:Hello, server!
接收到服务器响应:Hello, client!
连接关闭
结尾
尽管阻塞IO模型简单易懂,但在高并发或大规模的网络应用程序中,使用该模型可能会遇到线程创建和管理开销过大的问题。因此,在需要高并发处理的场景下,阻塞IO模型并不是最理想的选择。
为了解决这个问题,可以考虑使用其他IO模型,如非阻塞IO(NIO)或基于NIO的框架(如Netty)。
非阻塞IO模型通过使用非阻塞的IO操作和事件轮询机制,允许程序在等待IO操作完成的同时继续执行其他任务,从而提高了系统的并发能力。而基于NIO的框架则提供了更高级别的抽象和更灵活的IO操作方式,例如使用选择器(Selector)来管理多个IO通道,实现单线程处理多个IO连接。
这些模型可以使用较少的线程处理多个请求,并且能更好地利用系统资源,提高并发处理能力。
非阻塞IO和基于NIO的框架具有以下优势:
- 更高的并发性能:不像阻塞IO模型那样频繁地创建和管理线程,,非阻塞IO和基于NIO的框架能够通过一个线程处理多个IO连接,减少了线程创建和管理的开销,从而提高了系统的并发性能。
- 更灵活的IO操作方式:非阻塞IO和基于NIO的框架提供了更灵活的IO操作方式,例如事件驱动的编程模型和选择器机制,使得程序能够更高效地管理和处理多个IO连接。
- 资源利用率更高:更加节约资源,由于非阻塞IO和基于NIO的框架使用了较少的线程,可以更有效地利用系统资源,避免了线程创建和上下文切换的开销。
在选择适合的IO模型时,需要根据具体的应用场景和性能需求来权衡各种模型的优劣。阻塞IO模型适用于简单的、低并发的应用场景,而非阻塞IO和基于NIO的框架则更适合需要处理大量并发连接的高性能应用。
到此这篇关于一文带你搞懂什么是BIO的文章就介绍到这了,更多相关Java BIO内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!