【网络IO】(三)多路复用器由单线程到多线程的演进之路

前言

上一篇文章中,咱们介绍了单线程下多路复用器的使用。

在单线程的环境下,所有的socket都是被线性处理的,如果一个socket处理的时间很长,就会影响之后socket的处理。

为了提高网络IO的效率,必须要将多路复用器在多线程环境下应用起来。

本文首先提供一个简单的单线程多路复用器代码,然后慢慢将它向多线程迭代。

在这个过程中会出现两种不同的多线程解决方案——一个多路复用器版本和多个多路复用器版本。

然后会通过代码来说明哪种方案更好。

单线程下的工作模式

在单线程下的代码很简单,其实在上一篇文章中已经有了,这里把代码粘贴过来。

static ServerSocketChannel server = null;
static Selector selector = null;
static int port = 9090;

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

private static void serviceStart() throws IOException {
    initServer();

    listen();
}



private static void initServer() throws IOException {
    server = ServerSocketChannel.open();

    server.configureBlocking(false);

    server.bind(new InetSocketAddress(port));

    selector = Selector.open();

    server.register(selector, SelectionKey.OP_ACCEPT);

}

/*
	服务端主程序
*/
private static void listen() throws IOException {
    while(true) {
        if (selector.select(50) > 0) {
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();

            while(iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();

                if(key.isAcceptable()) {
                    acceptHandler(key);
                } else if(key.isReadable()) {
                    readHandler(key);
                }
            }
        }
    }
}

/*
	服务端acceptHandler
*/
private static void acceptHandler(SelectionKey key) throws IOException {
    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
    SocketChannel client = channel.accept();
    client.configureBlocking(false);

    ByteBuffer buffer = ByteBuffer.allocateDirect(8192);

    client.register(selector, SelectionKey.OP_READ, buffer);
}

/*
	服务端readHandler
*/
private static void readHandler(SelectionKey key) throws IOException {
    SocketChannel client = (SocketChannel) key.channel();
    ByteBuffer buffer = (ByteBuffer) key.attachment();

    buffer.clear();

    int read = 0;
    while(true) {
        read = client.read(buffer);

        if (read > 0) {
            buffer.flip();
            while(buffer.hasRemaining()) {
                client.write(buffer);
            }
            buffer.clear();
        } else if (read < 0) {
            client.close();
            break;
        } else {
            break;
        }
    }
}

/*
	服务端writeHandler
*/
private static void writeHandler(SelectionKey key) throws IOException {
        System.out.println("write handler...");
        SocketChannel client = (SocketChannel) key.channel();
        ByteBuffer buffer = (ByteBuffer) key.attachment();

        buffer.flip();
        while(buffer.hasRemaining()) {
            client.write(buffer);
        }
        buffer.clear();
        key.cancel();
        client.close();
    }

单线程下加入WriteHandler

在上面的代码中,server端的代码逻辑是接收到客户端的信息后直接将信息写回,也就是说read操作和write操作是在同一个函数中完成的,这会造成某个函数执行的时间变长。

更好的解决方式是将这个函数拆成更小的执行单元,并且交给多路复用器进行调度。

那么上面的代码会在下面这几个地方有修改

/*
	服务端主程序
*/
private static void listen() throws IOException {
    while(true) {
        if (selector.select(50) > 0) {
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();

            while(iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();

                if(key.isAcceptable()) {
                    acceptHandler(key);
                } else if(key.isReadable()) {
                    readHandler(key);
                } else if (key.isWritable()) {
                	writeHandler(key)
                }
            }
        }
    }
}

/*
	服务端readHandler
*/
private static void readHandler(SelectionKey key) throws IOException {
    SocketChannel client = (SocketChannel) key.channel();
    ByteBuffer buffer = (ByteBuffer) key.attachment();

    buffer.clear();

    int read = 0;
    while(true) {
        read = client.read(buffer);

        if (read > 0) {
           	client.register(selector, SelectionKey.OP_WRITE, buffer);
        } else if (read < 0) {
            client.close();
            break;
        } else {
            break;
        }
    }
}

/*
	服务端writeHandler
*/
private static void writeHandler(SelectionKey key) throws IOException {
    System.out.println("write handler...");
    SocketChannel client = (SocketChannel) key.channel();
    ByteBuffer buffer = (ByteBuffer) key.attachment();

    buffer.flip();
    while(buffer.hasRemaining()) {
        client.write(buffer);
    }
    buffer.clear();
    key.cancel();
    client.close();
}

代码分析

上面这段代码的思路就是,在readHandler中并不做回写的操作,而是再向多路复用器注册一个回写的事件。

当多路复用器判定当前环境可以回写的时候,会调用writeHandler进行操作。

多线程下的工作模式

在单线程的环境下,上面的代码运行的很完美。

但是单线程意味着同一时刻只能处理一个socket,如果这个socket特别的费时,就会影响后面socket的处理。

所以为了有效的理由机器资源,用多线程势在必行。

单个多路复用器

在上面的代码中,readHandler和writeHandler是比较耗时的操作,理所当然应该抛到单独的线程中进行处理。

主线程就可以专注于接收新客户端并建立连接。

那么需要改动的代码如下所示:

 /*
  服务端readHandler
 */
 private static void readHandler(SelectionKey key) {
     new Thread(()-> {
         try {
             SocketChannel client = (SocketChannel) key.channel();
             ByteBuffer buffer = (ByteBuffer) key.attachment();

             buffer.clear();

             int read = 0;
             while(true) {
                 read = client.read(buffer);

                 if (read > 0) {
                     client.register(selector, SelectionKey.OP_WRITE, buffer);
                 } else if (read < 0) {
                     client.close();
                     break;
                 } else {
                     break;
                 }
             }
         } catch (Exception e) {
             System.out.println(e);
         }

     }).start();

 }

 /*
     服务端writeHandler
 */
 private static void writeHandler(SelectionKey key){
     try{
         new Thread(() -> {
             SocketChannel client = (SocketChannel) key.channel();
             ByteBuffer buffer = (ByteBuffer) key.attachment();

             buffer.flip();
             while(buffer.hasRemaining()) {
                 try {
                     client.write(buffer);
                 } catch (IOException e) {
                     e.printStackTrace();
                 }
             }
             buffer.clear();
             key.cancel();
             try {
                 client.close();
             } catch (IOException e) {
                 e.printStackTrace();
             }
         }).start();
     } catch (Exception e) {
         System.out.println(e);
     }

 }

上面的代码把费时费力的读取和回写操作都委托给其他线程,主线程只需要集中在接纳新客户端即可。

但实际上,上面的代码是有瑕疵的,问题就出在了多线程打破了多路复用器的线性化处理

按原本的线性化逻辑

服务器的代码流程如下

  1. 多路复用器一开始仅监听一个server socket。

  2. 然后客户端发来握手请求与服务端建立连接,server socket有状态更新。

  3. 多路复用器监听到server socket的状态有更新后,主程序调用acceptHandler来迎接新客户端。

  4. 在acceptHandler中,将新客户端对应的socket委托给多路复用器监听。

  5. 多路复用器如果监听到客户端的socket有状态更新,会调用readHandler进行处理。

  6. 在readHandler中,会将回写的事件注册进多路复用器,交给多路复用器来处理。

  7. 多路复用器判断一个socket可写状态只是判断SEND_QUEUE是否有空间,如果有空间,就会调用writeHandler进行写操作。

上面这些步骤由于是在单线程环境下执行的,所以先后顺序的固定的,不会有超出意料的事情发生。

多线程下的逻辑

如果将上面的代码逻辑加入到多线程的环境下,就会由很多意料外的事情发生。

比如,当多路复用器监听到客户端的socket有状态更新,会调用readHandler进行处理。

readHandler会重新开启一个线程,将有状态的socket从多路复用器的监听容器中取出来处理。

但是由于是多线程状态,readHandler重新开启的线程可能还没来得及处理这个socket,主线程就会再调用一次readHandler对这个socket进行处理。

也即是说,同一时刻可能有多个readHandler对socket进行处理,同理,writeHandler也有相同问题。

这就是多线程下的问题,当然也有解决办法,就是在处理这个socket前,在线程中手动地调用
key.cancel()将这个socket从多路复用器的监听容器中取出,这样就不会重复调用了,事后再决定要不要将这个socket放回。

但这就很麻烦了。

多个多路复用器

上文中虽然实现了多路复用器的多线程版本,但问题显而易见。

每次处理有状态更新的socket都要先将它们从多路复用器的监听容器中取出,这样涉及到太多次的系统调用,用户态与内核态之间的切换严重影响应用程序的性能。

而这一切的罪魁祸首就是,多线程打破了多路复用器的线性处理。*

那么一个可行的思路就是——依然是多个线程,但是每个线程上都有一个多路复用器,每个多路复用器在自己的线程中是线性操作的。

这样就解决了上一个版本的问题,但又利用了多线程的优势。

代码实现

先来看代码实现:

public class MultiSelectorThreads {
	public static void main(String[] args) {
		SelectorThreadGroup selectorThreadGroup = new SelectorThreadGroup(3);

        selectorThreadGroup.bind(9090);

        try {
            System.in.read();
        } catch (IOException e) {
            e.printStackTrace();
        }
	}
}

class SelectorThreadGroup {
	SelectorThread[] selectorThreads;

    AtomicInteger indexCtl = new AtomicInteger(0);

    public SelectorThreadGroup(int num) {
        this.selectorThreads = new SelectorThread[num];

        for(int i = 0; i < num; i++) {
            this.selectorThreads[i] = new SelectorThread(this);

            new Thread(this.selectorThreads[i]).start();
        }
    }

    public void bind(int port) {
        ServerSocketChannel server = null;
        try {
            server = ServerSocketChannel.open();

            server.configureBlocking(false);

            server.bind(new InetSocketAddress(port));

            register(server);
        } catch (IOException e) {
            e.printStackTrace();
        }


    }

    public void register(Channel c) {
        try {
            int index = indexCtl.incrementAndGet() % selectorThreads.length;
            SelectorThread chosenSelector = selectorThreads[index];

            chosenSelector.queue.put(c);
            chosenSelector.selector.wakeup();

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class SelectorThread implements Runnable {
    public Selector selector;
    public LinkedBlockingQueue<Channel> queue;
    private SelectorThreadGroup group;

    public SelectorThread(SelectorThreadGroup group) {
        try {
            selector = Selector.open();
            queue = new LinkedBlockingQueue<>();
            this.group = group;
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        try {
            while(true) {
                int num = selector.select(); // blocking

                if (num > 0) {
                    Set<SelectionKey> keys = selector.selectedKeys();
                    Iterator<SelectionKey> iter = keys.iterator();

                    while(iter.hasNext()) {
                        SelectionKey key = iter.next();
                        iter.remove();

                        if (key.isAcceptable()) {
                            acceptHandler(key);
                        } else if (key.isReadable()) {
                            readHandler(key);
                        }
                    }
                }

                // other task
                while (!queue.isEmpty()) {
                    Channel c = queue.take();

                    if (c instanceof ServerSocketChannel) {
                        ServerSocketChannel server = (ServerSocketChannel) c;
                        server.configureBlocking(false);

                        server.register(selector, SelectionKey.OP_ACCEPT);
                    } else if (c instanceof SocketChannel) {
                        SocketChannel client = (SocketChannel) c;
                        client.configureBlocking(false);

                        ByteBuffer buffer = ByteBuffer.allocateDirect(4096);

                        client.register(selector,SelectionKey.OP_READ, buffer);
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void acceptHandler(SelectionKey key) {
        try {
            ServerSocketChannel server = (ServerSocketChannel) key.channel();
            SocketChannel client = server.accept();

            group.register(client);

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

    }

    private void readHandler(SelectionKey key) {
        SocketChannel client = (SocketChannel) key.channel();
        ByteBuffer buffer = (ByteBuffer) key.attachment();

        buffer.clear();

        int read = 0;

        while(true) {
            try {
                read = client.read(buffer);
                if (read > 0) {
                    buffer.flip();

                    while(buffer.hasRemaining()) {
                        client.write(buffer);
                    }

                    buffer.clear();
                } else if (read < 0) {
                    client.close();
                    key.cancel();
                    break;
                } else {
                    break;
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

在这套代码中,需要注意两个类SelectorThreadGroupSelectorThread

就像之前我们说的,这套代码要实现的是多线程中如何应用多路复用器。

解决方案就是在每个线程中都有个独立的多路复用器。

那么SelectorThread就是每一个多路复用器的线程。

SelectorThreadGroup用来管理调度这些线程。

SelectorThreadGroup

多路复用器线程是独立运行的,但是需要有个统合调度它们的地方,这个地方就是SelectorThreadGroup。

这个类中我们需要注意几点

  1. 通过一个数组来管理所有的SelectorThread,而多路复用器的个数是可以自定义的。

  2. register方法中,通过“轮询”的方式将socket分配给不同的多路复用器。

    需要注意,将socket分配给多路复用器并不是直接在复用器上注册,而是将该socket添加到SelectorThread中的一个容器中,selector会自行去容器中处理这些socket。

  3. 本类同样负责生成server socket,委托给多路复用器监听的操作放在register方法中执行。

SelectorThread

就像之前我们说的,这套代码要实现的是多线程中如何应用多路复用器。

这就是代码中的SelectorThread。

在这个类中需要注意下面几点:

  1. 这个类中有个多路复用器的实例selector和一个线程安全的容器queue

  2. selector就是当前线程的多路复用器。

  3. 当别的线程想向当前线程的多路复用器委托socket的时候,并不是直接调用 类似socket.register(selector, ...)的代码,而是通过将该socket放进queue中,让本线程自己来注册。

    这样做的原因是,selector.select()方法是阻塞方法, 所以如果复用器线程被这个方法阻塞住了,唤醒该线程的操作与注册socket的操作会因为不同线程执行顺序的不同而有差异。

    因此,将要注册的socket放到本线程的一个容器中,让本线程自己实现注册,就不会有线程执行顺序不可控的问题了。

  4. 复用器线程会一直监听复用器的状态,当复用器监听的socket没有状态更新的时候,整个线程会阻塞在selector.select()方法处。

    一旦监听的socket有状态更新,select()方法就会返回,执行下面的代码。

    根据socket的类型,调用accptHandler或者readHandler进行处理。

    注意,在执行accpetHandler函数中,有一部需要把新的socket委托给某个多路复用器。

    这个操作在本线程是完成不了的,需要通过上层代码,也就是SelectorThreadGroup,确定要将这个新socket委托给哪个多路复用器。

  5. 监听完多路复用器的状态后,就会去检查queue中有没有需要处理的socket,通过不同的socket类型调用不同的handler进行处理。

总结

本文从单线程的多路复用器代码开始,慢慢的向多线程环境演化。

首先尝试了单个多路复用器的多线程版本,发现这种方式打破了多路复用器线性的执行顺序,从而造成了线程间的合作出现问题。

然后提出了多个多路复用器的多线程版本,在这个版本中,每个多路复用器都在单一的线程中工作,这样既保证了多路复用器线性执的执行顺序,又利用了多线程的效率,一举两得。

而最后这种做法,正式netty的雏形。

下一篇文章会对netty进行初步的讲解。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值