1. BIO、NIO、AIO介绍
在不同系统或进程间数据交互,或高并发场景下都选哟网络通信。早期是基于性能低下的同步阻塞IO(BIO)实现。后支持非阻塞IO(NIO)。
前置须知:javsse,java多线程,javaIO,java网络模型
目的:局域网内通信,多系统间底层消息传递机制,高并发下大数据通信,游戏应用。
2 .java的io演进
2.1 IO模型基本说明
IO模型:性能取决于用什么通信模式或架构进行数据传输和接收。java共支持3种网络编程的IO,见标题
2.2 IO模型
javaBIO
同步并阻塞。服务器一个链接一个线程,即客户端请求服务器只启动一个线程处理,如果链接空闲则浪费线程开销。
javaNIO
同步非阻塞。服务器一个线程处理多个链接(请求)。即客户端请求都会注册到多路复用上。轮询到有IO请求就进行处理。
javaAIO(NIO2.0版本)
异步非阻塞。服务器一个有效请求一个线程,客户端IO请求都由OS先完成了在通知服务器创建线程处理,一般用于连接数较多且链接时间较长应用。
2.3 BIO、NIO、AIO使用场景
- BIO:连接小且固定架构。JDK1.4前唯一选择,简单。
- NIO:连接多且较短架构。比如聊天,弹幕,服务间通讯等,JDK1.4后支持,复杂。
- AIO:连接多且较长家都。比如相册服务,充分调用OS参与并发,JDK1.7后支持,复杂。
3. Java BIO
3.1 BIO介绍
相关类接口间java.io。一个链接创建一个线程。可通过线程池优化成多客户端链接。
3.2 BIO机制
3.3 传统的BIO编程实例
网络编程CS架构实现两个进程间通信,服务端提供IP+PORT,客户端通过链接操作向服务端监听的端口地址发起请求。基于TCP三次握手建立链接,通过套接字Socket进行通信。
同步阻塞种服务端serverSocket负责绑定IP地址,启动监听端口。客户端Socket负责发起请求。通过输入输出流进行同步阻塞通信。
特点:C/S完全同步,耦合。
public class Client {
public static void main(String[] args) throws IOException {
//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 world!服务端");
ps.flush();
}
}
/**
* 目标:客户端发送消息,服务端接受消息
*/
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 is = socket.getInputStream();
//4.把字节输入流包装成一个缓冲字符输入流 要以行为单位读取
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String msg;
if ((msg = br.readLine()) != null) {
System.out.println("服务端接收到:" + msg);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
小结:
- 在以上通信中,服务端会一直等待客户端的消息,如果客户端没有进行消息的发送,服务端将一直进入阻塞状态
- 同时服务端是按照行 获取信息的,客户端也必须按照行 发送。否则服务端进入等待消息的阻塞态。
3.4 BIO实现多发和多收消息
在3中,只能客户端发送消息,服务端接收消息,并不能反复的接收和发送消息。改进:
Client:
PrintStream ps = new PrintStream(os);
Scanner sc = new Scanner(System.in);
while(true) {
System.out.println("input:");
String msg = sc.nextLine();
ps.println(msg);
ps.flush();
}
Server:
while ((msg = br.readLine()) != null) {
System.out.println("服务端接收到:" + msg);
}
小结:
- 服务端只能处理一个客户端请求,因为单线程,一次只能与一个客户端进行消息通信。
3.5 BIO下接收多个客户端
需在服务端引入多线程解决多客户端请求。这样就实现了一个客户端一个线程模型。
/**
* 目标:实现服务端同时处理多个客户端的socket连接
* 实现:服务端每接收一个客户端socket请求对象后都交给一个独立线程处理客户端的数据交互。
*/
public class Server {
public static void main(String[] args) {
try{
//1.注册端口
ServerSocket ss = new ServerSocket(9999);
//2.定义死循环,不断接收客户端的Socket链接
while(true) {
Socket socket = ss.accept();
//3.创建一独立的线程来处理与这个客户端的socket通信
new ThreadServerReader(socket).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class ThreadServerReader extends Thread {
private Socket socket;
public ThreadServerReader(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 (Exception e) {
e.printStackTrace();
}
}
}
小结:
- 每个Socket接收都会创建线程,线程竞争,切换上下文会影响性能。
- 每个线程都会占用栈空间和CPU资源
- 并不是每个Socket都进行IO操作,无意义的线程处理
- 客户端的并发访问增加时。服务端将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出,线程创建失败,最终导致进程宕机或僵死。
3.6 伪异步IO编程
采用线程池和任务队列实现,当客户端接入,将Socket封装成一个Task交由后端的线程池进行处理。JDK线程池维护一个消息队列和N个活跃线程。对消息队列中socket任务进行处理,由于线程池可设置消息队列的大小和最大线程数,因此,资源可控,无论多少客户端并发访问,都不会资源不够。
/**
* 目标:伪异步通信架构
*/
public class Server {
public static void main(String[] args) {
try {
ServerSocket ss = new ServerSocket(9999);
//初始化线程池对象
HandlerSocketServerPool pool = new HandlerSocketServerPool(6, 10);
while(true) {
Socket socket = ss.accept();
//把socket封装成任务对象交给一个线程池来处理
Runnable target = new ServerRunnableTarget(socket);
pool.execute(target);
}
} catch (Exception e){
e.printStackTrace();
}
}
}
//线程池
public class HandlerSocketServerPool {
//1.创建一个线程池
private ExecutorService excutorService;
//2.初始化
public HandlerSocketServerPool(int maxThreadNum, int queueSize) {
excutorService = new ThreadPoolExecutor(3, maxThreadNum, 120, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(queueSize));
}
//3.提供方法提交任务给线程池的任务来暂存,等待线程池来执行
public void execute(Runnable target){
excutorService.execute(target);
}
}
//功能
public class ServerRunnableTarget implements Runnable {
private Socket socket;
public ServerRunnableTarget(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
//处理客户端socket请求
try {
InputStream is = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String msg;
if ((msg = br.readLine()) != null) {
System.out.println(msg);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
小结:
- 伪异步io采用线程池实现,因此避免了为每个请求都创建独立线程造成资源耗尽的问题,由于底层还是同步阻塞,没解决根本问题。
- 如果单个消息处理缓慢,或服务器全部阻塞。那么后面的socket的io消息都将在队列中排队,新的socket将被拒绝,客户端会大量链接超时。
3.7 基于BIO形势下的文件上传
/**
* 实现客户端任意类型文件给服务端保存
*/
public class Client {
public static void main(String[] args) {
try {
Socket socket = new Socket("127.0.0.1", 8888);
//把字节输出流包装成一个数据输出流
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
//发送文件后缀给服务端
dos.writeUTF(".png");
//把文件数据发送给服务端
InputStream is = new FileInputStream("//Path");
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) > 0) {
dos.write(buffer, 0, len);
}
dos.flush();
//通知服务端接收完毕
socket.shutdownOutput();
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 接收客户端任意类型文件并保存
*/
public class Server {
public static void main(String[] args) {
try {
ServerSocket socket = new ServerSocket(8888);
while (true) {
Socket accept = socket.accept();
//交给独立线程来处理与这个客户端的文件通信需求
new ServerReaderThread(accept).start();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class ServerReaderThread extends Thread {
private Socket socket;
public ServerReaderThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
//得到数据输入流读取客户端发送过来的数据
DataInputStream dis = new DataInputStream(socket.getInputStream());
//读取客户端发送的文件类型
String suffix = dis.readUTF();
System.out.println("收到文件,类型:" + suffix);
//定义字节输出管道,负责把客户端发过来的数据写出
FileOutputStream os = new FileOutputStream("C:\\path\\" + UUID.randomUUID().toString() + suffix);
//从数据输入流中读取文件数据,写出到字节输出流中
byte[] buffer = new byte[1024];
int len;
while ((len = dis.read(buffer)) > 0) {
os.write(buffer, 0, len);
}
os.close();
System.out.println("保存文件成功");
} catch (Exception e) {
e.printStackTrace();
}
}
}
3.8 javaBIO下的端口转发
需求:一个客户端消息可发送所有的客户端接收(群聊)
/**
* BIO下服务端端口转发
* 服务端需求:
* 1.注册端口
* 2.收到客户端的socket链接,交给独立的线程来处理
* 3.把当前连接的客户端socket存入到一个所谓的在线socket集合中保存
* 4.接收客户端消息,然后推送给当前所有在线的socket接收
*/
public class Server {
//定义静态集合
public static List<Socket> allSocketOnLine = new ArrayList<>();
public static void main(String[] args) {
try {
ServerSocket ss = new ServerSocket(9999);
while(true){
Socket socket = ss.accept();
//把登陆的客户端socket存入到一个在线集合中去
allSocketOnLine.add(socket);
//为当前登录成功的socket分配一个独立的线程来处理与之通信
new ServerReaderThread(socket).start();
}
}catch (Exception e){
e.printStackTrace();
}
}
}
public class ServerReaderThread extends Thread {
private Socket socket;
public ServerReaderThread(Socket socket){
this.socket = socket;
}
@Override
public void run(){
try {
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String msg;
while ((msg = br.readLine())!=null){
//服务端接收到客户端的消息推送给当前所有在线socket
sendMsgToAllClient(msg);
}
}catch (Exception e){
System.out.println("当前有人下线!");
//从在线socket中移除本socket
Server.allSocketOnLine.remove(socket);
e.printStackTrace();
}
}
//把当前客户端发来的消息推送全部在线socket
private void sendMsgToAllClient(String msg) throws IOException {
for (Socket sk : Server.allSocketOnLine) {
PrintStream ps = new PrintStream(sk.getOutputStream());
ps.println(msg);
ps.flush();
}
}
}
3.9 基于BIO下即时通信
项目功能
需要解决客户端到客户端的通信,即实现客户端间的端口消息转发。
功能说明:
1.客户端登陆:输入用户名和服务端ip
2.在线人数实时更新:用户登录同步更新客户端联系人列表
3.离线人数更新:下线同步
4.群聊:任一客户端消息转发所有客户端接收
5.私聊:选择某一对象发送消息
6.@消息:可@该用户,所有人可见
7.消息用户和时间点:服务端记录用户消息时间点,然后进行多路转发或选择。
服务端设计
服务端接收多个客户端
服务端需要接收多个客户端的接入。
- 1.服务端需要接收多个客户端,目前我们采取的策略是一个客户端对应一个服务端线程。
- 2.服务端除了要注册端口以外,还需要为每个客户端分配一个独立线程处理与之通信。
服务端主体代码,主要进行端口注册,和接收客户端,分配线程处理该客户端请求
服务端接收登录消息及检测离线
接收客户端的登陆消息。
实现步骤
- 需要在服务端处理客户端的线程的登陆消息。
- 需要注意的是,服务端需要接收客户端的消息可能有很多种。
- 分别是登陆消息,群聊消息,私聊消息 和@消息。
- 这里需要约定如果客户端发送消息之前需要先发送消息的类型,类型我们使用信号值标志(1,2,3)。
- 1代表接收的是登陆消息
- 2代表群发| @消息
- 3代表了私聊消息
- 服务端的线程中有异常校验机制,一旦发现客户端下线会在异常机制中处理,然后移除当前客户端用户,把最新的用户列表发回给全部客户端进行在线人数更新。
服务端接收群聊
接收客户端发来的群聊消息推送给当前在线的所有客户端
实现步骤
- 接下来要接收客户端发来的群聊消息。
- 需要注意的是,服务端需要接收客户端的消息可能有很多种。
- 分别是登陆消息,群聊消息,私聊消息 和@消息。
- 这里需要约定如果客户端发送消息之前需要先发送消息的类型,类型我们使用信号值标志(1,2,3)。
- 1代表接收的是登陆消息
- 2代表群发| @消息
- 3代表了私聊消息
服务端接收私聊
私聊消息的推送逻辑.
实现步骤
- 解决私聊消息的推送逻辑,私聊消息需要知道推送给某个具体的客户端
- 我们可以接收到客户端发来的私聊用户名称,根据用户名称定位该用户的Socket管道,然后单独推送消息给该Socket管道。
- 需要注意的是,服务端需要接收客户端的消息可能有很多种。
- 分别是登陆消息,群聊消息,私聊消息 和@消息。
- 这里需要约定如果客户端发送消息之前需要先发送消息的类型,类型我们使用信号值标志(1,2,3)。
- 1代表接收的是登陆消息
- 2代表群发| @消息
- 3代表了私聊消息
小结
demo地址:https://gitee.com/xuyu294636185/JAVA_IO_DEMO.git
- 本节我们解决了私聊消息的推送逻辑,私聊消息需要知道推送给某个具体的客户端Socket管道
- 我们可以接收到客户端发来的私聊用户名称,根据用户名称定位该用户的Socket管道,然后单独推送消息给该Socket管道。