1.同步阻塞IO流程
在阻塞式 IO 模型中,从 Java 应用程序发起 IO 系统调用开始,一直到系统调用返回,这段时间内发起 IO 请求的 Java 进程(或者线程)是阻塞的。直到返回成功后,应用进程才能开始处理用户空间的缓冲区数据。
举个例子,在 Java 中发起一个 socket 的 read 操作的系统调用,流程大致如下:
(1)从 Java 进行 IO 读后发起 read 系统调用开始,用户进程(或者线程)就进入阻塞状态。
(2)当系统内核收到 read 系统调用后就开始准备数据。一开始,数据可能还没有到达内核缓冲区(例如,还没有收到一个完整的 socket 数据包),这时内核就要等待。
(3)内核一直等到完整的数据到达,就会将数据从内核缓冲区复制到用户缓冲区(用户空间的内存),然后内核返回结果(例如返回复制到用户缓冲区中的字节数)。
(4)直到内核返回后用户线程才会解除阻塞的状态,重新运行起来。
BIO最突出的特点是:在内核执行 IO 操作的两个阶段,发起 IO 请求的用户进程(或者线程)被阻塞了。
1.1.BIO的优点
应用程序开发非常简单;在阻塞等待数据期间,用户线程挂起,基本不会占用 CPU 资源。
1.2.BIO的缺点
一般情况下操作系统会为每个连接配备一个独立的线程,一个线程维护一个连接的 IO 操作。在并发量小的情况下,这样做没有什么问题。在高并发的应用场景下,阻塞 IO 模型需要大量的线程来维护大量的网络连接,内存、线程切换开销会非常巨大,性能很低,基本上是不可用的。
2.BIO通信模型
服务器实现模式为一个连接对应一个线程,客户端有连接请求时,服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可通过线程池机制改善。
BIO通信模型图如下所示:
或者也可以说,一个请求对应一个线程。
通常由一个独立的Acceptor线程负责监听客户端的连接,接收客户端请求之后为每个客户端建立新线程进行链路处理,通过输出流返回给客户端,然后再进行线程销毁。
BIO是基于字节流和字符流进行操作。
BIO以数据流为核心,在读取与写入时都是通过流进行操作。
InputStream是Java socket中提供的默认读写网络流的接口类,其内部由SocketInputStream实现;在调用read方法时如果流还没有准备完成则会阻塞整个调用线程直到流准备完成。需要写入数据时同样需要将数据写入OutputStream类型的流中,内部由SocketOutputStream实现。
3.适用场景
BIO适用于连接数较小且固定的框架;适用单个大数据对象的高效传输。
缺点也是显而易见:扩展性差,针对高并发情况线程数量急剧增加,性能会急剧下降,甚至出现崩溃。
NIO的面世,这也和BIO 模型的优化有关;BIO模型是阻塞的,阻塞就导致不能使用单线程处理多个请求,但如果将BIO 模型修改,调用read() write()方法不再是阻塞的,那这样就可以使用单线程处理多个请求了;而这样的优化正是NIO的精髓所在。
4.BIO简单demo
经典的C/S模型。
4.1.Server端demo
如下所示:
public class Server {
public static void main(String[] args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 监听 8080 端口进来的 TCP 链接
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
while (true) {
// 这里会阻塞,直到有一个请求的连接进来
SocketChannel socketChannel = serverSocketChannel.accept();
// 开启一个新的线程来处理这个请求,然后在 while 循环中继续监听 8080 端口
SocketHandler handler = new SocketHandler(socketChannel);
new Thread(handler).start();
}
}
}
public class SocketHandler implements Runnable {
private SocketChannel socketChannel;
public SocketHandler(SocketChannel socketChannel) {
this.socketChannel = socketChannel;
}
@Override
public void run() {
ByteBuffer buffer = ByteBuffer.allocate(1024);
try {
// 将请求数据读入 Buffer 中
int num;
while ((num = socketChannel.read(buffer)) > 0) {
// 读取 Buffer 内容之前先 flip 一下
buffer.flip();
// 提取 Buffer 中的数据
byte[] bytes = new byte[num];
buffer.get(bytes);
String re = new String(bytes, "UTF-8");
System.out.println("收到请求:" + re);
// 回应客户端
ByteBuffer writeBuffer = ByteBuffer.wrap(("我已经收到你的请求,你的请求内容是:" + re).getBytes());
socketChannel.write(writeBuffer);
buffer.flip();
}
} catch (IOException e) {
IOUtils.closeQuietly(socketChannel);
}
}
}
阻塞模式的IO其实就是服务端为每次客户端请求分配一个线程去执行,首先accept是个阻塞操作,当有请求到达时才会返回。然后立即分配一个线程去处理这个请求。请注意这个线程不会立即读写,还需要等到通道读写准备就绪才可以读写,在这之前会一直阻塞。
4.2.Client端demo
如下所示:
public class SocketChannelClientTest {
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost", 8080));
// 发送请求
ByteBuffer buffer = ByteBuffer.wrap("1234567890".getBytes());
socketChannel.write(buffer);
// 读取响应
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int num;
if ((num = socketChannel.read(readBuffer)) > 0) {
readBuffer.flip();
byte[] re = new byte[num];
readBuffer.get(re);
String result = new String(re, "UTF-8");
System.out.println("返回值: " + result);
}
}
}
示例中发送1234567890给服务端,并打印服务端返回的数据。
5.伪异步IO模型(BIO+线程池)
为了改进“一连接一线程”的模型,我们可以适用线程池来管理这些线程,实现1个或多个线程处理N个客户端的模型,底层还是使用同步阻塞I/O,所以叫“伪异步IO模型”。如下图所示:
限制了线程数量,如果发生大量并发请求,超出最大限制就只能等待,直到有空闲线程才能被复用。