从NIO到Netty

70 阅读9分钟

有了网络IO知识之后,开始看java层面是如何来对多路复用器进行封装.

单线程版多路复用

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.sql.ClientInfoStatus;
import java.util.Iterator;
import java.util.Set;

/**
 * Created by 祝程 on 12/25/21.
 */
public class MultiSingleThread {

    private ServerSocketChannel server;
    /**
     * 一个多路复用器的抽象, 底层可能是 select poll epoll
     */
    private Selector selector;

    private void initServer() {
        try {
            // 对应的系统调用是  socket = fd6
            server = ServerSocketChannel.open();
            server.configureBlocking(false);
            // 对应的系统调用是  bind(fd6,9090) && listen(fd6)
            server.bind(new InetSocketAddress(9090));

            /**
             * 如果是
             * select poll 则是在jvm层面开辟一个空间
             * epoll 则 调用系统调用的 epoll_create=fd4 开辟内核空间
             */
            selector = Selector.open();
            /**
             * 如果是 select poll 则 把 这个 文件描述符放入jvm空间
             * 如果是 epoll 则调用  epoll_ctl(fd4,fd6,read)
             */
            server.register(selector, SelectionKey.OP_ACCEPT);
        } catch (Exception e) {

        }
    }

    public void start() {
        initServer();
        System.out.println("server start");
        try {
            while (true) {
                Set<SelectionKey> keys = selector.keys();
                System.out.println("keysize....." + keys.size());

                /**
                 * 如果是 select poll 就是把 jvm里面的文件描述符传入到内核
                 * 内核做遍历之后返回一共有几个可以读写  select(fds)  poll(fds)
                 * 如果是 epoll 则是调用 epoll_wait 内核把链表空间的文件描述符返回
                 */
                while (selector.select(500) > 0) {
                    // 返回了有状态的 fd集合
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();
                    Iterator<SelectionKey> iter = selectionKeys.iterator();
                    while (iter.hasNext()) {
                        SelectionKey key = iter.next();
                        iter.remove();
                        /**
                         * 有数据的文件描述符可能是服务端的fd 那么就需要处理新的客户端链接
                         * 也有可能是客户端链接产生的数据,那么就需要进行业务处理
                         */
                        if (key.isReadable()) {
                            handlerRead(key);
                        } else if (key.isAcceptable()) {
                            handlerAccept(key);
                        }
                    }
                }
            }
        } catch (Exception e) {

        }
    }


    /**
     * 处理服务端收到的客户端链接请求
     *
     * @param key
     * @throws Exception
     */
    private void handlerAccept(SelectionKey key) throws Exception {
        // 收到新的客户端链接,需要把这个客户端注册到多路复用器中
        ServerSocketChannel channel = (ServerSocketChannel) key.channel();
        SocketChannel client = channel.accept();
        client.configureBlocking(false);
        ByteBuffer buf = ByteBuffer.allocate(8096);
        client.register(selector, SelectionKey.OP_READ, buf);
        System.out.println("新客户端链接" + client.getRemoteAddress());
    }

    /**
     * 处理客户端发送来的数据
     *
     * @param key
     * @throws Exception
     */
    private void handlerRead(SelectionKey key) throws Exception {
        SocketChannel client = (SocketChannel) key.channel();

        ByteBuffer byteBuffer = (ByteBuffer) key.attachment();
        byteBuffer.clear();
        while (true) {
            int read = client.read(byteBuffer);
            if (read > 0) {
                // 客户端发什么就往回写什么,这是属于具体业务范畴
                byteBuffer.flip();
                while (byteBuffer.hasRemaining()) {
                    client.write(byteBuffer);
                }
                byteBuffer.clear();
            } else if (read == 0) {
                break;
            } else {
                client.close();
                break;
            }
        }

    }

    public static void main(String[] args) {
        new MultiSingleThread().start();
    }
}

额外补充, 客户端断开,但是服务端没有close client会怎么样

客户端启动

首先以 poll 的方式启动服务器

strace -ff -o poll java -Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.PollSelectorProvider MultiSingleThread

查看服务端的文件描述符 4号对应的是服务端的serversocket的文件描述符. 在这里插入图片描述 查看网络状态,也可以看到9090正在被监听 在这里插入图片描述 推论当前系统调用

  1. socket() = 4
  2. bind(4,9090)
  3. listen(4)
  4. 不断的 循环 poll(4) 在这里插入图片描述 在这里插入图片描述

客户端链接

看到三次握手 在这里插入图片描述 网络状态看到服务端和客户端建立连接 在这里插入图片描述 查看服务端的文件描述符,发现多了一个 7 就是对于客户端这个socket的文件描述符 在这里插入图片描述 系统调用推断

  1. poll 返回了一个 4,然后 accept(4) = 7 . 表示serversocket4返回数据,然后读取4返回了一个文件描述符7
  2. 因为是poll 所以 java层面把7存在了jvm空间,接下来就是继续重复调用 poll,只是里面的fd多了7 在这里插入图片描述 在这里插入图片描述 验证通过

客户端登出

网络抓包看到 客户端给服务端发送了一个 fin 服务端回了一个fin之后就结束了 在这里插入图片描述 查看服务端文件描述符状态,看到7的状态变成 CLOSE_WAIT 在这里插入图片描述 网络状态也可以看到变为 CLOSE_WAIT 在这里插入图片描述 并且服务端一直在空轮询系统调用,不断读取到文件描述符7上面的空,因为服务端没有处理这个事件,所以一直被读取. 在这里插入图片描述

CLOSE_WAIT 含义

当一方收到了 FIN 请求之后,状态就会变成 CLOSE_WAIT, 自己如果也发出 FIN请求就会关闭该socket

TIME_WAIT 含义

当服务端开启了 client.close() 之后, 再次测试链接断开.发现客户端产生了 TIME_WAIT状态 在这里插入图片描述 客户端断开链接的过程如下,当客户端最后一次发送ACK的时候服务端直接关闭资源了,但是客户端不知道这个ack有没有到达服务端,所以等待两倍的MSL,自己到服务端最大一次MSL,服务端来最长一次MSL,超过这个时间就表示请求肯定送达了 在这里插入图片描述

TIME_WAIT影响

当一个客户端处于短链接场景中,建立一个socket发送一个请求然后马上断开.然后再建立请求.这时候 客户端 的socket可能有大量的处于 TIME_WAIT 状态的socket,这种socket也是不能被重复使用的.

epoll方式的调用流程

  1. socket bind listen 都不变 在这里插入图片描述
  2. 内核中开辟一个空间,文件描述符7来表示 在这里插入图片描述
  3. 把4放到7的空间中,然后开始监听7 在这里插入图片描述
  4. 客户端链接服务端后. epoll把7空间里面的 红黑树里面的4 挪到了 链表 里面, 调用epoll_wait 发现链表里面有数据,就返回了4的文件描述符. accept 4 读取到了客户端链接 8. 再把8用 epoll_ctl 放入红黑树7中 在这里插入图片描述

write 事件

之前的模型里面都只是处理 accept 事件和 read事件. 分别表示有新的客户端建立连接和已有客户端发送来数据. write事件的响应只依赖于 socket里面的 send-Q 是不是可以写入 在这里插入图片描述 所以注册write监听的事件时机是基于业务的 在之前的模型里面读取到了事件之后马上就对client进行了写操作,实际场景中读写可能是分离的.先读取到数据之后,觉得可以写了之后 再注册写事件,下次轮询就判断 send-Q 是不是可以写入,如果可以写入就进行写入操作.

多线程版多路复用器

在单线程的版本里面 读写操作可能包含很多业务逻辑 如果一个连接是处于一个耗时操作那么所有其他的连接都得等待. 所以设计一个模型是主线程处理连接任务,当有读写请求的时候放到子线程中处理.

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
/**
 * Created by 祝程 on 12/26/21.
 */
public class MultiThread {
    private ServerSocketChannel server;
    /**
     * 一个多路复用器的抽象, 底层可能是 select poll epoll
     */
    private Selector selector;
    private void initServer() {
        try {
            // 对应的系统调用是  socket = fd6
            server = ServerSocketChannel.open();
            server.configureBlocking(false);
            // 对应的系统调用是  bind(fd6,9090) && listen(fd6)
            server.bind(new InetSocketAddress(9090));
            /**
             * 如果是
             * select poll 则是在jvm层面开辟一个空间
             * epoll 则 调用系统调用的 epoll_create=fd4 开辟内核空间
             */
            selector = Selector.open();
            /**
             * 如果是 select poll 则 把 这个 文件描述符放入jvm空间
             * 如果是 epoll 则调用  epoll_ctl(fd4,fd6,read)
             */
            server.register(selector, SelectionKey.OP_ACCEPT);
        } catch (Exception e) {

        }
    }
    public void start() {
        initServer();
        System.out.println("server start");
        try {
            while (true) {
                Set<SelectionKey> keys = selector.keys();

                /**
                 * 如果是 select poll 就是把 jvm里面的文件描述符传入到内核
                 * 内核做遍历之后返回一共有几个可以读写  select(fds)  poll(fds)
                 * 如果是 epoll 则是调用 epoll_wait 内核把链表空间的文件描述符返回
                 */
                while (selector.select(500) > 0) {

                    // 返回了有状态的 fd集合
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();
                    Iterator<SelectionKey> iter = selectionKeys.iterator();
                    while (iter.hasNext()) {
                        SelectionKey key = iter.next();
                        iter.remove();
                        /**
                         * 有数据的文件描述符可能是服务端的fd 那么就需要处理新的客户端链接
                         * 也有可能是客户端链接产生的数据,那么就需要进行业务处理
                         */
                        if (key.isReadable()) {
                            key.cancel();
                            handlerRead(key);
                        } else if (key.isAcceptable()) {
                            handlerAccept(key);
                        }else if (key.isWritable()){
                            key.cancel();
                            handlerWrite(key);
                        }
                    }
                }
            }
        } catch (Exception e) {
            System.out.println("dd");
        }
        System.out.println("ds");
    }

    /**
     * 处理写事件。
     * 只要send-Q是空的就是可以写
     * @param key
     * @throws Exception
     */
    private void handlerWrite(SelectionKey key)throws Exception{
        new Thread(()->{
            try {
                System.out.println("write");
                ByteBuffer buffer = (ByteBuffer) key.attachment();
                buffer.flip();
                SocketChannel client = (SocketChannel) key.channel();
                client.write(buffer);
                buffer.clear();
                client.register(selector,SelectionKey.OP_READ,buffer);
            }catch (Exception e){

            }
        }).start();
    }
    /**
     * 处理服务端收到的客户端链接请求
     *
     * @param key
     * @throws Exception
     */
    private void handlerAccept(SelectionKey key) throws Exception {
        // 收到新的客户端链接,需要把这个客户端注册到多路复用器中
        ServerSocketChannel channel = (ServerSocketChannel) key.channel();
        SocketChannel client = channel.accept();
        client.configureBlocking(false);
        ByteBuffer buf = ByteBuffer.allocate(8096);
        client.register(selector, SelectionKey.OP_READ, buf);
        System.out.println("新客户端链接" + client.getRemoteAddress());
    }

    /**
     * 处理客户端发送来的数据
     *
     * @param key
     * @throws Exception
     */
    private void handlerRead(SelectionKey key) throws Exception {
        new Thread(()->{
            try {
                SocketChannel client = (SocketChannel) key.channel();

                ByteBuffer byteBuffer = (ByteBuffer) key.attachment();
                byteBuffer.clear();
                while (true) {
                    int read = client.read(byteBuffer);
                    if (read > 0) {
                        // 客户端发什么就往回写什么,这是属于具体业务范畴
                        client.register(selector,SelectionKey.OP_WRITE,byteBuffer);
                    } else if (read == 0) {
                        break;
                    } else {
                        client.close();
                        break;
                    }
                }
            }catch (Exception e){

            }

        }).start();
    }
    public static void main(String[] args) {
        new MultiThread().start();
        System.out.println("dsd");
    }
}

上面的模型属于单 selector 多线程版本. selector取到 fd之后开辟线程来处理业务,不阻塞对selector的轮询. 但是这个模型有一个缺点是: 因为 handlerRead 和 handlerWrite 都是在子线程当中,主线程下次轮询可能子线程的这个fd还没有处理完成, 内核中还是有相应的标志位,就可能导致重复消费. 所以需要不断的 key.cancel(); 然后在处理完读写请求之后再重新注册回selector. 这样就造成了很多的 ==系统调用==

多selector多线程模型

我们再来重新回想一下模型的推导过程. 在这里插入图片描述 多selector多线程模型的好处就是,既利用了多核cpu增加处理速度,又不会需要额外的 注销注册系统调用. ==创建多个 selector每个selector里面是单线程.== 多selector多线程模型代码

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;

/**
 * Created by 祝程 on 12/27/21.
 * 多selector模型
 */
public class MultiSelector {
    private Selector mainSelector;
    private Selector selector01;
    private Selector selector02;

    private void initServer() throws Exception {
        mainSelector = Selector.open();
        selector01 = Selector.open();
        selector02 = Selector.open();

        ServerSocketChannel server = ServerSocketChannel.open();
        server.bind(new InetSocketAddress(9090));
        server.configureBlocking(false);
        server.register(mainSelector, SelectionKey.OP_ACCEPT);
        new Thread(() -> {
            while (true) {
                try {
                    while (selector01.select(500) > 0) {
                        System.out.println("selector01监听到事件" + selector01.keys().size());
                        Set<SelectionKey> keys = selector01.selectedKeys();
                        Iterator<SelectionKey> iter = keys.iterator();
                        while (iter.hasNext()) {
                            SelectionKey key = iter.next();
                            iter.remove();
                            if (key.isReadable()) {
                                handlerRead(key);
                            } else if (key.isWritable()) {
                                handlerWrite(key);
                            }
                        }
                    }
                } catch (Exception e) {
                    System.out.println("f");
                }

            }
        }).start();
        new Thread(() -> {
            while (true) {
                try {
                    while (selector02.select(500) > 0) {
                        Set<SelectionKey> keys = selector02.selectedKeys();
                        Iterator<SelectionKey> iter = keys.iterator();
                        while (iter.hasNext()) {
                            SelectionKey key = iter.next();
                            SocketChannel client = (SocketChannel) key.channel();
                            ByteBuffer byteBuffer = (ByteBuffer) key.attachment();
                            if (key.isReadable()) {
                                handlerRead(key);
                            } else if (key.isWritable()) {
                                handlerWrite(key);
                            }
                        }
                    }
                } catch (Exception e) {

                }

            }
        }).start();
        System.out.println("server up 9090");
    }


    /**
     * 处理客户端发送来的数据
     *
     * @param key
     * @throws Exception
     */
    private void handlerRead(SelectionKey key) throws Exception {
        SocketChannel client = (SocketChannel) key.channel();

        ByteBuffer byteBuffer = (ByteBuffer) key.attachment();
        byteBuffer.clear();
        while (true) {
            int read = client.read(byteBuffer);
            if (read > 0) {
                // 客户端发什么就往回写什么,这是属于具体业务范畴
                byteBuffer.flip();
                client.register(key.selector(),SelectionKey.OP_WRITE,byteBuffer);
            } else if (read == 0) {
                break;
            } else {
                client.close();
                key.cancel();
                break;
            }
        }

    }

    private void handlerWrite(SelectionKey key) throws Exception {
        System.out.println("线程--" + Thread.currentThread().getName() + "收到write请求,开始写入");

        ByteBuffer buffer = (ByteBuffer) key.attachment();
        buffer.flip();
        SocketChannel client = (SocketChannel) key.channel();
        client.write(buffer);
        buffer.clear();

        key.cancel();
    }

    private void start() throws Exception {
        initServer();
        int i = 0;
        while (true) {

            while (mainSelector.select(1000) > 0) {

                Set<SelectionKey> keys = mainSelector.selectedKeys();
                Iterator<SelectionKey> iter = keys.iterator();
                while (iter.hasNext()) {

                    SelectionKey key = iter.next();
                    iter.remove();
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    SocketChannel client = channel.accept();
                    client.configureBlocking(false);
                    /**
                     * 分发到不同的selector中
                     */
                    ByteBuffer buffer = ByteBuffer.allocate(8096);
                    if (i % 2 == 0) {
                        System.out.println("线程--" + Thread.currentThread().getName() + "收到accept请求,开始分发 selector01");
                        client.register(selector01, SelectionKey.OP_READ, buffer);
                    } else {
                        System.out.println("线程--" + Thread.currentThread().getName() + "收到accept请求,开始分发 selector02");
                        client.register(selector02, SelectionKey.OP_READ, buffer);
                    }
                }
            }
        }
    }

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

        new MultiSelector().start();
    }

}

架构图 在这里插入图片描述 上面的架构图和netty的架构图就有那么一点意思了.

在这里插入图片描述