乐趣区

关于netty:折腾了我一周原来Netty网络编程就是这么个破玩意儿

1、阻塞

  • 阻塞模式下,相干办法都会导致线程暂停

    • ServerSocketChannel.accept 会在没有连贯建设时让线程暂停
    • SocketChannel.read 会在通道中没有数据可读时让线程暂停
    • 阻塞的体现其实就是线程暂停了,暂停期间不会占用 cpu,但线程相当于闲置
  • 单线程下,阻塞办法之间相互影响,简直不能失常工作,须要多线程反对
  • 但多线程下,有新的问题,体现在以下方面

    • 32 位 jvm 一个线程 320k,64 位 jvm 一个线程 1024k,如果连接数过多,必然导致 OOM,并且线程太多,反而会因为频繁上下文切换导致性能升高
    • 能够采纳线程池技术来缩小线程数和线程上下文切换,但治标不治本,如果有很多连贯建设,但长时间 inactive,会阻塞线程池中所有线程,因而不适宜长连贯,只适宜短连贯

服务端代码

public class Server {public static void main(String[] args) {
        // 创立缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(16);
        // 取得服务器通道
        try(ServerSocketChannel server = ServerSocketChannel.open()) {
            // 为服务器通道绑定端口
            server.bind(new InetSocketAddress(8080));
            // 用户寄存连贯的汇合
            ArrayList<SocketChannel> channels = new ArrayList<>();
            // 循环接管连贯
            while (true) {System.out.println("before connecting...");
                // 没有连贯时,会阻塞线程
                SocketChannel socketChannel = server.accept();
                System.out.println("after connecting...");
                channels.add(socketChannel);
                // 循环遍历汇合中的连贯
                for(SocketChannel channel : channels) {System.out.println("before reading");
                    // 解决通道中的数据
                    // 当通道中没有数据可读时,会阻塞线程
                    channel.read(buffer);
                    buffer.flip();
                    ByteBufferUtil.debugRead(buffer);
                    buffer.clear();
                    System.out.println("after reading");
                }
            }
        } catch (IOException e) {e.printStackTrace();
        }
    }
}

客户端代码

public class Client {public static void main(String[] args) {try (SocketChannel socketChannel = SocketChannel.open()) {
            // 建设连贯
            socketChannel.connect(new InetSocketAddress("localhost", 8080));
            System.out.println("waiting...");
        } catch (IOException e) {e.printStackTrace();
        }
    }
}

运行后果

  • 客户端 – 服务器建设连贯前:服务器端因 accept 阻塞
  • 客户端 – 服务器建设连贯后,客户端发送音讯前:服务器端因通道为空被阻塞
  • 客户端发送数据后,服务器解决通道中的数据。再次进入循环时,再次被 accept 阻塞
  • 之前的客户端再次发送音讯,服务器端因为被 accept 阻塞,无奈解决之前客户端发送到通道中的信息

2、非阻塞

  • 能够通过 ServerSocketChannel 的 configureBlocking (false) 办法将 取得连贯设置为非阻塞的。此时若没有连贯,accept 会返回 null
  • 能够通过 SocketChannel 的 configureBlocking (false) 办法将从通道中 读取数据设置为非阻塞的。若此时通道中没有数据可读,read 会返回 – 1

服务器代码如下

public class Server {public static void main(String[] args) {
        // 创立缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(16);
        // 取得服务器通道
        try(ServerSocketChannel server = ServerSocketChannel.open()) {
            // 设置为非阻塞模式,没有连贯时返回 null,不会阻塞线程
            server.configureBlocking(false);
            // 为服务器通道绑定端口
            server.bind(new InetSocketAddress(8080));
            // 用户寄存连贯的汇合
            ArrayList<SocketChannel> channels = new ArrayList<>();
            // 循环接管连贯
            while (true) {SocketChannel socketChannel = server.accept();
                // 通道不为空时才将连贯放入到汇合中
                if (socketChannel != null) {System.out.println("after connecting...");
                    channels.add(socketChannel);
                }
                // 循环遍历汇合中的连贯
                for(SocketChannel channel : channels) {
                    // 解决通道中的数据
                    // 设置为非阻塞模式,若通道中没有数据,会返回 0,不会阻塞线程
                    channel.configureBlocking(false);
                    int read = channel.read(buffer);
                    if(read > 0) {buffer.flip();
                        ByteBufferUtil.debugRead(buffer);
                        buffer.clear();
                        System.out.println("after reading");
                    }
                }
            }
        } catch (IOException e) {e.printStackTrace();
        }
    }
}

这样写存在一个问题,因为设置为了非阻塞,会始终执行 while (true) 中的代码,CPU 始终处于繁忙状态,会使得性能变低,所以理论状况中不应用这种办法解决申请

3、Selector

多路复用

单线程能够配合 Selector 实现对多个 Channel 可读写事件的监控,这称之为多路复用

  • 多路复用仅针对网络 IO,一般文件 IO 无奈 利用多路复用
  • 如果不必 Selector 的非阻塞模式,线程大部分工夫都在做无用功,而 Selector 可能保障

    • 有可连贯事件时才去连贯
    • 有可读事件才去读取
    • 有可写事件才去写入

      • 限于网络传输能力,Channel 未必时时可写,一旦 Channel 可写,会触发 Selector 的可写事件

4、应用及 Accpet 事件

要应用 Selector 实现多路复用,服务端代码如下改良

public class SelectServer {public static void main(String[] args) {ByteBuffer buffer = ByteBuffer.allocate(16);
        // 取得服务器通道
        try(ServerSocketChannel server = ServerSocketChannel.open()) {server.bind(new InetSocketAddress(8080));
            // 创立选择器
            Selector selector = Selector.open();
            
            // 通道必须设置为非阻塞模式
            server.configureBlocking(false);
            // 将通道注册到选择器中,并设置感兴趣的事件
            server.register(selector, SelectionKey.OP_ACCEPT);
            while (true) {
                // 若没有事件就绪,线程会被阻塞,反之不会被阻塞。从而防止了 CPU 空转
                // 返回值为就绪的事件个数
                int ready = selector.select();
                System.out.println("selector ready counts :" + ready);
                
                // 获取所有事件
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                
                // 应用迭代器遍历事件
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {SelectionKey key = iterator.next();
                    
                    // 判断 key 的类型
                    if(key.isAcceptable()) {
                        // 取得 key 对应的 channel
                        ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                        System.out.println("before accepting...");
                        
                        // 获取连贯并解决,而且是必须解决,否则须要勾销
                        SocketChannel socketChannel = channel.accept();
                        System.out.println("after accepting...");
                        
                        // 处理完毕后移除
                        iterator.remove();}
                }
            }
        } catch (IOException e) {e.printStackTrace();
        }
    }
}

步骤解析

  • 取得选择器 Selector
Selector selector = Selector.open();
  • 将通道设置为非阻塞模式,并注册到选择器中,并设置感兴趣的事件

    • channel 必须工作在非阻塞模式
    • FileChannel 没有非阻塞模式,因而不能配合 selector 一起应用
    • 绑定的事件类型能够有

      • connect – 客户端连贯胜利时触发
  • accept – 服务器端胜利承受连贯时触发

    • read – 数据可读入时触发,有因为接管能力弱,数据暂不能读入的状况
  • write – 数据可写出时触发,有因为发送能力弱,数据暂不能写出的状况
// 通道必须设置为非阻塞模式
server.configureBlocking(false);
// 将通道注册到选择器中,并设置感兴趣的实际
server.register(selector, SelectionKey.OP_ACCEPT);
  • 通过 Selector 监听事件,并取得就绪的通道个数,若没有通道就绪,线程会被阻塞

    • 阻塞直到绑定事件产生

      int count = selector.select();
    • 阻塞直到绑定事件产生,或是超时(工夫单位为 ms)

      int count = selector.select(long timeout);
    • 不会阻塞,也就是不论有没有事件,立即返回,本人依据返回值查看是否有事件

      int count = selector.selectNow();
  • 获取就绪事件并失去对应的通道,而后进行解决
// 获取所有事件
Set<SelectionKey> selectionKeys = selector.selectedKeys();
                
// 应用迭代器遍历事件
Iterator<SelectionKey> iterator = selectionKeys.iterator();

while (iterator.hasNext()) {SelectionKey key = iterator.next();
                    
    // 判断 key 的类型,此处为 Accept 类型
    if(key.isAcceptable()) {
        // 取得 key 对应的 channel
        ServerSocketChannel channel = (ServerSocketChannel) key.channel();

        // 获取连贯并解决,而且是必须解决,否则须要勾销
        SocketChannel socketChannel = channel.accept();

        // 处理完毕后移除
        iterator.remove();}
}

事件产生后是否不解决

事件产生后,要么解决,要么勾销(cancel),不能什么都不做,否则下次该事件仍会触发,这是因为 nio 底层应用的是程度触发

5、Read 事件

  • 在 Accept 事件中,若有客户端与服务器端建设了连贯,须要将其对应的 SocketChannel 设置为非阻塞,并注册到抉择其中
    增加 Read 事件,触发后进行读取操作
  • 增加 Read 事件,触发后进行读取操作
public class SelectServer {public static void main(String[] args) {ByteBuffer buffer = ByteBuffer.allocate(16);
        // 取得服务器通道
        try(ServerSocketChannel server = ServerSocketChannel.open()) {server.bind(new InetSocketAddress(8080));
            // 创立选择器
            Selector selector = Selector.open();
            // 通道必须设置为非阻塞模式
            server.configureBlocking(false);
            // 将通道注册到选择器中,并设置感兴趣的实际
            server.register(selector, SelectionKey.OP_ACCEPT);
            // 为 serverKey 设置感兴趣的事件
            while (true) {
                // 若没有事件就绪,线程会被阻塞,反之不会被阻塞。从而防止了 CPU 空转
                // 返回值为就绪的事件个数
                int ready = selector.select();
                System.out.println("selector ready counts :" + ready);
                // 获取所有事件
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                // 应用迭代器遍历事件
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {SelectionKey key = iterator.next();
                    // 判断 key 的类型
                    if(key.isAcceptable()) {
                        // 取得 key 对应的 channel
                        ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                        System.out.println("before accepting...");
                        // 获取连贯
                        SocketChannel socketChannel = channel.accept();
                        System.out.println("after accepting...");
                        // 设置为非阻塞模式,同时将连贯的通道也注册到抉择其中
                        socketChannel.configureBlocking(false);
                        socketChannel.register(selector, SelectionKey.OP_READ);
                        // 处理完毕后移除
                        iterator.remove();} else if (key.isReadable()) {SocketChannel channel = (SocketChannel) key.channel();
                        System.out.println("before reading...");
                        channel.read(buffer);
                        System.out.println("after reading...");
                        buffer.flip();
                        ByteBufferUtil.debugRead(buffer);
                        buffer.clear();
                        // 处理完毕后移除
                        iterator.remove();}
                }
            }
        } catch (IOException e) {e.printStackTrace();
        }
    }
}

删除事件

当解决完一个事件后,肯定要调用迭代器的 remove 办法移除对应事件,否则会呈现谬误。起因如下

以咱们下面的 Read 事件 的代码为例

  • 当调用了 server.register (selector, SelectionKey.OP_ACCEPT) 后,Selector 中保护了一个汇合,用于寄存 SelectionKey 以及其对应的通道

    // WindowsSelectorImpl 中的 SelectionKeyImpl 数组
    private SelectionKeyImpl[] channelArray = new SelectionKeyImpl[8];
    public class SelectionKeyImpl extends AbstractSelectionKey {
        // Key 对应的通道
        final SelChImpl channel;
        ...
    }
  • 选择器中的通道对应的事件产生后,selecionKey 会被放到另一个汇合中,然而 selecionKey 不会主动移除,所以须要咱们在解决完一个事件后,通过迭代器手动移除其中的 selecionKey。否则会导致已被解决过的事件再次被解决,就会引发谬误

断开解决

当客户端与服务器之间的连贯断开时,会给服务器端发送一个读事件,对异样断开和失常断开须要加以不同的形式进行解决

  • 失常断开

    • 失常断开时,服务器端的 channel.read (buffer) 办法的返回值为 – 1,所以当完结到返回值为 – 1 时,须要调用 key 的 cancel 办法勾销此事件,并在勾销后移除该事件

      int read = channel.read(buffer);
      // 断开连接时,客户端会向服务器发送一个写事件,此时 read 的返回值为 -1
      if(read == -1) {
          // 勾销该事件的解决
          key.cancel();
          channel.close();} else {...}
      // 勾销或者解决,都须要移除 key
      iterator.remove();
  • 异样断开

    • 异样断开时,会抛出 IOException 异样,在 try-catch 的 catch 块中捕捉异样并调用 key 的 cancel 办法即可

音讯边界

不解决音讯边界存在的问题

将缓冲区的大小设置为 4 个字节,发送 2 个汉字(你好),通过 decode 解码并打印时,会呈现乱码

ByteBuffer buffer = ByteBuffer.allocate(4);
// 解码并打印
System.out.println(StandardCharsets.UTF_8.decode(buffer));
你�
��

这是因为 UTF-8 字符集下,1 个汉字占用 3 个字节,此时缓冲区大小为 4 个字节,一次读工夫无奈解决完通道中的所有数据,所以一共会触发两次读事件。这就导致 你好 的 好 字被拆分为了前半部分和后半局部发送,解码时就会呈现问题

解决音讯边界

传输的文本可能有以下三种状况

  • 文本大于缓冲区大小

    • 此时须要将缓冲区进行扩容
  • 产生半包景象
  • 产生粘包景象

解决思路大抵有以下三种

  • 固定音讯长度,数据包大小一样,服务器按预约长度读取,当发送的数据较少时,须要将数据进行填充,直到长度与音讯规定长度统一。毛病是节约带宽
  • 另一种思路是按分隔符拆分,毛病是效率低,须要一个一个字符地去匹配分隔符
  • TLV 格局,即 Type 类型、Length 长度、Value 数据

    (也就是在音讯结尾用一些空间寄存前面数据的长度),如 HTTP 申请头中的 Content-Type 与 Content-Length

    。类型和长度已知的状况下,就能够不便获取音讯大小,调配适合的 buffer,毛病是 buffer 须要提前调配,如果内容过大,则影响 server 吞吐量

    • Http 1.1 是 TLV 格局
  • Http 2.0 是 LTV 格局

下文的音讯边界解决形式为 第二种:按分隔符拆分

附件与扩容

Channel 的 register 办法还有 第三个参数:附件,能够向其中放入一个 Object 类型的对象,该对象会与注销的 Channel 以及其对应的 SelectionKey 绑定,能够从 SelectionKey 获取到对应通道的附件

public final SelectionKey register(Selector sel, int ops, Object att)

可通过 SelectionKey 的 attachment () 办法取得附件

ByteBuffer buffer = (ByteBuffer) key.attachment();

咱们须要在 Accept 事件产生后,将通道注册到 Selector 中时,对每个通道增加一个 ByteBuffer 附件,让每个通道产生读事件时都应用本人的通道,防止与其余通道发生冲突而导致问题

// 设置为非阻塞模式,同时将连贯的通道也注册到抉择其中,同时设置附件
socketChannel.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(16);
// 增加通道对应的 Buffer 附件
socketChannel.register(selector, SelectionKey.OP_READ, buffer);

当 Channel 中的数据大于缓冲区时,须要对缓冲区进行扩容操作。此代码中的扩容的断定办法:Channel 调用 compact 办法后,的 position 与 limit 相等,阐明缓冲区中的数据并未被读取(容量太小),此时创立新的缓冲区,其大小扩充为两倍。同时还要将旧缓冲区中的数据拷贝到新的缓冲区中,同时调用 SelectionKey 的 attach 办法将新的缓冲区作为新的附件放入 SelectionKey 中

// 如果缓冲区太小,就进行扩容
if (buffer.position() == buffer.limit()) {ByteBuffer newBuffer = ByteBuffer.allocate(buffer.capacity()*2);
    // 将旧 buffer 中的内容放入新的 buffer 中
    ewBuffer.put(buffer);
    // 将新 buffer 作为附件放到 key 中
    key.attach(newBuffer);
}

革新后的服务器代码如下

public class SelectServer {public static void main(String[] args) {
        // 取得服务器通道
        try(ServerSocketChannel server = ServerSocketChannel.open()) {server.bind(new InetSocketAddress(8080));
            // 创立选择器
            Selector selector = Selector.open();
            // 通道必须设置为非阻塞模式
            server.configureBlocking(false);
            // 将通道注册到选择器中,并设置感兴趣的事件
            server.register(selector, SelectionKey.OP_ACCEPT);
            // 为 serverKey 设置感兴趣的事件
            while (true) {
                // 若没有事件就绪,线程会被阻塞,反之不会被阻塞。从而防止了 CPU 空转
                // 返回值为就绪的事件个数
                int ready = selector.select();
                System.out.println("selector ready counts :" + ready);
                // 获取所有事件
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                // 应用迭代器遍历事件
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                
                while (iterator.hasNext()) {SelectionKey key = iterator.next();
                    iterator.remove();
                    // 判断 key 的类型
                    if(key.isAcceptable()) {
                        // 取得 key 对应的 channel
                        ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                        System.out.println("before accepting...");
                        // 获取连贯
                        SocketChannel socketChannel = channel.accept();
                        System.out.println("after accepting...");
                        // 设置为非阻塞模式,同时将连贯的通道也注册到抉择其中,同时设置附件
                        socketChannel.configureBlocking(false);
                        ByteBuffer buffer = ByteBuffer.allocate(16);
                        socketChannel.register(selector, SelectionKey.OP_READ, buffer);
                        
                        
                    } else if (key.isReadable()) {SocketChannel channel = (SocketChannel) key.channel();
                        System.out.println("before reading...");
                        // 通过 key 取得附件(buffer)ByteBuffer buffer = (ByteBuffer) key.attachment();
                        int read = channel.read(buffer);
                        if(read == -1) {key.cancel();
                            channel.close();} else {
                            // 通过分隔符来分隔 buffer 中的数据
                            split(buffer);
                            // 如果缓冲区太小,就进行扩容
                            if (buffer.position() == buffer.limit()) {ByteBuffer newBuffer = ByteBuffer.allocate(buffer.capacity()*2);
                                // 将旧 buffer 中的内容放入新的 buffer 中
                                buffer.flip();
                                newBuffer.put(buffer);
                                // 将新 buffer 放到 key 中作为附件
                                key.attach(newBuffer);
                            }
                        }
                        System.out.println("after reading...");
                       
                       
                    }
                }
            }
        } catch (IOException e) {e.printStackTrace();
        }
    }

    private static void split(ByteBuffer buffer) {buffer.flip();
        for(int i = 0; i < buffer.limit(); i++) {
            // 遍历寻找分隔符
            // get(i)不会挪动 position
            if (buffer.get(i) == '\n') {
                // 缓冲区长度
                int length = i+1-buffer.position();
                ByteBuffer target = ByteBuffer.allocate(length);
                // 将后面的内容写入 target 缓冲区
                for(int j = 0; j < length; j++) {
                    // 将 buffer 中的数据写入 target 中
                    target.put(buffer.get());
                }
                // 打印后果
                ByteBufferUtil.debugAll(target);
            }
        }
        // 切换为写模式,然而缓冲区可能未读完,这里须要应用 compact
        buffer.compact();}
}

ByteBuffer 的大小调配

  • 每个 channel 都须要记录可能被切分的音讯,因为 ByteBuffer 不能被多个 channel 独特应用,因而须要为每个 channel 保护一个独立的 ByteBuffer
  • ByteBuffer 不能太大,比方一个 ByteBuffer 1Mb 的话,要反对百万连贯就要 1Tb 内存,因而须要设计大小可变的 ByteBuffer
  • 调配思路能够参考

    • 一种思路是首先调配一个较小的 buffer,例如 4k,如果发现数据不够,再调配 8k 的 buffer,将 4k buffer 内容拷贝至 8k buffer,长处是音讯间断容易解决,毛病是数据拷贝消耗性能
    • 另一种思路是用多个数组组成 buffer,一个数组不够,把多进去的内容写入新的数组,与后面的区别是音讯存储不间断解析简单,长处是防止了拷贝引起的性能损耗

6、Write 事件

服务器通过 Buffer 向通道中写入数据时,可能因为通道容量小于 Buffer 中的数据大小,导致无奈一次性将 Buffer 中的数据全副写入到 Channel 中,这时便须要分屡次写入,具体步骤如下

  • 执行一次写操作,向将 buffer 中的内容写入到 SocketChannel 中,而后判断 Buffer 中是否还有数据
  • 若 Buffer 中还有数据,则 须要将 SockerChannel 注册到 Seletor 中,并关注写事件,同时将未写完的 Buffer 作为附件一起放入到 SelectionKey 中
 int write = socket.write(buffer);
// 通道中可能无奈放入缓冲区中的所有数据
if (buffer.hasRemaining()) {
    // 注册到 Selector 中,关注可写事件,并将 buffer 增加到 key 的附件中
    socket.configureBlocking(false);
    socket.register(selector, SelectionKey.OP_WRITE, buffer);
}
  • 增加写事件的相干操作 key.isWritable(),对 Buffer 再次进行写操作

    • 每次写后须要判断 Buffer 中是否还有数据(是否写完)。若写完,须要移除 SelecionKey 中的 Buffer 附件,防止其占用过多内存,同时还需移除对写事件的关注
SocketChannel socket = (SocketChannel) key.channel();
// 取得 buffer
ByteBuffer buffer = (ByteBuffer) key.attachment();
// 执行写操作
int write = socket.write(buffer);
System.out.println(write);
// 如果曾经实现了写操作,须要移除 key 中的附件,同时不再对写事件感兴趣
if (!buffer.hasRemaining()) {key.attach(null);
    key.interestOps(0);
}

整体代码如下

public class WriteServer {public static void main(String[] args) {try(ServerSocketChannel server = ServerSocketChannel.open()) {server.bind(new InetSocketAddress(8080));
            server.configureBlocking(false);
            Selector selector = Selector.open();
            server.register(selector, SelectionKey.OP_ACCEPT);
            while (true) {selector.select();
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {SelectionKey key = iterator.next();
                    // 解决后就移除事件
                    iterator.remove();
                    if (key.isAcceptable()) {
                        // 取得客户端的通道
                        SocketChannel socket = server.accept();
                        // 写入数据
                        StringBuilder builder = new StringBuilder();
                        for(int i = 0; i < 500000000; i++) {builder.append("a");
                        }
                        ByteBuffer buffer = StandardCharsets.UTF_8.encode(builder.toString());
                        // 先执行一次 Buffer->Channel 的写入,如果未写完,就增加一个可写事件
                        int write = socket.write(buffer);
                        System.out.println(write);
                        // 通道中可能无奈放入缓冲区中的所有数据
                        if (buffer.hasRemaining()) {
                            // 注册到 Selector 中,关注可写事件,并将 buffer 增加到 key 的附件中
                            socket.configureBlocking(false);
                            socket.register(selector, SelectionKey.OP_WRITE, buffer);
                        }
                    } else if (key.isWritable()) {SocketChannel socket = (SocketChannel) key.channel();
                        // 取得 buffer
                        ByteBuffer buffer = (ByteBuffer) key.attachment();
                        // 执行写操作
                        int write = socket.write(buffer);
                        System.out.println(write);
                        // 如果曾经实现了写操作,须要移除 key 中的附件,同时不再对写事件感兴趣
                        if (!buffer.hasRemaining()) {key.attach(null);
                            key.interestOps(0);
                        }
                    }
                }
            }
        } catch (IOException e) {e.printStackTrace();
        }
    }
}

7、优化

多线程优化

充分利用多核 CPU,分两组选择器

  • 单线程配一个选择器(Boss),专门解决 accept 事件
  • 创立 cpu 外围数的线程(Worker),每个线程配一个选择器,轮流解决 read 事件
实现思路
  • 创立一个负责解决 Accept 事件的 Boss 线程,与多个负责解决 Read 事件的 Worker 线程
  • Boss 线程执行的操作

    • 承受并解决 Accepet 事件,当 Accept 事件产生后,调用 Worker 的 register (SocketChannel socket) 办法,让 Worker 去解决 Read 事件,其中须要依据标识 robin 去判断将任务分配给哪个 Worker

      // 创立固定数量的 Worker
      Worker[] workers = new Worker[4];
      // 用于负载平衡的原子整数
      AtomicInteger robin = new AtomicInteger(0);
      // 负载平衡,轮询调配 Worker
      workers[robin.getAndIncrement()% workers.length].register(socket);
    • register (SocketChannel socket) 办法会通过同步队列实现 Boss 线程与 Worker 线程之间的通信,让 SocketChannel 的注册工作被 Worker 线程执行。增加工作后须要调用 selector.wakeup () 来唤醒被阻塞的 Selector

      public void register(final SocketChannel socket) throws IOException {
          // 只启动一次
          if (!started) {// 初始化操作}
          // 向同步队列中增加 SocketChannel 的注册事件
          // 在 Worker 线程中执行注册事件
          queue.add(new Runnable() {
              @Override
              public void run() {
                  try {socket.register(selector, SelectionKey.OP_READ);
                  } catch (IOException e) {e.printStackTrace();
                  }
              }
          });
          // 唤醒被阻塞的 Selector
          // select 相似 LockSupport 中的 park,wakeup 的原理相似 LockSupport 中的 unpark
          selector.wakeup();}
  • Worker 线程执行的操作

    • 从同步队列中获取注册工作,并解决 Read 事件
实现代码
public class ThreadsServer {public static void main(String[] args) {try (ServerSocketChannel server = ServerSocketChannel.open()) {
            // 以后线程为 Boss 线程
            Thread.currentThread().setName("Boss");
            server.bind(new InetSocketAddress(8080));
            // 负责轮询 Accept 事件的 Selector
            Selector boss = Selector.open();
            server.configureBlocking(false);
            server.register(boss, SelectionKey.OP_ACCEPT);
            // 创立固定数量的 Worker
            Worker[] workers = new Worker[4];
            // 用于负载平衡的原子整数
            AtomicInteger robin = new AtomicInteger(0);
            for(int i = 0; i < workers.length; i++) {workers[i] = new Worker("worker-"+i);
            }
            while (true) {boss.select();
                Set<SelectionKey> selectionKeys = boss.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {SelectionKey key = iterator.next();
                    iterator.remove();
                    // BossSelector 负责 Accept 事件
                    if (key.isAcceptable()) {
                        // 建设连贯
                        SocketChannel socket = server.accept();
                        System.out.println("connected...");
                        socket.configureBlocking(false);
                        // socket 注册到 Worker 的 Selector 中
                        System.out.println("before read...");
                        // 负载平衡,轮询调配 Worker
                        workers[robin.getAndIncrement()% workers.length].register(socket);
                        System.out.println("after read...");
                    }
                }
            }
        } catch (IOException e) {e.printStackTrace();
        }
    }

    static class Worker implements Runnable {
        private Thread thread;
        private volatile Selector selector;
        private String name;
        private volatile boolean started = false;
        /**
         * 同步队列,用于 Boss 线程与 Worker 线程之间的通信
         */
        private ConcurrentLinkedQueue<Runnable> queue;

        public Worker(String name) {this.name = name;}

        public void register(final SocketChannel socket) throws IOException {
            // 只启动一次
            if (!started) {thread = new Thread(this, name);
                selector = Selector.open();
                queue = new ConcurrentLinkedQueue<>();
                thread.start();
                started = true;
            }
            
            // 向同步队列中增加 SocketChannel 的注册事件
            // 在 Worker 线程中执行注册事件
            queue.add(new Runnable() {
                @Override
                public void run() {
                    try {socket.register(selector, SelectionKey.OP_READ);
                    } catch (IOException e) {e.printStackTrace();
                    }
                }
            });
            // 唤醒被阻塞的 Selector
            // select 相似 LockSupport 中的 park,wakeup 的原理相似 LockSupport 中的 unpark
            selector.wakeup();}

        @Override
        public void run() {while (true) {
                try {selector.select();
                    // 通过同步队列取得工作并运行
                    Runnable task = queue.poll();
                    if (task != null) {
                        // 取得工作,执行注册操作
                        task.run();}
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();
                    Iterator<SelectionKey> iterator = selectionKeys.iterator();
                    while(iterator.hasNext()) {SelectionKey key = iterator.next();
                        iterator.remove();
                        // Worker 只负责 Read 事件
                        if (key.isReadable()) {
                            // 简化解决,省略细节
                            SocketChannel socket = (SocketChannel) key.channel();
                            ByteBuffer buffer = ByteBuffer.allocate(16);
                            socket.read(buffer);
                            buffer.flip();
                            ByteBufferUtil.debugAll(buffer);
                        }
                    }
                } catch (IOException e) {e.printStackTrace();
                }
            }
        }
    }
}

本文由 传智教育博学谷 教研团队公布。

如果本文对您有帮忙,欢送 关注 点赞 ;如果您有任何倡议也可 留言评论 私信,您的反对是我保持创作的能源。

转载请注明出处!

退出移动版