前语

本篇文章会对Java中的网络IO模型的概念进行解说,并给出详细的Java代码完成,主要涉及如下部分。

  1. BIO(同步堵塞IO模型)的概念和Java编程完成;
  2. Non Blocking IO(同步非堵塞IO模型)的概念和Java编程完成;
  3. IO多路复用的概念;
  4. NIONew IO)的概念和Java编程完成。

在开始本篇文章内容之前,有一个简略的关于Socket的常识需求阐明:在进行网络通信的时候,需求一对Socket,一个运转于客户端,一个运转于服务端,一起服务端还会有一个服务端Socket,用于监听客户端的衔接。下图进行一个简略示意。

IO模型-服务端与客户端的socket模型图

那么整个通信流程如下所示。

  1. 服务端运转后,会在服务端创立listen-socketlisten-socket会绑定服务端的ipport,然后服务端进入监听状况;
  2. 客户端恳求服务端时,客户端创立connect-socketconnect-socket描绘了其要衔接的服务端的listen-socket,然后connect-socketlisten-socket建议衔接恳求;
  3. connect-socketlisten-socket成功衔接后(TCP三次握手成功),服务端会为已衔接的客户端创立一个代表该客户端的client-socket,用于后续和客户端进行通信;
  4. 客户端与服务端通过socket进行网络IO操作,此刻就完成了客户端和服务端中的不同进程的通信。

需求知道的便是,在客户端与服务端通信的过程中,出现了三种socket,分别是。

  1. listen-socket。是服务端用于监听客户端建立衔接的socket
  2. connect-socket。是客户端用于衔接服务端的socket
  3. client-socket。是服务端监听到客户端衔接恳求后,在服务端生成的与客户端衔接的socket

(注:上述中的socket,能够被称为套接字,也能够被称为文件描绘符。)

正文

一. BIO

BIO,即同步堵塞IO模型。用户进程调用read时建议IO操作,此刻用户进程由用户态转换到内核态,只有在内核态中将IO操作履行完后,才会从内核态切换回用户态,这期间用户进程会一向堵塞。

BIO示意图如下。

IO模型-同步堵塞IO模型图

简略的BIOJava编程完成如下。

服务端完成

public class BioServer {
    public static void main(String[] args) throws IOException {
        // 创立listen-socket
        ServerSocket listenSocket = new ServerSocket(8080);
        // 进入监听状况,是一个堵塞状况
        // 有客户端衔接时从监听状况回来
        // 并创立代表这个客户端的client-socket
        Socket clientSocket = listenSocket.accept();
        // 获取client-socket输入流
        BufferedReader bufferedReader = new BufferedReader(
                new InputStreamReader(clientSocket.getInputStream()));
        // 读取客户端发送的数据
        // 假如数据没准备好,会进入堵塞状况
        String data = bufferedReader.readLine();
        System.out.println(data);
        // 获取client-socket输出流
        BufferedWriter bufferedWriter = new BufferedWriter(
                new OutputStreamWriter(clientSocket.getOutputStream()));
        // 服务端向客户端发送数据
        bufferedWriter.write("来自服务端的回来数据\n");
        // 改写流
        bufferedWriter.flush();
    }
}

客户端完成

public class BioClient {
    public static final String SERVER_IP = "127.0.0.1";
    public static final int SERVER_PORT = 8080;
    public static void main(String[] args) throws IOException {
        // 客户端创立connect-socket
        Socket connectSocket = new Socket(SERVER_IP, SERVER_PORT);
        // 获取connect-socket输出流
        BufferedWriter bufferedWriter = new BufferedWriter(
                new OutputStreamWriter(connectSocket.getOutputStream()));
        // 客户端向服务端发送数据
        bufferedWriter.write("来自客户端的恳求数据\n");
        // 改写流
        bufferedWriter.flush();
        // 获取connect-socket输入流
        BufferedReader bufferedReader = new BufferedReader(
                new InputStreamReader(connectSocket.getInputStream()));
        // 读取服务端发送的数据
        String returnData = bufferedReader.readLine();
        System.out.println(returnData);
    }
}

BIO的问题就在于服务端在accept时是堵塞的,并且在主线程中,一次只能accept一个SocketacceptSocket后,读取客户端数据时又是堵塞的。

二. Non Blocking IO

Non Blocking IO,即同步非堵塞IO。是用户进程调用read时,用户进程由用户态转换到内核态后,此刻假如没有体系资源数据能够被读取到内核缓冲区中,回来read失利,并从内核态切换回用户态。也便是用户进程建议IO操作后会当即得到一个操作成果。

Non Blocking IO示意图如下所示。

IO模型-同步非堵塞IO模型图

简略的Non Blocking IOJava编程完成如下。

public class NonbioServer {
    public static final List<SocketChannel> clientSocketChannels = new ArrayList<>();
    public static void main(String[] args) throws Exception {
        // 客户端创立listen-socket管道
        // 管道支撑非堵塞形式和一起读写
        ServerSocketChannel listenSocketChannel = ServerSocketChannel.open();
        // 设置为非堵塞形式
        listenSocketChannel.configureBlocking(false);
        // 绑定监听的端口号
        listenSocketChannel.socket().bind(new InetSocketAddress(8080));
        // 在子线程中遍历clientSocketChannels并读取客户端数据
        handleSocketChannels();
        while (true) {
            // 非堵塞方式监听客户端衔接
            // 假如无客户端衔接则回来空
            // 有客户端衔接则创立代表这个客户端的client-socket管道
            SocketChannel clientSocketChannel = listenSocketChannel.accept();
            if (clientSocketChannel != null) {
                // 设置为非堵塞形式
                clientSocketChannel.configureBlocking(false);
                // 添加到clientSocketChannels中
                // 用于子线程遍历并读取客户端数据
                clientSocketChannels.add(clientSocketChannel);
            } else {
                LockSupport.parkNanos(1000 * 1000 * 1000);
            }
        }
    }
    public static void handleSocketChannels() {
        new Thread(() -> {
            while (true) {
                // 遍历每一个client-socket管道
                Iterator<SocketChannel> iterator = clientSocketChannels.iterator();
                while (iterator.hasNext()) {
                    SocketChannel clientSocketChannel = iterator.next();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    int read = 0;
                    try {
                        // 将客户端发送的数据读取到ByteBuffer中
                        // 这一步的操作也对错堵塞的
                        read = clientSocketChannel.read(byteBuffer);
                    } catch (IOException e) {
                        // 移除产生反常的client-socket管道
                        iterator.remove();
                        e.printStackTrace();
                    }
                    if (read == 0) {
                        System.out.println("客户端数据未安排妥当");
                    } else {
                        System.out.println("客户端数据为:" + new String(byteBuffer.array()));
                    }
                }
                LockSupport.parkNanos(1000 * 1000 * 1000);
            }
        }).start();
    }
}

上述是Non Blocking IO的一个简略服务端的完成,相较于BIO,服务端在accept时对错堵塞的,在读取客户端数据时也对错堵塞的,但是还是存在如下问题。

  1. 一次只能accept一个Socket
  2. 需求在用户进程中遍历一切的SocketChannel并调用read() 办法获取客户端数据,此刻假如客户端数据未准备安排妥当,那么这一次的read() 操作的开销便是浪费的。

三. IO多路复用

在上述的BIONon Blocking IO中,一次体系调用,只会获取一个IO的状况,而假如采纳IO多路复用机制,则能够一次体系调用获取多个IO的状况。

也便是获取多个IO的状况能够复用一次体系调用。

最简略的IO多路复用方式是依据select模型完成,过程如下。

  1. 在用户进程中将需求监控IO文件描绘符(Socket)注册到IO多路复用器中;
  2. 履行select操作,此刻用户进程由用户态转换到内核态(一次体系调用),然后在内核态中会轮询注册到IO多路复用器中的IO是否准备安排妥当,并得到一切准备安排妥当的IO的文件描绘符列表,最终回来这些文件描绘符列表;
  3. 用户进程在select操作回来前会一向堵塞,直至select操作回来,此刻用户进程就获得了一切安排妥当的IO的文件描绘符列表;
  4. 用户进程获得了安排妥当的IO的文件描绘符列表后,就能够对这些IO进行相应的操作了。

换言之,IO多路复用中,只需求一次体系调用,IO多路复用器就能够告诉用户进程,哪些IO现已准备安排妥当能够进行操作了,而假如不选用IO多路复用,则需求用户进程自己遍历每个IO并调用accept() 或许read() 办法去判别,且一次accept() 或许read() 办法调用只能判别一个IO

四. NIO

NIO,即New IO。关于NIO,有如下三大组件。

  1. channel(管道)。介于buffer(字节缓冲区)和Socket(套接字)之间,用于数据的读写操作;
  2. buffer(字节缓冲区)。是用户程序和channel(管道)之间进行读写数据的中心区域;
  3. selectorIO多路复用器)。服务端的listen-socketclient-socket,客户端的connect-socket,都能够注册在selector上,注册的时候还需求指定监听的事情,比如为listen-socket指定监听的事情为ACCEPT事情,该事情产生则表明客户端建立了衔接,还比如为client-socket指定监听的事情为READ事情,该事情产生则表明客户端发送的数据现已可读。

NIO的代码完成如下所示。

服务端完成

public class NioServer {
    private static Selector selector;
    public static void main(String[] args) {
        try {
            // 敞开并得到多路复用器
            selector = Selector.open();
            // 服务端创立listen-socket管道
            ServerSocketChannel listenSocketChannel = ServerSocketChannel.open();
            // 设置为非堵塞形式
            listenSocketChannel.configureBlocking(false);
            // 为管道绑定端口
            listenSocketChannel.socket().bind(new InetSocketAddress(8080));
            // 将listen-socket管道注册到多路复用器上,并指定监听ACCEPT事情
            listenSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            while (true) {
                // 获取产生的事情,这个操作是堵塞的
                selector.select();
                // 拿到有事情产生的SelectionKey调集
                // SelectionKey表明管道与多路复用器的绑定联系
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                // 遍历每个产生的事情,然后判别事情类型
                // 依据事情类型,进行不同的处理
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    iterator.remove();
                    if (selectionKey.isAcceptable()) {
                        // 处理客户端衔接事情
                        handlerAccept(selectionKey);
                    } else if (selectionKey.isReadable()) {
                        // 处理客户端数据可读事情
                        handlerRead(selectionKey);
                    }
                }
                LockSupport.parkNanos(1000 * 1000 * 1000);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    private static void handlerAccept(SelectionKey selectionKey) {
        // 从事情中获取到listen-socket管道
        ServerSocketChannel listenSocketChannel = (ServerSocketChannel) selectionKey.channel();
        try {
            // 为衔接的客户端创立client-socket管道
            SocketChannel clientSocketChannel = listenSocketChannel.accept();
            // 设置为非堵塞形式
            clientSocketChannel.configureBlocking(false);
            // 将client-socket管道注册到多路复用器上,并指定监听READ事情
            clientSocketChannel.register(selector, SelectionKey.OP_READ);
            // 给客户端发送数据
            clientSocketChannel.write(ByteBuffer.wrap("衔接已建立\n".getBytes()));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    private static void handlerRead(SelectionKey selectionKey) {
        // 从事情中获取到client-socket管道
        SocketChannel clientSocketChannel = (SocketChannel) selectionKey.channel();
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        try {
            // 读取客户端数据
            int read = clientSocketChannel.read(byteBuffer);
            if (read <= 0) {
                // 封闭管道
                clientSocketChannel.close();
                // 从多路复用器移除绑定联系
                selectionKey.cancel();
            } else {
                System.out.println(new String(byteBuffer.array()));
            }
        } catch (IOException e1) {
            try {
                // 封闭管道
                clientSocketChannel.close();
            } catch (IOException e2) {
                e2.printStackTrace();
            }
            // 从多路复用器移除绑定联系
            selectionKey.cancel();
            e1.printStackTrace();
        }
    }
}

客户端完成

public class NioClient {
    private static Selector selector;
    public static final String SERVER_IP = "127.0.0.1";
    public static final int SERVER_PORT = 8080;
    public static void main(String[] args) {
        try {
            // 敞开并得到多路复用器
            selector = Selector.open();
            // 创立connect-socket管道
            SocketChannel connectSocketChannel = SocketChannel.open();
            // 设置为非堵塞形式
            connectSocketChannel.configureBlocking(false);
            // 设置服务端IP和端口
            connectSocketChannel.connect(new InetSocketAddress(SERVER_IP, SERVER_PORT));
            // 将connect-socket管道注册到多路复用器上,并指定监听CONNECT事情
            connectSocketChannel.register(selector, SelectionKey.OP_CONNECT);
            while (true) {
                // 获取产生的事情,这个操作是堵塞的
                selector.select();
                // 拿到有事情产生的SelectionKey调集
                // SelectionKey表明管道与多路复用器的绑定联系
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                // 遍历每个产生的事情,然后判别事情类型
                // 依据事情类型,进行不同的处理
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    iterator.remove();
                    if (selectionKey.isConnectable()) {
                        // 处理衔接建立事情
                        handlerConnect(selectionKey);
                    } else if (selectionKey.isReadable()) {
                        // 处理服务端数据可读事情
                        handlerRead(selectionKey);
                    }
                }
                LockSupport.parkNanos(1000 * 1000 * 1000);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    private static void handlerConnect(SelectionKey selectionKey) throws IOException {
        // 拿到connect-socket管道
        SocketChannel connectSocketChannel = (SocketChannel) selectionKey.channel();
        if (connectSocketChannel.isConnectionPending()) {
            connectSocketChannel.finishConnect();
        }
        // 设置为非堵塞形式
        connectSocketChannel.configureBlocking(false);
        // 将connect-socket管道注册到多路复用器上,并指定监听READ事情
        connectSocketChannel.register(selector, SelectionKey.OP_READ);
        // 向服务端发送数据
        connectSocketChannel.write(ByteBuffer.wrap("客户端发送的数据\n".getBytes()));
    }
    private static void handlerRead(SelectionKey selectionKey) {
        // 拿到connect-socket管道
        SocketChannel connectSocketChannel = (SocketChannel) selectionKey.channel();
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        try {
            // 读取服务端数据
            int read = connectSocketChannel.read(byteBuffer);
            if (read <= 0) {
                // 封闭管道
                connectSocketChannel.close();
                // 从多路复用器移除绑定联系
                selectionKey.cancel();
            } else {
                System.out.println(new String(byteBuffer.array()));
            }
        } catch (IOException e1) {
            try {
                // 封闭管道
                connectSocketChannel.close();
            } catch (IOException e2) {
                e2.printStackTrace();
            }
            // 从多路复用器移除绑定联系
            selectionKey.cancel();
            e1.printStackTrace();
        }
    }
}

总结

本篇文章中所评论的IO模型,都是同步IO模型,而何谓同步异步,何谓堵塞非堵塞,能够总结如下。

  1. 同步异步同步IO表明需求在用户进程中自动的去问询操作体系数据是否准备好,假如没有准备好,还需求持续的问询,直到数据准备好停止;而异步IO则是在用户进程中只需求问询操作体系一次,后续数据准备好后操作体系会自动的将数据给到用户进程;
  2. 堵塞非堵塞堵塞IO便是建议一次体系调用后,会一向堵塞直到有成果回来;而非堵塞IO便是建议一次体系调用后,会当即得到一个回来成果。

实际上在Java中是有对异步IOAIO)做支撑,但是AIO依赖操作体系的底层完成,而目前LinuxAIO的支撑不成熟,所以AIO的运用并不多,像主流的网络使用结构Netty也都没有运用到AIO