java IO模式

1.1 同步,异步,阻塞,非阻塞

首先了解一下同步,异步,阻塞,非阻塞

同步和异步关注的是**消息通信机制**

  • 同步
    同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。
  • 异步
    异步则是相反,*调用*在发出之后,这个调用就直接返回了,所以没有返回结果

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.

  • 阻塞调用
    是指调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果后才会返回
  • 非阻塞调用
    在不能得到结果之前,该调用不会阻塞当前线程

1.2 三种 io模式

  • java bio

同步阻塞型,服务器实现为一个连接一个线程,客户端有连接请求时服务器端需要启动一个线程进行处理,连接不做任何事情会造成不必要的线程开销

java 非阻塞启动线程 java阻塞io和非阻塞io区别_java 非阻塞启动线程

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

java 非阻塞启动线程 java阻塞io和非阻塞io区别_服务端_02

  • java aio
    nio(2.0) :异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的i/o请求都是由os先完成了在通知服务器应用去启动线程进行处理,一般适用于连接数较多且连接时间较长的应用。

适用场景

  • BIO 方式适用于连接数较小且固定的架构
  • NIO 方式适用于连接数较多且连接比较短的架构,如 聊天服务器,弹幕系统,服务器间通讯
  • AIO 方式适用于连接数较多且连接比较长 如相册服务器,充分调用os 参与并发操作

2 JAVA BIO

  • java bio 是传统 java io 线程

java 非阻塞启动线程 java阻塞io和非阻塞io区别_客户端_03

服务端逻辑

*/
public class Server {
    public static void main(String[] args) {
        try{
            //1 定义一个server socket 对象进行服务端的端口注册
            ServerSocket ss = new ServerSocket(9999);
            //2 监听客户端socket请求
            Socket socket = ss.accept();
            //3 获取字节输入流
            InputStream inputStream = socket.getInputStream();
            // 包装成缓冲字符输入流
            BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
            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) throws IOException {
        //1创建 socket 对象请求服务端连接
        Socket socket = new Socket("127.0.0.1",9999);
        //2 从SOcket 对象中获取一个字节输出流
        OutputStream os = socket.getOutputStream();
        // 3 把字节输出流包装成一个打印流
        PrintStream ps = new PrintStream(os);
        ps.println("hello 服务端 ");
        ps.flush();
    }
}

客户端挂掉,服务端也会挂

  • 在上面通信中,服务端会一直等待客户端的消息,如果客户端没有进行消息的发送,服务端将一直进入阻塞状态
  • 同时服务端是按照行获取消息的,这意味着客户端必须按照行进行消息的发送,否则客户端将进入等待消息

2.1 BIO 传统多发和多收

客户端,服务端 多发多收模式

/**
 * 客户端 多发模式
 */
public class Client {
    public static void main(String[] args) throws IOException {
        //1创建 socket 对象请求服务端连接
        Socket socket = new Socket("127.0.0.1",9999);
        //2 从SOcket 对象中获取一个字节输出流
        OutputStream os = socket.getOutputStream();
        // 3 把字节输出流包装成一个打印流
        PrintStream ps = new PrintStream(os);
        Scanner  sc = new Scanner(System.in);
        while (true){
            String msg = sc.nextLine();
            ps.println(msg);
            ps.flush();
        }

    }
}

在这里服务端只能接受一个客户端,是因为服务端只有一个线程,只能接受一个客户端

2.2 BIO 服务端接受多个客户端

服务端

/**
 * 同时接受多个客户端socket通信请求
 * 接受一个客户但socket请求对象后,交给一个独立
 */
        ServerSocket ss;
        {
            try {
                // 注册端口
                ss = new ServerSocket(9999);
                while(true){
                    Socket socket =ss.accept();
                    // 创建一个独立的线程来处理与客户端连接的请求
                    new ServerThreadReader(socket).start();
                }

socket独立线程

public class ServerThreadReader extends  Thread{

    private Socket socket;

    public  ServerThreadReader(Socket socket){
        this.socket =socket;
    }

    @Override
    public void run() {
        try {
            InputStream is =socket.getInputStream();
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            String msg=null;
            while((msg=br.readLine())!=null){
                System.out.println(msg);
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

总结

  • 每个socket 接收到,都会创建一个线程,线程的竞争,切换上下文影响性能
  • 每个线程都会占用栈空间和cpu资源
  • 客户端的并发访问增加时,服务端将呈现1:1的线程开销

3 伪异步io 编程

由于客户端的并发访问增加时。服务端将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出。

我们可以采用伪异步i/o通信框架,采用线程池和任务队列实现,当客户端接入时,将客户端的socket封装成一个task,交给后端的线程池中进行处理。jdk线程池维护一个消息队列和N个活跃的线程,对消息对列中socket任务进行处理。

java 非阻塞启动线程 java阻塞io和非阻塞io区别_服务端_04

服务端设计 初始化socket 线程池对象,最后封装成一个任务对象交给线程处理

public static void main(String[] args) {
        try {
            ServerSocket ss = new ServerSocket(9999);
            //定义循环来接受客户端的socket编程请求
            //初始化一个线程池对象
            HandlerSocketServerPool socketServerPool = new HandlerSocketServerPool(10,6);
            while (true){
                Socket socket =ss.accept();
              // 把socket对象交给一个线程池进行处理
                //把socket封装成一个任务对象交给线程处理
                Runnable target = new ServerRunnableTarget(socket);
                socketServerPool.execute(target);

            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

线程池对象:用于创建线程池

//1 创建一个线程池的成员变量用于存储线程池对象
    private ExecutorService executorService;

    /**
     *
     * @param maxThreadNum 最大线程数量
     * @param queueSize    任务数量
     */
    public HandlerSocketServerPool(int maxThreadNum,int queueSize){
        executorService = new ThreadPoolExecutor(3,maxThreadNum,120, TimeUnit.SECONDS,new ArrayBlockingQueue<Runnable>(queueSize));
    }
    /**
     * 提供一个方法来提交任务给线程池的任务队列来暂存,等待线程池来处理
     */
    public void execute(Runnable target){
        executorService.execute(target);

    }

任务对象:将socket封装成任务 runable

public class ServerRunnableTarget implements  Runnable {
    private Socket socket;

    public ServerRunnableTarget(Socket socket) {
        this.socket = socket;
    }


    @Override
    public void run() {
    // 处理接受到的socket
        //3 获取字节输入流
        InputStream inputStream = null;
        try {
            inputStream = socket.getInputStream();
            // 包装成缓冲字符输入流
            BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
            String msg ;
            while((msg= br.readLine())!=null){
                System.out.println("服务器接受到:" + msg);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}
  • 伪异步io采用了线程池实现,避免了为每个请求创建一个独立线程造成线程资源耗尽的问题,由于底层依然采用的是同步阻塞,无法解决问题
  • 如果线程池都被阻塞,那么socket中的io消息将在队列中排队,客户端会发生大量连接超时

2.4 基于BIO实现文件上传

客户端实现将本地文件传送到socket中

public static void main(String[] args) {
        try{
            //请求于服务端的socket连接
           Socket socket = new Socket("127.0.0.1",8888);
           //将字节输出流包装成一个数据输出流
            DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());
            //上传后缀
            dataOutputStream.writeUTF(".png");
            //把文件数据发送给服务端进行接收
            InputStream is = new FileInputStream("F:\\360MoveData\\Users\\DELL\\Desktop\\下载.png");
            byte[] bs = new byte[1024];
            int len =0;
            while((len= is.read(bs))>0){
                dataOutputStream.write(bs,0,len);
            }
            dataOutputStream.flush();
            is.close();
            socket.shutdownOutput();//通知服务器这边数据发送完毕

        } catch (UnknownHostException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

服务端监听socket,并将其封装成线程

public static void main(String[] args) {
        try{
            ServerSocket ss = new ServerSocket(8888);
             while(true){
                 Socket socket =ss.accept();
                new ServerReaderThread(socket).start();
             }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

服务端线程的封装

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();
            // 定义一个字节输出管道负责把客户端发来的文件数据写出去
            OutputStream os = new FileOutputStream("F:\\360MoveData\\Users\\DELL\\Desktop\\server\\"+ UUID.randomUUID().toString()+suffix);

            byte[] bytes = new byte[1024];
            int len=0;
            while((len=dis.read(bytes))>0){
                 os.write(bytes,0,len);
            }
            os.flush();
            os.close();


        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

2.5 java BIO 模式下的端口转发思想

实现一个客户端发送的消息可以发送给所有客户端

java 非阻塞启动线程 java阻塞io和非阻塞io区别_客户端_05

3 JAVA NIO

  • NIO 有三大核心部分:channel(通道),buffer(缓冲区),selector (选择器)
  • java NIO 非阻塞模式,使一个线程从某通道发送请求或读取数据 ,仅能得到目前可用的数据,没有数据可用,就不会获取。

3.1 NIO 与 BIO的比较

  • BIO 以流的方式处理数据,NIO以块的方式处理数据
  • BIO 是阻塞的,NIO是非阻塞的
  • BIO基于字节流和字符流进行操作,NIO基于Channel和Buffer进行操作,数据是从通道读取到缓冲区中,或者从缓冲区写入到通道中 Selector(选择器)用于监听多个通道事件,使用单个线程就可以监听多个客户端通道。

NIO

BIO

面向缓冲区(Buffer)

面向流(Stream)

非阻塞

阻塞

选择器 selectors

缓冲区

本质是一块可以写入数据,可以从中读取数据的内存,这块内存被包装成NIO BUFFER 对象

Channel

java NIO 的通道类似流,既可以从通道中读取数据,也可以写数据到通道,但流的读写是单向的,通道可以非阻塞读取和写入通道,通道支持读取或写入缓冲区,支持异步地读写

Selector选择器

selector 可以检查一个或多个NIO通道,确定哪些通道准备好进行读取或写入,一个单独线程可以管理多个Channel,从而管理多个网络连接,提高效率

java 非阻塞启动线程 java阻塞io和非阻塞io区别_java 非阻塞启动线程_06

通道表示打开到IO设备 如文件,套接字,负责传输,Buffer 负责存取数据

3.1 缓冲区

用于特定基本数据类型的容器,是Buffer抽象类的子类,Buffer 主要用于与NIO通道进行交互

java 非阻塞启动线程 java阻塞io和非阻塞io区别_客户端_07

Buffer类

像一个数组,保存多个相同类型的数据

如 ByteBuffer,CharBuffer,ShortBuffer,IntBuffer,LongBuffer,FloatBuffer

Buffer中具有以下的概念

  • 容量:buffer的固定大小,创建后不能更改
  • 限制:表示缓冲区中可以操作数据的大小,不能为负,且不能大于其容量
  • 位置:下一个要读取或写入的数据
  • 标记:一个索引可以通过mark方法回到位置

使用Buffer读写数据一般遵循以下四个步骤:

  • 1.写入数据到Buffer
  • 2.调用flip()方法,转换为读取模式
  • 3.从Buffer中读取数据
  • 4.调用buffer.clear()方法或者buffer.compact()方法清除缓冲区

直接内存与非直接内存

  • 对于直接内存,jvm将会在IO操作上具有更高的性能,因为直接作用与本地系统的IO操作,非直接内存,会先从本进程复制到直接内存,在利用本地IO处理
本地IO------>直接内存------>非直接内存--------->直接内存-------->本地IO

而直接内存是

本地IO------>直接内存------>本地Io

直接内存使用allocateDirect创建,但是需要耗费更高的性能

使用场景

  • 有很大数据需要存储
  • 适合频繁的IO操作

3.2 通道(Channel)

Channel表示IO源与目标打开的连接,channel类似于传统的流,不过其本身不能直接访问数据,Channel 只能与BUffer进行交互.

通道特点:

  • 通道可以同时进行读写,而流只能读或写
  • 通道可以实现异步读写数据

常用的Channel实现类

  • FileChannel:用于读取,写入,映射和操作文件的通道
  • DatagramChannel:通过UDP读写网络中的数据通道
  • SocketChannel:通过TCP 读写网络中的数据
  • ServerSocketChannel 可以监听新进来的TCP连接,对每一个新进来的连接都会创建一个SocketChannel.

获取通道的一种方式是对支持通道的对象调用getChannel() 方法

int read(ByteBuffer dst) 从 从  Channel 到 中读取数据到  ByteBuffer
long  read(ByteBuffer[] dsts) 将 将  Channel 到 中的数据“分散”到  ByteBuffer[]
int  write(ByteBuffer src) 将 将  ByteBuffer 到 中的数据写入到  Channel
long write(ByteBuffer[] srcs) 将 将  ByteBuffer[] 到 中的数据“聚集”到  Channel
long position() 返回此通道的文件位置
FileChannel position(long p) 设置此通道的文件位置
long size() 返回此通道的文件的当前大小
FileChannel truncate(long s) 将此通道的文件截取为给定大小
void force(boolean metaData) 强制将所有对此通道的文件更新写入到存储设备中

3.3 通道写出文件

public void write(){
        try {
            // 字节输出流通向目标文件
            FileOutputStream fos = new FileOutputStream("F:\\360MoveData\\Users\\DELL\\Desktop\\server\\新建 文本文档.txt");
           //2 得到字节输出流对应的通道 channel
            FileChannel channel = fos.getChannel();
            //3 分配缓冲区
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            buffer.put("hello".getBytes());
            //4把缓冲区切换成可读模式
            buffer.flip();
            channel.write(buffer);
            channel.close();
            System.out.println("数据写出文件");

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }


    }

3.4 通道读取文件

public void read() throws IOException {
      //定义一个文件输入流与源文件接同
      FileInputStream fis = new FileInputStream("F:\\360MoveData\\Users\\DELL\\Desktop\\server\\新建 文本文档.txt");
      //需要得到文件字节输入流的文件通道
      FileChannel channel = fis.getChannel();
      //定义一个缓冲区
      ByteBuffer buffer = ByteBuffer.allocate(1024);
      //写入数据到缓冲区
      channel.read(buffer);
      
      buffer.flip();
      String rs = new String(buffer.array(),0,buffer.remaining());
      System.out.println(rs);
  }

3.5 复制文件逻辑

public void copy() throws IOException {
       // 定义文件位置
        File srcFile = new File("F:\\360MoveData\\Users\\DELL\\Desktop\\下载.png");
        File copyFile = new File("F:\\360MoveData\\Users\\DELL\\Desktop\\下载_copy.png");
       //获取文件输入流
        FileInputStream fis = new FileInputStream(srcFile);
        //获取文件输出流
        FileOutputStream fos = new FileOutputStream(copyFile);
        //获取文件通道
        FileChannel fileChannel = fis.getChannel();
        FileChannel  copyChannel =  fos.getChannel();
        // 分配缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);


        while(true){
           // 必需先清空缓冲区
            buffer.clear();
            //写入数据到缓冲区
           int len= fileChannel.read(buffer);
           if(len<0){
             break;
           }
           //切换模式为可读模式
            buffer.flip();
            copyChannel.write(buffer);
        }
           fileChannel.close();
           copyChannel.close();
           System.out.println("复制文件成功!");

   }

也可以使用transferFom或transferTo进行复制文件

@Test
    public void copy_trans() throws IOException {
       // 定义源文件位置
       FileInputStream srcFile = new FileInputStream("F:\\360MoveData\\Users\\DELL\\Desktop\\下载.png");
       FileChannel channel=srcFile.getChannel();
       FileOutputStream copyFile = new FileOutputStream("F:\\360MoveData\\Users\\DELL\\Desktop\\下载_copy.png");
       FileChannel copyChannel = copyFile.getChannel();
        //复制文件
       // copyChannel.transferFrom(channel,channel.position(),channel.size());
        channel.transferTo(channel.position(),channel.size(),copyChannel);
   
       // 关闭原通道
       channel.close();
       //关闭现通道
       copyChannel.close();;




   }

4 选择器

选择器是selectableChannel对象的多路复用器,slelector可以同时监控多个selectableChannel的io状况,select可以使单独线程管理多个Channel

java 非阻塞启动线程 java阻塞io和非阻塞io区别_数据_08

  • java NIO ,用非阻塞的IO方式,使用一个线程,处理多个客户端连接,就会使用选择器
  • selector 可以检测到多个注册的通道上是否有事件发生(多个channel以事件的方式可以注册到同一个selector),如果有时间发生,便获取事件然后针对每个事件进行相应的处理
  • 只有在来连接真正有读写事件发生时,才会进行读写

服务器流程

  • 当客户端连接服务端时,服务端通过serverSocketChannel得到SocketChannel
public class Server {
    public static void main(String[] args) throws IOException {
        //1 获取通道
        ServerSocketChannel ssChannel = ServerSocketChannel.open();
        //2 切换为非阻塞模式
        ssChannel.configureBlocking(false);
        //3 绑定连接的端口
         ssChannel.bind(new InetSocketAddress(9999));

         //4 获取选择器Selector
        Selector selector =Selector.open();
        //5 将通道注册到选择器上 ,并开始指定接受事件
        ssChannel.register(selector, SelectionKey.OP_ACCEPT);
        //6 使用selector 选择器轮询已经就绪好的事件
        while(selector.select()>0){
            //7 获取选择器中所有注册通道中已经就绪好的事件
            Iterator<SelectionKey> it =  selector.selectedKeys().iterator();
            //8 开始便利就绪好的事件
            while(it.hasNext()){
                //提取当前这个事件
                SelectionKey sk  = it.next();
                //判断这个事件是什么
                if(sk.isAcceptable()){
                // 直接获取当前接入的客户端通道
                    SocketChannel socketChannel = ssChannel.accept();
                    // 切换为非阻塞模式
                    socketChannel.configureBlocking(false);
                    //注册到选择器中
                    socketChannel.register(selector,SelectionKey.OP_READ);
                }else if(sk.isReadable()){
                    // 获取当前选择器上的读选择器就绪事件
                    SocketChannel socketChannel = (SocketChannel) sk.channel();
                    //读取数据
                    ByteBuffer buf = ByteBuffer.allocate(1024);
                    int len=0;
                    while((len=socketChannel.read(buf))>0){
                        buf.flip();
                        System.out.println(new String(buf.array(),0,len));
                        buf.clear();
                    }
                }
                it.remove();//移除当前事件
            }
        }

    }
}

客户端

public class Client {
    public static void main(String[] args) throws IOException {
        //1 获取通道
        SocketChannel socketChannel =SocketChannel.open();
        //切换成非阻塞模式
        socketChannel.configureBlocking(false);
        //分配缓冲区大小
        ByteBuffer buffer =ByteBuffer.allocate(1024);
        Scanner sc = new Scanner(System.in);
        while(true){
           String msg = sc.nextLine();
            buffer.put(("你好"+msg).getBytes());
            buffer.flip();
            socketChannel.write(buffer);
            buffer.clear();
        }
    }
}