理解NIO---从BIO的阻塞开始

in 理解NIO with 0 comment

BIO 就是传统的 java.io 包,它是基于流模型实现的,交互的方式是同步、阻塞方式,也就是说在读入输入流或者输出流时,在读写动作完成之前,线程会一直阻塞在那里,它们之间的调用时可靠的线性顺序。它的有点就是代码比较简单、直观;缺点就是 IO 的效率和扩展性很低,容易成为应用性能瓶颈。

BIO的阻塞

主要从Socket通信说起

Socket网络编程

在JavaAPI(java.net)支持由本地系统系统套接字库提供的阻塞函数可以实现网络编程。先看下一个简单的Socker网络编程Demo

SocketServer服务器

public class BIOSocketServer {
    public static void main(String[] args) throws IOException {
        byte[] bytes = new byte[1024];
        ServerSocket serverSocket = new ServerSocket();
        serverSocket.bind(new InetSocketAddress(9999));
        while (true){
            // 等待链接
            System.out.println("服务端等待链接");
            // 建立链接 这里会阻塞
            Socket socket = serverSocket.accept();
            // 验证阻塞,如果在上一行代码阻塞,这里不会输出
            System.out.println("服务端链接成功");
            // 读取数据,这里还是会阻塞
            socket.getInputStream().read(bytes);
            // 验证阻塞,如果在上一行代码阻塞,这里不会输出
            System.out.println("获得数据");
            // 打印
            String data = new String(bytes);
            System.out.println("数据为:"+data);
        }
    }
}

SocketClient客户端

public class BIOSocketClient {
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket();
        Scanner sc = new Scanner(System.in);
        // 建立连接
        socket.connect(new InetSocketAddress(9999));
        System.out.println("已建立连接,请输入数据");
        while (true){
            String data = sc.next();
            // 写数据
            socket.getOutputStream().write(data.getBytes());
        }
    }
}

以上就是一个小Demo实现Socket网络通信
项目运行结果

Socket运行结果

先运行服务器

image.png

可以看见目前就是按照代码逻辑运行,首先会输出服务器等待连接。后面的操作都没有继续执行。因为accept方法将后面的代码逻辑阻塞了。

继续启动一个客户端
结果如下,客户端输出 "已建立连接,请输入数据"
image.png

查看服务端控制台

image.png
服务端输出正常 “服务端链接成功”

现在两端已经成功建立连接,客户端随便发送一条数据
image.png
image.png
服务端成功打印。

NIO阻塞问题

可以根据上面的运行结果,可以看见,在程序运行过程中,服务端的accept方法和read方法都阻塞了代码。也就是代码在accept和read方法后的代码在这两个方法执行前,都不会阻塞。

那么想象,如果多个客户端去连接服务端会怎么样。

image.png

可以看见控制台,启动了两个Client端,但是在服务端只打印了一个已建立连接。因为read方法,将代码阻塞了,只要第一个Client端不发送消息,循环不会进行,所以第二个Client并没有真正连接上Server。
这里就体现了read方法的阻塞。
也就是得出结论,在单线程的场景下,简单的BIO网络编程只能有一个Client连接到Server。即无法处理并发。

解决NIO的阻塞

在NIO的阻塞,可以使用多线程的方式来完成。每次accept阻塞等待来自客户端请求,一旦受到连接请求就建立通信套接字同时开启一个新的线程来处理这个套接字的数据读写请求,然后立刻又继续accept等待其他客户端连接请求,即为每一个客户端连接请求都创建一个线程来单独处理。

现在对Server进行改造

public class BIOSocketServerThread {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket();
        serverSocket.bind(new InetSocketAddress(9999));
        while (true) {
            // 等待链接
            System.out.println("服务端等待链接");
            // 建立链接 这里会阻塞
            Socket socket = serverSocket.accept();
            System.out.println("服务端链接成功");
            new Thread(new SocketThread(socket)).start();
        }
    }
    static class SocketThread implements Runnable{
        byte[] bytes = new byte[1024];
        Socket socket;

        public SocketThread(Socket socket) {
            this.socket = socket;
        }

        @Override
        public void run() {
            try {
                socket.getInputStream().read(bytes);
                String data = new String(bytes);
                System.out.println("数据为:"+data);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

改造Server端后,重新运行代码

先启动Server端
image.png

再启动一个Client端
image.png
一个连接已经建立成功,

再启动一个Client端
image.png
两个连接已经完成。解决了只能单个连接,解决了并发的问题。

多线程的问题

但是但是,假设假设,现在很多很多用户,都来连接,但是,大部分用户都不发消息,考虑下这个方案的影响。

  1. 在任何时候都可能有大量的线程处于休眠状态,虽然已经建立了连接,但是都是在等待输入或者输出数据就绪,这算是一种资源浪费。
  2. 需要为每个线程的调用栈都分配内存。
  3. 即使JVM在物理上可以支持非常大数量的线程,但是在达到该极限之前,上下文切换所带来的开销就会带来很多麻烦。

这种多线程并发方案对于支撑中小数量的客户端可能还可以接受,但是为了支撑大量用户(如十万、百万。。)连接所需要的资源会使得它非常不理想。

学习来源《Netty实战》