JAVA BIO深入剖析
1. Java BIO 基本介绍
- Java BIO 就是传统的 java io 编程,其相关的类和接口在 java.io
- BIO(blocking I/O) : 同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需
要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户连接服务器).
2. Java BIO 工作机制
对 BIO 编程流程的梳理
- 服务器端启动一个 ServerSocket,注册端口,调用accpet方法监听客户端的Socket连接。
- 客户端启动 Socket 对服务器进行通信,默认情况下服务器端需要对每个客户 建立一个线程与之通讯
3. 传统BIO编程
网络编程的基本模型是Client/Server模型,也就是两个进程之间进行相互通信,其中服务端提供位置信(绑定IP地址和端口),客户端通过连接操作向服务端监听的端口地址发起连接请求,基于TCP协议下进行三次握手连接,连接成功后,双方通过网络套接字(Socket)进行通信。
传统的同步阻塞模型开发中,服务端ServerSocket负责绑定IP地址,启动监听端口;客户端Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。
基于BIO模式下的通信,客户端 - 服务端是完全同步,完全耦合的。
/**
目标: Socket网络编程。
Java提供了一个包:java.net下的类都是用于网络通信。
Java提供了基于套接字(端口)Socket的网络通信模式,我们基于这种模式就可以直接实现TCP通信。
只要用Socket通信,那么就是基于TCP可靠传输通信。
功能1:客户端发送一个消息,服务端接口一个消息,通信结束!!
创建客户端对象:
(1)创建一个Socket的通信管道,请求与服务端的端口连接。
(2)从Socket管道中得到一个字节输出流。
(3)把字节流改装成自己需要的流进行数据的发送
创建服务端对象:
(1)注册端口
(2)开始等待接收客户端的连接,得到一个端到端的Socket管道
(3)从Socket管道中得到一个字节输入流。
(4)把字节输入流包装成自己需要的流进行数据的读取。
Socket的使用:
构造器:public Socket(String host, int port)
方法: public OutputStream getOutputStream():获取字节输出流
public InputStream getInputStream() :获取字节输入流
ServerSocket的使用:
构造器:public ServerSocket(int port)
小结:
通信是很严格的,对方怎么发你就怎么收,对方发多少你就只能收多少!!
*/
public class Server {
public static void main(String[] args){
System.out.println("服务端 启动 !!!");
try {
//1.定义一个ServerSocket对象,进行服务端的端口注册
ServerSocket serverSocket = new ServerSocket(9999);
//2.监听客户端的连接请求
Socket socket = serverSocket.accept();
//3.从socket管道中得到字节数入流对象
InputStream is = socket.getInputStream();
//4.把字节输入流包装成字符输入流,再包装成缓冲字符输入流
BufferedReader br = new BufferedReader(new InputStreamReader(is));
//5.打印接收数据
String msg;
while ((msg = br.readLine()) != null){
System.out.println("服务端接收到:"+msg);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
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 ps = new PrintStream(os);
//4.向服务端发数据
ps.print("hello! i am from client !"); // 没有换行符,客户端执行完socket断掉,服务端还没读取到一行,发现客户端socket断了只能抛出异常了。
//ps.println("hello! i am from client !"); // 有换行符,客户端发送完socket断掉,服务器读取到一行数据,输出完正常断掉socket。
ps.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
小结
- 在以上通信中,服务端会一致等待客户端的消息,如果客户端没有进行消息的发送,服务端将一直进入阻塞状态。
- 同时服务端是按照行获取消息的,这意味着客户端也必须按照行进行消息的发送,否则服务端将进入等待消息的阻塞状态!
4. BIO模式下的多发和多收消息
/**
* 目标:
* 服务端可以反复接收消息
* 客户端可以反复发送消息
*/
public class Server {
public static void main(String[] args){
System.out.println("服务端 启动 !!!");
try {
//1.定义一个ServerSocket对象,进行服务端的端口注册
ServerSocket serverSocket = new ServerSocket(9999);
//2.监听客户端的连接请求
Socket socket = serverSocket.accept();
//3.从socket管道中得到字节数入流对象
InputStream is = socket.getInputStream();
//4.把字节输入流包装成字符输入流,再包装成缓冲字符输入流
BufferedReader br = new BufferedReader(new InputStreamReader(is));
//5.打印接收数据
String msg;
while ((msg = br.readLine()) != null){
System.out.println("服务端接收到:"+msg);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
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 ps = new PrintStream(os);
//4.向服务端发数据
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();
}
}
}
结果:
5. BIO模式下接收多个客户端
在上述的案例中,一个服务端只能接收一个客户端的通信请求,那么如果服务端需要处理很多个客户端的消息通信请求应该如何处理呢,此时我们就需要在服务端引入线程了,也就是说客户端每发起一个请求,服务端就创建一个新的线程来处理这个客户端的请求,这样就实现了一个客户端一个线程的模型,图解模式如下:
/**
* 目标:服务端可以同时接收多个客户端的socket通信连接
* 思路:服务端每接收到一个客户端socket连接请求对象之后交给一个独立的线程去处理数据交互
*/
public class Server {
public static void main(String[] args){
System.out.println("服务端 启动 !!!");
try {
//1.端口注册
ServerSocket serverSocket = new ServerSocket(9999);
//2.不断接收客户端连接请求
while (true){
Socket socket = serverSocket.accept();
new ServerThreadReader(socket).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class ServerThreadReader extends Thread{
private Socket socket;
public ServerThreadReader(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
//3.从socket管道中得到字节数入流对象
InputStream is = socket.getInputStream();
//4.把字节输入流包装成字符输入流,再包装成缓冲字符输入流
BufferedReader br = new BufferedReader(new InputStreamReader(is));
//5.打印接收数据
String msg;
while ((msg = br.readLine()) != null){
System.out.println("服务端接收到:"+msg);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端同上
小结
- 1.每个Socket接收到,都会创建一个线程,线程的竞争、切换上下文影响性能;
- 2.每个线程都会占用栈空间和CPU资源;
- 3.并不是每个socket都进行IO操作,无意义的线程处理;
- 4.客户端的并发访问增加时。服务端将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出,线程创建失败,最终导致进程宕机或者僵死,从而不能对外提供服务。
6. 伪异步I/O编程
在上述案例中:客户端的并发访问增加时。服务端将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出,线程创建失败,最终导致进程宕机或者僵死,从而不能对外提供服务。
接下来我们采用一个伪异步I/O的通信框架,采用线程池和任务队列实现,当客户端接入时,将客户端的Socket封装成一个Task(该任务实现java.lang.Runnable线程任务接口)交给后端的线程池中进行处理。JDK的线程池维护一个消息队列和N个活跃的线程,对消息队列中Socket任务进行处理,由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。
图示如下:
/**
* 目标:服务端可以同时接收多个客户端的socket通信连接
* 思路:服务端每接收到一个客户端socket连接请求对象之后交给一个独立的线程去处理数据交互
*/
public class Server {
public static void main(String[] args){
System.out.println("服务端 启动 !!!");
try {
//端口注册
ServerSocket serverSocket = new ServerSocket(9999);
//初始化线程池对象
HandlerSocketServerPool pool = new HandlerSocketServerPool(5,10);
while (true){
//接收客户端连接请求
Socket socket = serverSocket.accept();
//把socket封装成任务对象
Runnable target = new ServerRunnableTarget(socket);
//把任务交给线程池执行
pool.execute(target);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class HandlerSocketServerPool {
//线程池对象
private ExecutorService executorService;
/**
* 初始化线程池对象
* int corePoolSize,
* int maximumPoolSize,
* long keepAliveTime,
* TimeUnit unit,
* BlockingQueue<Runnable> workQueue)
*/
public HandlerSocketServerPool(int maximumPoolSize, int queueSize){
executorService = new ThreadPoolExecutor(3, maximumPoolSize,
120, TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(queueSize));
}
//提交任务给队列暂存,等线程池来处理
public void execute(Runnable target){
executorService.execute(target);
}
}
public class ServerRunnableTarget implements Runnable{
private Socket socket;
public ServerRunnableTarget(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();
}
}
}
客户端同上
由于核心线程数是3,所以当第四个客户端连接进去并发送消息的时候,服务器并不能开启新线程处理,而是将它放到队列中;当之前的客户端连接断开之后,就会按队列中的先后顺序去处理客户端连接。
小结
- 伪异步io采用了线程池实现,因此避免了为每个请求创建一个独立线程造成线程资源耗尽的问题,但由于底层依然是采用的同步阻塞模型,因此无法从根本上解决问题。
- 如果单个消息处理的缓慢,或者服务器线程池中的全部线程都被阻塞,那么后续socket的i/o消息都将在队列中排队。新的Socket请求将被拒绝,客户端会发生大量连接超时。
7. 基于BIO的文件上传
/**
* 客户端:上传任意类型的文件
*/
public class Client {
public static void main(String[] args) {
try {
//1.创建socket对象请求服务器的连接
Socket socket = new Socket("127.0.0.1",9999);
//2.从socket中获取字节输出流,把字节输出流包装成数据输出流
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
//3.把上传文件的后缀给服务器
dos.writeUTF(".png");
//4.把文件数据发送给服务器
InputStream is = new FileInputStream("/Users/lilianyun/IdeaProjects/IOTest2/src/com/BIO/demo05/clientFile/music.png");
//5.向服务端发数据
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) > 0){
dos.write(buffer,0,len);
}
dos.flush();
dos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 服务端:接收任意类型的数据并保存到磁盘
*/
public class Server {
public static void main(String[] args){
System.out.println("服务端 启动 !!!");
try {
//1.端口注册
ServerSocket serverSocket = new ServerSocket(9999);
//2.不断接收客户端连接请求
while (true){
Socket socket = serverSocket.accept();
//3.交给独立线程处理与客户端的文件通信
new ServerThreadReader(socket).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class ServerThreadReader extends Thread{
private Socket socket;
public ServerThreadReader(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
//4.从socket管道中得到字节数入流对象,包装成数据输入流
DataInputStream dis = new DataInputStream(socket.getInputStream());
//5.读取客户端发来的文件类型
String suffix = dis.readUTF();
//6.定义字节输出管道,将客户端发来的文件写出去
OutputStream os = new FileOutputStream("/Users/lilianyun/IdeaProjects/IOTest2/src/com/BIO/demo05/serverFile/"+ UUID.randomUUID().toString()+suffix);
//7.从数据输入流中读取数据,写到字节输出流中去
byte[] buffer = new byte[1024];
int len;
while((len = dis.read(buffer)) > 0){
os.write(buffer,0,len);
}
os.close();
System.out.println("文件保存成功");
} catch (IOException e) {
e.printStackTrace();
}
}
}
小结
- 客户端怎么发,服务端就怎么接收
8. Java BIO模式下的端口转发
需求:一个客户端发送的消息可以发送给所有客户端接收(群聊思想)
9. 基于BIO模式下的即时通信
基于BIO模式下的即时通信,我们需要解决客户端到客户端的通信,也就是需要实现客户端与客户端的端口消息转发逻辑。
功能清单简单说明
-
客户端登陆功能
可以启动客户端进行登录,客户端登陆只需要输入用户名和服务端ip地址即可。 -
在线人数实时更新
客户端用户户登陆以后,需要同步更新所有客户端的联系人信息栏。 -
离线人数更新
检测到有客户端下线后,需要同步更新所有客户端的联系人信息栏。 -
群聊
任意一个客户端的消息,可以推送给当前所有客户端接收。 -
私聊
可以选择某个员工,点击私聊按钮,然后发出的消息可以被该客户端单独接收。 -
@消息
可以选择某个员工,然后发出的消息可以@该用户,但是其他所有人都能 -
消息用户和消息时间点
服务端可以实时记录该用户的消息时间点,然后进行消息的多路转发或者选择。