文章目录
二、JAVA BIO
1、 Java BIO基本介绍
Java BlO就是传统的Java IO编程,其相关的类和接口在Java.io 包中。
BIO(blocking I/O)同步阻塞,服务器实现模式为:一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善 (实现多个客户连接服务器)。
2、 java BIO工作机制
3、传统的BIO编程实例回顾
网络编程的基本模型是Client/Server模型,也就是两个进程之间进行相互通信,其中服务端提供位置信(绑定IP地址和端口),客户端通过连接操作向服务端监听的端口地址发起连接请求,基于TCP协议下进行三次握手连接,连接成功后,双方通过网络套接字(Socket)进行通信。
传统的同步阻塞模型开发中,服务端ServerSocket负责绑定IP地址,启动监听端口;客户端Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。
基于BIO模式下的通信,客户端-服务端是完全同步,完全藕合的。
3.1、客户端案例如下
package 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 socket = new Socket("127.0.0.1", 9999);
//2、从socket对象中获取一个输出流
OutputStream out = socket.getOutputStream();
//3、把字节输出流包装成打印流
PrintStream printStream = new PrintStream(out);
printStream.println("hello World! 服务端,你好");
printStream.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
3.2、服务端案例如下
package bio;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 目标:客户端发送消息,服务端接收消息
*/
public class Server {
public static void main(String[] args) {
try {
System.out.println("===服务端启动===");
//1、对服务端端口进行注册 9999
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;
/*while ((msg = br.readLine()) != null){
System.out.println("服务端接收到:"+msg);
}*/
if ((msg = br.readLine()) != null){
System.out.println("服务端接收到:" + msg);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
3.3、输出
===服务端启动===
服务端接收到:hello World! 服务端,你好
3.4、小结
在以上通信中,服务端会一直等待客户端的消息,如果客户端没有进行消息的发送,服务端将一直进入阻塞状态。
同时服务端是按照行获取消息的,这意味着客户端也必须按照行进行消息的发送,否则服务端将进入等待消息的阻塞状态!
4、BIO模式下多发和多收消息
在上面的案例中,只能实现客户端发送消息,服务端接收消息, 并不能实现反复的收消息和反复的发消息,我们只需要在客户端案例中,加上反复按照行发送消息的逻辑即可! 案例代码如下:
4.1、客户端代码如下
package 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 socket = new Socket("127.0.0.1",9999);
//2、从socket对象中获取一个输出流
OutputStream out = socket.getOutputStream();
//3、把字节输出流包装成打印流
PrintStream printStream = new PrintStream(out);
Scanner sc = new Scanner(System.in);
while (true){
System.out.print("请说:");
String msg = sc.nextLine();
printStream.println(msg);
printStream.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
4.2、服务端代码如下
package bio;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 目标:服务端可以反复的接收消息,客户端可以反复的发送消息
*/
public class Server {
public static void main(String[] args) {
try {
System.out.println("===服务端启动===");
//1、对服务端端口进行注册 9999
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;
while ((msg = br.readLine()) != null){
System.out.println("服务端接收到:"+msg);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
4.3、输出
客户端:
请说:hello
请说:what are you doing?
请说:
服务端:
===服务端启动===
服务端接收到:hello
服务端接收到:what are you doing?
5、BIO模式下接收多个客户端
5.1、概述
在上述的案例中,一个服务端只能接收一个客户端的通信请求,那么如果服务端需要处理很多个客户端的消 息通信请求应该如何处理呢,此时我们就需要在服务端引入线程了,也就是说客户端每发起一个请求,服务端就创建一个新的线程来处理这个客户端的请求,这样就实现了一个客户端一个线程的模型。
图解模式如下:
5.2、客户端案例代码
package 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 socket = new Socket("127.0.0.1",9999);
//2、从socket对象中获取一个输出流
OutputStream out = socket.getOutputStream();
//3、把字节输出流包装成打印流
PrintStream printStream = new PrintStream(out);
Scanner sc = new Scanner(System.in);
while (true){
System.out.print("请说:");
String msg = sc.nextLine();
printStream.println(msg);
printStream.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
5.3、服务端案例代码
package bio;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 目标:服务端可以实现同时接收多个客户端的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 ServerThreadReader(socket).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
5.4、线程类
public class ServerThreadReader extends Thread {
private Socket socket;
public ServerThreadReader(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
//1、从socket对象中得到一个字节输入流
InputStream is = socket.getInputStream();
//2、使用缓存字符输入流包装字节输入流
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String msg;
while ((msg = br.readLine()) != null){
System.out.println(msg);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
5.5、输出
===client1:
请说:
ppp
请说:
你在干嘛?
请说:
我是第一个client
请说:
===client2:
请说:
lll
请说:
还钱!!!
请说:
我是第二个client
请说:
===client3:
请说:
我是第三个client
请说:
===server:
lll
ppp
你在干嘛?
还钱!!!
我是第二个client
我是第一个client
我是第三个client
5.6、小结
- 每个Socket接收到,都会创建一个线程,线程的竞争、切换上下文影响性能;
- 每个线程都会占用栈空间和CPU资源;
- 并不是每个socket都进行lO操作,无意义的线程处理;
- 客户端的并发访问增加时。服务端将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出,
- 线程创建失败,最终导致进程宕机或者僵死,从而不能对外提供服务。
6、伪异步I/O编程
6.1、概述
在上述案例中:客户端的并发访问增加时。服务端将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出,线程创建失败,最终导致进程宕机或者僵死,从而不能对外提供服务。
接下来我们采用一个伪异步I/O的通信框架,采用线程池和任务队列实现,当客户端接入时,将客户端的Socket封装成一个Task (该任务实现Java. lang. Runnable(线程任务接口)交给后端的线程池中进行处理。JDK的线程池维护一个消息队列和N个活跃的线程,对消息队列中Socket任务进行处理,由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。
如下图所示:
6.2、客户端案例代码
package 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 socket = new Socket("127.0.0.1",9999);
//2、从socket对象中获取一个输出流
OutputStream out = socket.getOutputStream();
//3、把字节输出流包装成打印流
PrintStream printStream = new PrintStream(out);
Scanner sc = new Scanner(System.in);
while (true){
System.out.print("请说:");
String msg = sc.nextLine();
printStream.println(msg);
printStream.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
6.3、服务端案例代码
package bio;
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.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 目标:开发实现伪异步通讯架构
* 思路:服务端没接收到一个客户端socket请求对象之后都交给一个独立的线程来处理客户端的数据交互需求
*/
public class Server {
public static void main(String[] args) {
try {
//1.注册端口
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();
}
}
}
6.4、线程池处理类
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){
executorService = new ThreadPoolExecutor(3, maxThreadNum, 120, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(queueSize));
}
/**
* 3.提供一个方法来提交任务给线程池的任务队列来暂存,等待线程池来处理
*/
public void execute(Runnable target){
executorService.execute(target);
}
}
6.5、Socket任务类
public class ServerRunnableTarget implements Runnable {
private Socket socket;
public ServerRunnableTarget(Socket socket){
this.socket = socket;
}
@Override
public void run() {
//处理接收到的客户端socket通信需求
try {
//1.从socket管道中得到一个字节输入流对象
InputStream is = socket.getInputStream();
//2.把字节输入流包装成一个缓存字符输入流
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String msg;
while((msg = br.readLine()) != null){
System.out.println("服务端收到:" + msg);
}
} catch (Exception e){
e.printStackTrace();
}
}
}
6.5、输出
服务端收到:client1
服务端收到:client2
服务端收到:client3
服务端收到:client4
java.net.SocketException: Connection reset
at java.base/java.net.SocketInputStream.read(SocketInputStream.java:186)
at java.base/java.net.SocketInputStream.read(SocketInputStream.java:140)
at java.base/sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
at java.base/sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
at java.base/sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
at java.base/java.io.InputStreamReader.read(InputStreamReader.java:185)
at java.base/java.io.BufferedReader.fill(BufferedReader.java:161)
at java.base/java.io.BufferedReader.readLine(BufferedReader.java:326)
at java.base/java.io.BufferedReader.readLine(BufferedReader.java:392)
at com.zhangxudong.ServerRunnableTarget.run(ServerRunnableTarget.java:23)
at
java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.j
ava:1128)
at
java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.
java:628)
at java.base/java.lang.Thread.run(Thread.java:834)
服务端收到:client5
6.6、小结
- 伪异步旧采用了线程池实现,因此避免了为每个请求创建一个独立线程造成线程资源耗尽的问题,但由于底层 依然是采用的同步阻塞模型,因此无法从根采上解决问题。
- 如果单个消息处理的缓慢,或者服务器线程池中的全部线程都被阻塞,那么后续socket的I/O消息都将在队列 中排队。新的Socket请求将被拒绝,客户端会发生大量连接超时。