1、BIO基本介绍
●Java BIO就是传统的java io编程,其相关的类和接口在java.io
●BIO(blocking I/0):同步阻塞, 服务器实现模式为一个连接一个线程, 即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户连接服务器).
1.1、BIO工作机制
1.2、传统的BIO编程实例回顾
网络编程的基本模型是Client/Server模型,也就是两个进程之间进行相互通信,其中服务端提供位置信(绑定IP地址和端口), 客户端通过连接操作向服务端监听的端口地址发起连接请求,基于TCP协议 下进行三次握手连接,连接成功后,双方通过网络套接字(Socket) 进行通信。
传统的同步阻塞模型开发中,服务端ServerSocket负责绑定IP地址, 启动监听端口;客户端Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。
基于BIO模式下的通信,客户端-服务端是完全同步,完全耦合的。
客户端案例如下:
package com.jialidun.one;
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 os = socket.getOutputStream();
//3、把字节输出流包装成一个打印流
PrintStream printStream = new PrintStream(os);
printStream.println("Hello World!服务器,您好!");
//刷新并关闭
printStream.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
服务器端案例:
package com.jialidun.one;
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、定义一个ServerSocket对象进行服务端呃端口注册
ServerSocket serverSocket = new ServerSocket(9999);
//2、监听客户端的Socket链接请求
Socket socket = serverSocket.accept();
//3、从socket管道中得到有个字节输入流对象
InputStream inputStream = socket.getInputStream();
//4、把字节输入流包装成一个缓存字符流
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String msg;
if ((msg=bufferedReader.readLine())!=null){
System.out.println("服务端接收到:"+msg);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
小结:
●在以上通信中,服务端会一直等待客户端的消息,如果客户端没有进行消息的发送,服务端将一直进入阻塞状态。
●同时服务端是按照行获取消息的,这意味着客户端也必须按照行进行消息的发送,否则服务端将等待消息的阻塞状态!
1.3、BIO模式下多发和多收消息
在上面的案例中,只能实现客户端发送消息,服务端接收消息,并不能实现反复的收消息和反复的发消息,我们只需要在客户端案例中,加上反复按照行发送消息的逻辑即可!案例代码如下:
客户端代码如下:
package com.jialidun.two;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;
/**
* 客户端
*/
public class Client {
public static void main(String[] args) {
try {
//1、创建一个Socket对象请求服务端的链接
Socket socket = new Socket("127.0.0.1",9999);
//2、从Socket对象中获取一个字节输出流
OutputStream os = socket.getOutputStream();
//3、把字节输出流包装成一个打印流
PrintStream printStream = new PrintStream(os);
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();
}
}
}
服务端案例:
package com.jialidun.two;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 目标:服务端可以反复接收消息,客户端可以反复的发送消息
*/
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 inputStream = socket.getInputStream();
//4、把字节输入流包装成一个缓存字符流
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String msg;
while ((msg=bufferedReader.readLine())!=null){
System.out.println("服务端接收到:"+msg);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
1.4、BIO模式下接收多个客户端
概述
在上述的案例中,一个服务端只能接收一个客户端的通信请求, 那么如果服务端需要处理很多个客户端的消息通信请求应该如何处理呢,此时我们就需要在服务端引入线程了,也就是说客户端每发起一个请求,服务端就创建一个新的线程来处理这个客户端的请求,这样就实现了-个客户端一个线程的模型,图解模式如下:
服务端:
package com.jialidun.three;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 需求:实现服务端可以同时接收多个客户端的Socket通信需求。
* 思路:是服务端每接收到一个客户端Socket请求对象之后
* 都交给一个独立的线程来处理客户端的数据交互需求。
*/
public class Server {
public static void main(String[] args) {
try {
//1、注册端口
ServerSocket serverSocket = new ServerSocket(9999);
//2、定义一个死循环,负责不断的接收客户端的Socket链接请求
while (true){
Socket socket = serverSocket.accept();
//3、创建一个独立的线程来处理与这个客户端的socket通信需求。
new ServerThreadReader(socket).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
创建一个线程类:
package com.jialidun.three;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Socket;
public class ServerThreadReader extends Thread{
private Socket socket;
public ServerThreadReader(Socket socket){
this.socket = socket;
}
@Override
public void run(){
try {
//从socket对象中得到一个字节输入流
InputStream inputStream = socket.getInputStream();
//使用缓冲字符输入流包装字节输入流
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String msg;
while ((msg=bufferedReader.readLine())!=null){
System.out.println(msg);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端:
package com.jialidun.three;
import java.io.IOException;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;
/**
* 客户端
*/
public class Client {
public static void main(String[] args) {
try {
//1、请求与服务端的Socket的对象链接
Socket socket = new Socket("127.0.0.1",9999);
//2、得到一个打印流
PrintStream ps = new PrintStream(socket.getOutputStream());
//3、使用循环不断的发送消息给服务端接收
Scanner sc = new Scanner(System.in);
while (true){
System.out.println("请说:");
String msg = sc.nextLine();
ps.println(msg);
//刷新
ps.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
小结:
1.每个Socket接收到,都会创建一个线程,线程的竞争,切换上下文影响性能;
2.每个线程都会占用栈空间和CPU资源;
3.并不是每个socket都进行I0操作,无意义的线程处理;
4.客户端的并发访问增加时。服务端将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出,线程创建失败,最终导致进程宕机或者僵死,从而不能对外提供服务。
1.5、伪异步I/O编程
概述
在上述案例中:客户端的并发访问增加时。服务端将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出,线程创建失败,最终导致进程宕机或者僵死,从而不能对外提供服务。
接下来我们采用一个伪异步I/O的通信框架,采用线程池和任务队列实现,当客户端接入时,将客户端的Socket封装成一个Task(该任务实现java.lang.Runnable线程任务接口)交给后端的线程池中进行处理。 JDK的线程池维护一个消息队列和N个活跃的线程,对消息队列中Socket任务进行处理,由于线程池可以设置消息队列的大小池维护一一个消息队列和N个活跃的线程,对消息队列中Socket任务进行处理,由于线程池可以设置消息队列的大小
图示如下:
服务端:
package com.jialidun.four;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 需求:开发实现伪异步通信架构
*/
public class Server {
public static void main(String[] args) {
try {
//1、注册端口
ServerSocket ss = new ServerSocket(9999);
//2、定义一个循环接收客户端的Socket链接请求
//初始化一个线程池对象
HandlerSocketServerPool handlerSocketServerPool = new HandlerSocketServerPool(6, 10);
while(true){
Socket socket = ss.accept();
//3、把socket对象交给一个线程池进行处理
//把socket封装成一个任务对象交给线程池处理
Runnable target = new ServerRunnableTarget(socket);
handlerSocketServerPool.execute(target);
}
}catch (Exception e){
e.printStackTrace();
}
}
}
处理接收客户端通信:
package com.jialidun.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;
public class ServerRunnableTarget implements Runnable{
private Socket socket;
public ServerRunnableTarget(Socket socket){
this.socket = socket;
}
@Override
public void run() {
//处理接收到的客户端Socket通信需求
try {
System.out.println("服务器启动!");
//1、从socket管道中得到有个字节输入流对象
InputStream inputStream = socket.getInputStream();
//2、把字节输入流包装成一个缓存字符流
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String msg;
while ((msg=bufferedReader.readLine())!=null){
System.out.println("服务端接收到:"+msg);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
线程池创建:
package com.jialidun.four;
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 ThreadPoolExecutor( int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
*/
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);
}
}
客户端:
package com.jialidun.four;
import java.io.IOException;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;
/**
* 客户端
*/
public class Client {
public static void main(String[] args) {
try {
//1、请求与服务端的Socket的对象链接
Socket socket = new Socket("127.0.0.1",9999);
//2、得到一个打印流
PrintStream ps = new PrintStream(socket.getOutputStream());
//3、使用循环不断的发送消息给服务端接收
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();
}
}
}
小结:
●伪异步io采用了线程池实现,因此避免了为每个请求创建一个独立线程造 成线程资源耗尽的问题,但由于底层依然是采用的同步阻塞模型,因此无法从根本上解决问题。
●如果单个消息处理的缓慢,或者服务器线程池中的全部线程都被阻塞,那么后续socket的i/o消 息都将在队列中排队。新的Socket请求将被拒绝,客户端会发生大量连接超时。
1.6、基于BIO形式下的文件上传
目标
支持任意类型文件形式的上传
客户端实现代码如下:
package com.jialidun.file;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.Socket;
/**
* 目标:实现客户端上传任意类型的文件数据给服务端保存起来
*/
public class Client {
public static void main(String[] args) {
try (
InputStream is = new FileInputStream("E:\\Files\\4099b44ccfabd4a9d772038347d20243.m4a");
){
//1、请求与服务端的socket链接
Socket socket = new Socket("127.0.0.1",8888);
//2、把字节输出流包装成一个数据输出流
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
//3、先发送上传文件的后缀给服务端
dos.writeUTF(".m4a");
//4、把文件数据发送给服务端进行接收
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();
}
}
}
服务端实现代码如下:
package com.jialidun.file;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 目标:服务端开发,可以实现接收客户端的任意类型文件,并保存到服务端磁盘
*/
public class Server {
public static void main(String[] args) {
try {
ServerSocket ss = new ServerSocket(8888);
while (true){
Socket socket = ss.accept();
//交给一个独立的线程来处理与这个客户端的文件通信需求。
new ServerReaderThread(socket).start();
}
}catch (Exception e){
e.printStackTrace();
}
}
}
创建线程:
package com.jialidun.file;
import java.io.DataInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.util.UUID;
public class ServerReaderThread extends Thread{
private Socket socket;
public ServerReaderThread(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("E:\\image\\"
+ UUID.randomUUID().toString()+suffix);
//4、从数据输入流中读取文件数据,写出到字节输出流中去
byte[] buffer = new byte[1024];
int len;
while ((len=dis.read(buffer))>0){
fos.write(buffer,0,len);
}
}catch (Exception e){
e.printStackTrace();
}finally {
//释放资源
if(fos!=null) {
try {
fos.close();
System.out.println("服务端接收文件保存成功!");
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
1.7、BIO模式下的端口转发思想
需求:需要实现一个客户端的消息可以发送给所有的客户端去接收。(群聊实现)
服务端实现:
package com.jialidun.chart;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
/**
* 目标: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){
}
}
}
创建线程:
package com.jialidun.chart;
import java.io.*;
import java.net.Socket;
public class ServerReaderThread extends Thread{
private Socket socket;
public ServerReaderThread(Socket socket){
this.socket = socket;
}
@Override
public void run(){
try {
//1、从socket中去获取当前客户端的输入流
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String msg;
while((msg=br.readLine())!=null){
sendMsgToAllClient(msg);
}
}catch (Exception e){
System.out.println("当前有人下线了!");
//从在线socket集合中移除笨socket
Server.allSocketOnLine.remove(socket);
}
}
/**
* 把当前客户端发来的消息推送给全部在线的socket
* @param msg
*/
private void sendMsgToAllClient(String msg) throws IOException {
for (Socket sk : Server.allSocketOnLine) {
PrintStream ps = new PrintStream(sk.getOutputStream());
ps.println(msg);
ps.flush();
}
}
}