先来了解一下Java中的IO流,IO(Input Output)用于实现数据的输入和输出操作,Java把不同的输入/输出源抽象表述为流(Stream)。
- 按照数据流向,可以分为输入流和输出流。
- 按照数据类型,可以分为字节流和字符流。
- 按照处理功能,可以分为节点流和处理流。
Java共支持3种网络编程的IO模型:BIO、NIO、AIO,它们之间的区别在于处理请求的过程是同步还是异步,堵塞还是非堵塞。
同步与异步
同步指的是方法一旦调用,调用者必须等到方法调用返回后,才能继续执行后面的行为。
异步指的是调用后立即返回,调用者不必等待方法内的代码执行结束,就可以继续执行后面的行为。
堵塞与非堵塞
阻塞指的是遇到同步等待后,一直等待同步方法处理完成。
非堵塞指的是遇到同步等待后,不等待方法执行完成,先操作其它流程,隔断时间后再观察同步方法是否完成。
BIO、NIO、AIO适用场景
- BIO方式适用于连接数目比较少且固定的架构,这种方式对服务器资源要求比较高,但是程序简单易理解。
- NIO方式适用于连接数目多且连接比较短的架构,比如连天服务器、服务器间通讯等。
- AIO方式适用于连接数目多且连接比较长的架构,比如图像服务器等。
BIO
BIO(Blocking IO)同步堵塞IO,是JDK1.4前的传统IO模型。服务器实现模式是一个连接一个线程,客户端有连接请求时,服务端会启动一个线程来处理请求,所以在性能和可靠性方面存在着巨大的瓶颈,在高并发的场景下,机器资源很快就会被耗尽。
BIO编程流程
- 服务端启动一个ServerSocket
- 客户端启动Socket与服务端通信,默认情况下服务端会对每个客户端请求创建一个线程与之通信。
- 客户端发出请求后,等待服务端线程响应。
- 客户端线程等待请求结束后,再继续执行。
BIO应用实例
BIO的主要阻塞的点是:
- 建立连接
- 读:in.read()
- 写:out.write()
(1)BIO服务端代码
import java.io.*;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
public class BIOServer {
public static void main(String[] args) throws Exception {
ServerSocket server = null;
try {
server = new ServerSocket();
server.bind(new InetSocketAddress(9000));
while (true) {
//客户端每一个socket创建一个线程
Socket socket = server.accept();
// 启动请求处理线程
new Thread(new HandleTask(socket)).start();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
server.close();
}
}
}
class HandleTask implements Runnable {
private Socket socket;
public HandleTask(Socket socket){
this.socket = socket;
}
@Override
public void run() {
InputStream in = null;
OutputStream out = null;
try {
// 读数据
in = socket.getInputStream();
byte[] buffer = new byte[1024];
while (in.read(buffer) != -1) {
System.out.println("读取请求数据:>> " + new String(buffer));
}
// 写数据
out = socket.getOutputStream();
out.write("World".getBytes());
out.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
IOCloseUtils.close(out,in);
}
}
}
(2)客户端代码
public class BIOClient {
public static void main(String[] args) {
Socket socket = null;
OutputStream out = null;
InputStream in = null;
try {
// 与Server建立连接
socket = new Socket("127.0.0.1", 9000);
// 发送数据
out = socket.getOutputStream();
out.write("Hello".getBytes());
out.flush();
// 完成发送数据
socket.shutdownOutput();
// 接收数据
in = socket.getInputStream();
byte[] buffer = new byte[1024];
// 阻塞式读取
while (in.read(buffer) != -1) {
System.out.println("读取相应数据:>> " + new String(buffer));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
IOCloseUtils.close(socket,out,in);
}
}
}
(3)工具类
public class IOCloseUtils {
public static void close(AutoCloseable... closeables) {
for (AutoCloseable closeable : closeables) {
if (closeable != null) {
try {
closeable.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
BIO进阶版
基于线程池的伪异步 IO,为解决同步堵塞IO面临的一个请求需要一个线程处理的问题,我们可以对线程模型进行优化,通过线程池来处理多个客户端的请求接入,通过线程池控制控制服务端线程数。由于线程池和消息队列都是有界的,因此,无论客户端并发连接数多大,它都不会导致线程个数过于膨胀或者内存溢出,相对于传统的一连接一线程模型,是一种改良。但是,这种方案底层依旧采用同步堵塞模型,因此没有解决BIO本质的缺陷。
实例代码
代码只是针对线程添加了线程池。
public class BIOThreadPoolServer {
public static void main(String[] args) throws Exception {
//创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(5);
ServerSocket server = null;
try {
server = new ServerSocket();
server.bind(new InetSocketAddress(9000));
while (true) {
//客户端每一个socket创建一个线程
Socket socket = server.accept();
// 启动请求处理线程
threadPool.submit(new HandleTask(socket));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
server.close();
}
}
}
NIO
NIO(Non-Blocking IO)同步非堵塞IO,是JDK1.4开始提供的API。线程发起IO请求,立即返回(非堵塞)。同步指的是必须等待IO缓冲区内的数据准备就绪,非堵塞指的是用户线程不用等待IO缓冲区,可以先做一些其他操作,但是需要定时轮询检查IO缓冲区数据是否已就绪。
NIO是支持面向缓冲区,基于通道的IO操作,它有三大核心部分:
- Channel(通道)
- Buffer(缓冲区)
- Selector(多路复用选择器)
Buffer
缓冲区的本质是一块可以写入数据,然后可以从中读取数据的内存,可以理解成是一个容器对象,该对象提供了一组方法,可以更轻松地操作内存块。
Channel
BIO的stream是单向的,Channel是双向的,既可以从通道中读取数据,又可以写数据到通道。也可以异步读写数据。
Selector
Selector可以能够检查一个或多个NIO通道,并确定哪些通道已经准备好进行读取或写入。所以一个单独的线程可以管理多个channel。
- 每个Channel会对应一个Buffer。
- 一个线程对象一个Selector,一个Selector对应多个Channel。
NIO应用实例
与BIO的Socket和ServerSocket类相对应,NIO也提供了SocketChannel和ServerSocketChannel这种不同的套接字通道实现。
(1)NIO服务端代码
public class NIOServer {
public static void main(String[] args) throws Exception {
// 创建通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
// 绑定端口
serverSocketChannel.bind(new InetSocketAddress(9001));
// 获取选择器
Selector selector = Selector.open();
// 将通道注册到选择器上
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (selector.select() > 0) {
// 获取选择器中的所有注册的通道中已经就绪好的事件
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey sk = it.next();
// 判断这个事件具体是什么
if (sk.isAcceptable()) {
SocketChannel channel = serverSocketChannel.accept();
channel.configureBlocking(false);
// 将该通道注册到选择器上
channel.register(selector, SelectionKey.OP_READ);
System.out.println("客户端注册到Selector:" + selector.keys().size());
} else if (sk.isReadable()) {
SocketChannel sChannel = (SocketChannel) sk.channel();
// 读取数据
ByteBuffer buf = ByteBuffer.allocate(1024);
int len = 0;
while ((len = sChannel.read(buf)) > 0) {
buf.flip();
System.out.println("来自客户端的请求:" + new String(buf.array(), 0, buf.remaining()));
buf.clear();
}
}
// 取消
it.remove();
}
}
}
}
(2)NIO客户端
public class NIOClient {
public static void main(String[] args) throws IOException {
// 获取通道
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9001));
socketChannel.configureBlocking(false);
// 分配缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
// 发送数据
String str = "你好,服务端";
buf.put(str.getBytes());
buf.flip();
socketChannel.write(buf);
buf.clear();
// 关闭通道
socketChannel.close();
}
}
AIO
AIO(Asynchronous IO)异步非堵塞IO,与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。
AIO应用实例
(1)AIO服务端代码
public class AIOServer {
public static void main(String[] args) throws IOException, InterruptedException {
AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress("127.0.0.1", 9002));
server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
@Override
public void completed(AsynchronousSocketChannel result, Void attachment) {
try {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("服务端 TO 客户端 :say hello".getBytes());
buffer.flip();
result.write(buffer, null, new CompletionHandler<Integer, Void>(){
@Override
public void completed(Integer result, Void attachment) {
System.out.println("服务端发送消息成功");
}
@Override
public void failed(Throwable exc, Void attachment) {
System.out.println("发送失败");
}
});
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
result.read(readBuffer, null, new CompletionHandler<Integer, Void>() {
//成功时调用
@Override
public void completed(Integer result, Void attachment) {
System.out.println("接收客户端消息:" + new String(readBuffer.array()));
}
//失败时调用
@Override
public void failed(Throwable exc, Void attachment) {
System.out.println("读取失败");
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
//失败时
@Override
public void failed(Throwable exc, Void attachment) {
exc.printStackTrace();
}
});
}
}
(2)AIO客户端代码
public class AIOClient {
public static void main(String[] args) throws IOException, ExecutionException, InterruptedException {
AsynchronousSocketChannel client = AsynchronousSocketChannel.open();
Future<Void> future = client.connect(new InetSocketAddress("127.0.0.1", 9002));
future.get();
ByteBuffer buffer = ByteBuffer.allocate(1024);
client.read(buffer, null, new CompletionHandler<Integer, Void>() {
//成功时调用
@Override
public void completed(Integer result, Void attachment) {
System.out.println("接收服务端消息:" + new String(buffer.array()));
}
//失败时调用
@Override
public void failed(Throwable exc, Void attachment) {
System.out.println("客户端接收消息失败");
}
});
ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
writeBuffer.put("客户端 TO 服务端 :say hi".getBytes());
writeBuffer.flip();
Future<Integer> write = client.write(writeBuffer);
Integer r = write.get();
if(r>0){
System.out.println("客户端消息发送成功");
}
//休眠线程
TimeUnit.SECONDS.sleep(1000L);
}
}
BIO、NIO、AIO对比
同步堵塞IO-BIO | 伪异步IO | 非堵塞IO-NIO | 异步IO-AIO | |
---|---|---|---|---|
客户端个数:IO线程 | 1:1 | M:N | M:1(1个IO线程处理多个请求) | M:0(不需要额外启动线程,被动回调) |
堵塞 | 堵塞 | 堵塞 | 非堵塞 | 非堵塞 |
同步 | 同步 | 同步 | 同步 | 异步 |
API使用难度 | 简单 | 简单 | 复杂 | 复杂 |
调试难度 | 简单 | 简单 | 复杂 | 复杂 |
可靠性 | 非常差 | 差 | 高 | 高 |
吞吐量 | 低 | 中 | 高 | 高 |
Gitee地址:gitee.com/renxiaoshi/…