通信架构BIO、NIO、AIO
1、BIO、NIO、AIO介绍
(1)BIO
Java BIO也称同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销
(2)NIO
Java NIO 也称同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求就进行处理
(3)AIO
Java AIO(也称NIO.2)也称 异步 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理,一般适用于连接数较多且连接时间较长的应用
2、BIO、NIO、AIO适用场景
(1)BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序简单易理解。
(2)NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等。
编程比较复杂,JDK1.4 开始支持。
(3)AIO 方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用 OS 参与并发操作,
编程比较复杂,JDK7 开始支持。
3、BIO详讲
(1)BIO工作机制
(2)回顾
- 网络编程的基本模型是Client/Server模型,也就是两个进程之间进行相互通信,其中服务端提供位置信(绑定IP地址和端口),客户端通过连接操作向服务端监听的端口地址发起连接请求,基于TCP协议下进行三次握手连接,连接成功后,双方通过网络套接字(Socket)进行通信。
- 传统的同步阻塞模型开发中,服务端ServerSocket负责绑定IP地址,启动监听端口;客户端Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。 基于BIO模式下的通信,客户端 - 服务端是完全同步,完全耦合的。
(3)案例讲解
以下案例中,只能实现客户端发送消息,服务端接收消息,并不能实现反复的收消息和反复的发消息,我们只需要在客户端案例中,加上反复按照行发送消息的逻辑即可!
没修改前(单发)
server
public class server {
public static void main(String[] args) {
try {
System.out.println("============================server start=============================");
//1、定义ServerSocket对象进行服务端口注册
ServerSocket ss = new ServerSocket(9999);
//2、监听客户端的Socket连接对象
Socket socket = ss.accept();
//3、从Socket管道中得到一个字节输入流对象
InputStream is = socket.getInputStream();
//4、把字节输入流包装成一个字符输入流
// BufferedInputStream bis = new BufferedInputStream(is);
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String msg;
if((msg=br.readLine())!=null){
System.out.println("服务端接收到"+msg);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
client
public class client {
public static void main(String[] args) {
try {
System.out.println("============================client start=============================");
//1、创建Socket对象请求服务端连接
Socket socket = new Socket("127.0.0.1",9999);
//2、从Socket对象获取一个字节输出流
OutputStream os = socket.getOutputStream();
//3、把字节输出流包装成一个打印流
PrintStream ps = new PrintStream(os);
ps.println("hello server");
ps.flush();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
修改后(多发)
server
public class server {
public static void main(String[] args) {
try {
System.out.println("============================server start=============================");
// 1、定义ServerSocket对象进行服务端口注册
ServerSocket ss = new ServerSocket(9999);
// 2、监听客户端的Socket连接对象
Socket socket = ss.accept();
// 3、从Socket管道中得到一个字节输入流对象
InputStream is = socket.getInputStream();
// 4、把字节输入流包装成一个字符输入流
// BufferedInputStream bis = new BufferedInputStream(is);
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String msg;
while ((msg = br.readLine()) != null) {
System.out.println("服务端接收到" + msg);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
cleint
public class client {
public static void main(String[] args) {
try {
System.out.println("============================client start=============================");
// 1、创建Socket对象请求服务端连接
Socket socket = new Socket("127.0.0.1", 9999);
// 2、从Socket对象获取一个字节输出流
OutputStream os = socket.getOutputStream();
// 3、把字节输出流包装成一个打印流
PrintStream ps = new PrintStream(os);
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("说:");
String msg = sc.next();
ps.println(msg);
ps.flush();
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
(4)Bio模式下接收多客户端
在上述的案例中,一个服务端只能接收一个客户端的通信请求,那么如果服务端需要处理很多个客户端的消息通信请求应该如何处理呢?,此时我们就需要在服务端引入线程了,也就是说客户端每发起一个请求,服务端就创建一个新的线程来处理这个客户端的请求,这样就实现了一个客户端一个线程的模型
实现多客户端连接BIO进行通信案例:
serverTread
public class serverThread extends Thread {
private Socket socket;
public serverThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
//从socket对象中得到一个字节输入流
InputStream is = socket.getInputStream();
//使用缓存字符输入流包装字节输入流
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String msg ;
while((msg=br.readLine())!=null){
System.out.println(msg);
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
server
/**
* 服务端接收多个客户端 思路:服务端每接收一个客户端Socket请求对象之后都交给一个独立的线程处理客户端的数据交互需求
*
* @author Style
*
*/
public class server {
public static void main(String[] args) {
System.out.println("============================server start=============================");
try {
//1、注册端口
ServerSocket ss = new ServerSocket(9999);
//2、定义一个死循环,负责不断的接收客户端的Socket连接请求
while(true){
Socket socket = ss.accept();
//3、创建独立的线程来处理与这个客户端的Socket通信需求
new serverThread(socket).start();
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
cleint
public class client {
public static void main(String[] args) {
System.out.println("============================client start=============================");
try {
// 1、创建Socket对象请求服务端连接
Socket socket = new Socket("127.0.0.1", 9999);
//2、得到一个打印流
PrintStream ps = new PrintStream(socket.getOutputStream());
//3、使用循环不带的发送消息给服务端接收
Scanner sc = new Scanner(System.in);
while(true){
System.out.print("请说:");
String msg =sc.next();
ps.println(msg);
ps.flush();
}
} catch (UnknownHostException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
(5)总结:
- 每个Socket接收到,都会创建一个线程,线程的竞争、切换上下文影响性能;
- 每个线程都会占用栈空间和CPU资源;
- 并不是每个socket都进行IO操作,无意义的线程处理;
- 客户端的并发访问增加时。服务端将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出,线程创建失败,最终导致进程宕机或者僵死,从而不能对外提供服务。
4、伪异步I/O编程
(1)概述
在上述案例中:客户端的并发访问增加时。服务端将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出,线程创建失败,最终导致进程宕机或者僵死,从而不能对外提供服务。接下来我们采用一个伪异步I/O的通信框架,采用线程池和任务队列实现,当客户端接入时,将客户端的Socket封装成一个Task(该任务实现java.lang.Runnable线程任务接口)交给后端的线程池中进行处理。JDK的线程池维护一个消息队列和N个活跃的线程,对消息队列中Socket任务进行处理,由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。
(2)使用伪异步IO实现案例
实际就是使用线程池控制线程个数
server
public class server {
public static void main(String[] args) {
System.out.println("============================server start=============================");
try {
//1、注册端口
ServerSocket ss = new ServerSocket(9999);
//2、定义一个循环接收客户端的Socket连接请求
//初始化一个线程池
pretentIOPool pool = new pretentIOPool(3, 10);
while(true){
Socket socket = ss.accept();
//3、把socket对象交给一个线程池进行处理
//把Socket封装成一个任务对象交给线程池处理
Runnable target = new pretendThread(socket);
pool.execute(target);
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
pretendThread
public class pretendThread implements Runnable {
private Socket socket;
public pretendThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
//从socket对象中得到一个字节输入流
InputStream is = socket.getInputStream();
//使用缓存字符输入流包装字节输入流
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String msg ;
while((msg=br.readLine())!=null){
System.out.println(msg);
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
pretendIOPool
public class pretentIOPool {
// 1、创建一个线程池的成员变量用于存储一个线程池对象
private ExecutorService executorservice;
// 2、创建类的对象的时候需要初始化线程池对象
public pretentIOPool(int maxTreadNum, int queueSize) {
executorservice = new ThreadPoolExecutor(3, maxTreadNum, 120, TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(queueSize));
}
// 3、提供一个方法来提交任务给线程池的任务队列来暂存,等着线程池来处理
public void execute(Runnable target) {
executorservice.execute(target);
}
}
cleint
public class client {
public static void main(String[] args) {
System.out.println("============================client start=============================");
try {
// 1、创建Socket对象请求服务端连接
Socket socket = new Socket("127.0.0.1", 9999);
//2、得到一个打印流
PrintStream ps = new PrintStream(socket.getOutputStream());
//3、使用循环不带的发送消息给服务端接收
Scanner sc = new Scanner(System.in);
while(true){
System.out.print("请说:");
String msg =sc.next();
ps.println(msg);
ps.flush();
}
} catch (UnknownHostException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
(3)总结
- 伪异步io采用了线程池实现,因此避免了为每个请求创建一个独立线程造成线程资源耗尽的问题,但由于底层依然是采用的同步阻塞模型,因此无法从根本上解决问题。
- 如果单个消息处理的缓慢,或者服务器线程池中的全部线程都被阻塞,那么后续socket的i/o消息都将在队列中排队。新的Socket请求将被拒绝,客户端会发生大量连接超时。
5、基于BIO形式下的文件上传
案例:实现文件上传
server
/**
* 接收客户端任意类型文件
*
* @author Style
*
*/
public class server {
public static void main(String[] args) {
try{
System.out.println("============================server start=============================");
//1、定义ServerSocket对象进行服务端口注册
ServerSocket ss = new ServerSocket(9999);
while(true){
Socket socket = ss.accept();
//交给一个独立线程处理
FileTread fileTread = new FileTread(socket);
Thread tread =new Thread(fileTread);
tread.start();
//new Thread(new FileTread(socket)).start();
}
}catch(Exception e){
e.printStackTrace();
}
}
}
cleint
/**
* 实现客户端文件上传
* @author Style
*
*/
public class client {
public static void main(String[] args) {
try{
System.out.println("============================client start=============================");
//1、请求与服务器的socket连接
Socket socket = new Socket("127.0.0.1",9999);
//2、将字节输出流包装成一个数据输出流
DataOutputStream dos = new DataOutputStream(socket.getOutputStream(