原理和实战完美诠释NIO的强大之处

平时工作中,很大部分时间都投入了业务。我们对于一些框架、设计思想等都没有太去的关注,第一个深入一个技术底层是比较枯燥与孤独的;第二个就是没有人带领去用一个有趣或者通俗易懂的教导;但如果真的是明白了那些大牛们的思维方式,我们都会异口同声的称赞他们,真的就是牛逼啊。

学习底层的一些原理知识,我建议有2种方式:

1、多看源代码,在源代码中与之前接触到的理论相结合,最后会恍然大悟;

2、多跟大神们交流,在没接触之前,你不会觉得自己有多菜;

之前写过的Demo:

案例1:https://github.com/chengcheng222e/io-learn.git

案例2:https://github.com/chengcheng222e/vernon-socket

案例3:https://github.com/chengcheng222e/vernon-netty.git

1、Socket通信编程

1.1 只能接受一次连接的代码块

package com.cyblogs.io.learn.bio;

import java.io.IOException;  
import java.net.ServerSocket;  
import java.net.Socket;  
import java.util.Scanner;

public class BIOServer {

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket=new ServerSocket(8080);
        System.out.println("BIOServer has started,listening on port:" + serverSocket.getLocalSocketAddress());

        Socket clientSocket = serverSocket.accept(); //等待被接受
        System.out.println("Conection from " + clientSocket.getRemoteSocketAddress());

        Scanner input=new Scanner(clientSocket.getInputStream());
        String request=input.nextLine();
        System.out.println(request);

        String response="From BIOServer response: " + request + "\n";
        clientSocket.getOutputStream().write(response.getBytes());
    }
}

我们启动服务,然后通过telnet的方式来摸你client来发起通讯。

启动服务

http://static.cyblogs.com/QQ%E6%88%AA%E5%9B%BE20181125101907.png

连接服务

http://static.cyblogs.com/QQ%E6%88%AA%E5%9B%BE20181125101939.png

服务端返回

http://static.cyblogs.com/QQ%E6%88%AA%E5%9B%BE20181125102005.png

这里就会发现,如果我们不去发起一个连接,那么服务端就就会一直停留在这里。

Scanner input=new Scanner(clientSocket.getInputStream());  

1.2 接受多次连接的代码块

但这个程序有问题,就是接受到一个请求后,代码就接走完了。主进程main函数会立马跳出。我们改造一下,希望得到的效果是,我们可以通过多个client去连接服务端。

package com.cyblogs.io.learn.bio;

import java.io.IOException;  
import java.net.ServerSocket;  
import java.net.Socket;  
import java.util.Scanner;

public class BIOServer {

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8080);
        System.out.println("BIOServer has started,listening on port:" + serverSocket.getLocalSocketAddress());

        while (true) {
            Socket clientSocket = null;
            try {
                clientSocket = serverSocket.accept(); //等待被接受
                System.out.println("Conection from " + clientSocket.getRemoteSocketAddress());

                Scanner input = new Scanner(clientSocket.getInputStream());
                while (true) {
                    if (input.hasNext()) {
                        String request = input.nextLine();
                        if ("quit".equals(request)) {
                            break;
                        }
                        System.out.println(request);
                        String response = "From BIOServer response " + request + "\n";
                        clientSocket.getOutputStream().write(response.getBytes());
                    }
                }
            } finally {
                if (clientSocket != null) clientSocket.close();
            }
        }
    }
}

这类就很能明显的看出来。当多个客户端连接之后,之后之前的客户端退出之后,后面的的客户端才能关于服务端进行交互。

http://static.cyblogs.com/QQ%E6%88%AA%E5%9B%BE20181125114150.png

上述的例子就是一个简单的例子来描述一个IO的阻塞,而且非常的浪费资源。原因为是只有一个线程,而且还是阻塞的。

2、如何提高性能?

http://static.cyblogs.com/QQ%E6%88%AA%E5%9B%BE20181125133712.png

如上图所示,之前说是有一个线程在做核心业务的处理而且是阻塞的。那如果说每次来一个请求,我都单独分出一个线程来做。但是为了考虑到每次来都分配一个,到时候CPU会处理不过来,我就利用一个线程池来管理一个固定大小的线程池。如果超过了我的处理能力,我就让它排队。感觉上这个思维比之前的就好很多,那具体我们再看看改造后的代码。

http://static.cyblogs.com/QQ%E6%88%AA%E5%9B%BE20181125152920.png

package com.cyblogs.io.learn.bio;

import java.io.IOException;  
import java.net.ServerSocket;  
import java.net.Socket;  
import java.util.concurrent.ExecutorService;  
import java.util.concurrent.Executors;

public class BIOServer {

    public static void main(String[] args) throws IOException {
        ExecutorService executor = Executors.newFixedThreadPool(3);

        ServerSocket serverSocket = new ServerSocket(8080);
        System.out.println("BIOServer has started,listening on port:" + serverSocket.getLocalSocketAddress());

        RequestHandler requestHandler = new RequestHandler();// 为了让多线程去切换
        while (true) {
            Socket clientSocket = null;
            clientSocket = serverSocket.accept(); //等待被接受
            System.out.println("Conection from " + clientSocket.getRemoteSocketAddress());
            // 多线程处理
            executor.submit(new ClientHandler(clientSocket, requestHandler));
        }
    }
}
package com.cyblogs.io.learn.bio;

public class RequestHandler {

    public String handler(String request) {
        return "From BIOServer response " + request + "\n";
    }
}
package com.cyblogs.io.learn.bio;

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

public class ClientHandler implements Runnable {

    private Socket clientSocket;
    private RequestHandler requestHandler;

    public ClientHandler(Socket clientSocket, RequestHandler requestHandler) {
        this.clientSocket = clientSocket;
        this.requestHandler = requestHandler;
    }


    public void run() {
        try {
            Scanner input = new Scanner(clientSocket.getInputStream());
            while (true) {
                if (input.hasNext()) {
                    String request = input.nextLine();
                    if ("quit".equals(request)) {
                        break;
                    }
                    System.out.println(request);
                    String response = requestHandler.handler(request);
                    clientSocket.getOutputStream().write(response.getBytes());
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);

        }
    }
}

为了摸你这个场景,我打开4个客户端。

http://static.cyblogs.com/QQ%E6%88%AA%E5%9B%BE20181125152828.png

第4个客户端会处理阻塞中,那么真正的堵塞在哪儿呢?

String request = input.nextLine();  

所以,之前的场景,不管如何都属于BIO的范畴,BIO最终还是存在瓶颈。为了提高性能,就出现了NIO

问题:之前的设计存在哪些设计缺陷?(留一个问题)

3、了解NIO的设计

之前的问题都是在于一直在等待用的输入,所以每个线程还是一直非常被浪费。所以,解决问题的关键在于能不能不要一直等用户的输入,而是在真正在输入的时候再创建出一个线程来处理。处理完毕后资源又会得到释放。

package com.cyblogs.io.learn.nio;

import com.cyblogs.io.learn.bio.RequestHandler;

import java.io.IOException;  
import java.net.InetSocketAddress;  
import java.nio.ByteBuffer;  
import java.nio.channels.SelectionKey;  
import java.nio.channels.Selector;  
import java.nio.channels.ServerSocketChannel;  
import java.nio.channels.SocketChannel;  
import java.util.Iterator;  
import java.util.Set;

public class NIOServer {  
    // Channel[Server Client] Selector Buffer
    public static void main(String[] args) throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress(8081));
        System.out.println("BIOServer has started, listening on port: " + serverSocketChannel.getLocalAddress());

        Selector selector = Selector.open();
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        RequestHandler requestHandler = new RequestHandler();
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        while (true) {
            // 整个过程,单线程只有这类是阻塞的
            int select = selector.select();
            if (select == 0) {
                continue;
            }
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                if (key.isAcceptable()) {
                    ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
                    SocketChannel clientChannel = serverChannel.accept();
                    System.out.println("Connection from " + clientChannel.getRemoteAddress());
                    clientChannel.configureBlocking(false);
                    clientChannel.register(selector, SelectionKey.OP_READ);
                }

                if (key.isReadable()) {
                    SocketChannel channel = (SocketChannel) key.channel();
                    channel.read(buffer);
                    String request = new String(buffer.array()).trim();
                    buffer.clear();
                    System.out.println(String.format("From %s : %s", channel.getRemoteAddress(), request));
                    String response = requestHandler.handler(request);
                    channel.write(ByteBuffer.wrap(response.getBytes()));
                }
                iterator.remove();
            }

        }
    }
}

但是,对于NIO最好修饰的一个框架就是Netty。可以参考一下上面vernon-netty的一个小demo。这边文章主要是初步了解一个BIO到NIO过渡的手写过程。后面我们再聊聊Netty是如何封装NIO的。

https://netty.io/

Netty is a NIO client server framework which enables quick and easy development of network applications such as protocol servers and clients. It greatly simplifies and streamlines network programming such as TCP and UDP socket server.