从零开始学习Java网络编程(二)之超级简单的bio

122 阅读7分钟

传统的BIO编程

在NIO没有发布之前,Java网络编程都是使用BIO,网络编程的基本模型是Client/Server模型,也就是两个进程之间进行相互通信,其中服务端提供位置信息(绑定的IP地址和监听端口),客户端通过连接操作向服务端监听的地址发起连接请求,通过三次握手建立连接,如果连接建立成功,双方就可以通过网络套接字(Socket)进行通信。在基于传统同步阻塞模型开发中,ServerSocket负责绑定IP地址,启动监听端口:Socket负责发起连接操作。连接成功之后,双方通过输入和和输出流进行同步阻塞式通信。

  1. 创建一个最简单的demo,每次只能处理一个请求,只有等待前一个请求完成之后才能处理下一个请求
package com.im.chat.one;
​
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
​
/**
 * @author jay.li
 * @Title: OneThread
 * @Package com.im.chat.one
 * @Description:
 * @date 2024/10/30
 */
public class OneThread {
​
    public static void main(String[] args) {
        ServerSocket serverSocket = null;
        try {
            serverSocket = new ServerSocket(9876);
        } catch (IOException e) {
            System.out.println("创建ServerSocket异常:" + e.getMessage());
            return;
        }
        try {
            Socket socket = null;
            while (true) {
                socket = serverSocket.accept();
                System.out.println("收到客户端连接:" + socket.getInetAddress().getHostAddress());
                // 处理客户端请求
                handlerAccept(socket);
            }
        } catch (IOException e) {
            System.out.println("accept异常:" + e.getMessage());
        } finally {
            try {
                serverSocket.close();
                serverSocket = null;
            } catch (IOException e) {
                System.out.println("关闭ServerSocket异常:" + e.getMessage());
            }
        }
    }
​
    public static void handlerAccept(Socket socket) {
        BufferedReader in = null;
        PrintWriter out = null;
​
        try {
            in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            out = new PrintWriter(socket.getOutputStream(), true);
            String currentTime = null;
            String body = null;
            while (true) {
                body = in.readLine();
                if (body == null) {
                    break;
                }
                System.out.println("接收到客户端请求:" + body);
                currentTime = new java.util.Date(System.currentTimeMillis()).toString();
                out.println(currentTime);
            }
        } catch (Exception e) {
            if (in != null) {
                try {
                    in.close();
                    in = null;
                } catch (IOException ex) {
                    System.out.println("关闭输入流异常:" + ex.getMessage());
                }
            }
​
            if (out != null) {
                out.close();
                out = null;
            }
​
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException ex) {
                    System.out.println("关闭Socket异常:" + ex.getMessage());
                }
            }
        }
    }
}

2. 单一的一个线程去处理请求,效率不尽人意,只能说在demo层面是没有问题的,供给讲课学习是最简单的,Java是支持多线程的,既然单线程处理请求太慢,那就开始使用多线程(线程池),每当有请求过来需要读写数据流的时候,就从线程池分配一个线程去处理。

image-20230117113316873.png

package com.im.chat.multiple;
​
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
​
/**
 * @author jay.li
 * @Title: MoreThread
 * @Package com.im.chat.multiple
 * @Description:
 * @date 2024/10/30
 */
public class MoreThread {
​
    public static void main(String[] args) {
        Server server = new Server(9876);
        // 启动服务
        server.start();
    }
}
​
class Server {
​
    private final int port;
​
    private ServerSocket serverSocket;
​
    private SimpleThreadPool pool = new SimpleThreadPool(2, 9, "customer-thread-");
​
​
    public int getPort() {
        return port;
    }
​
    public Server(int port) {
        // 设置服务端端口号
        this.port = port;
​
        try {
          // 创建ServerSocket服务
            serverSocket = new ServerSocket(port);
        } catch (IOException e) {
            System.out.println("创建ServerSocket异常:" + e.getMessage());
​
            pool.shutdown();
        }
    }
    
    // 启动服务
    public void start() {
        try {
            Socket socket = null;
            while (true) {
              // 创建套接字,拿到新的客户端
                socket = serverSocket.accept();
                // 处理客户端请求
                Socket finalSocket = socket;
                pool.execute(() -> {
                    handlerAccept(finalSocket);
                });
​
            }
        } catch (IOException e) {
            System.out.println("accept异常:" + e.getMessage());
        } finally {
            try {
                serverSocket.close();
                serverSocket = null;
            } catch (IOException e) {
                System.out.println("关闭ServerSocket异常:" + e.getMessage());
            }
​
            pool.shutdown();
        }
    }
​
    private void handlerAccept(Socket socket) {
        BufferedReader in = null;
        PrintWriter out = null;
​
        try {
            in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            out = new PrintWriter(socket.getOutputStream(), true);
            String currentTime = null;
            String body = null;
            while (true) {
                body = in.readLine();
                if (body == null) {
                    break;
                }
                System.out.printf("当前线程是:%s,当前客户端ip %s, 请求body %s%n", Thread.currentThread().getName(),
                        socket.getRemoteSocketAddress(), body);
                currentTime = new java.util.Date(System.currentTimeMillis()).toString();
                out.println(currentTime);
            }
        } catch (Exception e) {
            if (in != null) {
                try {
                    in.close();
                    in = null;
                } catch (IOException ex) {
                    System.out.println("关闭输入流异常:" + ex.getMessage());
                }
            }
​
            if (out != null) {
                out.close();
                out = null;
            }
​
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException ex) {
                    System.out.println("关闭Socket异常:" + ex.getMessage());
                }
            }
        }
    }
​
}
​
​
class SimpleThreadPool {
    private final int corePoolSize;
    private final int maximumPoolSize;
​
    // 任务队列,用于存储待执行的任务
    private final Queue<Runnable> taskQueue;
    private final Worker[] workers;
    private int currentPoolSize;
​
    private String poolNamePrefix;
​
    private AtomicBoolean isShutdownInitiated = new AtomicBoolean(false);
​
    private AtomicLong taskCount = new AtomicLong(0);
​
    public SimpleThreadPool(int corePoolSize, int maximumPoolSize, String poolNamePrefix) {
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.taskQueue = new LinkedBlockingQueue<>(1000);
        this.workers = new Worker[maximumPoolSize];
        this.currentPoolSize = 0;
        this.poolNamePrefix = poolNamePrefix;
        // 初始化核心线程池
        for (int i = 0; i < corePoolSize; i++) {
            workers[i] = new Worker(poolNamePrefix + i);
            workers[i].start();
            currentPoolSize++;
        }
    }
​
    public void execute(Runnable task) {
        if (isShutdownInitiated.get()) {
            throw new IllegalStateException("线程池已关闭");
        }
​
        if (taskCount.incrementAndGet() > corePoolSize && currentPoolSize < maximumPoolSize) {
            workers[currentPoolSize++] = new Worker(poolNamePrefix + currentPoolSize);
            workers[currentPoolSize - 1].start();
        }
        if (!taskQueue.offer(task)) {
            taskCount.decrementAndGet();
​
            System.out.println("###任务队列已满###丢弃任务");
        }
    }
​
​
​
    public void shutdown() {
        isShutdownInitiated.compareAndSet(false, true);
        for (Worker thread : workers) {
            thread.interrupt();
        }
​
        System.out.println("关闭线程池成功");
    }
​
    private class Worker extends Thread {
​
        public Worker(String name) {
            super(name);
        }
​
        @Override
        public void run() {
            Runnable task;
            while (!isShutdownInitiated.get()) {
                synchronized (taskQueue) {
                    while (taskQueue.isEmpty()) {
                        try {
                            taskQueue.wait(1);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    task = taskQueue.poll();
                }
​
                if (task != null) {
                    try {
                        task.run();
                    } catch (RuntimeException e) {
                        // handle exceptions in task execution
                    } finally {
                        taskCount.decrementAndGet();
                    }
                }
            }
        }
    }
}

客户端请求:

package com.im.chat.multiple;
​
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
​
/**
 * @author jay.li
 * @Title: Clent
 * @Package com.im.chat.multiple
 * @Description:
 * @date 2024/10/31
 */
public class Client {
​
    public static void main(String[] args) throws IOException {
​
        for (int i = 0; i < 100; i++) {
            Socket socket = new Socket("127.0.0.1", 9876);
            BufferedReader in = null;
            PrintWriter out = null;
            in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            out = new PrintWriter(socket.getOutputStream(), true);
            out.println("QUERY TIME ORDER: " + i);
            String resp = in.readLine();
            System.out.println("Now is : " + resp);
            socket.close();
        }
    }
}

到此为止,同步阻塞式I/0开发的时间服务器程序已经讲解完毕。我们发现,BIO主要的问题在于每当有一个新的客户端请求接入时,服务端必须创建一个新的线程处理新接入的客户端链路,一个线程只能处理一个客户端连接。在高性能服务器应用领域,往往需要面向成千上万个客户端的并发连接,这种模型显然无法满足高性能、高并发接入的场景。

虽然我们使用了线程池避免了因为请求并发过多造成的服务器资源耗尽,但是我们无法解决阻塞的问题,在读取或者写入数据的时候,这个过程是阻塞的,意味着不管是写入还是读取数据,这个时候,都必须等待它做完了才能进行下一步。比如我们在线程池中处理数据的节点为:

BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
String currentTime = null;
String body = null;
while (true) {
  // 读取数据阻塞
    body = in.readLine();
    if (body == null) {
         break;
    }
    System.out.printf("当前线程是:%s,当前客户端ip %s, 请求body %s%n", Thread.currentThread().getName(),
socket.getRemoteSocketAddress(),
body);
currentTime = new java.util.Date(System.currentTimeMillis()).toString();
// 写入数据同样阻塞
out.println(currentTime);
}

为什么会阻塞呢?

首先我们看两个Java同步I/O的API说明,它明确的说明了在读取数据时是会阻塞的。当对Socket的输入流进行读取操作的时候,它会一直阻塞下去,直到发生如下三种事件。

  1. 有数据可读;
  2. 可用数据已经读取完毕;
  3. 发生空指针或者I/O异常。

这意味着当对方发送请求或者应答消息比较缓慢,或者网络传输较慢时,读取输入流一方的通信线程将被长时间阻塞,如果对方要60s才能够将数据发送完成,读取一方的I/O线程也将会被同步阻塞60s,在此期间,其他接入消息只能在消息队列中排队。这个是如果底层逻辑不变,不管怎么优化都是解决不了的。

    /**
     * Reads characters into a portion of an array.  This method will block
     * until some input is available, an I/O error occurs, or the end of the
     * stream is reached.
     *
     * <p> If {@code len} is zero, then no characters are read and {@code 0} is
     * returned; otherwise, there is an attempt to read at least one character.
     * If no character is available because the stream is at its end, the value
     * {@code -1} is returned; otherwise, at least one character is read and
     * stored into {@code cbuf}.
     *
     * @param      cbuf  Destination buffer
     * @param      off   Offset at which to start storing characters
     * @param      len   Maximum number of characters to read
     *
     * @return     The number of characters read, or -1 if the end of the
     *             stream has been reached
     *
     * @throws     IndexOutOfBoundsException
     *             If {@code off} is negative, or {@code len} is negative,
     *             or {@code len} is greater than {@code cbuf.length - off}
     * @throws     IOException  If an I/O error occurs
     */
    public abstract int read(char[] cbuf, int off, int len) throws IOException;

下面我们接着对输出流进行分析,当调用OutputStream的write方法写输出流的时候,它将会被阻塞,直到所有要发送的字节全部写入完毕,或者发生异常。学习过TCP/IP相关知识的人都知道,当消息的接收方处理缓慢的时候,将不能及时地从TCP缓冲区读取数据,这将会导致发送方的TCPwindowsize不断减小,直到为0,双方处于Keep-Alive状态,消息发送方将不能再向TCP缓冲区写入消息,这时如果采用的是同步阻塞I/O,write操作将会被无限期阻塞,直到TCP windowsize大于0或者发生I/O异常。

通过对输入和输出流的API文档进行分析,我们了解到读和3写操作都是同步阻塞的,阻塞的时间取决于对方I/0线程的处理速度和网络I/O的传输速度。本质上来讲,我们无法保证生产环境的网络状况和对端的应用程序能足够快,如果我们的应用程序依赖对方的处理速度,它的可靠性就非常差。