java · 13 2 月, 2022 0

JAVA中BIO的实现方式以及优化

在日常开发中,总是会涉及到对IO相关的操作,而在JAVA中,网络编程包含了BIONIO、以及AIO这几种类型,今天这篇文章主要讲解在JAVA BIO的工作模式,以及针对BIO的常见的优化方式。

实现

在较早的开发中,BIO的开发其实还是很简单的,还是以例子的方式加以说明:

/**
 * 该测试类主要通过bio的方式创建, 接收客户端,并处理消息
 *
 * @author <a href="mailto:xianglj1991@163.com">xianglujun</a>
 * @since 2022/2/13 13:55
 */
public class SocketServerDemo {

    public static void main(String[] args) {
        try {
            // 创建服务端的socket, 并监听在端口9090
            ServerSocket serverSocket = new ServerSocket(9090);
            // 该方法会发生阻塞, 等待客户端链接
            while (true) {
                Socket socket = serverSocket.accept();
                System.out.println("客户端链接成功..");
                InputStream is = socket.getInputStream();
                printMsg(is);
            }

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

    /**
     * 从流中读取消息
     *
     * @param is 输入流
     */
    private static void printMsg(InputStream is) throws IOException {
        byte[] bytes = new byte[1024];
        while (is.read(bytes) != -1) {
            // 输出消息
            String m = new String(bytes);
            System.out.println("接收到客户端消息: " + m);
        }
    }
}

在该实例中,使用了最早起的BIO开发方式,主要包含一下几点:

  • accept()方法会产生阻塞, 等待有新的客户端链接上来之后, 才会继续向后面执行
  • read(bytes): 该方法主要从输入流中读取数据, 直到客户端断开链接为止

在上面的程序中,主要包含了一下几个问题:

  • 当有多个客户端链接的时候,会产生阻塞,只有在前一个客户端数据处理完成并断开后,才能够处理下一个链接
  • 无法支撑并发的环境
telnet localhost 9090

我们通过telnet命令可以测试,如下图:

BIO

这个时候,客户端能够正常的发送数据到服务端,服务端也能够正常的输出。

BIO

 

 

 

 

 

 

 

 

此时,如果我们再次开启第二个客户端进行链接时,这是就会发现客户端2产生了阻塞,必须要等待第一个客户端处理完成之后才能进行后续的操作。因此,我们针对这种情况进行优化。

优化1

针对以上程序优化的第一个思路就是,在接收客户端请求的时候,客户端之间不受影响, 因此我们对每个客户端的链接,使用单独的线程进行处理。

package com.jdk.test.demo.java.bio;

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

/**
 * 该测试类主要通过bio的方式创建, 接收客户端,并处理消息
 *
 * @author <a href="mailto:xianglj1991@163.com">xianglujun</a>
 * @since 2022/2/13 13:55
 */
public class SocketServerDemo2 {

    public static void main(String[] args) {
        try {
            // 创建服务端的socket, 并监听在端口9090
            ServerSocket serverSocket = new ServerSocket(9090);
            // 该方法会发生阻塞, 等待客户端链接
            while (true) {
                Socket socket = serverSocket.accept();
                System.out.println("客户端链接成功..");
                new Thread(() -> {
                    InputStream is = null;
                    try {
                        is = socket.getInputStream();
                        printMsg(is);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }

                }).start();
            }

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

    /**
     * 从流中读取消息
     *
     * @param is 输入流
     */
    private static void printMsg(InputStream is) throws IOException {
        byte[] bytes = new byte[1024];
        while (is.read(bytes) != -1) {
            // 输出消息
            String m = new String(bytes);
            System.out.println("接收到客户端消息: " + m);
        }
    }
}

可以看到,对于最开始的版本中,在优化1中的实现,主要是引入了线程,针对每个链接都采用单独的线程对数据进行处理, 提高了对链接的处理效率。

我们启动服务端程序,通过telnet命令测试是否可以同时处理多个请求:

BIO

使用客户端2同时链接并发送消息:

BIO

最终呈现的结果如下:

BIO

因此通过多线程的方式, 能够同时处理多个客户端的请求,并响应多个客户端的请求结果。但是还是具有以下缺点:

  • 当客户端链接不断增加的时候,系统中的线程数量会急速增加,导致线程数量不可控
  • 当线程数量急剧增加的时候,导致上线问频繁的切换,性能也会有所下降
  • 资源使用(内存、CPU)使用的升高

优化2

针对以上存在的问题,我们很自然的能够想到的处理方式就是使用线程池,这样的话,就能够对线程数量的控制。

package com.jdk.test.demo.java.bio;

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 该测试类主要通过bio的方式创建, 接收客户端,并处理消息
 *
 * @author <a href="mailto:xianglj1991@163.com">xianglujun</a>
 * @since 2022/2/13 13:55
 */
public class SocketServerDemo3 {

    private static final ThreadPoolExecutor EXECUTOR = new ThreadPoolExecutor(5, 10, 10, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100));

    public static void main(String[] args) {
        try {
            // 创建服务端的socket, 并监听在端口9090
            ServerSocket serverSocket = new ServerSocket(9090);
            // 该方法会发生阻塞, 等待客户端链接
            while (true) {
                Socket socket = serverSocket.accept();
                System.out.println("客户端链接成功..");
                EXECUTOR.submit(() -> {
                    InputStream is = null;
                    try {
                        is = socket.getInputStream();
                        printMsg(is);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }

                });
            }

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

    /**
     * 从流中读取消息
     *
     * @param is 输入流
     */
    private static void printMsg(InputStream is) throws IOException {
        byte[] bytes = new byte[1024];
        while (is.read(bytes) != -1) {
            // 输出消息
            String m = new String(bytes);
            System.out.println("接收到客户端消息: " + m);
        }
    }
}

可以通过代码发现,在优化3中的实现,其实也是多线程版本,只是让线程变的可控,至于无限制的创建线程。

但是线程池版本一样有一下缺点:

  • 当线程池中线程满了之后,无法再处理后续的客户端请求,导致后续的客户端失败

通过以上实验可以得知,其实BIO的使用场景是比较有限的,它无法在比较高的并发场景中最大的处理更多的客户端请求,因此当我们的系统并发较小时,BIO可以作为一个方法使用。所以在JDK较早版本中,引入了NIO的使用,在下篇文章中将介绍NIO的一些使用。