javaBIO
Posted 是馄饨呀
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了javaBIO相关的知识,希望对你有一定的参考价值。
文章目录
馄饨高兴的去面试,最近看了ThreadLocal的原理,知道内存泄漏是怎么产生的,心想,问到这里的时候又能说出一二了。
面试官:那我就先考考你基础吧,IO听过吗?
我心想:不会吧,IO这里我就用过文件上传,心想面试官是不是要抓住IO这好好考。
我:了解过
面试官:那你说说BIO、NIO、AIO有什么区别吧。
我:BIO为同步阻塞、NIO为同步非阻塞、AIO为异步非阻塞
面试官:恩恩,那你对它们了解多少?
我:我就知道个概念。
面试官:恩恩,好,回去等通知吧。
凉凉月色为你等待通知 ~
1 BIO
1.1 定义
同步并阻塞,服务器实现模式为一个连接一个线程,客户端有连接请求时服务器端就需要启动一个线程,如果这个连接不做任何事情会造成不必要的线程开销。
1.2 使用场景
BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序简单易理解。
1.3 工作机制
客户端
1.创建socket对象通通过ip和端口向服务端建立连接
2.获取IO流数据
3.发送数据
服务端
1.创建Socket对象进行服务端端口注册
2.监听客户端的Socket链接请求
3.获取IO数据
4.输出数据
2 java实现简单数据传输
我们通过最简单的Java代码实现客户端服务端的数据发送,我们创建Server服务端类和Client客户端类。
import java.io.*;
import java.net.Socket;
public class Client {
public static void main(String[] args) {
System.out.println("客户端开始创建连接");
try {
//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 word 服务端你好");
ps.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class Server {
public static void main(String[] args) {
System.out.println("服务端启动");
try {
//1.创建ServerSocket的实例进行服务端的端口注册
ServerSocket ss = new ServerSocket(9999);
//2.监听客户端的Socket链接请求
Socket socket = ss.accept();
//3.从Socket管道中得到一个字节输入流对象
InputStream inputStream = socket.getInputStream();
//4.把字节输入流包装成一个缓冲字符输入流
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String str;
if((str = bufferedReader.readLine()) != null) {
System.out.println("服务端接收到数据" + str);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
这里我们发现客户端发送的时候发送的是一行字符,如果我服务端if((str = bufferedReader.readLine()) != null)
的if
用while
,那么服务端会一直等待客户端发送的数据,但是客户端发送完一条数据就结束了,这时候服务端在等待时候发现客户端已经结束,那么服务端就会报错,去进行重试。
服务端启动
服务端接收到数据hello word 服务端你好
java.net.SocketException: Connection reset
这个只适用于发送一行数据
2.1 BIO下的多发和多收机制
客户端可以反复的发送数据,服务端可以反复的接收数据
我们发现在server
层中只需要用while
循环来一直接收客户端的数据就可以了,那么我们改进客户端,将客户端的数据改为可以多次发送
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;
/**
* @ClassName Client
* @Description
* @Author asus
* @Date 2021/8/3 9:04
* @Version 1.0
**/
public class Client {
public static void main(String[] args) {
System.out.println("客户端开始创建连接");
try {
//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){
String msg = sc.nextLine();
System.out.print("发送数据.....");
ps.println(msg);
ps.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @ClassName Server
* @Description
* @Author asus
* @Date 2021/8/3 9:04
* @Version 1.0
**/
public class Server {
public static void main(String[] args) {
System.out.println("服务端启动");
try {
//1.创建ServerSocket的实例进行服务端的端口注册
ServerSocket ss = new ServerSocket(9999);
//2.监听客户端的Socket链接请求
Socket socket = ss.accept();
//3.从Socket管道中得到一个字节输入流对象
InputStream inputStream = socket.getInputStream();
//4.把字节输入流包装成一个缓冲字符输入流
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String str;
while ((str = bufferedReader.readLine()) != null) {
System.out.println("服务端接收到数据" + str);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
我们可以同时开两个客户端,然后向服务端进行发送。
先用客户端一发送
很明显我们发现服务端只能接收一个客户端的请求,第二个不能,这个是因为服务端在用Socket socket = ss.accept();
接受连接后,就会跳转到while ((str = bufferedReader.readLine()) != null)
来一直接收客户端一的请求,使得在客户端二启动后,根本执行不到Socket socket = ss.accept();
这一步,当然服务端也就接收不到客户端二发送的请求了。
服务端只能处理一个客户端的请求,因为服务端是单线程。一次只能与一个客户端进行消息通信。
2.2 BIO模式下接受多个客户端
我们在上面发现,我一个服务端只能允许一个客户端去发送请求,那么怎么实现服务端接收多个客户端的请求呢,我们可以创建多个线程来进行客户端的接收。
客户端
package bio_three;
import java.io.IOException;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;
/**
* @ClassName Client
* @Description
* @Author asus
* @Date 2021/8/3 20:22
* @Version 1.0
**/
public class Client {
public static void main(String[] args) {
try {
//1.对服务端进行ip认证
Socket socket = new Socket("127.0.0.1",9999);
//2.得到一个打印流
PrintStream ps = new PrintStream(socket.getOutputStream());
//3.利用死循环不断的发送消息给客户端
Scanner scanner = new Scanner(System.in);
while (true){
System.out.print("请说:");
String msg = scanner.nextLine();
ps.println(msg);
ps.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
创建服务端的接收客户端数据的类,继承自线程类
package bio_three;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Socket;
/**
* @ClassName ServerThreadReader
* @Description
* @Author asus
* @Date 2021/8/3 20:05
* @Version 1.0
**/
public class ServerThreadReader extends Thread{
private Socket socket;
public ServerThreadReader(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) {
e.printStackTrace();
}
}
}
服务端
package bio_three;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @ClassName Server
* @Description
* @Author asus
* @Date 2021/8/3 20:02
* @Version 1.0
**/
public class Server {
public static void main(String[] args) {
try {
//1.注册端口
ServerSocket serverSocket = new ServerSocket(9999);
//2.定义一个死循环,负责不断的接收客户端的请求
while (true){
Socket socket = serverSocket.accept();
//创建一个独立的线程来处理与这个客户端的socket
new ServerThreadReader(socket).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
我们会发现,在多个客户端发送的时候,服务端通过创建多个线程,让线程管理来实现一个服务对应多个客户端进行数据的接收。
2.3 BIO总结
虽然我们实现了服务端接收多个客户端发送的请求,但是也存在一些问题。
- 1.每个Socket接收到,都会创建一个线程,线程的竞争、切换上下文影响性能;
- 2.每个线程都会占用栈空间和CPU资源;
- 3.并不是每个socket都进行IO操作,无意义的线程处理;
- 4.客户端的并发访问增加时。服务端将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出,线程创建失败,最终导致进程宕机或者僵死,从而不能对外提供服务。
3 伪异步I/O编程
我们在BIO模式中发现,每次需要一个客户端请求,那么服务端这里就会创建一个线程,但是线程过多的情况下,服务端将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出,线程创建失败,最终导致进程宕机或者僵死,从而不能对外提供服务。
我们可以通过线程池和任务队列来实现,当客户端的socket封装成一个Task(该任务实现java.lang.Runnable线程任务接口)交给后端的线程池进行处理。JDK的线程池维护一个消息队列和N个活跃的线程,对消息队列中Socket任务进行处理,由于线程池可以设置消息队列的大小和最大线程数,所以它占用的资源是可控制的,无论多少个客户端并发访问,都不会导致资源的耗尽而宕机。
但是,因为线程池是限制线程数量的,所以也导致客户端连接的数量是有限的,在线程满的情况下,只有等到一个客户端用完释放的时候,其他客户端才能在对服务端进行请求。
我们使用线程池的方式来处理客户端的请求,我们将服务端的socket对象封装成一个任务对象,交给线程池来进行任务的处理。
3.1 服务端类
首先我们创建服务端类
package bio_four;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @ClassName Server
* @Description
* @Author asus
* @Date 2021/8/3 21:28
* @Version 1.0
**/
public class Server {
public static void main(String[] args) {
try {
//1.创建ServerSocket注册端口
ServerSocket ss = new ServerSocket(9999);
//2.定义一个循环接收客户端的socket连接请求
//初始化一个线程池对象
HandlerSocketServerPool pool = new HandlerSocketServerPool(3,10);
while (true){
Socket socket = ss.accept();
//3.把socket对象交给一个线程池管理
//把socket封装成一个任务对象交给线程池处理
Runnable target = new ServerRunnableTarget(socket);
pool.execute(target);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
我们会发现,我们在注册端口后,我们创建了HandlerSocketServerPool线程池,用它来进行任务的处理
3.2 线程池类
我们来看封装的线程池
package bio_four;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @ClassName HandlerSocketServerPool
* @Description
* @Author asus
* @Date 2021/8/3 21:20
* @Version 1.0
**/
public class HandlerSocketServerPool {
//1.创建一个线程池的成员变量用于存储一个线程池对象
private ExecutorService executorService;
/**
* 2.创建这个类的对象的时候就需要初始化线程池对象
* public ThreadPoolExecutor(int corePoolSize,
* int maximumPoolSize,
* long keepAliveTime,
* TimeUnit unit,
* BlockingQueue<Runnable> workQueue)
*
*/
public HandlerSocketServerPool(int maxThreadNum,int queueSize){
//ArrayBlockingQueue阻塞队列
executorService = new ThreadPoolExecutor(
3,
maxThreadNum,
120,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(queueSize));
}
/**
* 3、提供一个方法来提交任务给线程池的队列来暂存,等着线程池来处理
*/
public void execute(Runnable runnable){
executorService.execute(runnable);
}
}
首先,我们创建ExecutorService 来进行线程池对象的存储,在构造方法中初始化一个线程池来赋值给ExecutorService 对象,我们创建一个execute方法将任务放入线程池的任务队列中,等待线程池进行任务的处理。
3.3 任务对象类
然后我们将socket对象封装成为一个任务对象,让线程池来执行这个任务。
package bio_four;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @ClassName ServerRunnableTarget
* @Description 实现Runnable接口来定义任务对象
* @Author asus
* @Date 2021/8/3 21:42
* @Version 1.0
**/
public class ServerRunnableTarget implements Runnable{
private Socket socket;
public ServerRunnableTarget(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
//处理接收到客户端Socket通信需求
try {
//1.从Socket管道中得到一个字节输入流对象
InputStream inputStream = socket.getInputStream();
//2.把字节输入流包装成一个缓冲字符输入流
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String str;
if ((str = bufferedReader.readLine()) != null) {
System.out.println("服务端接收到数据" + str);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
这个跟前面的服务端处理请求是一样的。
接下来我们将封装的任务队列交给线程池进行处理,也就是下面这个步骤。
Runnable target = new ServerRunnableTarget(socket);
pool.execute(target);
3.4 客户端类
这个跟以前的一样
public class Client {
public static void main(String[] args) {
try {
// 1.简历一个与服务端的Socket对象:套接字
Socket socket =<以上是关于javaBIO的主要内容,如果未能解决你的问题,请参考以下文章