目录
第一部分:I/O模型简介
1. I/O 模型简单的理解
就是用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能
2. Java 共支持 3 种网络编程模型/IO 模式
BIO(同步并阻塞)
NIO(同步非阻塞)
AIO(异步非阻塞)
3.阻塞与非阻塞
主要指的是访问IO的线程是否会阻塞(或处于等待);
线程访问资源,该资源是否准备就绪的一种处理方式。
阻塞:
当一个线程占用了临界区资源,那么其它需要使用这个资源的线程都必须在这个临界区上等待。等待会导致线程挂起,这样就形成了阻塞。
如果占用资源的线程一直没有释放资源,那么其它的线程在这个临界区上都不能继续工作。
非阻塞:
表明多个线程之间的执行是不会相互影响的。
4.同步和异步
主要是指的数据的请求方式;
同步和异步是指访问数据的一种机制。
一、同步和异步的概念
1.同步:
所有的操作都做完,才返回给用户。这样用户在线等待的时间太长,给用户一种卡死了的感觉(就是系统迁移中,点击了迁移,界面就不动了,但是程序还在执行,卡死了的感觉)。这种情况下,用户不能关闭界面,如果关闭了,即迁移程序就中断了。
2.异步:
将用户请求放入消息队列,并反馈给用户,系统迁移程序已经启动,你可以关闭浏览器了。然后程序再慢慢地去写入数据库去。这就是异步。但是用户没有卡死的感觉,会告诉你,你的请求系统已经响应了。你可以关闭界面了。
二、Java中交互方式分为同步和异步两种:
相同的地方:
都属于交互方式,都是发送请求。
不同的地方:
同步交互:指发送一个请求,需要等待返回,然后才能够发送下一个请求,有个等待过程;
异步交互:指发送一个请求,不需要等待返回,随时可以再发送下一个请求,即不需要等待。 区别:一个需要等待,一个不需要等待,在部分情况下,我们的项目开发中都会优先选择不需要等待的异步交互方式。
总结一下:
同步和异步是相对于应用和内核的交互方式而言的,
同步 需要主动去询问,
而异步的时候内核在IO事件发生的时候通知应用程序,
阻塞和非阻塞仅仅是系统在调用系统调用的时候函数的实现方式而已。
第二部分:BIO(同步并阻塞)
1.BIO的理解
Java BIO就是传统的 socket编程.
BIO(blocking I/O) : 同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户连接服务器)。
2.BIO工作机制
3.举个小例子
4.BIO问题分析
1. 每个请求都需要创建独立的线程,与对应的客户端进行数据 Read,业务处理,数据 Write
2. 并发数较大时,需要创建大量线程来处理连接,系统资源占用较大
3. 连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费
5.DEMO
1.BIO执行流程:
1、创建ServerSocket对象,绑定IP、端口号并开始监听
2、Server端通过accept()函数阻塞调用
3、Client端发起连接
4、在内核层面完成tcp三次握手, 并创建Socket四元组(server: ip port/client: ip port)以及相应的数据缓冲区
5、Server端accept()函数返回新的Socket
6、开始处理消息
2.客户端(Client)
package TEMP.Temp7BIO;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class Client {
public static void main(String[] args) throws Exception {
Socket client = new Socket("localhost", 2826);
PrintWriter printWriter = new PrintWriter(client.getOutputStream());
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
while(true) {
String line = reader.readLine();
if (null != line) {
printWriter.println(line);
printWriter.flush();
}
}
}
}
3.服务端(Server)
package TEMP.Temp7BIO;
// 1创建ServerSocket对象,绑定IP、端口号并开始监听
// 2Server端通过accept()函数阻塞调用
// 3Client端发起连接
// 4在内核层面完成tcp三次握手,
// 并创建Socket四元组(server: ip port/client: ip port)以及相应的数据缓冲区
// 5Server端accept()函数返回新的Socket
// 6开始处理消息
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class Server {
public static void main(String[] args) {
//创建socket对象
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(2826, 20);
} catch (IOException e) {
e.printStackTrace();
}
//创建一个线程池
ExecutorService service = new ThreadPoolExecutor(0, 100, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
while (true) {
try {
Socket client = serverSocket.accept();
System.out.println("client connected: " + client.getPort());
service.execute(() -> {
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(client.getInputStream()));
while (true) {
String line = reader.readLine();
if (null != line) {
System.out.println(line);
}
}
} catch (IOException e) {
e.printStackTrace();
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
运行结果:
第三部分:NIO(同步非阻塞)
1.NIO的理解
同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求就进行处理
小插曲:多路复用器
Java通过统一的Selector类使用多路复用器,无需手动配置.
1.了解Selector
1.一个多路复用器Selector可以同时轮询多个Channel,由于JDK使用了epoll代替传统的select实现,所以它并没有最大连接句柄1024/2048的限制。这也就意味着只需要一个线程负责Selector的轮询,就可以介入成千上万的客户端。
2.Selector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接。
2.使用Selector的好处
仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。
事实上,可以只用一个线程处理所有的通道。对于操作系统来说,线程之间上下文切换的开销很大,而且每个线程都要占用系统的一些资源(如内存)。因此,使用的线程越少越好。
但是呢,现代的操作系统和CPU在多任务方面表现的越来越好,所以多线程的开销随着时间的推移,变得越来越小了。实际上,如果一个CPU有多个内核,不使用多任务可能是在浪费CPU能力。
so.....我们只要知道使用Selector能够处理多个通道就足够了。
2.工作机制
3.举个小例子
4.DEMO
1. NIO执行流程
1、打开ServerSocketChannel监听通道
2、设置为非阻塞模式,绑定IP、端口号
3、accept()函数非阻塞调用,如果内核中没有建立起新的连接就返回-1
4、客户端发起连接
5、accept()函数返回新建的Socket fd(SocketChannel)
6、将新的SocketChannel设置为非阻塞,非阻塞的读取数据,SocketChannel::read 不会阻塞
2.客户端(Client)
package TEMP.Temp8NIO;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class Client {
public static void main(String[] args) throws IOException {
SocketChannel client = SocketChannel.open();
client.bind(new InetSocketAddress("127.0.0.1", 8088));
client.connect(new InetSocketAddress("127.0.0.1", 9999));
ByteBuffer buffer = ByteBuffer.allocateDirect(2826);
buffer.put("Hello World.".getBytes());
buffer.flip();
client.write(buffer);
client.close();
}
}
3.服务端(Server)
package TEMP.Temp8NIO;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.List;
public class Server {
public static void main(String[] args) throws IOException {
List<SocketChannel> clients = new ArrayList<>();
ServerSocketChannel server = ServerSocketChannel.open();
server.bind(new InetSocketAddress("127.0.0.1", 9999));
server.configureBlocking(false);
while (true) {
SocketChannel socketChannel = server.accept();
if (socketChannel != null) {
socketChannel.configureBlocking(false);
clients.add(socketChannel);
System.out.println("Client connected: " + socketChannel.socket().getPort());
}
ByteBuffer buffer = ByteBuffer.allocateDirect(2826);
for (SocketChannel channel : clients) {
int num = channel.read(buffer);
if (num > 0) {
buffer.flip();
byte[] bytes = new byte[buffer.limit()];
buffer.get(bytes);
System.out.println(channel.socket().getPort() + ": " + new String(bytes));
buffer.clear();
}
}
}
}
}
运行结果:
第四部分:AIO(异步非阻塞)
1.AIO的理解
AIO 引入异步通道的概念,采用了 Proactor 模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用.
2.工作机制
Proactor 模式是一个消息异步通知的设计模式,Proactor 通知的不是就绪事件,而是操作完成事
件,这也就是操作系统异步 IO 的主要模型。
小插入:了解Proactor模式和Reactor模式
1.Reactor模式
Reactor模式应用于同步I/O的场景。我们以读操作为例来看看Reactor中的具体步骤:
读取操作:
应用程序注册读就需事件和相关联的事件处理器
事件分离器等待事件的发生
当发生读就需事件的时候,事件分离器调用第一步注册的事件处理器
事件处理器首先执行实际的读取操作,然后根据读取到的内容进行进一步的处理
2. Proactor模式
读取操作:
应用程序初始化一个异步读取操作,然后注册相应的事件处理器,此时事件处理器不关注读取就绪事件,而是关注读取完成事件,这是区别于Reactor的关键。
事件分离器等待读取操作完成事件
在事件分离器等待读取操作完成的时候,操作系统调用内核线程完成读取操作,并将读取的内容放入用户传递过来的缓存区中。这也是区别于Reactor的一点,Proactor中,应用程序需要传递缓存区。
事件分离器捕获到读取完成事件后,激活应用程序注册的事件处理器,事件处理器直接从缓存区读取数据,而不需要进行实际的读取操作。
注意!!!!Proactor模式:操作系统必须支持异步I/O
3.Reactor模式和Proactor模式的区别
Proactor中写入操作和读取操作,只不过感兴趣的事件是写入完成事件。
从上面可以看出,Reactor和Proactor模式的主要区别就是真正的读取和写入操作是有谁来完成的。
Reactor中需要应用程序自己读取或者写入数据,
Proactor模式中,应用程序不需要进行实际的读写过程,它只需要从缓存区读取或者写入即可,操作系统会读取缓存区或者写入缓存区到真正的IO设备.
3.举个小例子
第五部分:BIO、NIO、AIO之间的区别
1. BIO(Blocking I/O):
服务器实现模式为一个连接一个线程,客户端有连接请求时服务器就要启动一个线程进行处理,若这个连接不做任何操作还造成不必要的线程开销,可以通过线程池机制来改善。BIO方式适合连接数目小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,在JDK1.4以前时唯一的IO。
2.NIO(NEW I/O):
服务器实现模式为一个请求一个线程,客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有IO请求时才启动一个线程进行处理,NIO方式适合用于连接数目多且连接时间短(轻操作)的架构,如聊天服务器,并发局限于应用中,编程复杂,JDK 1.4之后开始支持。
3.AIO(Asynchronous I/O):服务器实现模式为一个有效请求一个线程,客户端的IO请求都是由操作系统先完成再通知服务器用其启动线程进处理,AIO适合用于连接数目多且连接比较长(重操作)的架构,如相册服务器,充分调用OS参与并发操作,编程复杂,JDK 1.7开始支持。
我们来通过一个生活中的例子了解一夏BIO: 来到厨房,开始烧水NIO,并坐在水壶面前一直等待水烧开。
NIO: 来到厨房,开AIO烧水,但不坐在水壶面前一直等,而去做其他事,然后每隔几分钟到厨房看一下有没有烧开。
AIO: 来到厨房,开始烧水,不一直等待水烧开,而是坐在水壶上面装个开关,水烧开之后它会通知我们。