JDK源码-IO系列:IO多路复用示例以及JDK源码解析

测试代码:

Server:

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 {
    public static void main(String[] args) throws IOException {
        int port = 8080;
        if (args != null && args.length > 0) {
            try {
                port = Integer.valueOf(args[0]);
            } catch (NumberFormatException e) {
                // 采用默认值
            }
        }

        MultiplIOServer timeServer = new MultiplIOServer(port);
        new Thread(timeServer, "NIO-MultiplIOServer-001").start();
    }
}

class MultiplIOServer implements  Runnable{

    private Selector selector;

    private ServerSocketChannel servChannel;

    private volatile boolean stop;

    //在构造方法中进行资源初始化,创建多路复用器Selector、ServerSocketChannel,对Channel和TCP参数进行配置。
    //例如,将ServerSocketChannel设置为异步非阻塞模式,它的backlog设置为1024。
    //系统资源初始化成功后,将ServerSocket Channel注册到Selector,监听SelectionKey.OP_ACCEPT操作位;如果资源初始化失败(例如端口被占用),则退出。
    public MultiplIOServer(int port) {
        try {
            selector = Selector.open();
            servChannel = ServerSocketChannel.open();
            servChannel.configureBlocking(false);
            servChannel.socket().bind(new InetSocketAddress(port), 1024);
            servChannel.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("The time server is start in port : " + port);
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        }
    }

    public void stop() {
        this.stop = true;
    }

    @Override
    public void run() {
        while (!stop) {
            try {
                //在线程的run方法的while循环体中循环遍历selector,它的休眠时间为1s,
                //无论是否有读写等事件发生,selector每隔1s都被唤醒一次,selector也提供了一个无参的select方法。
                //当有处于就绪状态的Channel时,selector将返回就绪状态的Channel的SelectionKey集合,
                //通过对就绪状态的Channel集合进行迭代,可以进行网络的异步读写操作。
                selector.select(120000);
                Set selectedKeys = selector.selectedKeys();
                Iterator it = selectedKeys.iterator();
                SelectionKey key = null;
                while (it.hasNext()) {
                    key = (SelectionKey) it.next();
                    it.remove();
                    try {
                        handleInput(key);//这里可以用线程池启线程去单独处理客户端的请求业务
                    } catch (Exception e) {
                        if (key != null) {
                            key.cancel();
                            if (key.channel() != null)
                                key.channel().close();
                        }
                    }
                }
            } catch (Throwable t) {
                t.printStackTrace();
            }
        }

        // 多路复用器关闭后,所有注册在上面的Channel和Pipe等资源都会被自动去注册并关闭,所以不需要重复释放资源
        if (selector != null)
            try {
                selector.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
    }

    private void handleInput(SelectionKey key) throws IOException {

        if (key.isValid()) {
            //根据SelectionKey的操作位进行判断即可获知网络事件的类型,
            if (key.isAcceptable()) {
                //通过ServerSocketChannel的accept接收客户端的连接请求并创建SocketChannel实例,
                //完成上述操作后,相当于完成了TCP的三次握手,TCP物理链路正式建立。
                //注意,我们需要将新创建的SocketChannel设置为异步非阻塞,同时也可以对其TCP参数进行设置,
                //例如TCP接收和发送缓冲区的大小等,作为入门的例子,没有进行额外的参数设置。
                ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
                SocketChannel sc = ssc.accept();
                sc.configureBlocking(false);
                // Add the new connection to the selector
                sc.register(selector, SelectionKey.OP_READ);
            }
            if (key.isReadable()) {
                //首先创建一个ByteBuffer,由于我们事先无法得知客户端发送的码流大小,
                //作为例程,我们开辟一个1M的缓冲区。然后调用SocketChannel的read方法读取请求码流。
                //注意,由于我们已经将SocketChannel设置为异步非阻塞模式,因此它的read是非阻塞的。
                //使用返回值进行判断,看读取到的字节数
                SocketChannel sc = (SocketChannel) key.channel();
                ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                int readBytes = sc.read(readBuffer);
                //返回值有以下三种可能的结果
                //返回值大于0:读到了字节,对字节进行编解码;
                //返回值等于0:没有读取到字节,属于正常场景,忽略;
                //返回值为-1:链路已经关闭,需要关闭SocketChannel,释放资源。
                if (readBytes > 0) {
                    //当读取到码流以后,我们进行解码,首先对readBuffer进行flip操作,
                    //它的作用是将缓冲区当前的limit设置为position,position设置为0,用于后续对缓冲区的读取操作。
                    //然后根据缓冲区可读的字节个数创建字节数组,
                    //调用ByteBuffer的get操作将缓冲区可读的字节数组复制到新创建的字节数组中,
                    //最后调用字符串的构造函数创建请求消息体并打印。
                    //如果请求指令是"QUERY TIME ORDER"则把服务器的当前时间编码后返回给客户端
                    readBuffer.flip();
                    byte[] bytes = new byte[readBuffer.remaining()];
                    readBuffer.get(bytes);
                    String body = new String(bytes, "UTF-8");
                    System.out.println("The time server receive order : "
                            + body);
                    String currentTime = "QUERY TIME ORDER"
                            .equalsIgnoreCase(body) ? new java.util.Date(
                            System.currentTimeMillis()).toString()
                            : "BAD ORDER";
                    //异步发送应答消息给客户端
                    doWrite(sc, currentTime);
                } else if (readBytes < 0) {
                    // 对端链路关闭
                    key.cancel();
                    sc.close();
                } else
                    ; // 读到0字节,忽略
            }
        }
    }

    private void doWrite(SocketChannel channel, String response)
            throws IOException {
        //首先将字符串编码成字节数组,根据字节数组的容量创建ByteBuffer,
        //调用ByteBuffer的put操作将字节数组复制到缓冲区中,然后对缓冲区进行flip操作,
        //最后调用SocketChannel的write方法将缓冲区中的字节数组发送出去。
        //需要指出的是,由于SocketChannel是异步非阻塞的,它并不保证一次能够把需要发送的字节数组发送完,
        //此时会出现“写半包”问题,我们需要注册写操作,不断轮询Selector将没有发送完的ByteBuffer发送完毕,
        //可以通过ByteBuffer的hasRemain()方法判断消息是否发送完成。
        //此处仅仅是个简单的入门级例程,没有演示如何处理“写半包”场景。
        if (response != null && response.trim().length() > 0) {
            byte[] bytes = response.getBytes();
            ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
            writeBuffer.put(bytes);
            writeBuffer.flip();
            channel.write(writeBuffer);
        }
    }
}

Client代码

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.SocketChannel;
import java.util.Iterator;
import java.util.Set;


public class NioClient {
    public static void main(String[] args) {
        int port = 8080;
        new Thread(new TimeClientHandle("127.0.0.1", port), "TimeClient- 001").start();
    }
}


class TimeClientHandle implements Runnable {
    private String host;
    private int port;
    private Selector selector;
    private SocketChannel socketChannel;
    private volatile boolean stop;

    public TimeClientHandle(String host, int port) {
        this.host = host;
        this.port = port;
        try {
            selector = Selector.open();
            socketChannel = SocketChannel.open();
            socketChannel.configureBlocking(false);
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        }
    }

    @Override
    public void run() {
        try {
            doConnect();
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        }
        while (!stop) {
            try {
                selector.select(10000);
                Set selectedKeys = selector.selectedKeys();
                Iterator it = selectedKeys.iterator();
                SelectionKey key = null;
                while (it.hasNext()) {
                    key = (SelectionKey) it.next();
                    it.remove();
                    try {
                        handleInput(key);
                    } catch (Exception e) {
                        if (key != null) {
                            key.cancel();
                            if (key.channel() != null)
                                key.channel().close();
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
                System.exit(1);
            }
        }

        //线程退出循环后,我们需要对连接资源进行释放,以实现“优雅退出”.
        //由于多路复用器上可能注册成千上万的Channel或者pipe,如果一一对这些资源进行释放显然不合适。
        //因此,JDK底层会自动释放所有跟此多路复用器关联的资源。
        //多路复用器关闭后,所有注册在上面的Channel和Pipe等资源都会被自动去注册并关闭,所以不需要重复释放资源
        if (selector != null)
            try {
                selector.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
    }

    private void handleInput(SelectionKey key) throws IOException {
        //我们首先对SelectionKey进行判断,看它处于什么状态。
        if (key.isValid()) {
            // 判断是否连接成功
            SocketChannel sc = (SocketChannel) key.channel();
            //如果是处于连接状态,说明服务端已经返回ACK应答消息。
            //这时我们需要对连接结果进行判断,调用SocketChannel的finishConnect()方法,
            //如果返回值为true,说明客户端连接成功;如果返回值为false或者直接抛出IOException,说明连接失败。
            //在本例程中,返回值为true,说明连接成功。
            if (key.isConnectable()) {
                if (sc.finishConnect()) {
                    //将SocketChannel注册到多路复用器上,注册SelectionKey.OP_READ操作位,
                    //监听网络读操作,然后发送请求消息给服务端。
                    sc.register(selector, SelectionKey.OP_READ);
                    doWrite(sc);
                } else
                    System.exit(1);// 连接失败,进程退出
            }
            //客户端是如何读取时间服务器应答消息的。
            if (key.isReadable()) {
                //如果客户端接收到了服务端的应答消息,则SocketChannel是可读的,
                //由于无法事先判断应答码流的大小,我们就预分配1M的接收缓冲区用于读取应答消息,
                //调用SocketChannel的read()方法进行异步读取操作。由于是异步操作,所以必须对读取的结果进行判断。
                ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                int readBytes = sc.read(readBuffer);
                if (readBytes > 0) {
                    //如果读取到了消息,则对消息进行解码,最后打印结果。执行完成后将stop置为true,线程退出循环。
                    readBuffer.flip();
                    byte[] bytes = new byte[readBuffer.remaining()];
                    readBuffer.get(bytes);
                    String body = new String(bytes, "UTF-8");
                    System.out.println("Now is : " + body);
                    this.stop = true;
                } else if (readBytes < 0) {
                    // 对端链路关闭
                    key.cancel();
                    sc.close();
                } else
                    ; // 读到0字节,忽略
            }
        }

    }

    //首先对SocketChannel的connect()操作进行判断,如果连接成功,
    //则将SocketChannel注册到多路复用器Selector上,注册SelectionKey.OP_READ,
    //如果没有直接连接成功,则说明服务端没有返回TCP握手应答消息,
    //但这并不代表连接失败,我们需要将SocketChannel注册到多路复用器Selector上,
    //注册SelectionKey.OP_CONNECT,当服务端返回TCP syn-ack消息后,
    //Selector就能够轮询到这个SocketChannel处于连接就绪状态。
    private void doConnect() throws IOException {
        // 如果                                                                                                                                                                                               直接连接成功,则注册到多路复用器上,发送请求消息,读应答
        if (socketChannel.connect(new InetSocketAddress(host, port))) {
            socketChannel.register(selector, SelectionKey.OP_READ);
            doWrite(socketChannel);
        } else {
            socketChannel.register(selector, SelectionKey.OP_CONNECT);
        }
    }

    //构造请求消息体,然后对其编码,写入到发送缓冲区中,最后调用SocketChannel的write方法进行发送。
    //由于发送是异步的,所以会存在“半包写”问题。最后通过hasRemaining()方法对发送结果进行判断,
    //如果缓冲区中的消息全部发送完成,打印"Send order 2 server succeed."
    private void doWrite(SocketChannel sc) throws IOException {
        byte[] req = ("QUERY TIME ORDER "+Thread.currentThread().getName()).getBytes();
        ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
        writeBuffer.put(req);
        writeBuffer.flip();
        sc.write(writeBuffer);
        if (!writeBuffer.hasRemaining())
            System.out.println("Send order 2 server succeed. " + Thread.currentThread().getName());
    }
}

源码解读

服务端Server

创建Selector

Selector.open()

一、创建EPollSelectorImpl。
通过JNI调用linux的系统调用pipe函数,创建一个管道,得到“读fd”fd0,“写fd”fd1。

EPollSelectorImpl(SelectorProvider sp) throws IOException {
        super(sp);
        //通过JNI,调用linux内核系统pipe函数,与内核交互。创建fd0和fd1
        long pipeFds = IOUtil.makePipe(false);
        fd0 = (int) (pipeFds >>> 32);
        fd1 = (int) pipeFds;
        //创建EPollarrayWrapper。Java层的对象,核心思想,封装了linux内核的epoll生命周期的相关操作,包括epoll_create/ctl/wait等。实例化时会调用JNI的init方法
        pollWrapper = new EPollArrayWrapper();
        //最终调用linux的epoll_ctl将“读fd”的读事件添加到epfd中。
        pollWrapper.initInterrupt(fd0, fd1);
        fdToKey = new HashMap<>();
    }

EPollArrayWrapper

    void initInterrupt(int fd0, int fd1) {
        outgoingInterruptFD = fd1;
        incomingInterruptFD = fd0;
        epollCtl(epfd, EPOLL_CTL_ADD, fd0, EPOLLIN);
    }

ServerSocketChannel绑定并监听端口

			private ServerSocketChannel servChannel;
            servChannel = ServerSocketChannel.open();
            servChannel.configureBlocking(false);
            //bind最终回通过内核的系统调用bind方法,将servChannel的fd与监听的ip:port绑定。
            servChannel.socket().bind(new InetSocketAddress(port), 1024);

Channel注册到Selector

servChannel.register(selector, SelectionKey.OP_ACCEPT);

register的逻辑含义,拿到servChannel对应的fd,让EPollArrayWrapper,知道关心这个fd的事件。注意此时并没有跟linux内核打交道,不会将servChannel对应的fd添加到epfd中。

selector.Select

selector.select(120000)

关键两步:
1、updateRegistrations
2、updated = epollWait(pollArrayAddress, NUM_EPOLLEVENTS, timeout, epfd);

EPollArrayWrapper

    int poll(long timeout) throws IOException {
    		//由于上一步channel已经register到Selector中了,updateRegistrations方法,就是取出来所有注册的fd,通过与内核交互,调用epool_ctl将所有注册的fd,添加到epfd中。
        updateRegistrations();
        //与内核打交道,调用内核的epoll_wait方法,获取有数据的fd。
        updated = epollWait(pollArrayAddress, NUM_EPOLLEVENTS, timeout, epfd);
        for (int i=0; i<updated; i++) {
            if (getDescriptor(i) == incomingInterruptFD) {
                interruptedIndex = i;
                interrupted = true;
                break;
            }
        }
        return updated;
    }
    private void updateRegistrations() {
        synchronized (updateLock) {
            int j = 0;
            while (j < updateCount) {
                int fd = updateDescriptors[j];
                short events = getUpdateEvents(fd);
                boolean isRegistered = registered.get(fd);
                int opcode = 0;

                if (events != KILLED) {
                    if (isRegistered) {
                        opcode = (events != 0) ? EPOLL_CTL_MOD : EPOLL_CTL_DEL;
                    } else {
                        opcode = (events != 0) ? EPOLL_CTL_ADD : 0;
                    }
                    if (opcode != 0) {
                        epollCtl(epfd, opcode, fd, events);
                        if (opcode == EPOLL_CTL_ADD) {
                            registered.set(fd);
                        } else if (opcode == EPOLL_CTL_DEL) {
                            registered.clear(fd);
                        }
                    }
                }
                j++;
            }
            updateCount = 0;
        }
    }

客户端

connect方法

连接服务器关键代码:

socketChannel.connect(new InetSocketAddress(host, port))

本质上会调用Net.java中的native方法

    private static native int connect0(boolean preferIPv6,
                                       FileDescriptor fd,
                                       InetAddress remote,
                                       int remotePort)
        throws IOException;

底层会与Server建立tcp连接,经历TCP 3次握手,wireshark抓包如下:
在这里插入图片描述

Server唤醒

与此同时服务器侧。
selector.select(120000)会被唤醒。

selector.select(120000)
本质上是EpollArrayWrapper.epollWait函数阻塞。当有epoll管理的fd有事件时候,或者timeout事件到后会被唤醒。
	...	
	//selectedKeys()方法拿到有io事件的fd。对应到内核epoll_wait函数。就是拿到epoll_event。
Set selectedKeys = selector.selectedKeys();
Iterator it = selectedKeys.iterator();
SelectionKey key = null;
while (it.hasNext()) {
    key = (SelectionKey) it.next();
   ...
	//通过readyOps是连接事件。
	if (key.isAcceptable()) {
		ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
		//serverSocketChannel的accept方法,会创建一个FD。然后通过accept系统调用,得到客户端的Socket,并且封装一个新的SocketChannelImpl对象,该对象包含一个连接的local,remote的IP:port信息以及对应的fd等。
		SocketChannel sc = ssc.accept();
		sc.configureBlocking(false);
		//注册这个SocketChannelImpl到Selector中。
		sc.register(selector, SelectionKey.OP_READ);
	}

看上面的逻辑。是不是和Linux Epoll代码很相似。Linux IO多路复用 epoll模式

只是Linux epoll_wait收到客户端的连接后,通过accept拿到客户端的fd后,再通过epoll_ctl将客户端的fd添加到epoll的红黑树中,监听客户端fd。
而JDK这一段,register不会通过epoll_ctl调用内核。实在select的时候,才会取调用epoll_ctl添加客户端fd到红黑树。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值