javaBIO

Posted 是馄饨呀

tags:

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

馄饨高兴的去面试,最近看了ThreadLocal的原理,知道内存泄漏是怎么产生的,心想,问到这里的时候又能说出一二了。
面试官:那我就先考考你基础吧,IO听过吗?
我心想:不会吧,IO这里我就用过文件上传,心想面试官是不是要抓住IO这好好考。
我:了解过
面试官:那你说说BIO、NIO、AIO有什么区别吧。
我:BIO为同步阻塞、NIO为同步非阻塞、AIO为异步非阻塞
面试官:恩恩,那你对它们了解多少?
我:我就知道个概念。
面试官:恩恩,好,回去等通知吧。
凉凉月色为你等待通知 ~

1 BIO

1.1 定义

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

1.2 使用场景

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

1.3 工作机制


客户端
1.创建socket对象通通过ip和端口向服务端建立连接
2.获取IO流数据
3.发送数据
服务端
1.创建Socket对象进行服务端端口注册
2.监听客户端的Socket链接请求
3.获取IO数据
4.输出数据

2 java实现简单数据传输

我们通过最简单的Java代码实现客户端服务端的数据发送,我们创建Server服务端类和Client客户端类。

import java.io.*;
import java.net.Socket;
public class Client {

    public static void main(String[] args) {
        System.out.println("客户端开始创建连接");
        try {
            //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 word 服务端你好");
            ps.flush();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
public class Server {
    public static void main(String[] args) {
        System.out.println("服务端启动");
        try {
            //1.创建ServerSocket的实例进行服务端的端口注册
            ServerSocket ss = new ServerSocket(9999);
            //2.监听客户端的Socket链接请求
            Socket socket = ss.accept();
            //3.从Socket管道中得到一个字节输入流对象
            InputStream inputStream = socket.getInputStream();
            //4.把字节输入流包装成一个缓冲字符输入流
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
            String str;
            if((str = bufferedReader.readLine()) != null) {
                System.out.println("服务端接收到数据" + str);

            }

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

这里我们发现客户端发送的时候发送的是一行字符,如果我服务端if((str = bufferedReader.readLine()) != null)ifwhile,那么服务端会一直等待客户端发送的数据,但是客户端发送完一条数据就结束了,这时候服务端在等待时候发现客户端已经结束,那么服务端就会报错,去进行重试。

服务端启动
服务端接收到数据hello word 服务端你好
java.net.SocketException: Connection reset

这个只适用于发送一行数据

2.1 BIO下的多发和多收机制

客户端可以反复的发送数据,服务端可以反复的接收数据

我们发现在server层中只需要用while循环来一直接收客户端的数据就可以了,那么我们改进客户端,将客户端的数据改为可以多次发送

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;

/**
 * @ClassName Client
 * @Description
 * @Author asus
 * @Date 2021/8/3 9:04
 * @Version 1.0
 **/
public class Client {

    public static void main(String[] args) {
        System.out.println("客户端开始创建连接");
        try {
            //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();
                System.out.print("发送数据.....");
                ps.println(msg);
                ps.flush();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * @ClassName Server
 * @Description
 * @Author asus
 * @Date 2021/8/3 9:04
 * @Version 1.0
 **/
public class Server {
    public static void main(String[] args) {
        System.out.println("服务端启动");
        try {
            //1.创建ServerSocket的实例进行服务端的端口注册
            ServerSocket ss = new ServerSocket(9999);
            //2.监听客户端的Socket链接请求
            Socket socket = ss.accept();
            //3.从Socket管道中得到一个字节输入流对象
            InputStream inputStream = socket.getInputStream();
            //4.把字节输入流包装成一个缓冲字符输入流
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
            String str;
            while ((str = bufferedReader.readLine()) != null) {
                System.out.println("服务端接收到数据" + str);

            }

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

我们可以同时开两个客户端,然后向服务端进行发送。
先用客户端一发送



很明显我们发现服务端只能接收一个客户端的请求,第二个不能,这个是因为服务端在用Socket socket = ss.accept();接受连接后,就会跳转到while ((str = bufferedReader.readLine()) != null)来一直接收客户端一的请求,使得在客户端二启动后,根本执行不到Socket socket = ss.accept();这一步,当然服务端也就接收不到客户端二发送的请求了。

服务端只能处理一个客户端的请求,因为服务端是单线程。一次只能与一个客户端进行消息通信。

2.2 BIO模式下接受多个客户端

我们在上面发现,我一个服务端只能允许一个客户端去发送请求,那么怎么实现服务端接收多个客户端的请求呢,我们可以创建多个线程来进行客户端的接收。

客户端

package bio_three;

import java.io.IOException;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;

/**
 * @ClassName Client
 * @Description
 * @Author asus
 * @Date 2021/8/3 20:22
 * @Version 1.0
 **/
public class Client {
    public static void main(String[] args) {
        try {
            //1.对服务端进行ip认证
            Socket socket = new Socket("127.0.0.1",9999);
            //2.得到一个打印流
            PrintStream ps = new PrintStream(socket.getOutputStream());
            //3.利用死循环不断的发送消息给客户端
            Scanner scanner = new Scanner(System.in);
            while (true){
                System.out.print("请说:");
                String msg = scanner.nextLine();
                ps.println(msg);
                ps.flush();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

创建服务端的接收客户端数据的类,继承自线程类

package bio_three;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Socket;

/**
 * @ClassName ServerThreadReader
 * @Description
 * @Author asus
 * @Date 2021/8/3 20:05
 * @Version 1.0
 **/
public class ServerThreadReader extends Thread{
    private Socket socket;

    public ServerThreadReader(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();
        }
    }
}

服务端

package bio_three;

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

/**
 * @ClassName Server
 * @Description
 * @Author asus
 * @Date 2021/8/3 20:02
 * @Version 1.0
 **/
public class Server {
    public static void main(String[] args) {
        try {
            //1.注册端口
            ServerSocket serverSocket = new ServerSocket(9999);
            //2.定义一个死循环,负责不断的接收客户端的请求
            while (true){
                Socket socket = serverSocket.accept();
                //创建一个独立的线程来处理与这个客户端的socket
                new ServerThreadReader(socket).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}




我们会发现,在多个客户端发送的时候,服务端通过创建多个线程,让线程管理来实现一个服务对应多个客户端进行数据的接收。

2.3 BIO总结

虽然我们实现了服务端接收多个客户端发送的请求,但是也存在一些问题。

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

3 伪异步I/O编程

我们在BIO模式中发现,每次需要一个客户端请求,那么服务端这里就会创建一个线程,但是线程过多的情况下,服务端将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出,线程创建失败,最终导致进程宕机或者僵死,从而不能对外提供服务。

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

但是,因为线程池是限制线程数量的,所以也导致客户端连接的数量是有限的,在线程满的情况下,只有等到一个客户端用完释放的时候,其他客户端才能在对服务端进行请求。

我们使用线程池的方式来处理客户端的请求,我们将服务端的socket对象封装成一个任务对象,交给线程池来进行任务的处理。

3.1 服务端类

首先我们创建服务端类

package bio_four;

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

/**
 * @ClassName Server
 * @Description
 * @Author asus
 * @Date 2021/8/3 21:28
 * @Version 1.0
 **/
public class Server {
    public static void main(String[] args) {
        try {
            //1.创建ServerSocket注册端口
            ServerSocket ss = new ServerSocket(9999);
            //2.定义一个循环接收客户端的socket连接请求
            //初始化一个线程池对象
            HandlerSocketServerPool pool = new HandlerSocketServerPool(3,10);
            while (true){
                Socket socket = ss.accept();
                //3.把socket对象交给一个线程池管理
                //把socket封装成一个任务对象交给线程池处理
               Runnable target = new ServerRunnableTarget(socket);
               pool.execute(target);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

我们会发现,我们在注册端口后,我们创建了HandlerSocketServerPool线程池,用它来进行任务的处理

3.2 线程池类

我们来看封装的线程池

package bio_four;

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

/**
 * @ClassName HandlerSocketServerPool
 * @Description
 * @Author asus
 * @Date 2021/8/3 21:20
 * @Version 1.0
 **/
public class HandlerSocketServerPool {
    //1.创建一个线程池的成员变量用于存储一个线程池对象
    private ExecutorService executorService;

    /**
     * 2.创建这个类的对象的时候就需要初始化线程池对象
     * public ThreadPoolExecutor(int corePoolSize,
     *                           int maximumPoolSize,
     *                           long keepAliveTime,
     *                           TimeUnit unit,
     *                           BlockingQueue<Runnable> workQueue)
     *
     */
    public HandlerSocketServerPool(int maxThreadNum,int queueSize){
        //ArrayBlockingQueue阻塞队列
        executorService = new ThreadPoolExecutor(
            3,
            maxThreadNum,
            120,
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(queueSize));
    }

    /**
     * 3、提供一个方法来提交任务给线程池的队列来暂存,等着线程池来处理
     */
    public void execute(Runnable runnable){
        executorService.execute(runnable);
    }
}

首先,我们创建ExecutorService 来进行线程池对象的存储,在构造方法中初始化一个线程池来赋值给ExecutorService 对象,我们创建一个execute方法将任务放入线程池的任务队列中,等待线程池进行任务的处理。

3.3 任务对象类

然后我们将socket对象封装成为一个任务对象,让线程池来执行这个任务。

package bio_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;

/**
 * @ClassName ServerRunnableTarget
 * @Description 实现Runnable接口来定义任务对象
 * @Author asus
 * @Date 2021/8/3 21:42
 * @Version 1.0
 **/
public class ServerRunnableTarget implements Runnable{
    private Socket socket;

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

    @Override
    public void run() {
        //处理接收到客户端Socket通信需求
        try {
            //1.从Socket管道中得到一个字节输入流对象
            InputStream inputStream = socket.getInputStream();
            //2.把字节输入流包装成一个缓冲字符输入流
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
            String str;
            if ((str = bufferedReader.readLine()) != null) {
                System.out.println("服务端接收到数据" + str);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

这个跟前面的服务端处理请求是一样的。

接下来我们将封装的任务队列交给线程池进行处理,也就是下面这个步骤。

Runnable target = new ServerRunnableTarget(socket);
               pool.execute(target);

3.4 客户端类

这个跟以前的一样

public class Client {
   public static void main(String[] args) {
      try {
         // 1.简历一个与服务端的Socket对象:套接字
         Socket socket =<

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

Numpy+Pandas+Matplotlib学习

Numpy+Pandas+Matplotlib学习

Numpy+Pandas+Matplotlib学习

微信小程序代码片段

VSCode自定义代码片段——CSS选择器

谷歌浏览器调试jsp 引入代码片段,如何调试代码片段中的js