IO模型概述
什么是IO模型?
即规定用什么样的方式来对数据进行发送和接收,很大程度的觉得了通信的性能。比如使用UDP协议通信的方式就比采用TCP通信的方式高效很大,但是二者的使用也要看不同的使用场景,有的场景使用UDP效果会更好,而有的场景则必须采用TCP。IO模型也是如此,在不同的场景也因该使用不同的IO模型。找对象也是如此,不是好看的有钱的才好,适合自己的才是真的好。
名词介绍
-
同步 : 自己亲自出马持银行卡到银行取钱(使用同步IO时,Java自己处理IO读写)。
-
异步 : 委托一小弟拿银行卡到银行取钱,然后给你(使用异步IO时,Java将IO读写委托给OS处理,需要将数据缓冲区地址和大小传给OS(银行卡和密码),OS需要支持异步IO操作API)。
-
阻塞 : ATM排队取款,你只能等待(使用阻塞IO时,Java调用会一直阻塞到读写完成才返回)。
-
非阻塞 : 柜台取款,取个号,然后坐在椅子上做其它事,等号广播会通知你办理,没到号你就不能去,你可以不断问大堂经理排到了没有,大堂经理如果说还没到你就不能去(使用非阻塞IO时,如果不能读写Java调用会马上返回,当IO事件分发器会通知可读写时再继续进行读写,不断循环直到读写完成)。
同步阻塞:用户进程在发起一个IO操作后,必须等待IO操作完成才可以进行下一步操作
同步非阻塞:用户进程在发起一个IO操作以后可以边等待返回边做其他事情,但是进程需要时不时的询问IO操作是否就绪,这种询问的操作会造成CPU资源的浪费
![](https://img-blog.csdnimg.cn/img_convert/5eebc19300d0b1fcbcee052c964d7655.png#align=left&display=inline&height=594&margin=[object Object]&originHeight=594&originWidth=1582&size=0&status=done&style=none&width=1582)
异步非阻塞:用户进程只需要发起一个IO操作之后就可以立即返回,等IO操作真正的完成了以后,应用程序会得到IO操作完成的通知,此时用户进程只需对数据处理好就行了。不需要实际的IO读写操作,因为真正的IO读写操作已经由内核完成了。
IO模型的种类
BIO
同步并阻塞。服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器就需要启动一个线程进行处理。如果这个线程什么事也不干,单纯挂着是对线程资源的浪费,这个问题可以通过线程池机制改善。
NIO
同步非阻塞。服务器模式为一个请求一个线程,客户端发送的请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求,才启动一个线程进行处理。
AIO
异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O 请求都是由OS先完成再通知服务器去启动线程进行处理
不同IO模型的适用场景
BIO
BIO适合连接数目小而且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4 以前的唯一选择,但程序简单易理解。
NIO
NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等。编程比较复杂,JDK1.4 开始支持。
AIO
AIO 方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用 OS 参与并发操作,编程比较复杂,JDK7 开始支持。
浅谈BIO
概述
1.JavaBIO就是传统的Java IO编程,其相关的类和接口在java.io
2.BIO,同步阻塞通信模式,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就要启用一个线程来进行处理,但是你在不用的时候就会造成线程资源的极大浪费。就好比你拉了一条专线,这条专线就是给你使用的,你要是不适用别人也不能用,那么这个专线的资源就浪费了。不过这个问题可以使用线程池来处理,可以减少一定的浪费。
3.BIO比较适合较固定而且连接数目比较少的架构,这种方式对服务器的资源要求比较高,因为一个请求一个连接。
BIO编程流程的概述:
1.服务器端启动一个监听指定端口的ServerSocker,并调用accept方法等待客户端通信
2.客户端启动Socket对服务器进程通信,默认情况下服务器需要对每一个客户建立一个线程与之通信
3.客户端发出请求后,先咨询服务器是否有线程响应,如果没有则会等待,或者被拒绝
4.如果有响应,客户端会等待线程请求结束后,再继续执行
一些易于理解的知识点
1.服务器套接字一次可以与一个套接字连接,如果多台客户端同时提出连接请求,服务器套接字会将请求连接的客户端存入队列中,然后从中取出一个套接字与服务器新建的套接字相连接,若请求连接的数量大于最大的容纳数,则多出的的连接请求被拒绝;默认的队列大小为50。
实操
实例说明:
- 使用
BIO
模型编写一个服务器端,监听6666
端口,当有客户端连接时,就启动一个线程与之通讯。 - 要求使用线程池机制改善,可以连接多个客户端。
- 服务器端可以接收客户端发送的数据(
telnet
方式即可)。 - 代码演示:
实现代码
package com.pjh.BIO;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @ClassName: BIOTest1
* @Author: 86151
* @Date: 2021/6/5 11:58
* @Description: TODO
*/
public class BIOTest1 {
public static void main(String[] args) throws IOException {
/*创建一个Socket*/
final ServerSocket serverSocket = new ServerSocket(8888);
/*创建一个线程池如果有客户端请求就创建一个线程与之通信*/
ExecutorService executorService = Executors.newCachedThreadPool();
System.out.println("服务器端创建Socket");
/*while循环等待通信*/
while(true){
//打印线程信息
System.out.println("线程信息id = " + Thread.currentThread().getId() + "名字 = " + Thread.currentThread().getName());
//监听,等待客户端连接
System.out.println("等待连接....");
final Socket socket = serverSocket.accept();
System.out.println("serverSocket.getLocalSocketAddress():"+serverSocket.getLocalSocketAddress());
System.out.println("socket.getLocalSocketAddress():"+socket.getLocalSocketAddress());
/*从线程池中拿出一个线程与这个客户端通信*/
executorService.execute(new Runnable() {
public void run() {
handle(socket);
}
});
}
}
public static void handle(Socket socket){
System.out.println("handle");
System.out.println("getLocalSocketAddress():"+socket.getLocalSocketAddress());
System.out.println("socket.getInetAddress():"+socket.getInetAddress());
System.out.println("socket.getLocalAddress():"+socket.getLocalAddress());
try {
/*输出线程信息*/
System.out.println("线程信息id = " + Thread.currentThread().getId() + "名字 = " + Thread.currentThread().getName());
byte[] bytes = new byte[1024];
//通过socket获取输入流
InputStream inputStream = socket.getInputStream();
/*循环读取客户端信息*/
while(true){
System.out.println("线程信息id = " + Thread.currentThread().getId() + "名字 = " + Thread.currentThread().getName());
System.out.println("read....");
int read = inputStream.read(bytes);
System.out.println("read:"+read);
if (read != -1) {
System.out.println(new String(bytes, 0, read));//输出客户端发送的数据
} else {
break;
}
}
}catch (IOException e) {
e.printStackTrace();
}finally {
System.out.println("关闭与客户端的连接");
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
使用客户端的telnet连接
连接上之后服务器端的输出
这里特别要注意这里的继续监听的端口与创建的socket的端口是一致的,而不是像网上有的博主说的那样会在本地重新选择一个端口来进行通信。
客户端发送消息给服务器端
问题分析
-
每个请求都需要创建独立的线程,与对应的客户端进行数据
Read
,业务处理,数据Write
。 -
当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大。
3.连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read
操作上,造成线程资源浪费。