java之BIO简介

Posted 爱上口袋的天空

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java之BIO简介相关的知识,希望对你有一定的参考价值。

一、IO简介

I/O模型:就是用什么样的通道或者说是通信模式和架构进行数据的传输和接收,很大程度上决定了程序通信的性能,Java共支持3种网络编程的I/O模型:BIO、NIO、AIO

实际 通行需求下,要根据不同的业务场景和性能需求决定选择不同的I/O模型


二、IO模型

1、BIO

同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销


2、NIO

同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理


3、AIO

(又称为NIO 2.0)异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理,一般适用于连接数较多且连接时间较长的应用 


三、适用场景分析

1、BlO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序简单易理解。

2、NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等。编程比较复杂,JDK1.4开始支持。

3、AlO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS(OutputStream)参与并发操作,编程比较复杂,JDK7开始支持。


四、BIO 深入剖析 

1.简介

  • Java BIO就是传统的java io 编程,其相关的类和接口在java.io
  • BlO(blocking l/O):同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户连接服务器).

2.BIO工作模式

3.实例分析

 3.1、单发机制、但发单收

创建服务端

package bio;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class Server 
    public static void main(String[] args) 
        try 
            //对服务端端口进行注册 9999
            ServerSocket serverSocket = new ServerSocket(9999);
            //监听客户端socket请求
            Socket socket = serverSocket.accept();
            //从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;

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 
            //创建socket链接
            Socket socket = new Socket("127.0.0.1",9999);
            //从socket对象中获取一个输出流
            OutputStream out = socket.getOutputStream();
            //把字节输出流包装成打印流
            PrintStream printStream = new PrintStream(out);
            printStream.println("hello");
            printStream.flush();

         catch (IOException e) 
            e.printStackTrace();
        
    

小结:

  • 在以上通信中,服务端会一致等待客户端的消息,如果客户端没有进行消息的发送江服务端将一直进入阻塞状态。
  • 同时服务端是按照行获取消息的,这意味着客户端也必须按照行进行消息的发送,否则服务端将进入等待消息的阻塞状态!
  • 可以考虑改为if判断
  • 由于这种机制是端到端的,若连接的Client Socket断了,服务端的Socket也会出现异常机制。

3.2、多发和多收机制

在上一个demo的基础上作出一些修改,使得Client可以持续输入,然后后Server可以持续输出。保持两端之间通道的连接。

创建服务端: 

package bio.persistent;

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("已开启Socket,等待客户端连接中");
            //对服务端端口进行注册 9999
            ServerSocket serverSocket = new ServerSocket(9999);
            //监听客户端socket请求
            Socket socket = serverSocket.accept();
            System.out.println("匹配客户端成功");
            //从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.persistent;

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 
            //创建socket链接
            Socket socket = new Socket("127.0.0.1",9999);
            //从socket对象中获取一个输出流
            OutputStream out = socket.getOutputStream();
            //把字节输出流包装成打印流
            PrintStream printStream = new PrintStream(out);
            Scanner scanner = new Scanner(System.in);
            while (true)
                String msg = scanner.nextLine();
                printStream.println(msg);
                printStream.flush();
            


         catch (IOException e) 
            e.printStackTrace();
        
    


3.3、接收多个客户端

在上述的案例中,一个服务端只能接收一个客户端的通信请求,那么如果服务端需要处理很多个客户端的消息通信请求应该如何处理呢,此时我们就需要在服务端引入线程了,也就是说客户端每发起一个请求,服务端就创建一个新的线程来处理这个客户端的请求,这样就实现了一个客户端一个线程的模型,图解模式如下:

目标:实现服务端可以同时接收多个客户端的Socket通信需求。

思路:服务端每接收到一个客户端socket请求对象之后都交给一个独立的线程来处理客户端的数据交互需求

创建服务端:

package bio.Multi;

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 
            //注册 端口
            ServerSocket serverSocket = new ServerSocket(9999);
            //定义一个死循环,负责不断的去接受Client的Socket请求
            while (true)
                Socket socket = serverSocket.accept();
                new  Thread(()->
                    try 
                        InputStream in = socket.getInputStream();
                        BufferedReader br = new BufferedReader(new InputStreamReader(in));
                        String msg;
                        while ((msg = br.readLine()) != null)
                            System.out.println(Thread.currentThread().getName()+"接收到消息---->"+msg);
                        
                     catch (IOException e) 
                        e.printStackTrace();
                    
                ).start();
            
         catch (IOException e) 
            e.printStackTrace();
        
    

 创建客户端:这里每启动一个Client,Server都会启动一个新的Thread去处理Client的请求

package bio.Multi;

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 
            Socket socket = new Socket("127.0.0.1",9999);
            PrintStream ps = new PrintStream(socket.getOutputStream());
            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();
        

    

小结:

  • 每个Socket接收到,都会创建一个线程,线程的竞争、切换上下文影响性能
  • 每个线程都会占用栈空间和CPU资源;
  • 并不是每个socket都进行IO操作,无意义的线程处理(等待)
  • 客户端的并发访问增加时。服务端将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出,线程创建失败,最终导致进程宕机或者僵死,从而不能对外提供服务。

3.4、伪异步I/O编程

在上述案例中:客户端的并发访问增加时。服务端将呈现1:1的 线程开销,访问量越大,系统将发生线程栈溢出,线程创建失败,最终导致进程宕机或者僵死,从而不能对外提供服务。

接下来我们采用一个伪异步I/O的通信框架,采用线程池和任务队列实现,当客户端接入时,将客户端的Socket封装成一个Task(该任务实现java.lang.Runnable线程任务接口)交给后端的线程池中进行处理。JDK的线程池维护一个消息队列和N个活跃的线程,对消息队列中Socket任务进行处理,由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。

客户端源码分析:
 

package com.kgf.kgfjavalearning2021.io.bio;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;

/***
 * 创建客户端
 */
public class Client01 
   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.把输出流包装成一个打印流 
         PrintWriter pw = new PrintWriter(os);
         // 4.反复接收用户的输入 
         BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
         String line = null ;
         while((line = br.readLine()) != null)
            pw.println(line);
            pw.flush();
         
       catch (Exception e) 
         e.printStackTrace();
      
   

线程池处理类:
 

package com.kgf.kgfjavalearning2021.io.bio;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

// 线程池处理类
public class HandlerSocketThreadPool 
   
   // 线程池 
   private ExecutorService executor;
   
   public HandlerSocketThreadPool(int maxPoolSize, int queueSize)
      
      this.executor = new ThreadPoolExecutor(
            3, // 8
            maxPoolSize,  
            120L, 
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<Runnable>(queueSize) );
   
   
   public void execute(Runnable task)
      this.executor.execute(task);
   

服务端源码分析:

package com.kgf.kgfjavalearning2021.io.bio;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.ServerSocket;
import java.net.Socket;

/***
 * 创建服务端
 * 伪异步io采用了线程池实现,因此避免了为每个请求创建一个独立线程造成线程资源耗尽的问题,但由于底层依然是采用的同步阻塞模型,因此无法从根本上解决问题。
 * 如果单个消息处理的缓慢,或者服务器线程池中的全部线程都被阻塞,那么后续socket的i/o消息都将在队列中排队。新的Socket请求将被拒绝,客户端会发生大量连接超时。
 */
public class Server01 
   public static void main(String[] args) 
      try 
         System.out.println("----------服务端启动成功------------");
         ServerSocket ss = new ServerSocket(9999);

         // 一个服务端只需要对应一个线程池
         HandlerSocketThreadPool handlerSocketThreadPool =
               new HandlerSocketThreadPool(3, 1000);

         // 客户端可能有很多个
         while(true)
            Socket socket = ss.accept() ; // 阻塞式的!
            System.out.println("有人上线了!!");
            // 每次收到一个客户端的socket请求,都需要为这个客户端分配一个
            // 独立的线程 专门负责对这个客户端的通信!!
            handlerSocketThreadPool.execute(new ReaderClientRunnable(socket));
         

       catch (Exception e) 
         e.printStackTrace();
      
   



/***
 * 创建真正执行任务的类
 */
class ReaderClientRunnable implements Runnable

   private Socket socket ;

   public ReaderClientRunnable(Socket socket) 
      this.socket = socket;
   

   @Override
   public void run() 
      try 
         // 读取一行数据
         InputStream is = socket.getInputStream() ;
         // 转成一个缓冲字符流
         Reader fr = new InputStreamReader(is);
         BufferedReader br = new BufferedReader(fr);
         // 一行一行的读取数据
         String line = null;
         while((line = br.readLine())!=null) // 阻塞式的!!
            System.out.println("服务端收到了数据:"+line);
         
       catch (Exception e) 
         System.out.println("有人下线了");
      
   

伪异步io采用了线程池实现,因此避免了为每个请求创建一个独立线程造成线程资源耗尽的问题,但由于底层依然是采用的同步阻塞模型,因此无法从根本上解决问题。如果单个消息处理的缓慢,或者服务器线程池中的全部线程都被阻塞,那么后续socket的i/o消息都将在队列中排队。新的Socket请求将被拒绝,客户端会发生大量连接超时。
 


3.5 基于BIO形式下的文件上传

客户端开发

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("C:\\\\Users\\\\dlei\\\\Desktop\\\\BIO,NIO,AIO\\\\文件\\\\java.png");
        )
            //  1、请求与服务端的Socket链接
            Socket socket = new Socket("127.0.0.1" , 8888);
            //  2、把字节输出流包装成一个数据输出流
            DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
            //  3、先发送上传文件的后缀给服务端
            dos.writeUTF(".png");
            //  4、把文件数据发送给服务端进行接收
            byte[] buffer = new byte[1024];
            int len;
            while((len = is.read(buffer)) > 0 )
                dos.write(buffer , 0 , len);
            
            dos.flush();
            Thread.sleep(10000);
        catch (Exception e)
            e.printStackTrace();
        
    

服务端开发

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();
        
    

import java.io.DataInputStream;
import java.io.FileOutputStream;
import java.io.OutputStream;
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() 
        try
            // 1、得到一个数据输入流读取客户端发送过来的数据
            DataInputStream dis = new DataInputStream(socket.getInputStream());
            // 2、读取客户端发送过来的文件类型
            String suffix = dis.readUTF();
            System.out.println("服务端已经成功接收到了文件类型:" + suffix);
            // 3、定义一个字节输出管道负责把客户端发来的文件数据写出去
            OutputStream os = new FileOutputStream("C:\\\\Users\\\\dlei\\\\Desktop\\\\BIO,NIO,AIO\\\\文件\\\\server\\\\"+
                    UUID.randomUUID().toString()+suffix);
            // 4、从数据输入流中读取文件数据,写出到字节输出流中去
            byte[] buffer = new byte[1024];
            int len;
            while((len = dis.read(buffer)) > 0)
                os.write(buffer,0, len);
            
            os.close();
            System.out.println("服务端接收文件保存成功!");

        catch (Exception e)
            e.printStackTrace();
        
    


3.6 基于BIO模式下即时通信

功能清单简单说明:

1、 客户端登陆功能

        可以启动客户端进行登录,客户端登陆只需要输入用户名和服务端ip地址即可。

2 、在线人数实时更新。
        客户端用户户登陆以后,需要同步更新所有客户端的联系人信息栏。

3、 离线人数更新
        检测到有客户端下线后,需要同步更新所有客户端的联系人信息栏。

4、 群聊
        任意一个客户端的消息,可以推送给当前所有客户端接收。

5 、私聊
        可以选择某个员工,点击私聊按钮,然后发出的消息可以被该客户端单独接收。

6 、@消息
        可以选择某个员工,然后发出的消息可以@该用户,但是其他所有人都能

7 、消息用户和消息时间点
        服务端可以实时记录该用户的消息时间点,然后进行消息的多路转发或者选择。

以上是关于java之BIO简介的主要内容,如果未能解决你的问题,请参考以下文章

IO模型之BIO代码详解及其优化演进

Java网络编程系列之基于BIO的多人聊天室设计与实现

java之socket编程(BIO)

java并发之bio nio aio

java网络编程系列之JavaIO的“前世”:BIO阻塞模型

漫谈Java IO之普通IO流与BIO服务器