侧边栏壁纸
博主头像
王一川博主等级

努力成为一个不会前端的全栈工程师

  • 累计撰写 70 篇文章
  • 累计创建 20 个标签
  • 累计收到 39 条评论

目 录CONTENT

文章目录

NIO的前置-BIO

王一川
2022-04-26 / 0 评论 / 1 点赞 / 1,182 阅读 / 5,170 字
温馨提示:
本文最后更新于 2022-05-31,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

最近在研究 java 的 nio,发现网上很多视频上来直接开讲 nio 的三板斧 select、channel、buffer,各种专业名词:非阻塞式io、多路复用等,整的是云里雾里!!!之所以有这些困惑是因为没有去了解 nio 的背景,如果我们知道在什么情况下或者 nio 主要是解决哪些问题,在去学习 nio 的技术可能会是一个不错的选择,因此这篇文章带你游走上古时期的 java 网络编程,以及 nio 在什么背景下出现的,又解决了哪些技术难题。

一、从一个简单 c/s 开始

假如我们现在需要开发一个简单的客户端服务端程序,客户端发送数据给服务端,服务端接收到数据打印到控制台。

java 中为我们提供了基于 socket 通信的 api,首先我们编写一个服务端,开启一个端口监听

ServerSocket serverSocket = new ServerSocket(9999);

获取到 ServerSocket 尝试去监听这个端口的套接字

Socket socket = serverSocket.accept();

当有客户端连接到监听的端口,就可以尝试去获取客户端的输入,即获取一个输入流

InputStream inputStream = socket.getInputStream();

这个输入流即是客户端的输入,打印这个输入流即可,因此完整的服务端代码如下:

package tech.kpretty.bio.step1;

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

/**
 * 实现一个简单的服务端
 */
public class Server {
    public static void main(String[] args) throws IOException {
        // 创建一个绑定到指定端口的服务器套接字
        ServerSocket serverSocket = new ServerSocket(9999);
        // 侦听要与此套接字建立的连接并接受它。该方法阻塞,直到建立连接。
        Socket socket = serverSocket.accept();
        // 获取 socket 的输入流
        InputStream inputStream = socket.getInputStream();
        // 包装一个输入流便于打印
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));

        String line;

        while ((line = bufferedReader.readLine()) != null) {
            System.out.println(">" + line);
        }
        // 关闭资源
        bufferedReader.close();
        serverSocket.close();

    }
}

客户端只需要去连接对应 ip 和端口就可以与服务端建立连接

Socket socket = new Socket("127.0.0.1", 9999);

获取 socket 的输出流

OutputStream outputStream = socket.getOutputStream();

接下来只需要通过输出流将数据发送给服务端即可,完整的代码如下:

package tech.kpretty.bio.step1;

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

public class Client {
    public static void main(String[] args) throws IOException {
        // 创建一个 socket 并将其连接到指定主机上的指定端口号。
        Socket socket = new Socket("127.0.0.1", 9999);
        // 获取 socket 得到输出流
        OutputStream outputStream = socket.getOutputStream();
        // 封装输出流
        PrintStream printStream = new PrintStream(outputStream);

        printStream.println("你好,socke 服务端");
        
        // 关闭资源
        printStream.close();

        socket.close();
    }
}

首先启动服务端,会发现服务端首先会阻塞在serverSocket.accept()处,因为此刻服务端需要等待一个客户端的连接,这时候启动客户端,客户端迅速发送数据后程序停止,此时服务端也正常打印。如果我们在客户端的socket.getOutputStream()处打一个断点后 debug 启动会看到服务端阻塞在bufferedReader.readLine()处(可以给服务端每行代码后加一个输出),也就是说这样的服务端会有两处阻塞方法,一个是 accept 等待一个客户端连接,另一处 readLine 等待客户端的输入。若客户端长时间没有连接或者连接后什么事都不干那么服务端就会一直阻塞,这就是 bio,这就是 bio 的缺点。

二、实现多发多收

上面的代码发现客户端服务端就只进行了一次交互就双双停机(主要是客户端的挂了,服务端跟着殉情),如何实现客户端服务端的多发多收?其实只需要控制客户端一直发送即可,这里服务端代码保持不变,修改客户端代码,如下:

package tech.kpretty.bio.step2;

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

public class Client {
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("127.0.0.1", 9999);

        OutputStream outputStream = socket.getOutputStream();

        PrintStream printStream = new PrintStream(outputStream);

        Scanner scanner = new Scanner(System.in);

        while (true){
            System.out.print("client > ");
            String line = scanner.nextLine();
            printStream.println(line);
        }
    }
}

测试如下

突发奇想,客户端肯定不止一个,如果有多个客户端都连接上这个服务端并发送数据,服务端会受到所有数据吗,我们尝试启动一个服务端和多个客户端,验证结果如下:

发现服务端只能接收到最先连接的客户端,看代码我们不难发现,当有一个客户端连接时,服务端会从 accept 阻塞处恢复并运行到第二个阻塞处获取客户端的输入,之后后面的客户端连接则无法与服务端建立连接,本质就是 accept 只运行了一次。

三、改进:接收多个客户端

需要解决能够同时接收多个客户端,本质就是需要服务端多次执行 accept,但由于获取客户端的连接也是阻塞的,因此一个不错的 idea 就是每个客户端都在服务端开启一个线程用户处理客户端的输入,主线程循环执行 accept 即可,客户端代码不变,修改服务端代码,如下:

package tech.kpretty.bio.step3;

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

/**
 * 接收多客户端输入
 */
public class Server {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(9999);

        while (true) {
            Socket socket = serverSocket.accept();
            new Thread(() -> {
                try {
                    InputStream inputStream = socket.getInputStream();
                    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
                    String line;
                    while ((line = bufferedReader.readLine()) != null) {
                        System.out.println(Thread.currentThread().getName() + " >" + line);
                    }
                    bufferedReader.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }).start();
        }
    }
}

验证结果如下:

其架构图如下:

聪明的小伙伴已经发现问题了,服务端代码有一个严重的弊端,那就是服务端的线程数是有限的,一旦客户端过多服务端就会崩溃,同时当客户端长时间不发消息,服务端的线程就会一直阻塞,浪费资源。

四、另辟蹊径

针对上面服务端会崩溃的情况,可以另辟蹊径通过线程池技术来减少服务端线程创建销毁带来的资源损耗,同时也限制了线程数,从而避免服务器的崩溃,再次修改服务端代码,引入线程池

package tech.kpretty.bio.step3;

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

/**
 * 接收多客户端输入,引入线程池
 */
public class Server {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(9999);
        // 创建一个线程池
        ThreadPoolExecutor executorPool = new ThreadPoolExecutor(2, 8, 5, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10));

        while (true) {
            Socket socket = serverSocket.accept();
            executorPool.execute(() -> {
                try {
                    InputStream inputStream = socket.getInputStream();
                    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
                    String line;
                    while ((line = bufferedReader.readLine()) != null) {
                        System.out.println(Thread.currentThread().getName() + " >" + line);
                    }
                    bufferedReader.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
        }
    }
}

这种伪异步IO的方式看起来是可以解决上述问题,但本质还是阻塞IO,只不过是使用线程池来避免系统的资源浪费、服务端崩溃,但当客户端增加达到线程池的上线,则会触发线程池的拒绝策略,因此亟需一种新的非阻塞的IO模式来解决这个问题,这就是 nio

1

评论区