一、介绍
1、概述:
- Java BlO就是传锤的Java IO编程,其相关的类和接口在Java.io 包中
- BIO同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器 端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销【简单示意图 】
2、java BIO工作机制
3 传统的BIO编程实例回顾
- 网络编程的基本模型是Client/Server模型,也就是两个进程之间进行相互通信,其中服务端提供位置信(绑 定IP地址和端口),客户端通过连接操作向服务端监听的端口地址发起连接请求,基于TCP协议下进行三次握手连接,连接成功后,双方通过网络套接字(Socket)进行通信。
- 传统的同步阻塞模型开发中,服务端ServerSocket负责绑定IP地址,启动监听端口;客户端Socket负责发起 连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。
- 基于BIO模式下的通信,客户端-服务端是完全同步,完全藕合的。
二、示例
1、单线程通信
client代码:
public class BioClient {
public static void main(String[] args) {
OutputStream outputStream = null;
BufferedWriter bw = null;
Socket socket = null;
try {
socket = new Socket("127.0.0.1", 9999);
outputStream = socket.getOutputStream();
PrintStream ps = new PrintStream(outputStream);
ps.println("hello , I am form china");
ps.flush();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
server代码:
public class BioServer {
public static void main(String[] args) {
ServerSocket serverSocket = null;
Socket accept = null;
InputStream inputStream = null;
BufferedReader br = null;
try {
serverSocket = new ServerSocket(9999);
accept = serverSocket.accept();
inputStream = accept.getInputStream();
br = new BufferedReader(new InputStreamReader(inputStream));
String s = null;
if ((s = br.readLine()) != null){
System.out.println("服务器收到消息 ====" + s);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
输出:
服务器收到消息 ==== hello , I am form china
小结
- 在以上通信中,服务端会一直等待客户端的消息,如果客户端没有进行消息的发送,服务端将一直进入阻塞状态
- 同时服务端是按照行获取消息的,这意味育客户端也必须按照行进行消息的发送,否则服务端将进入等待消息的阻塞状态!
2、BIO模式下多发和多收消息
client代码:
public class TwoBioClient {
public static void main(String[] args) {
OutputStream outputStream = null;
BufferedWriter bw = null;
Socket socket = null;
try {
socket = new Socket("127.0.0.1", 9999);
outputStream = socket.getOutputStream();
PrintStream ps = new PrintStream(outputStream);
ps.println("hello , I am form china");
ps.flush();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
server代码:
public class TwoBioServer {
public static void main(String[] args) {
ServerSocket serverSocket = null;
Socket accept = null;
InputStream inputStream = null;
BufferedReader br = null;
try {
serverSocket = new ServerSocket(9999);
accept = serverSocket.accept();
inputStream = accept.getInputStream();
br = new BufferedReader(new InputStreamReader(inputStream));
String s = null;
if ((s = br.readLine()) != null){
System.out.println("服务器收到消息 ====" + s);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
输出:
服务器收到消息 ==== hello , I am form china
服务器收到消息 ==== hello , I am form china
服务器收到消息 ==== hello , I am form china
3、BIO模式下接收多个客户端
概述
在上述的案例中,一个服务端只能接收一个客户端的通信请求,那么如果服务端需要处理很多个客户端的消 息通信请求应该如何处理呢,此时我们就需要在服务端引入线程了,也就是说客户端每发起一个请求,服务端就创 建一个新的线程来处理这个客户端的请求,这样就实现了一个客户端一个线程的模型,
client代码:
public class MultiBioClient {
public static void main(String[] args) {
OutputStream outputStream = null;
BufferedWriter bw = null;
Socket socket = null;
try {
socket = new Socket("127.0.0.1", 9999);
outputStream = socket.getOutputStream();
PrintStream ps = new PrintStream(outputStream);
Scanner scanner = new Scanner(System.in);
while (true){
System.out.print("请说:");
String msg = scanner.nextLine();
ps.println(msg);
ps.flush();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
server代码:
public class MultiBioServer {
public static void main(String[] args) {
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(9999);
while (true){
Socket socket = serverSocket.accept();
new MultiServerThreadReader(socket).start();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
线程执行获取客户端信息并读取:
public class MultiServerThreadReader extends Thread{
private Socket socket;
public MultiServerThreadReader(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
InputStream inputStream = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
String s = null;
while ((s = br.readLine()) != null){
System.out.println("服务器收到消息 ====" + s);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
Idea配置多个client启动实例
输出:
===client1:
请说:
ppp
请说:
你在干嘛?
请说:
我是第一个client
请说:
===client2:
请说:
lll
请说:
还钱!!!
请说:
我是第二个client
请说:
===client3:
请说:
我是第三个client
请说
服务器收到消息 ==== lll
服务器收到消息 ==== ppp
服务器收到消息 ==== 你在干嘛?
服务器收到消息 ==== 还钱!!!
服务器收到消息 ==== 我是第二个client
服务器收到消息 ==== 我是第一个client
服务器收到消息 ==== 我是第三个client
小结
- 每个Socket接收到,都会创建一个线程,线程的竞争、切换上下文影响性能; ・
- 每个线程都会占用栈空间和CPU资源;
- 并不是每个socket都进行lO操作,无意义的线程处理;
- 客户端的并发访问增加时。服务端将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出,
线程创建失败,最终导致进程宕机或者僵死,从而不能对外提供服务。
4、使用线程池实现异步BIO通信
概述
- 在上述案例中:客户端的并发访问增加时。服务端将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出,线程创建失败,最终导致进程宕机或者僵死,从而不能对外提供服务。
- 接下来我们采用一个伪异步I/O的通信框架,采用线程池和任务队列实现,当客户端接入时,将客户端的Socket封装成一个Task(该任务实现Java. lang. Runnable(线程任务接口)交给后端的线程池中进行处理。JDK的线程池维护一个消息队列和N个活跃的线程,对消息队列中Socket任务进行处理,由于线程池
- 可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。
如图:
client代码:
public class AsyncBioClient {
public static void main(String[] args) {
OutputStream outputStream = null;
BufferedWriter bw = null;
Socket socket = null;
try {
socket = new Socket("127.0.0.1", 9999);
outputStream = socket.getOutputStream();
PrintStream ps = new PrintStream(outputStream);
Scanner scanner = new Scanner(System.in);
while (true){
System.out.print("请说:");
String msg = scanner.nextLine();
ps.println(msg);
ps.flush();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
server代码:
public class AsyncBioServer {
public static void main(String[] args) {
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(9999);
ExecutorService executorService = new ThreadPoolExecutor(3, 6, 10, TimeUnit.SECONDS, new LinkedBlockingDeque(2), //队列大小为2
new MyThreadFactory(),//这里用简单的线程工厂
new RejectedExecutionHandler() {
@Override
//错误策略,超过最大线程数后执行该方法
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.out.println("ThreadName:" + Thread.currentThread().getName() + "执行拒绝策略:");
}
}
);
while (true) {
Socket socket = serverSocket.accept();
executorService.execute(new ServerRunnable(socket));
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static class MyThreadFactory implements ThreadFactory {
@Override
public Thread newThread(Runnable r) {
return new Thread(r);
}
}
}
线程执行获取客户端信息并读取:
public class ServerRunnable implements Runnable{
private Socket socket;
public ServerRunnable(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
InputStream inputStream = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
String s = null;
while ((s = br.readLine()) != null){
System.out.println("服务器收到消息 ====" + s);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
输出:
===client1:
请说:
ppp
请说:
你在干嘛?
请说:
我是第一个client
请说:
===client2:
请说:
lll
请说:
还钱!!!
请说:
我是第二个client
请说:
===client3:
请说:
我是第三个client
请说
服务器收到消息 ==== lll
服务器收到消息 ==== ppp
服务器收到消息 ==== 你在干嘛?
服务器收到消息 ==== 还钱!!!
服务器收到消息 ==== 我是第二个client
服务器收到消息 ==== 我是第一个client
服务器收到消息 ==== 我是第三个client
小结
- 伪异步旧采用了线程池实现,因此避免了为每个请求创建一个独立线程造成线程资源耗尽的问题,但由于底层 依然是采用的同步阻塞模型,因此无法从根采上解决问题。
- 如果单个消息处理的缓慢,或者服务器线程池中的全部线程都被阻塞,那么后续socket的I/O消息
都将在队列 中排队。新的Socket请求将被拒绝,客户端会发生大量连接超时
5、基于BIO形式下的文件上传
目标
支持任意类型文件形式的上传
客户端开发
/**
* 目标:服务端开发,可以实现接收客户端的任意类型文件,并保存到服务器端磁盘
*/
public class FileClient {
public static void main(String[] args) {
try (
FileInputStream fis = new FileInputStream(new File("C:\\Users\\Administrator\\Desktop\\图片\\logo2.png"));
) {
//1、创建客户端
Socket socket = new Socket("127.0.0.1", 9999);
//2、把字节输出流包装成字节输出流
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
//3、发送后缀名称
dos.writeUTF(".png");
int len = 0;
byte[] buffer = new byte[1024];
//4、把文件数据发送给服务端进行接收
while ((len = fis.read()) > 0) {
dos.write(buffer, 0, len);
}
dos.flush();
//5、通知服务器,文件发送完毕
socket.shutdownOutput();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
服务端开发
public class FileServer {
public static void main(String[] args) {
try {
//1、创建服务器
ServerSocket ss = new ServerSocket(9999);
while (true){
Socket socket = ss.accept();
//2、交给一个独立的线程来处理与这个客户端的文件通信需求
new FileServerThread(socket).start();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
public class FileServerThread extends Thread {
private Socket socket;
public FileServerThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
FileOutputStream fos = null;
try {
//1、用字节流来获取客户端发送过来的数据
DataInputStream dis = new DataInputStream(socket.getInputStream());
//2、得到文件后缀名称
String suffix = dis.readUTF();
System.out.println("服务端已经成功接收到了文件类型:" + suffix);
//3、定义文件输出流,将得到的文件写出去
fos = new FileOutputStream("d:\\pic\\" + UUID.randomUUID().toString() + suffix);
//4、从文件输入流中读取文件数据,写出到字节输出流中
byte[] buffer = new byte[1024];
int len = 0;
while ((len = dis.read(buffer)) > 0){
fos.write(buffer, 0, len);
}
fos.flush();
System.out.println("服务端接收文件保存成功!");
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
fos.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
输出:
服务端已经成功接收到了文件类型:.png
服务端接收文件保存成功!
小结
- 同步阻塞模式下(BIO),客户端怎么发,服务端就必须对应的怎么收。如客户端用的是
DataOutputStream,那么服务端就该用DataInputStream,客户端dos.writeUTF(“.jpg”);服务端
就该String suffix = dis.readUTF(); - 客户端发完数据后必须通知服务端自己已经发完socket.shutdownOutput(),否则服务端会一直等
待。
三、源码下载
https://gitee.com/charlinchenlin/store-pos