一顿饭掌握Java BIO

794 阅读8分钟

认识自己的无知是认识世界最可靠的方法————无名之辈

当看到博客的时候就已经完成了一次网络会话,数据的传输依赖于网络通信和操作系统级别的IO操作。对于Java开发而言我们常用的IO模型:BIO、NIO、AIO 是对网络模型中的传输层进行了封装。为了更好的理解IO模型本身的逻辑,小胖决定按照BIO的方式活一天。

BIO 工作机理

服务端:ServerSocket注册端口,调用accept方法等待客户端的请求,如果一直没有客户端的连接就一直等待。 客户端: 通过Socket对象实现和ServerSocket连接,获取输出流将信息输出。 服务端:accept接受到客户端的连接,获取Socket对象,得到输入流,也就获取到了客户端的消息,到此一次通信结束。 在这里插入图片描述

被BIO控制的小胖

理解BIO的关键在于怎么理解同步和阻塞两个词。

同步 是消息通信机制的一个概念,指的是在发出消息之后一直等待消息的返回,如果消息不返回就一直等。

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

举个栗子来说:

上午12:00 小胖现在手上有三件事情要做:

  • 点外卖(干饭的时间到了,等外卖大概30分钟,吃饭10分钟)
  • 烧热水放到保温瓶(要冲杯奶茶午休起来喝,大概10分钟)
  • 整理桌面(中午在桌子上午休,大概2分钟)

如果小胖按照BIO的原理来执行:

  • 小胖打开app点了最喜欢的腰果鸡丁,此时小胖什么都不干,一直等外卖送到,外卖到了就开始吃饭,吃好饭现在是12:40分了。 在这里插入图片描述
  • 小胖开始去准备冲奶茶的热水,热水开了倒进保温瓶。现在是12:50分。
  • 小胖开始整理桌面,准备午休,完事之后是12:52。 在这里插入图片描述

分析这一过程: 小胖是一个线程,点外卖之后线程就一直处于等待的状态,等待的时间其实可以把去烧热水和整理桌面的,干等是无意义的,或许这就是小胖作为干饭人的执著吧。 在这里插入图片描述

Java 情景模拟

定义一个客户端,客户端从键盘输入表示和小胖交流,告诉小胖需要执行的任务

public class Client {
    public static void main(String[] args) throws Exception{
        Socket socket = new Socket("127.0.0.1", 9999);
        OutputStream outputStream = socket.getOutputStream();
        PrintStream ps = new PrintStream(socket.getOutputStream());
        Scanner scanner = new Scanner(System.in);
        while (true){
            System.out.print("please input:");
            String msg  = scanner.nextLine();
            ps.println(msg);
            ps.flush();
        }
    }
}

定义服务端,就是小胖本人了,任务进来就需要小胖处理,睡眠时间当做处理任务。

public class Server {
    public static void main(String[] args) throws Exception {
        ServerSocket serverSocket = new ServerSocket(9999);
        System.out.println("我是小胖,我准备好了");
        while (true){
            Socket socket = serverSocket.accept();
            try{
                InputStream inputStream = socket.getInputStream();
                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
                String msg;
                while ((msg = bufferedReader.readLine()) != null){
                    System.out.println(msg);
                    System.out.println("正在:"+msg);
                    Thread.sleep(1000*10);
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}

运行程序,首先启动服务端,再启动客户端,在客户端分别输入上述三个任务,没有执行完当前任务,下个任务进来就会阻塞。线程同步到当前任务执行完毕。(ps: 执行一下代码印象会更深刻哦) 在这里插入图片描述 在这里插入图片描述

小胖优化 BIO

因为小胖午休时间不够,下午没精力搬砖,深夜0点才下班。小胖想出了优化模型的方案:

  • 12:00-12:03 点完外卖,去烧水(耗时3分钟)
  • 12:03-12:05 开始整理桌面,12:05 完成桌面整理,任务3结束
  • 12:05-12:10 热水准备好了,任务2结束
  • 12:10-12:30 等待...
  • 12:30-12:40 干饭,任务1结束 在这里插入图片描述

Java 情景模拟

添加一个处理任务的线程类,线程随机设置睡眠时间,模拟任务的执行过程。

public class ServerThreadReader extends Thread {
    private Socket socket;
    ServerThreadReader(Socket socket){
        this.socket = socket;
    }
    @Override
    public void run(){
        try{
            InputStream inputStream = socket.getInputStream();
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
            String msg;
            while ((msg = bufferedReader.readLine()) != null){
                int time = (int)(10+Math.random()*(20-10+1));
                System.out.println("执行任务:"+msg+",耗时: "+time);
                Thread.sleep(1000*time);
                System.out.println(msg+" 执行完毕,"+" 耗时: "+time);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

服务端接到任务之后就交给线程处理

public class Server {
    public static void main(String[] args) throws Exception {
        ServerSocket serverSocket = new ServerSocket(9999);
        System.out.println("我是小胖,我准备好了...");
        while (true){
            Socket socket = serverSocket.accept();
            new ServerThreadReader(socket).start();
        }
    }
}

客户端模输入任务

public class Client {
    public static void main(String[] args) throws Exception{
        Socket socket = new Socket("127.0.0.1", 9999);
        PrintStream ps = new PrintStream(socket.getOutputStream());
        Scanner scanner = new Scanner(System.in);
        while (true){
            System.out.print("please input:");
            String msg  = scanner.nextLine();
            ps.println(msg);
            ps.flush();
        }
    }
}

首先运行服务端,让小胖准备接收任务,之后开启三个客户端,模拟任务同时下达到的效果,运行结果如下,可见三个任务在19s的时候都完成了。 在这里插入图片描述

分析: 小胖还是那个可爱的小胖,并没有三个小胖,只不过小胖将等待的时间去处理了别的任务。抽象一下就如下图所示:一个服务端,每进来一个任务都会创建一个线程,去执行对应的任务。 在这里插入图片描述

特点:

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

小胖的分身(伪异步I/O)

加入线程池技术,防止客户端增加服务器呈现1:1的线程开销

中午只做3事情对于小胖来说还是轻松搞定的,但是回到网络模型本身,服务不可能只提供给三个客户端使用,如果连接的客户端是300台呢? 相当于小胖要短时间内处理300件事情,小胖不禁摸了摸自己的肾,想起还没造小人的自己... 在这里插入图片描述 小胖午休前在脑海里想:要是我能分身就好了,分身出50个自己,每个人处理6件事情,或许还有指望... 迷糊中小胖看见一群人在有条不紊的工作,定眼一看,嚯哦,竟然是自己的分身,怪不得远看上去就那么可爱。团结就是力量,干起活来真飒,小胖的分身陆续的就回到了体内,30分钟后小胖从梦中惊醒,嘘了口气,还好只是一场梦。

帮小胖圆梦

分身就是服务端,分身的数量有限,当任少于分身的时候分身会陆续的回到体内,调度分身的过程用一个线程池去维护,再合适不过了,任务放到线程池里面,有空闲的分身就去执行。模拟的话数据量设置小些,重点在实现思想。

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

}

维护分身本身,分身即一个线程。

public class ServerRunnableTarget implements Runnable {
    private Socket socket;

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

    @Override
    public void run() {
        // 处理接受到的客户端通信需求
        try{
            InputStream inputStream = socket.getInputStream();
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
            String msg;
            while ((msg = bufferedReader.readLine()) != null){
                System.out.println(msg);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

服务端代码,拿到线程池,接受到任务就通知线程池

public class Server {
    public static void main(String[] args) {
        try {
            ServerSocket serverSocket = new ServerSocket(9999);
            // 初始化一个线程池
            HandlerSocketServerPool socketServerPool = new HandlerSocketServerPool(6,10);
            while (true) {
                Socket socket = serverSocket.accept();
                Runnable target = new ServerRunnableTarget(socket);
                socketServerPool.execute(target);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

客户端代码,复制布置任务

public class Client {
    public static void main(String[] args) throws Exception{
        Socket socket = new Socket("127.0.0.1", 9999);
        OutputStream outputStream = socket.getOutputStream();
        PrintStream ps = new PrintStream(socket.getOutputStream());
        Scanner scanner = new Scanner(System.in);
        while (true){
            System.out.print("please input:");
            String msg  = scanner.nextLine();
            ps.println(msg);
            ps.flush();
        }
    }
}

特点:

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

  • NIO 马上安排!

找合适的场景模拟技术属实不易,如果有帮助到您,希望能点个大拇哥,这就是我继续扯犊子的动力!