Loading

Java BIO 编程

Java BIO 基本介绍

  1. Java BIO 就是传统的java io 编程,其相关的类和接口在 java.io
  2. BIO(blocking I/O) : 同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户连接服务器)。
  3. BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,程序简单易理解

Java BIO 工作机制

​ 采用 BIO 通信模型 的服务端,通常由一个独立的 Acceptor 线程负责监听客户端的连接。我们一般通过在while(true) 循环中服务端会调用 ServerSocket的accept() 方法等待接收客户端的连接的方式监听请求,一旦接收到一个连接请求,就可以建立通信套接字Socket,并为这个通信套接字创建一个新的客户端线程处理这条 Socket 链路。如下图所示。

img

​ 但是在池化技术大行其道的今天,线程的创建和销毁时非常浪费资源的,使用线程池将线程利用起来重复使用,线程池可以设置等待队列的大小最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。那么上面的传统 BIO 工作机制图就变成了如下图所示的演进。

img

说了这么多很多刚入门的小伙伴依旧很懵逼,看不懂。下面我举一个生活中的例子:

传统的 BIO 模型工作机制 -:在生活中饭店一般都有大门口接待服务员(类似于 Acceptor 线程 )接待所有进入饭店的客人,当有一个客人进来吃饭后,饭店就招聘一个服务员(类似于创建线程)专门接待这桌客人,这个服务员在此期间不能做其他事情,当客人走后饭店就把这个服务员辞退(类似于销毁线程),有点类似于同生共死,当然世界上没有那家饭店愿意做这种事情,太不现实;

线程池技术优化后的 BIO 模型工作机制:饭店都会有固定的多个服务员,当有一个客人进来吃饭后,就指定某个服务员专门接待这桌客人(类似于从线程池中取出一个线程),服务员在此期间也不能做其他时间,但是不同的是,当客人走后就可以让这个服务员休息一会(类似于将线程放回线程池等待下次使用),等待下一桌客人的到来。

Java BIO 应用实例

1、代码实现思路

  1. 使用线程池替代单个线程,每建立一个客户端连接就从线程池中取出一个线程
  2. serverSocket的accept函数监听客户端连接,如果没有客户端连接,程序将阻塞在accept函数上
  3. 创建handler函数,处理Socket链路数据,接收来自客户端发送的请求,并打印在控制台

2、服务端代码实现

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @Description: 基于BIO通信模型的TimeServer
 * @Author: hh
 * @date 2020/10/14
 */
public class TimeServer {

    //1、创建一个线程池,如果有客户端连接就创建一个线程
    private static final ExecutorService threadPool = new ThreadPoolExecutor(
            10,
            10,
            120L,
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(100));

    public static void main(String[] args) {
        //1、指定监听端口
        int port = 9999;
        try (ServerSocket serverSocket = new ServerSocket(port);) {
            System.out.println("服务端启动,监听端口: " + port);
            while (true) {
                //2、创建一个无限循环监听客户端连接,如果没有客户端接入,则主线程将阻塞在 accept() 函数上
                System.out.println("等待连接。。。");
                Socket socket = serverSocket.accept();
                System.out.println("连接到一个客户端,主机地址:" + socket.getInetAddress().getHostAddress());
                threadPool.execute(() -> {
                    handler(socket);
                });
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * Socket链路处理器
     * @param socket
     */
    private static void handler(Socket socket) {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
            System.out.println(Thread.currentThread().getName() + " - 接收来自客户端信息");
            String body = null;
            //3、循环读取客户端发送信息
            while (true) {
                if ((body = reader.readLine()) == null) {
                    break;
                }
                //4、输出客户端发送信息
                System.out.println("服务端接收来自客户端信息:" + body);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

3、客户端发起请求

1、启动 TimeServer,控制台输出如下:

image-20201014224117450

2、如果是win10系统电脑,使用 win+R命令打开运行窗口,输入cmd命令

image-20201014223920218

3、点击"确定",进入命令行窗口,输入"telnet 127.0.0.1 9999",TimeServer控制台输出如下

image-20201014224210311

4、ctrl + ] 命令回显内容,回车,进入编辑状态,输入任何内容,TimeServer控制台将会输出相应内容

image-20201014224429341

BIO 模型问题分析

  • 每个请求都需要创建独立的线程,与对应的客户端进行数据 Read,业务处理,数据 Write 。
  • 当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大。
  • 连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费
posted @ 2020-10-14 23:13  PinGoo  阅读(465)  评论(0编辑  收藏  举报