网络IO模型

阻塞式I/O

Java网络IO模型、阻塞与非阻塞、同步与异步_io

默认情况下,所有的套接字的方法都是阻塞的,如上面的accept、recv。

对应的代码如下:

package com.morris.bio;

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

public class BioSingleThreadServer {

    public static final int PORT = 8899;

    public static void main(String[] args) throws IOException {

        ServerSocket serverSocket = new ServerSocket(); // socket(AF_INET6, SOCK_STREAM, IPPROTO_IP) = 5

        serverSocket.bind(new InetSocketAddress(PORT)); // bind(5, {sa_family=AF_INET6, sin6_port=htons(8899), inet_pton(AF_INET6, "::", &sin6_addr), sin6_flowinfo=htonl(0), sin6_scope_id=0}, 28) = 0
        // bind()里面会调用listen方法 listen(5, 50)

        System.out.println("server is start at " + PORT); //

        while (true) {
            try {
                Socket socket = serverSocket.accept(); // accept(3, {sa_family=AF_INET6, sin6_port=htons(34270), inet_pton(AF_INET6, "::ffff:127.0.0.1", &sin6_addr), sin6_flowinfo=htonl(0), sin6_scope_id=0}, [28]) = 5
                InputStream inputStream = socket.getInputStream();
                System.out.println("connect success " + socket.getPort());

                BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));

                System.out.println("receive from client: " + reader.readLine()); // recv(5, "hello\r\n", 8192, 0)           = 7

                socket.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

    }
}

上面的代码需要在jdk1.4上面运行才会出现accept的阻塞,高版本的jdk会使用poll方法阻塞,poll方法返回后才会去调用accept获取连接。

当然使用NIO的阻塞模式也会出现上面的效果,只不过recv函数会缓存read函数。

非阻塞式I/O

Java网络IO模型、阻塞与非阻塞、同步与异步_网络_02

NIO需要调用套接字的方法前,将套接字设置为非阻塞模式,这样调用方法时就不会因为阻塞而进入休眠,而是立即返回,如果没有数据,就会返回一个错误。

对应的代码如下:

package com.morris.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;
import java.util.Objects;

public class NioSingleThreadServer {

    public static final int PORT = 8899;

    public static void main(String[] args) throws IOException, InterruptedException {

        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // socket
        serverSocketChannel.bind(new InetSocketAddress(PORT)); // bind+listen
        serverSocketChannel.configureBlocking(false); // 设置accept不阻塞
        LinkedList<SocketChannel> socketChannels = new LinkedList<>(); // 存放所有的客户端连接

        while (true) {
            Thread.sleep(1000);
            // 获取连接
            SocketChannel socketChannel = serverSocketChannel.accept(); // 不阻塞 accept
            if (Objects.isNull(socketChannel)) { // 没有连接会返回null
                System.out.println("null");
            } else {
                System.out.println("connect success: " + socketChannel.socket().getPort());
                socketChannel.configureBlocking(false); // 设置read不阻塞
                socketChannels.add(socketChannel);
            }

            // 读取数据
            ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
            for (SocketChannel s : socketChannels) {
                if (s.isOpen()) {
                    int readLength = s.read(byteBuffer); // 不阻塞,没数据返回-1  read
                    if (readLength > 0) {
                        byteBuffer.flip();
                        byte[] bytes = new byte[byteBuffer.limit()];
                        byteBuffer.get(bytes);
                        System.out.println("receive from client: " + new String(bytes));
                        s.close();
                        byteBuffer.clear();
                    }
                }
            }
        }
    }
}

I/O复用(select/poll/epoll)

Java网络IO模型、阻塞与非阻塞、同步与异步_epoll_03

虽然I/O多路复用的函数也是阻塞的,但是其与以上两种还是有不同的,I/O多路复用是阻塞在select,epoll这样的系统调用之上,而没有阻塞在真正的I/O系统调用如accept之上。

对应的代码如下:

package com.morris.nio;

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 NioSingleSelectorServer {

    public static final int PORT = 8899;

    private static Selector selector;

    public static void main(String[] args) throws IOException {

        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(PORT));
        serverSocketChannel.configureBlocking(false);
        selector = Selector.open();

        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); // 关心接受连接事件
        System.out.println("server is start at " + PORT);

        while (true) {

            while (selector.select() > 0) { // selector.select()不带时间会一直阻塞,可以带一个超时时间
                Set<SelectionKey> selectionKeys = selector.selectedKeys();

                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    iterator.remove();

                    if(key.isValid()) {
                        if(key.isAcceptable()) {
                            ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
                            // select只会返回有数据的FD,真正获取连接和读取数据还需要调用accept和read
                            SocketChannel socketChannel = ssc.accept();
                            System.out.println("connect success: " + socketChannel.socket().getPort());
                            socketChannel.configureBlocking(false);
                            socketChannel.register(selector, SelectionKey.OP_READ);
                        }

                        if(key.isReadable()) {
                            SocketChannel socketChannel = (SocketChannel) key.channel();
                            ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
                            socketChannel.read(byteBuffer);

                            byteBuffer.flip();

                            byte[] bytes = new byte[byteBuffer.limit()];
                            byteBuffer.get(bytes);

                            System.out.println("receive from client: " + new String(bytes));
                            socketChannel.close();
                        }
                    }
                }
            }
        }
    }
}

异步I/O

Java网络IO模型、阻塞与非阻塞、同步与异步_epoll_04

异步IO的工作机制是告知内核启动某个操作,并让内核在整个操作(包括将数据从内核拷贝到用户空间)完成后通知我们。

总结

阻塞与非阻塞:调用方法能立刻返回就是非阻塞,调用方法要等有数据了才返回就是阻塞。

同步与异步:访问数据的方式,同步需要主动读写数据,在读写数据的过程中还是会阻塞;异步只需要I/O操作完成的通知,并不主动读写数据,由操作系统内核完成数据的读写。