课程地址:IO模式讲解(AIO&BIO&NIO)
推荐博客:从前慢-BIO、NIO、AIO
在Java的软件设计开发中,通信架构是不可避免的,我们在进行不同系统或者不同进程之间的数据交互,或者在高并发下的通信场景下都需要用到网络通信
相关的技术,对于一些经验丰富的程序员来说,Java早期的网络通信架构存在一些缺陷,其中最令人恼火的是基于性能低下的同步阻塞式的I/0通信(BIO)
,随着互联网开发下通信性能的高要求,Java在2002年开始支持了非阻塞式的I/O通信技术(NIO)
。大多数读者在学习网络通信相关技术的时候,都只是接触到零碎的通信技术点,没有完整的技术体系架构,以至于对于Java的通信场景总是没有清晰的解决方案。本次课程将通过大量清晰直接的案例从最基础的BIO式通信开始介绍到NIO ,AlO,读者可以清晰的了解到阻塞、同步、异步的现象、概念和特征以及优缺点。本课程结合了大量的案例让读者可以快速了解每种通信架构的使用。
2 Java的I/O演进之路
I/O模型说明
BIO
同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。客户端没有数据的时候,对应的线程也依然会等待,不能做其他任何事情,所以是阻塞式的
NIO
Java NIO : 同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即==客户端发送的连接请求都会注册到多路复用器上,多路复用器(Selector选择器)轮询到连接有 I/O 请求就进行处理 ==
我们只需要用一个线程来管理多路复用器,多路复用器会自动去轮询各个管道,去看哪些管道/通道有数据请求操作,如果有,就会分配新的线程取处理管道的通信,如果这个通道没有数据,线程是可以去做其他事情的,所以是同步非阻塞式的
AIO
Java AIO(NIO.2) :A表示 异步 异步非阻塞
,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理,一般适用于连接数较多且连接时间较长的应用
也就是说客户端的数据都是通过操作系统进行处理,操作系统处理完数据之后采取通知服务器进行过数据的解决/接收
BIO、NIO、AIO 适用场景分析
1 BIO方式适用于连接数目比较小(没有特别多的客户端,因为BIO模式下服务器要为每一个客户端分配一个线程)且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前
的唯一选择,但程序简单易理解。
2 NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等。编程比较复杂,JDK1.4
开始支持。
3 AIO 方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用 OS 参与并发操作,编程比较复杂,JDK7
开始支持。
3 JAVA BIO深入剖析
Java BIO 基本介绍
Java BIO 就是传统的 java io 编程,其相关的类和接口在 java.io
BIO(blocking I/O) : 同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户连接服务器).
Java BIO 工作机制
对 BIO 编程流程的梳理
- 服务器端启动一个 ServerSocket,注册端口,调用accpet方法监听客户端的Socket连接。
- 客户端启动 Socket对服务器进行通信,默认情况下服务器端需要对每个客户 建立一个线程与之通讯
同步阻塞案例演示
网络编程的基本模型是Client/Server模型,也就是两个进程之间进行相互通信,其中服务端提供位置信(绑定IP地址和端口),客户端通过连接操作向服务端监听的端口地址发起连接请求,基于TCP协议下进行三次握手连接,连接成功后,双方通过网络套接字(Socket)进行通信。
传统的同步阻塞模型开发中,服务端ServerSocket负责绑定IP地址,启动监听端口;客户端Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。
基于BIO模式下的通信,客户端-服务端是完全同步,完全耦合的。
服务端启动后会在accept()那里暂停,因为客户端没有发数据过来
服务端代码
package BIO;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
/**
* Created by 此生辽阔 on 2021/7/31 11:35
*/
//达到客户端发送消息,服务端能够接受
public class Server {
public static void main(String[] args) {
try {
System.out.println("服务端启动");
//1 定义ServerSocket对象进行服务端的端口注册
ServerSocket serverSocket = new ServerSocket(9999);
//2 监听客户端的Socket连接请求
Socket socket= serverSocket.accept();
//3 从socket管道中得到一个字节输入流对象,读取客户端推送的数据
InputStream inputStream = socket.getInputStream();
//4 把字节输入流包装成缓冲的字符输入流(方便按照行来读取)
//BufferedInputStream bis=new BufferedInputStream(inputStream );
BufferedReader br=new BufferedReader(new InputStreamReader(inputStream ));
String msg;
//while((msg=br.readLine())!=null)
if((msg=br.readLine())!=null)
{
System.out.println("服务端接收到"+msg);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端
package BIO;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
/**
* Created by 此生辽阔 on 2021/7/31 11:35
*/
public class Client {
public static void main(String[] args) {
try {
//1 创建Socket对象请求服务端的连接
Socket socket = new Socket("127.0.0.1",9999);//用本机作为服务器,请求的IP就是127.0.0.1
//2 从socket对象获得字节输出流
OutputStream os = socket.getOutputStream();
//字节输出流不方便写行数据,而且服务端已经定义为读行数据,所以我们传出去的也应该是字符流
//3 把字节输出流包装成打印流
PrintStream printStream = new PrintStream(os);
// printStream.print("helloworld ,服务端你好");
printStream.println("helloworld ,服务端你好");
printStream.flush();
} catch (IOException e) {
e.printStackTrace();
}
//端口与服务端注册端口保持一致,用9999
}
}
在上面的代码中,客户端发送的是一行数据,服务端接收的是一行数据,如果客户端发消息不加换行,服务端用while读都是会报错的
比如
printStream.print("helloworld ,服务端你好");
while((msg=br.readLine())!=null)
当然如果客户端和服务端把资源关闭的话是不会出现上述的问题的
BIO模式下的多发和多收机制
package BIO;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
/**
* Created by 此生辽阔 on 2021/7/31 11:35
*/
//达到客户端发送消息,服务端能够接受
public class Server {
public static void main(String[] args) {
try {
System.out.println("服务端启动");
//1 定义ServerSocket对象进行服务端的端口注册
ServerSocket serverSocket = new ServerSocket(9999);
//2 监听客户端的Socket连接请求
Socket socket= serverSocket.accept();
//3 从socket管道中得到一个字节输入流对象,读取客户端推送的数据
InputStream inputStream = socket.getInputStream();
//4 把字节输入流包装成缓冲的字符输入流(方便按照行来读取)
//BufferedInputStream bis=new BufferedInputStream(inputStream );
BufferedReader br=new BufferedReader(new InputStreamReader(inputStream ));
String msg;
while((msg=br.readLine())!=null)
{
System.out.println("服务端接收到"+msg);
}
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
package BIO;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;
/**
* Created by 此生辽阔 on 2021/7/31 11:35
*/
public class Client {
public static void main(String[] args) {
try {
//1 创建Socket对象请求服务端的连接
Socket socket = new Socket("127.0.0.1",9999);//用本机作为服务器,请求的IP就是127.0.0.1
//2 从socket对象获得字节输出流
OutputStream os = socket.getOutputStream();
//字节输出流不方便写行数据,而且服务端已经定义为读行数据,所以我们传出去的也应该是字符流
//3 把字节输出流包装成打印流
PrintStream printStream = new PrintStream(os);
Scanner scanner = new Scanner(System.in);
while(true){
System.out.println("请说:");
printStream.println(scanner.nextLine());
printStream.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
//端口与服务端注册端口保持一致,用9999
}
}
BIO模式下的实现接收多个客户端
在上述的案例中,一个服务端只能接收一个客户端的通信请求,那么如果服务端需要处理很多个客户端的消息通信请求应该如何处理呢,此时我们就需要在服务端引入线程了,也就是说客户端每发起一个请求,服务端就创建一个新的线程来处理这个客户端的请求,这样就实现了一个客户端一个线程的模型
为了测试刚刚的案例一个服务器能不能接收多个客户端,我重新写了一个客户端(就是Client一样的代码)然后进行了测试
可以看到服务器端没有收到客户端2发的消息
测试方法2
让客户端程序可以并行
勾选Allow parallel run
可以看到一个客户端可以并行运行了,但是服务端收不到新加入的客户端发送的消息
服务器端为什么不接受第二个客户端的请求?
因为服务器端只有一个线程,(BIO下)一个线程只能处理一个客户端的消息,每接入一个新的客户端,我们需要在服务器端创建一个线程来处理请求
来一个socket管道的客户端,我们就创建一个线程对象来处理它
代码实现
自定义线程类
package BIO;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Socket;
/**
* Created by 此生辽阔 on 2021/7/31 15:14
*/
public class ServerThread extends Thread{
private Socket socket;
public ServerThread(Socket socket) {
this.socket = socket;
}
@Override
public void run(){
//3 从socket管道中得到一个字节输入流对象,读取客户端推送的数据
InputStream inputStream = null;
try {
inputStream = socket.getInputStream();
//4 把字节输入流包装成缓冲的字符输入流(方便按照行来读取)
BufferedReader br=new BufferedReader(new InputStreamReader(inputStream ));
String msg;
while((msg=br.readLine())!=null)
{
System.out.println("服务端接收到:"+msg);
}
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
服务端代码
package BIO;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
/**
* Created by 此生辽阔 on 2021/7/31 11:35
*/
//达到客户端发送消息,服务端能够接受
public class Server {
public static void main(String[] args) {
try {
System.out.println("服务端启动");
//1 定义ServerSocket对象进行服务端的端口注册
ServerSocket serverSocket = new ServerSocket(9999);
//2 监听客户端的Socket连接请求
//定义一个死循环,负责不断地接受客户端的socket连接请求
while(true)
{
Socket socket= serverSocket.accept();
//每次把当前客户端的socket对象交给独立的线程对象执行
//那么服务端对每个客户端的处理都是交给线程对象的run方法来执行
new ServerThread(socket).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端代码
package BIO;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;
/**
* Created by 此生辽阔 on 2021/7/31 11:35
*/
public class Client {
public static void main(String[] args) {
try {
//1 创建Socket对象请求服务端的连接
Socket socket = new Socket("127.0.0.1",9999);//用本机作为服务器,请求的IP就是127.0.0.1
//2 从socket对象获得字节输出流
OutputStream os = socket.getOutputStream();
//字节输出流不方便写行数据,而且服务端已经定义为读行数据,所以我们传出去的也应该是字符流
//3 把字节输出流包装成打印流
PrintStream printStream = new PrintStream(os);
Scanner scanner = new Scanner(System.in);
while(true){
System.out.print("请说:");
printStream.println(scanner.nextLine());
printStream.flush();//刷新的操作就把按客户顿啊的消息推送给了服务端
}
} catch (IOException e) {
e.printStackTrace();
}
//端口与服务端注册端口保持一致,用9999
}
}
总结:把每一个客户端的请求分配到每一个线程取处理
小结
1 每个Socket接收到,都会创建一个线程,线程的竞争、切换上下文影响性能(客户端越多,服务端需要的线程越多);
2 每个线程都会占用栈空间和CPU资源;
3 并不是每个socket都进行IO操作,无意义的线程处理(即使客户端没有消息,服务端的线程也会阻塞等待);
4 客户端的并发访问增加时。服务端将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出,线程创建失败,最终导致进程宕机或者僵死,从而不能对外提供服务。
伪异步IO编程
弊端:如果线程池的最大线程数量是10,那么同时支持的客户端最多就只有10个
定义线程池
package BIO;
import org.omg.CORBA.TIMEOUT;
import java.net.Socket;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* Created by 此生辽阔 on 2021/7/31 15:40
*/
public class HandlerSocketServerPool{
private ExecutorService executorService;
//在创建这个类对象的时候就初始化线程池
public HandlerSocketServerPool(int maxThreadNum,int queueSize) {
this.executorService = new ThreadPoolExecutor(3,
maxThreadNum,
120,
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(queueSize));
}
//提供一个方法来提交任务给线程池的任务队列来暂存,等着线程池来处理
public void execute(Runnable target){
executorService.execute(target);
}
}
把socket对象封装成任务对象
package BIO;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Socket;
/**
* Created by 此生辽阔 on 2021/7/31 15:49
*/
public class ServerRunnanleTarget implements Runnable{
private Socket socket;
public ServerRunnanleTarget(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
//处理接受到的客户端socket通信需求.
//3 从socket管道中得到一个字节输入流对象,读取客户端推送的数据
InputStream inputStream = null;
try {
inputStream = socket.getInputStream();
//4 把字节输入流包装成缓冲的字符输入流(方便按照行来读取)
BufferedReader br=new BufferedReader(new InputStreamReader(inputStream ));
String msg;
while((msg=br.readLine())!=null)
{
System.out.println("服务端接收到:"+msg);
}
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
服务端代码
package BIO;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
/**
* Created by 此生辽阔 on 2021/7/31 11:35
*/
//达到客户端发送消息,服务端能够接受
public class Server {
public static void main(String[] args) {
try {
System.out.println("服务端启动");
//1 定义ServerSocket对象进行服务端的端口注册
ServerSocket serverSocket = new ServerSocket(9999);
//2 监听客户端的Socket连接请求
//初始化一个线程池对象
HandlerSocketServerPool pool = new HandlerSocketServerPool(6,10);
//定义一个死循环,负责不断地接受客户端的socket连接请求
while(true)
{
Socket socket= serverSocket.accept();
//每次把当前客户端的socket对象交给独立的线程对象执行
//那么服务端对每个客户端的处理都是交给线程池对象来处理
//怎么把socket封装成任务对象?
Runnable target=new ServerRunnanleTarget(socket);
pool.execute(target);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端代码
package BIO;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;
/**
* Created by 此生辽阔 on 2021/7/31 11:35
*/
public class Client {
public static void main(String[] args) {
try {
//1 创建Socket对象请求服务端的连接
Socket socket = new Socket("127.0.0.1",9999);//用本机作为服务器,请求的IP就是127.0.0.1
//2 从socket对象获得字节输出流
OutputStream os = socket.getOutputStream();
//字节输出流不方便写行数据,而且服务端已经定义为读行数据,所以我们传出去的也应该是字符流
//3 把字节输出流包装成打印流
PrintStream printStream = new PrintStream(os);
Scanner scanner = new Scanner(System.in);
while(true){
System.out.print("请说:");
printStream.println(scanner.nextLine());
printStream.flush();//刷新的操作就把按客户顿啊的消息推送给了服务端
}
} catch (IOException e) {
e.printStackTrace();
}
//端口与服务端注册端口保持一致,用9999
}
}
由于我们的核心线程数是3,所以可以看到直接收到了3个客户端的消息,第4个客户端会进入任务队列,并不能执行,如果我们把前面的客户端关掉一个,那么第4个客户端就可以执行了
实现任意类型文件上传
客户端
package BIO.file;
import java.awt.*;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
/**
* Created by 此生辽阔 on 2021/8/7 15:31
*/
//目标:实现客户端上传任意类型的文件数据给服务端保存起来
public class Client {
public static void main(String[] args) throws IOException {
//1请求与服务端的Socket连接
Socket socket = new Socket("127.0.0.1",8888);
//2 把字节输出流包装成数据输出流(数据输出流可以先发送一段字符串,再发送数据)
DataOutputStream dos= new DataOutputStream(socket.getOutputStream());
//3先发送文件后缀
dos.writeUTF(".jpg");//实际开发中比如接受到一张图片的名字,再截取
//4 把文件数据发送给服务端
InputStream is = new FileInputStream("D:\\test.jpg");
byte[]buffer=new byte[1024];
int len=0;
while((len=is.read(buffer))>0){
dos.write(buffer,0,len);
}
dos.flush();
dos.close();
socket.shutdownInput();//通知服务端这边的数据发送完毕
}
}
服务端处理客户端的线程
package BIO.file;
import java.io.*;
import java.net.Socket;
import java.util.UUID;
/**
* Created by 此生辽阔 on 2021/8/7 15:46
*/
public class ServerReaderThread extends Thread{
private Socket socket;
public ServerReaderThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
//1 得到一个额数据输入流读取客户端发送过来的数据
DataInputStream dis = new DataInputStream(socket.getInputStream());
//2读取客户端发送过来的文件类型
String suffix=dis.readUTF();
System.out.println("服务端接收到了文件类型:"+suffix);
//3 定义一个字节输出管道负责把客户端发来的文件数据写出去
OutputStream os= new FileOutputStream("D:\\" + UUID.randomUUID().toString() + suffix);
byte[]buffer=new byte[1024];
int len=0;
//4.从数据输入流中读取文件数据,写出到字节输出流
while((len=dis.read(buffer))>0){
os.write(buffer,0,len);
}
os.close();
dis.close();
System.out.println("服务端接收文件保存成功");
} catch (IOException e) {
e.printStackTrace();
}
}
}
服务端代码
package BIO.file;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* Created by 此生辽阔 on 2021/8/7 15:43
*/
//服务端:实现接收客户端的任意类型的文件,并保存到服务端磁盘
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket ss=new ServerSocket(8888);
while(true){
Socket socket=ss.accept();//监听
//交给一个独立的线程来处理与这个客户端文件通信需求
new ServerReaderThread(socket).run();
}
}
}
BIO模式下的端口转发思想
数据部件需要在服务端和客户端之间传输,客户端和客户端之间怎么进行数据传输呢
比如qq交流,实际上是一个客户端把消息发送给服务器,服务器再把消息转发给另一个客户端
客户端不仅需要发送消息,也需要接收消息,那么就需要线程
端口转发思想:你的消息发给服务端,服务端从所有的socket中把你的消息拿出来
服务端实现
package BIO.chat;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
/**
* Created by 此生辽阔 on 2021/8/7 16:31
*/
//BIO模式下的端口转发思想-服务端实现
//1 注册端口 2 接收客户端的socket连接,交给一个独立的线程处理 3把当前连接客户端socket存入到在线socket集合中
//4接收客户端消息,推送给当前所有在线的socket接收
public class Server {
//定义一个静态集合
public static List<Socket> allOnlineSocket=new ArrayList();
public static void main(String[] args) throws IOException {
ServerSocket ss=new ServerSocket(9999) ;
while(true){
Socket socket=ss.accept();
//把登录的客户端socket存入到一个在线集合中
allOnlineSocket.add(socket);
//为当前登录的socket分配一个独立的线程处理与之通信
new ServerReaderThread(socket).start();
}
}
}
package BIO.chat;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;
/**
* Created by 此生辽阔 on 2021/8/7 16:45
*/
public class ServerReaderThread extends Thread{
private Socket socket;
public ServerReaderThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
//1从socket中去获取当前客户端的输入流
try {
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String msg;
while((msg=br.readLine())!=null){
//2 服务器接收到了客户端的消息之后,推送给所有在线的socket
sendMsgToAllClient(msg);
}
} catch (IOException e) {
// e.printStackTrace();
//这里出现异常说明服务端的线程在等待客户端发消息,但是客户端已经发完了,说明下线了,所以需要把当前socket从list中移除
System.out.println("当前有人下线了!");
Server.allOnlineSocket.remove(socket);
}
}
private void sendMsgToAllClient(String msg) throws IOException {
for (Socket sk : Server.allOnlineSocket) {
PrintStream ps = new PrintStream(sk.getOutputStream());
ps.println(msg);
ps.flush();
}
}
}
BIO模式下的即时通讯项目
https://blog.csdn.net/ningmengshuxiawo/article/details/119489750