JavaIO
4.1、IO模型
4.1.1、IO模型基本说明
- I/O模型:就是用什么样的通道或者说是同行模式和架构进行数据的传送和接收,很大程度上决定了程序通信的性能,Java共支持3种网络编程的I/O模型:BIO、NIO、AIO
实际通讯需求下,要根据业务场景和需求决定选择不同的I/O模型
4.2、BIO
4.2.1、BIO基本介绍
同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销
4.2.2、BIO工作机制
- 在以上通信中,服务端会一直等待客户端的消息,如果客户端没有进行消息的发送,服务端将一直进入阻塞状态。
- 同时服务端是按行获取消息的,这意味着客户端也必须按照行进行消息的发送,否则服务端将进入等待消息的阻塞状态!
4.2.3、BIO模式下多发和多收信息
客户端
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
/*
客户端
* */
public class Client {
public static void main(String[] args) {
try {
//1.创建Socket对象请求服务端连接
Socket s = new Socket("127.0.0.1", 666);
//2.从socket对象获取一个字节输出流
OutputStream os = s.getOutputStream();
//3.把字节输出流包装成一个打印流
PrintStream ps = new PrintStream(os);
Scanner sc = new Scanner(System.in);
while (true) {
System.out.print("请输入:");
String msg = sc.nextLine();
ps.println(msg);
ps.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
服务器
import java.io.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
/*
目标:客户端发送消息,服务端接收消息
*/
public class Server {
public static void main(String[] args) {
try {
System.out.println("--------服务端启动啦---------");
//1.定义一个ServerSocket对象进行服务端的端口注册
ServerSocket ss = new ServerSocket(666);
//2.监听客户端的Socket请求
Socket s = ss.accept();
//3.从Socket通道中得到一个字节输入流对象
InputStream is = s.getInputStream();
//4.把字节输入缓冲流包装成一个缓冲字符输入流
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String msg;
while ((msg = br.readLine())!=null){
System.out.println("服务端接收到:"+msg);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 以上案例实现了客户端的多发多收,但是服务端只能处理一个客户端的请求,应为服务端是单线程的。一次只能与一个客户端进行消息通信。
4.2.4、BIO模式下接收多个客户端
客户端
/*
*
* 目标:实现服务端可以同时接收多个客户端的Socket通信需求
* 思路:服务端每接收到一个客户端的Socket请求对象之后都交给一个独立的线程来处理客户端的数据交互需求
*
* */
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) {
try {
//1.注册端口
ServerSocket ss = new ServerSocket(666);
//2.定义一个死循环,负责不断的接受客户端的Socket连接请求
while(true){
Socket s = ss.accept();
//3.创建一个独立的线程来处理与这个客户端的Socket通信请求
new ServerThreadReader(s).start();
}
} catch (IOException e) {A
}
}
}
客户端创建线程类
import java.io.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
public class ServerThreadReader extends Thread {
private Socket s;
public ServerThreadReader(Socket s){
this.s = s;
}
@Override
public void run(){
try {
//从Socket对象中得到一个字节输入流
InputStream is = s.getInputStream();
//使用缓冲字符流包装字节输入流
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String msg;
while ((msg=br.readLine())!=null){
System.out.println(msg);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
小结
- 每个Socket接收到都会创建一个线程,线程的竞争、切换上下文影响性能。
- 每个线程都会占用栈空间和cpu资源。
- 并不是每个Socket都进行IO操作,无意义的线程处理。
- 客户端的并发访问增加时。服务端将呈现1 :1的线程开销,访问量越大,系统将发生线程栈溢出,线程创建失败,最终导致进程宕机或者僵死,从而不能对外提供服务。
4.3、伪异步I/O编程
伪异步I/O的通信框架,采用线程池和任务队列实现,当客户端接入时,将客户端的Socket封装成一个Task(该任务实现java.lang.Runnable线程任务接口)交给后端的线程池中进行处理。JDK的线程池维护一个消息队列和N个活跃的线程,对消息队列中的Socket任务进行处理,由于线程池可以设置消息队列的大小和最大线程数,因此,他的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。
服务端
import java.net.Socket;
/*
* 目标:开发实现伪异步通信架构
* */
public class Server {
public static void main(String[] args) {
try{
//1.注册端口
ServerSocket ss = new ServerSocket(777);
//2.定义一个循环接收客户端的Socket请求
//初始化一个线程池对象
HandlerSocketServerPool pool = new HandlerSocketServerPool(6, 16);
while (true) {
Socket s = ss.accept();
//3.把Socket对象交给一个线程池进行处理,
//把Socket封装成一个任务对象交给线程池处理
Runnable target = new ServerRunnableTarget(s);
pool.execute(target);
}
}catch(Exception e){
e.printStackTrace();
}
}
}
在这里插入代码片
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class HandlerSocketServerPool {
//1.创建一个线程池的成员变量用于存储一个线程池对象
private ExecutorService executorService;
//2.创建这个类的对象的时候就需要初始化线程池对象
public HandlerSocketServerPool(int maxThreadNum,int queueSize){
executorService = new ThreadPoolExecutor(3,maxThreadNum,120,TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(queueSize));
}
/*
* 3.提供一个方法来提交任务给线程池的任务队列来暂存,等着线程池来处理
* */
public void execute(Runnable target){
executorService.execute(target);
}
}
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.CharBuffer;
public class ServerRunnableTarget implements Runnable {
private Socket s;
public ServerRunnableTarget(Socket s){
this.s = s;
}
@Override
public void run() {
//处理接收到的客户端Socket通信需求
try {
System.out.println("--------服务端启动啦----------");
//1.定义一个ServerSocket对象进行服务端的端口注册
ServerSocket ss = new ServerSocket(666);
//2.监听客户端的Socket请求
Socket s = ss.accept();
//3.从Socket通道中得到一个字节输入流对象
InputStream is = s.getInputStream();
//4.把字节输入缓冲流包装成一个缓冲字符输入流
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String msg;
if ((msg = br.readLine())!=null){
System.out.println("服务端接收到:"+msg);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
小结
- 伪异步io采用了线程池实现,因此避免了为每个请求创建一个独立线程造成线程资源耗尽的问题,但是由于底层依然是采用的同步阻塞模型,因此无法从根本上 解决问题。
- 如果单个消息处理的缓慢,或者服务器线程池中的全部线程都被阻塞,那么后续socket的io消息都将在队列中排队。新的Socket请求将被拒绝,客户端会发生大量的连接超时。