I/O模型
I/O模型简单的理解:就是用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能。
Java共支持是三种网络编程模型:BIO/NIO/AIO
BIO模型
Java BIO:同步阻塞(传统阻塞型),服务器实现模式是一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程来进行处理。如果这个连接不做任何事情就会造成不必要的线程开销
其中BIO常用的编程对象即ServerSocket
和Socket
,分别对应服务端和客户端。
需要了解的是ServerSocket#accept
方法是阻塞的,没有客户端连接进来会一直阻塞在那里。
InputStream#read也是一个阻塞方法,如果没有内容可读的话,会阻塞在那里
以一个例子,来理解客户端有连接请求时,服务端会创建一个与之对应的线程,并且这个连接不做任何事情也不会被断开,会造成不必要的线程开销
/**
* Created by myk
* 2020/1/6 下午2:19
* 使用BIO模型编写一个服务器端,监听6666端口,当有客户端连接时,就启动一个线程与之通讯。
*/
public class Server {
private static int PORT = 6666;
public static void main(String[] args) throws IOException {
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
ServerSocket serverSocket = new ServerSocket(PORT);
while (true) {
Socket socket = serverSocket.accept();
cachedThreadPool.submit(() -> {
handler(socket);
});
}
}
private static void handler(Socket socket) {
InputStream inputStream = null;
try {
inputStream = socket.getInputStream();
byte[] bytes = new byte[1024];
while (true) {
int read = inputStream.read(bytes);
if (read == -1) {
return;
}
System.out.println("线程信息:" + Thread.currentThread().getId() + ",名字:" + Thread.currentThread().getName());
System.out.println(new String(bytes, 0, read));
}
} catch (IOException e) {
try {
inputStream.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
}
这里使用一个线程池,当有客户端连接到服务端时即使用一个线程来处理
客户端使用telnet来模拟
telnet 127.0.0.1 6666
联通之后进入telnet命令行,敲击enter进入编辑模式,随后输入字符串使用回车发送消息,即实现向指定端口发送消息。
这里每一个telnet终端即相当于一个客户端,👇下面是模拟两个客户端连接,因此开启了两个线程。但是客户端连接上即便什么都不做,也是占用了一个连接,浪费了资源,造成了不必须的线程开销
BIO适用场景:
适用于连接数目比较小且固定的架构,这种方式对服务端机器资源要求比较高,因为每一个连接即一个线程。(可用线程池优化)
BIO编程流程:
- 服务端启动一个ServerSocket
- 客户端启动Socket对服务器进行通信,默认情况下服务器端需要对每一个用户建立一个线程与之通讯
- 客户端发出请求后,先咨询服务端是否有线程响应,如果没有则等待或这次请求被拒绝(如果是线程池模式下,建立的线程已经很多了)
- 如果有响应,则客户端等待服务端响应之后,可以接着再次请求
BIO存在的问题:
- 每个请求都需要服务端创建与之对应的独立的线程进行读写操作
- 如果并发数较大时,需要创建大量线程来处理连接,系统资源耗费比较大
- 连接建立后,如果当前线程暂时没有数据可读,则线程会阻塞在read方法那里,造成线程资源浪费
NIO模型
同步非阻塞,服务器实现模式为一个线程处理多个连接请求,即客户端发送的链接请求都会被注册到多路复用器上,多路复用器轮询到链接上有IO请求就进行处理。
NIO有三大核心部分:Channel(通道)、Buffer(缓冲区)、Selector(选择器)
NIO是面向缓冲区或者面向块编程的。数据读取到一个它稍后要处理的缓冲区,需要时可在缓冲区前后移动,这就增加了处理过程的灵活性,使用它可以提供非阻塞式的高伸缩性网络。
Java NIO的非阻塞模式,使一个线程从某个通道发送请求或者读取数据,但是它仅仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直到数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
综合以上,结合上图可以得知NIO是可以做到用一个线程来处理多个操作的。假设有1000个请求过来,根据实际情况可以分配5-10个线程来处理,而不像BIO那样非得分配1000个。
NIO和BIO的比较
- BIO以流的方式处理数据,而NIO以块的方式处理数据,块I/O的效率比流I/O高很多
- BIO是阻塞的,NIO是非阻塞的
- BIO基于字节流和字符流进行操作,而NIO基于Channel(通道)和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求、数据读写等),因此使用单个线程就可以监听到多个客户端通道。
Java AIO
- JDK7 引入了Asynchronous I/O,即AIO。在进行I/O编程中,常用有两种模式:Reactor和Proactor。Java的NIO就是Reactor,当有事件触发时,服务器端得到通知,进行相应的处理
- AIO 叫做异步不阻塞的IO,AIO引入异步通道的概念,采用了Proactor模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。
BIO/AIO/NIO对比:
BIO | NIO | AIO | |
---|---|---|---|
IO模型 | 同步阻塞 | 同步非阻塞(多路复用) | 异步非阻塞 |
编程难度 | 简单 | 复杂 | 复杂 |
可靠性 | 差 | 好 | 好 |
吞吐量 | 低 | 高 | 高 |