从BIO和NIO到Netty实践


一、什么是BIO

传统的BIO模型使用是java.net包中API实现Socket通信,而传统的BIO通信中阻塞共发现在两个 地方。

服务端ServerSocker::accept
客户端Socket数据读写

这种IO的弊端即是无法处理并发的客户端请求,因此可以通过为每个客户端单独分配一个线程,则客户的端的Socket数据读写不再阻塞新的连接请求,可以满足并发的客户端请求。

同样的在高并发,大量客户端连接造成大量线程,容易产生线程OOM,同时也有大量的线程上下文切换影响性能。

二、什么是NIO

  1. 在JAVA中NIO是指new IO,是JDK为实现非阻塞IO实现的一套新API
  2. 在linux中NIO是指非阻塞的IO,主要与poll和epoll内核调用有关

JAVA的NIO一种重要的方法为configureBlocking,示例代码如下:

package com.zte.sunquan.nio;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.List;

public class SocketServerNIO {
    public static void main(String[] args) throws Exception {
        List<SocketChannel> channels = new ArrayList<>();
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.bind(new InetSocketAddress(9090));
        ssc.configureBlocking(false);
        //非阻塞
        while (true) {
            Thread.sleep(1000);
            //不阻塞,但需要每次都问一下内核
            SocketChannel clientSocket = ssc.accept();
            if (clientSocket == null) {
                System.out.println("No client....");
            } else {
                //将clientSocket保留
                clientSocket.configureBlocking(false);
                System.out.println("client connected in:" + clientSocket.socket().getPort());
                channels.add(clientSocket);
            }

            ByteBuffer buffer = ByteBuffer.allocate(4096);
            //不阻塞,但这里每次也需要问一下内核,即使有些客户端没有事件
            for (SocketChannel channel : channels) {
                int num = channel.read(buffer);
                if (num > 0) {
                    buffer.flip();
                    byte[] content=new byte[buffer.limit()];
                    buffer.get(content);
                    String s = new String(content);
                    System.out.println(channel.socket().getPort()+" Read client msg:"+s);
                    buffer.clear();
                }
            }

        }
    }
}

如上代码中使用单个线程负责了IO读写和请求连接响应建立,当然可以优雅一点的方法,即将连接建立与IO读写分线程处理,让IO的读写不影响高并发下连接请求与建立。但上述代码仍有明显的弊端。考虑:

for (SocketChannel channel : channels) {
int num = channel.read(buffer);

在上述代码中,每次循环都要与所有建立的客户端进行一次read操作,涉及用户究竟与内核空间的切换,考虑在连接数特别多背景下,一次可能只会有几个连接有IO事件,如上的实现会造成大量的性能浪费。
那有没有一种可能,让内核主动告知我们哪些连接有IO事件,这样应用精确地去指定的连接上进行IO事件的处理,而不是傻傻地每个连接read一遍?

三、多路复用器

多路复用器使用,可以解决第二节最后的问题,通过selector.select,内核只会将有事件的socket返回,避免了应用循环遍历尝试。

下面示例代码描述了使用JAVA中NIO的API实现的服务端代码

package com.zte.sdn.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 NioServer {

    private Selector startServer() throws IOException {
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.bind(new InetSocketAddress(9090));
        ssc.configureBlocking(false);
        //打开一个多路复用器
        //poll系统调用:
        //epoll系统调用:
        Selector selector = Selector.open();
        ssc.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("Start server at port:9090");
        return selector;
    }


    public static void main(String[] args) throws IOException {
        NioServer nioServer = new NioServer();
        Selector selector = nioServer.startServer();
        while (true) {
            System.out.println("ask");
            //使用select向内核询问是否有事件(由于一开始只注册了ServerSocket的OP_ACCEPT)
            //所以第一次只判断是否有连接事件
            //后面由于注册客户端OP_READ,从面判断是否有可读事件
            while (selector.select(10) > 0) {
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    iterator.remove();
                    nioServer.handler(selectionKey, selector);
                }
            }
        }
    }

    private void handler(SelectionKey selectionKey, Selector selector) throws IOException {
        if (selectionKey.isAcceptable()) {
            //一个连接事件
            ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel();
            SocketChannel client = channel.accept();
            client.configureBlocking(false);
            //再注册进去
            client.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(8192));
            System.out.println("receive connect:" + client.getRemoteAddress());
        } else if (selectionKey.isReadable()) {
            //可读事件
            SocketChannel client = (SocketChannel) selectionKey.channel();
            ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
            buffer.clear();
            int read = 0;
            while (true) {
                read = client.read(buffer);
                if (read > 0) {
                    buffer.flip();
                    while (buffer.hasRemaining()) {
                        //讲到内容写回客户端
                        client.write(buffer);
                    }
                    System.out.println("receive client msg:" + new String(buffer.array()));
                    buffer.clear();
                } else if (read == 0) {
                    break;
                } else {
                    client.close();
                    break;
                }
            }
        }
    }
}

在上述示例中,一个线程完成了服务端接口客户端连接以及IO读写动作。思考上面编程思路的弊端是什么?
考虑到一个IO的读写如何非常耗时,必然会影响客户端建立连接并发性能,以及大量IO读写的性能。
自然地针对客户端连接与IO读写可以分不同selector单独处理,各司其职,所以改进的实现如下:

  else if (selectionKey.isReadable()) {
            executorService.submit(()->{
                //可读事件
                try {
                    SocketChannel client = (SocketChannel) selectionKey.channel();
                    ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
                    buffer.clear();
                    int read = 0;
                    while (true) {
                        read = client.read(buffer);
                        if (read > 0) {
                            buffer.flip();
                            while (buffer.hasRemaining()) {
                                //讲到内容写回客户端
                                client.write(buffer);
                            }
                            System.out.println("receive client msg:" + new String(buffer.array()));
                            buffer.clear();
                        } else if (read == 0) {
                            break;
                        } else {
                            client.close();
                            break;
                        }
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }
            });

        }

如上所示,实现了IO的处理异步化。但上述实现虽然缓解了大量长时间IO带来的性能问题,但不能从根本上解决,那有没有办法,将客户端连接事件与IO事件完全分离开?当然如果IO读取的数据业务处理比较耗时,则可以另起线程再进行异步处理。

四、多Selector版本

代码:

package com.zte.sdn.nio;

import java.io.IOException;
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 IOHandler extends Thread {
    private Selector selector;

    public IOHandler(Selector selector, String name) {
        this.selector = selector;
        this.setName(name);
    }

    @Override
    public void run() {
        while (true) {
            try {
                handler();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }


    private void handler() throws IOException {
        while (selector.select(10) > 0) {
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();
                iterator.remove();
                if (selectionKey.isReadable()) {
                    SocketChannel client = (SocketChannel) selectionKey.channel();
                    ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
                    buffer.clear();
                    int read = 0;
                    while (true) {
                        try {
                            read = client.read(buffer);
                            if (read > 0) {
                                buffer.flip();
                                while (buffer.hasRemaining()) {
                                    //讲到内容写回客户端
                                    client.write(buffer);
                                }
                                System.out.println(Thread.currentThread().getName() + " receive client msg:" + new String(buffer.array()));
                                buffer.clear();
                            } else if (read == 0) {
                                break;
                            } else {
                                client.close();
                                break;
                            }
                        } catch (Exception e) {
                            try {
                                client.close();
                            } catch (IOException ex) {
                                ex.printStackTrace();
                            }
                            break;
                        }

                    }
                }
            }
        }
    }
}

MultiNioServer

package com.zte.sdn.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;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;


/**
 * 多路复用器示例代码
 **/
public class MultiNioServer {

    private ExecutorService executorService = Executors.newFixedThreadPool(5);

    private Selector startServer() throws IOException {
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.bind(new InetSocketAddress(9090));
        ssc.configureBlocking(false);
        //打开一个多路复用器
        //poll系统调用:
        //epoll系统调用:
        Selector selector = Selector.open();
        ssc.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("Start server at port:9090");
        return selector;
    }


    public static void main(String[] args) throws IOException {
        MultiNioServer nioServer = new MultiNioServer();
        Selector selector = nioServer.startServer();
        Selector selector1 = Selector.open();
        Selector selector2 = Selector.open();
        Selector[] selectors = new Selector[]{selector1, selector2};
        new IOHandler(selector1, "A").start();
        new IOHandler(selector2, "B").start();
        int i = 0;
        while (true) {
            while (selector.select(10) > 0) {
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    iterator.remove();
                    if (selectionKey.isAcceptable()) {
                        //一个连接事件
                        ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel();
                        SocketChannel client = channel.accept();
                        client.configureBlocking(false);
                        //循环注册至另外的Selector
                        client.register(selectors[i++ % 2], SelectionKey.OP_READ, ByteBuffer.allocate(8192));
                        System.out.println("receive connect:" + client.getRemoteAddress());
                    }
                }
            }
        }
    }

}

五、IO总结

传统的NIO(不使用多路利用器),虽然解决了IO阻塞问题,但需要用户程序遍历地向内核询问所有客户端;
当使用了多路复用器(使用select/poll),每次循环需要将客户端列表传递给内核,由内核遍历将有事件的fd返回,避免用户态与内核态的频繁切换。
但使用select/poll仍然存在弊端,即每次循环都要进行fd列表的传递,如何能够避免每次循环向内核传递fd列表?
思考:如内核能提前申请一块空间存储(红黑树)事件句柄,在有客户端连接上,则记录在该空间,如此客户端程序询问是否事件时,则只需简单问一句,而不需要每次都传递fd列表。
这说的其实epoll内核调用能解决的。

我们老师去教室收作业进行批改的场景为例类比上述实现:

IO类型说明弊端
BIO老师来到教室,依次询问每个学生写了作业没有,写了则批改,没写则需等他写好再批改,期间有同学入学毕业则需排队等待两处阻塞
NIO老师来到教室,依次询问每个学生写了作业没有,写了则批改,没写则直接下一个,期间有同学入学毕业则需排队等待,相对较快依次询问,没写作业的也要问
NIO-select/poll老师每次来到教室,带着提前准备的名单贴到教室黑板,并告知在名单上同学且完成作业,报给我,后序老师直接拿到报名同学作业,批改即可不同于每个同学依次询问,但还要每次准备名单
NIO-epoll开班时,提前在教室黑板贴上人员名单,有新同学加入或毕业则相应的增加或删除,老师每次来到教室,再不用准备名单,同学自动举手通告老师作用写好或上厕所相应事件,老师获知后直接处理解决上述所有弊端

ps.select的fd有1024的数量约束,poll无此限制

六、Strace分析

七、Netty的线程模型

Netty拥有两个NIO线程池,分别是bossGroupworkerGroup,前者处理新建连接请求,然后将新建立的连接轮询交给workerGroup中的其中一个NioEventLoop来处理,后续该连接上的读写操作都是由同一个NioEventLoop来处理。注意,虽然bossGroup也能指定多个NioEventLoop(一个NioEventLoop对应一个线程),但是默认情况下只会有一个线程,因为一般情况下应用程序只会使用一个对外监听端口。

为何不能使用多线程来监听同一个对外端口么,即多线程epoll_wait到同一个epoll实例上?

这里会引来惊群的问题和epoll设置的是LT模式

现代linux中,多个socker同时监听同一个端口也是可行的,nginx 1.9.1也支持这一行为。linux 3.9以上内核支持SO_REUSEPORT选项,允许多个socker bind/listen在同一端口上。这样,多个进程可以各自申请socker监听同一端口,当连接事件来临时,内核做负载均衡,唤醒监听的其中一个进程来处理,reuseport机制有效的解决了epoll惊群问题

单线程模型

Reactor 单线程模型,是指所有的 I/O 操作都在同一个 NIO 线程上面完成的,此时NIO线程职责包括:接收新建连接请求、读写操作等。

多线程模型

Rector 多线程模型与单线程模型最大的区别就是有一组 NIO 线程来处理连接读写操作,一个NIO线程处理Accept。一个NIO线程可以处理多个连接事件,一个连接的事件只能属于一个NIO线程

主从模型

主从 Reactor 线程模型的特点是:服务端用于接收客户端连接的不再是一个单独的 NIO 线程,而是一个独立的 NIO 线程池。Acceptor 接收到客户端 TCP连接请求并处理完成后(可能包含接入认证等),将新创建的 SocketChannel注 册 到 I/O 线 程 池(sub reactor 线 程 池)的某个I/O线程上, 由它负责SocketChannel 的读写和编解码工作。Acceptor 线程池仅仅用于客户端的登录、握手和安全认证,一旦链路建立成功,就将链路注册到后端 subReactor 线程池的 I/O 线程上,由 I/O 线程负责后续的 I/O 操作。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值