【Netty】第二章 网络编程和 IO 概念剖析

【Netty】第二章 网络编程

一、网络编程

1.模拟阻塞模式下服务器单线程处理请求

服务端

package com.sisyphus.networkprogramming;

import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
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;

import static com.sisyphus.utils.ByteBufferUtil.debugRead;

@Slf4j
public class Server {
    public static void main(String[] args) throws IOException {
        //使用 nio 来理解阻塞模式,单线程

        //0.ByteBuffer,16字节
        ByteBuffer buffer = ByteBuffer.allocate(16);

        //1.创建服务器
        ServerSocketChannel ssc = ServerSocketChannel.open();

        //2.绑定监听端口
        ssc.bind(new InetSocketAddress(8080));

        //3.连接集合
        List<SocketChannel> channels = new ArrayList<>();
        while(true){
            //4.accept,建立与客户端的连接,SocketChannel,用来与客户端之间通信
            log.debug("connecting...");
            SocketChannel sc = ssc.accept();    //阻塞方法,会让线程暂停;连接建立以后就会继续运行
            log.debug("connected...{}", sc);
            channels.add(sc);
            for (SocketChannel channel : channels){
                //5.接收客户端发送的数据
                log.debug("before read...{}", channel);
                channel.read(buffer);   //阻塞方法,会让线程暂停,客户端发送数据以后会继续运行
                buffer.flip();          //切换到读模式
                debugRead(buffer);
                buffer.clear();
                log.debug("after read...{}", channel);
            }
        }
    }
}

客户端

package com.sisyphus.networkprogramming;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;

public class Client {
    public static void main(String[] args) throws IOException {
        SocketChannel sc = SocketChannel.open();
        sc.connect(new InetSocketAddress("localhost", 8080));
        System.out.println("waiting...");		//此处打断点
    }
}

运行模式启动 Server;调试模式启动 Client,并设置断点

控制台此时打印

13:18:32.281 [main] DEBUG com.sisyphus.networkprogramming.Server - connecting...
13:18:36.590 [main] DEBUG com.sisyphus.networkprogramming.Server - connected...java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:57294]
13:18:36.592 [main] DEBUG com.sisyphus.networkprogramming.Server - before read...java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:57294]

服务端等候客户端发送数据,Evaluate 执行 sc.write(Charset.defaultCharset().encode(“hello!”))
在这里插入图片描述

控制台此时打印

13:18:32.281 [main] DEBUG com.sisyphus.networkprogramming.Server - connecting...
13:18:36.590 [main] DEBUG com.sisyphus.networkprogramming.Server - connected...java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:57294]
13:18:36.592 [main] DEBUG com.sisyphus.networkprogramming.Server - before read...java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:57294]
13:21:07.544 [main] DEBUG io.netty.util.internal.logging.InternalLoggerFactory - Using SLF4J as the default logging framework
+--------+-------------------- read -----------------------+----------------+
position: [0], limit: [6]
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 68 65 6c 6c 6f 21                               |hello!          |
+--------+-------------------------------------------------+----------------+
13:21:07.553 [main] DEBUG com.sisyphus.networkprogramming.Server - after read...java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:57294]
13:21:07.553 [main] DEBUG com.sisyphus.networkprogramming.Server - connecting...

此时如果再次 Evaluate 发送新的数据,服务端是不会响应的,因为服务端只有一个线程,并且阻塞在了 SocketChannel.accept() 方法,当一个新的客户端建立连接后,才会执行后续代码,然后又会阻塞在 SocketChannel.read() 方法

以上,我们能看出阻塞模式的弊端,服务端的一个线程在同一时间只能处理一件事,直到处理完成再开始执行后续的代码

2.模拟非阻塞模式下服务器单线程处理请求

服务端

package com.sisyphus.networkprogramming;

import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
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;

import static com.sisyphus.utils.ByteBufferUtil.debugRead;

@Slf4j
public class Server {
    public static void main(String[] args) throws IOException {
        //使用 nio 来理解阻塞模式,单线程

        //0.ByteBuffer,16字节
        ByteBuffer buffer = ByteBuffer.allocate(16);

        //1.创建服务器
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false);   //设置非阻塞模式

        //2.绑定监听端口
        ssc.bind(new InetSocketAddress(8080));

        //3.连接集合
        List<SocketChannel> channels = new ArrayList<>();
        while(true){
            //4.accept,建立与客户端的连接,SocketChannel,用来与客户端之间通信
            SocketChannel sc = ssc.accept();    //非阻塞方法,如果没有连接建立,sc = null
            if(sc != null){
                log.debug("connected...{}", sc);
                sc.configureBlocking(false);    //设置非阻塞模式
                channels.add(sc);
            }
            for (SocketChannel channel : channels){
                //5.接收客户端发送的数据
                int read = channel.read(buffer);   //非阻塞方法,如果没有读到数据,read 返回 0,表示读到的数据量为 0
                if (read > 0){
                    buffer.flip();          //切换到读模式
                    debugRead(buffer);
                    buffer.clear();
                    log.debug("after read...{}", channel);
                }
            }
        }
    }
}

客户端代码无需改动,使用同样的方法自行模拟测试

非阻塞模式虽然不需要阻塞,但是即使没有任何连接,线程也会不断地进行轮询,浪费 CPU 性能

3.使用 Selector 改进

Selector 的作用就是用来管理多个 Channel 的,并且能够监测这些 Channel 上是否有事件发生,有事件发生再去执行,没有事件发生就阻塞。但如果我们没有对事件处理,那么也不会阻塞

事件的类型

  • accept:服务端,连接请求
  • connect:客户端,连接建立完成
  • read:服务端,可读事件
  • write:客户端,可写事件

可以通过下面三个方法监听是否有事件发生,返回值代表有多少 channel 发生了事件

int count = selector.select();	//阻塞,直到绑定事件发生
int count = selector.select(long timeout);	//阻塞,直到绑定事件发生或者超时
int count = selector.selectNow();	//不阻塞,无论是否有事件,立刻返回,自己根据返回值检查
package com.sisyphus.networkprogramming;

import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;

import static com.sisyphus.utils.ByteBufferUtil.debugRead;

@Slf4j
public class Server {
    public static void main(String[] args) throws IOException {
        //1.创建 Selector,管理多个 Channel
        Selector selector = Selector.open();
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false);

        //2.建立 selector 和 channel 的联系(注册)
        SelectionKey sscKey = ssc.register(selector, 0, null);
        //key 只关注 accept 事件
        sscKey.interestOps(SelectionKey.OP_ACCEPT);
        log.debug("register key:{}", sscKey);

        ssc.bind(new InetSocketAddress(8080));
        while(true){
            //3.select 方法,没有事件就阻塞线程,有事件就唤醒线程,并且如果有未处理事件,就不会阻塞
            selector.select();
            //4.处理事件,selectionKeys 内部包含了所有发生的事件,当 selector 监听到事件发生后,会向该集合中添加一个 SelectionKey,但不会主动删除
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();//想在遍历过程中删除,就需要使用迭代器
            while(iterator.hasNext()){
                SelectionKey key = iterator.next();
                iterator.remove();  //主动删除 SelectionKey,否则下次会再次遍历到
                log.debug("key:{}", key);
                //5.区分事件类型
                if(key.isAcceptable()){ //如果是 accept 事件
                    ServerSocketChannel channel = (ServerSocketChannel)key.channel();
                    SocketChannel sc = channel.accept();
                    sc.configureBlocking(false);
                    SelectionKey scKey = sc.register(selector, 0, null);
                    scKey.interestOps(SelectionKey.OP_READ);
                    log.debug("{}", sc);
                    log.debug("scKey:{}", scKey);
                }else if(key.isReadable()){ //如果是 read 事件
                    try{
                        SocketChannel channel = (SocketChannel) key.channel();  //拿到触发事件的 channel
                        ByteBuffer buffer = ByteBuffer.allocate(16);
                        int read = channel.read(buffer);    //正常断开,read() 返回 -1
                        if(read == -1){
                            key.cancel();
                        }else {
                            buffer.flip();
                            debugRead(buffer);
                        }
                    }catch (IOException e){
                        e.printStackTrace();
                        key.cancel();   //因为出现异常了,需要将 key 取消(从 selector 的 key 集合中删除)
                    } 
                }
            }
        }
    }
}

select() 何时不阻塞

  • 事件发生时
    • 客户端发起连接请求,会触发 accept 事件
    • 客户端发送数据、客户端正常或异常关闭,都会触发 read 事件,如果发送的数据大于 buffer 缓冲区,会触发多次 read 事件
    • channel 可写,会触发 write 事件
    • 在 linux 下 nio bug 发生时
  • 调用 selector.wakeup()
  • 调用 selector.close()
  • selector 所在线程被中断

消息边界问题
互联网场景下,消息的长度和内容无法确定,容易出现消息被截断导致内容含义无法识别的问题
在这里插入图片描述

  • 一种思路是固定消息长度,数据包大小一样,服务器按预定长度读取,缺点是浪费带宽
  • 另一种思路是按分隔符拆分,缺点是效率低
  • TLV 格式,即 Type 类型、Length 长度、Value 数据,类型和长度已知的情况下,就可以方便获取消息大小,分配合适的 buffer,缺点是 buffer 需要提前分配,如果内容过大,则影响 server 吞吐量
    • http 1.1 是 TLV 格式
    • http 2.0 是 LTV 格式

解决方案
在这里插入图片描述
每个 channel 都需要记录可能被切分的消息,因为 ByteBuffer 不是线程安全的,因此需要为每个 channel 维护一个独立的 ByteBuffer,让 buffer 作为附件与 SelectionKey 绑定

package com.sisyphus.networkprogramming;

import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;

import static com.sisyphus.utils.ByteBufferUtil.debugAll;
import static com.sisyphus.utils.ByteBufferUtil.debugRead;

@Slf4j
public class Server {
    public static void main(String[] args) throws IOException {
        //1.创建 Selector,管理多个 Channel
        Selector selector = Selector.open();
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false);

        //2.建立 selector 和 channel 的联系(注册)
        SelectionKey sscKey = ssc.register(selector, 0, null);
        //key 只关注 accept 事件
        sscKey.interestOps(SelectionKey.OP_ACCEPT);
        log.debug("register key:{}", sscKey);

        ssc.bind(new InetSocketAddress(8080));
        while(true){
            //3.select 方法,没有事件就阻塞线程,有事件就唤醒线程,并且如果有未处理事件,就不会阻塞
            selector.select();
            //4.处理事件,selectionKeys 内部包含了所有发生的事件,当 selector 监听到事件发生后,会向该集合中添加一个 SelectionKey,但不会主动删除
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();//想在遍历过程中删除,就需要使用迭代器
            while(iterator.hasNext()){
                SelectionKey key = iterator.next();
                iterator.remove();  //主动删除 SelectionKey,否则下次会再次遍历到
                log.debug("key:{}", key);
                //5.区分事件类型
                if(key.isAcceptable()){ //如果是 accept 事件
                    ServerSocketChannel channel = (ServerSocketChannel)key.channel();
                    SocketChannel sc = channel.accept();
                    sc.configureBlocking(false);
                    ByteBuffer buffer = ByteBuffer.allocate(16);
                    SelectionKey scKey = sc.register(selector, 0, buffer);  //buffer 作为附件绑定 scKey
                    scKey.interestOps(SelectionKey.OP_READ);
                    log.debug("{}", sc);
                    log.debug("scKey:{}", scKey);
                }else if(key.isReadable()){ //如果是 read 事件
                    try{
                        SocketChannel channel = (SocketChannel) key.channel();  //拿到触发事件的 channel
                        ByteBuffer buffer = (ByteBuffer) key.attachment();
                        int read = channel.read(buffer);    //正常断开,read() 返回 -1
                        if(read == -1){
                            key.cancel();
                        }else {
                            split(buffer);  //解决黏包问题
                            if (buffer.position() == buffer.limit()){   //解决半包问题
                                ByteBuffer newBuffer = ByteBuffer.allocate(buffer.capacity() * 2);
                                newBuffer.flip();
                                newBuffer.put(buffer);
                                key.attach(newBuffer);
                            }
                        }
                    }catch (IOException e){
                        e.printStackTrace();
                        key.cancel();   //因为出现异常了,需要将 key 取消(从 selector 的 key 集合中删除)
                    }
                }
            }
        }
    }

    private static void split(ByteBuffer source){
        source.flip();
        for(int i = 0; i < source.limit(); i++){
            //找到一条完整消息
            if(source.get(i) == '\n'){
                int length = i + 1 - source.position();
                //把这条完整消息存入新的 ByteBuffer
                ByteBuffer target = ByteBuffer.allocate(256);
                //从 source 读,向 target 写
                for(int j = 0; j < length; j++){
                    target.put(source.get());
                }
                debugAll(target);
            }
        }
        source.compact();
    }
}

容量可变的 ByteBuffer

ByteBuffer 不能太大,假如一个 ByteBuffer 1MB,要支持百万连接就需要 1TB 内存,因此需要设计容量可变的 ByteBuffer

  • 一种思路是首先分配一个较小的 buffer,例如初始分配 4KB,如果在 read 过程中发现容量不够,那么再创建一个 8KB 的 buffer,然后将原先读取到了 4KB 数据的 buffer 拷贝至 8 KB,再将剩余的数据部分接到尾部。这种思路的优点是消息是连续的,容易处理,缺点是数据拷贝耗费性能
  • 另一种思路是用多个数组组成 buffer,缺点是消息存储不连续,解析复杂,优点是避免了拷贝引起的性能损耗

4.可写事件

服务端

package com.sisyphus.networkprogramming;

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.nio.charset.Charset;
import java.util.Iterator;

public class WriteServer {
    public static void main(String[] args) throws IOException {
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false);

        Selector selector = Selector.open();
        ssc.register(selector, SelectionKey.OP_ACCEPT);

        ssc.bind(new InetSocketAddress(8080));

        while(true){
            selector.select();
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while(iterator.hasNext()){
                SelectionKey key = iterator.next();
                iterator.remove();
                if (key.isAcceptable()){
                    SocketChannel sc = ssc.accept();
                    sc.configureBlocking(false);
                    SelectionKey sckey = sc.register(selector, 0, null);
                    sckey.interestOps(SelectionKey.OP_READ);
                    //1.向客户端发送大量数据
                    StringBuilder sb = new StringBuilder();
                    for(int i = 0; i < 5000000; i++){
                        sb.append("a");
                    }
                    ByteBuffer buffer = Charset.defaultCharset().encode(sb.toString());
                    //2.返回值代表实际写入的字节数
                    int write = sc.write(buffer);
                    System.out.println(write);

                    //3.判断是否有剩余内容
                    if(buffer.hasRemaining()){
                        //4.关注可写事件,但不覆盖原有事件
                        sckey.interestOps(sckey.interestOps() + SelectionKey.OP_WRITE);
                        //5.把未写完的数据挂到 sckey 上
                        sckey.attach(buffer);
                    }
                }else if(key.isWritable()){
                    ByteBuffer buffer = (ByteBuffer) key.attachment();
                    SocketChannel sc = (SocketChannel) key.channel();
                    int write = sc.write(buffer);
                    System.out.println(write);
                    //6.清理操作
                    if (!buffer.hasRemaining()){
                        key.attach(null);   //清除 buffer
                        key.interestOps(key.interestOps() - SelectionKey.OP_WRITE); //不需要关注可写事件
                    }
                }
            }
        }
    }
}

客户端

package com.sisyphus.networkprogramming;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class WriteClient {
    public static void main(String[] args) throws IOException {
        SocketChannel sc = SocketChannel.open();
        sc.connect(new InetSocketAddress("localhost", 8080));

        //3.接收数据
        int count = 0;
        while(true){
            ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
            count += sc.read(buffer);
            System.out.println(count);
            buffer.clear();
        }
    }
}

5.小结

阻塞

  • 阻塞模式下,相关方法都会阻塞线程
    • ServerSocketChannel.accept() 会再没有连接建立的时候阻塞
    • SocketChannel.read() 会在没有数据可读的时候阻塞
    • 阻塞期间不会占用 CPU,但线程相当于被闲置了
  • 单线程的情况下,阻塞方法之间相互影响,几乎不能正常工作,需要多线程支持
  • 多线程下又会有新的问题
    • 64 位 JVM,一个线程占用 1024 KB 内存,如果连接数过多,必然导致 OOM。此外,线程太多,会因为频繁切换上下文导致性能降低
    • 可以采用线程池技术来减少线程数和线程上下问切换,但治标不治本,如果有很多连接建立,但长时间不活跃,那么线程池中的线程就相当于“占着茅坑不拉屎”,既然我们用了线程池,就肯定会设置线程数上限,可能会出现线程池中所有线程都被阻塞的情况。因此不适合长连接,只适合短连接

非阻塞

  • 非阻塞模式下,相关方法不会阻塞线程
    • 在 ServerSocketChannel.accept 没有连接建立的时候,会返回 null
    • SocketChannel.read 在没有数据可读时,会返回 0
    • 写数据时,线程只需等待数据写入 Channel,无需等待 Channel 通过网络将数据发送出去
  • 但非阻塞模式下,即使没有事件发生,线程仍在不断运行,浪费 CPU 性能
  • 数据复制过程中,线程实际还是阻塞的(AIO 改进的地方)

多路复用
单线程可以配合 Selector 完成对多个 Channel 事件的监控,称之为多路复用

  • 多路复用仅针对网络 IO,文件 IO 无法实现多路复用
  • Selector 能够保证当事件发生后才去处理事件

6.利用多线程进行优化

单线程虽然可以同时监听多个事件,但是同时只能处理一个事件

分两组选择器

  • 单线程配一个选择器,专门处理 accept 事件
  • 创建 CPU 核心数的线程,每个线程配一个选择器,轮流处理 read 事件

服务端

package com.sisyphus.networkprogramming;

import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.atomic.AtomicInteger;

import static com.sisyphus.utils.ByteBufferUtil.debugAll;

@Slf4j
public class MultiThreadServer {
    public static void main(String[] args) throws IOException {
        Thread.currentThread().setName("boss"); //修改 main 线程名称
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false);
        Selector boss = Selector.open();
        SelectionKey bossKey = ssc.register(boss, 0, null);
        bossKey.interestOps(SelectionKey.OP_ACCEPT);
        ssc.bind(new InetSocketAddress(8080));
        //1.创建固定数量的 worker 并初始化
        Worker[] workers = new Worker[2];
        for(int i = 0; i < workers.length; i++){
            workers[i] = new Worker("worker-" + i);
        }
        AtomicInteger index = new AtomicInteger();
        while(true){
            boss.select();
            Iterator<SelectionKey> iterator = boss.selectedKeys().iterator();
            while(iterator.hasNext()){
                SelectionKey key = iterator.next();
                iterator.remove();
                if (key.isAcceptable()){
                    SocketChannel sc = ssc.accept();
                    log.debug("connected...{}", sc.getRemoteAddress());
                    sc.configureBlocking(false);
                    //2.关联 selector
                    log.debug("before register...{}", sc.getRemoteAddress());
                    //round robin
                    workers[index.getAndIncrement() % workers.length].register(sc);
                    log.debug("after register...{}", sc.getRemoteAddress());
                }
            }
        }
    }

    static class Worker implements Runnable{
        private Thread thread;
        private Selector selector;
        private String name;
        private volatile boolean start = false; //还未初始化

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

        //初始化线程和 selector
        public void register(SocketChannel sc) throws IOException {
            //确保只会执行一次
            if(!start){
                selector = Selector.open();
                thread = new Thread(this, name);
                thread.start();
                start = true;
            }
            selector.wakeup();  //唤醒 select 方法
            sc.register(selector, SelectionKey.OP_READ, null);  //boss 线程内执行
        }

        @Override
        public void run(){
            while(true){
                try {
                    selector.select();  //select 方法会阻塞
                    Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                    while(iterator.hasNext()){
                        SelectionKey key = iterator.next();
                        iterator.remove();
                        if (key.isReadable()){
                            ByteBuffer buffer = ByteBuffer.allocate(16);
                            SocketChannel channel = (SocketChannel)key.channel();
                            log.debug("read...{}", channel.getRemoteAddress());
                            channel.read(buffer);
                            buffer.flip();
                            debugAll(buffer);
                        }
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

客户端

package com.sisyphus.networkprogramming;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;

public class TestClient {
    public static void main(String[] args) throws IOException {
        SocketChannel sc = SocketChannel.open();
        sc.connect(new InetSocketAddress("localhost", 8080));
        sc.write(Charset.defaultCharset().encode("123456789@abcdef"));
        System.in.read();
    }
}

二、IO 概念剖析

1.Stream 对比 Channel

  • Stream 不会自动缓冲数据,Channel 会利用系统提供的发送缓冲区和接收缓冲区
  • Stream 仅支持阻塞 API,Channel 同时支持阻塞和非阻塞 API,网络 Channel 可以配合 Selector 实现多路复用
  • 二者均为全双工,即读写可以同时进行

2.IO 模型

当调用一次 channel.read() 或者 stream.read() 后,会切换至操作系统内核态来完成真正的数据读取,而读取又分为两个阶段:

  • 等待数据阶段
  • 复制数据阶段
    在这里插入图片描述
    同步阻塞 IO
    指的就是用户线程调用 read() 方法后,Linux 内核空间执行 “等待数据” 和 “复制数据” 时,用户线程被阻塞

同步非阻塞 IO
用户线程在代码中周期性地调用 read() 方法,当某一次调用过程中 Linux 内核空间读取到了数据,用户线程才会被阻塞,直到 “复制数据” 完成

多路复用
用户线程的一个 selector 可以绑定多个 channel,每个 channel 可以监听多个同种事件,每次用户线程 select 后,可以一次性处理多个 channel 中的多个事件。也就是说,select 只需要进行一次 “等待数据”,就可以进行多次 “复制数据”,这里的 “复制数据” 也可以是 “建立连接” 等等的各种事件

异步 IO
同步和异步的本质区别

  • 同步:线程自己去获取结果(只有一个线程)
  • 异步:线程自己不去获取结果,而是由其他线程返回结果(至少两个线程)

因此,不论是阻塞 IO、非阻塞 IO 还是多路复用,它们都是同步 IO

在异步 IO 模型下,用户线程调用 read() 方法后,Linux 内核空间开启另一个线程去处理,然后立刻返回一个结果。此时用户线程得到了返回结果可以继续执行后续的代码,Linux 内核空间中的刚刚被新开启的线程完成任务后会主动调用回调方法,将异步结果返回给用户线程

为了加深大家对异步回调的理解,举个例子。你是老板,下午要跟客户吃饭。你打电话给秘书让他找人帮你选个餐馆定个位置,并且让秘书再找另一个人盯着那个人,如果那个人订到了餐馆就立马通过微信把定好的餐馆信息发给自己。秘书知道后回复你“好的,马上去办”。然后你就接着忙别的事情了,一个小时后你微信收到了餐馆的信息

我们用代码模拟一下

老板类

pubic class Boss{
	private Secretary secretary;

	public void callSecretary(){
		//wechatCallback 可以看作是对 “如果订到了餐馆,给老板发微信” 这一功能的抽象
		String result = secretary.callOtherAsync(new wechatCallback(){
			@Override
			public void onSuccess(String result){
				//订到了餐馆,给指定的微信号发信息
				System.out.println("餐馆信息:" + result);
			}
	
			@Override
			public void onFailure(Throwable throwable){
				//出了异常,没有定到餐馆,,给指定的微信号发信息
				throwable.printStackTrace();
			}
		});

		//收到秘书的回复(这里的 result 属于同步结果,千万不要跟异步结果搞混,这是理解异步的关键一环)
		if(result.substring(0,2) != "好的"){
			//如果秘书表示做不了,那么老板要想其他办法	
		}

		//秘书可以帮忙订餐馆,那么老板可以忙别的事情了
		doSomething();
	}

定到餐馆后微信通知老板的接口

public interface wechatCallback{
	void onSuccess(String result);
	void onFailure(Throwable throwable);
}

秘书类

public class Secretary{
	final int POOL_SIZE = 10;

	//秘书认识的可以帮忙的人
	private ExecutorService executor = Executors.newFixedThreadPool(POOL_SIZE);

	public String callOtherAsync(wechatCallback){
		if(excutor.getActiveCount() == POOL_SIZE){
			return "抱歉老板,我认识的订餐馆的人都在忙"
		}
		
		//找一个人帮忙订餐馆
		Future<String> future = executor.submit(() -> {
			//订餐馆,并返回餐馆信息
			return orderRestaurant();
		});

		//再找另一个人盯着订餐馆的人,有结果后立刻微信通知老板(调用回调方法,返回异步结果)
		excutor.submit(() -> {
			try{
				//获取餐馆信息
				String result = future.get();
				wechatCallback.onSuccess(result);
			} catch(Exception e){
				wechatCallback.onFailure(e);
			}
		});

		//立刻告知老板能不能做(同步结果)
		return "好的,我刚刚已经让人去办了";
	}
}

文件读取的 AIO

package com.sisyphus.io;

import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.channels.CompletionHandler;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

import static com.sisyphus.utils.ByteBufferUtil.debugAll;

@Slf4j
public class AIOFileChannel {
    public static void main(String[] args) throws IOException {
        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(Paths.get("C:\\Users\\313yp\\Desktop\\data.txt"), StandardOpenOption.READ)){
            ByteBuffer buffer = ByteBuffer.allocate(16);
            log.debug("read begin...");
            /*
             * 参数1 ByteBuffer
             * 参数2 读取的起始位置
             * 参数3 附件
             * 参数4 回调对象 CompletionHandler
             * */
            channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
                @Override
                public void completed(Integer result, ByteBuffer attachment) {
                    log.debug("read completed...");
                    buffer.flip();
                    debugAll(buffer);
                }

                @Override
                public void failed(Throwable exc, ByteBuffer attachment) {
                    exc.printStackTrace();
                }
            });
        } catch (IOException e) {
            e.printStackTrace();
        }
        log.debug("主线程未阻塞,doSomethingElse...");
        System.in.read();
    }
}

执行结果

16:29:27.955 [main] DEBUG com.sisyphus.io.AIOFileChannel - read begin...
16:29:27.959 [main] DEBUG com.sisyphus.io.AIOFileChannel - 主线程未阻塞,doSomethingElse...
16:29:27.959 [Thread-20] DEBUG com.sisyphus.io.AIOFileChannel - read completed...
16:29:27.965 [Thread-20] DEBUG io.netty.util.internal.logging.InternalLoggerFactory - Using SLF4J as the default logging framework
+--------+-------------------- all ------------------------+----------------+
position: [0], limit: [8]
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 73 69 73 79 70 68 75 73 00 00 00 00 00 00 00 00 |sisyphus........|
+--------+-------------------------------------------------+----------------+

3.零拷贝

BIO 下,客户端读取磁盘数据发送给服务端的过程
在这里插入图片描述

  1. Java 本身不具备 IO 能力,因此 read() 方法被调用后,要从 Java 程序的用户态切换到内核态,调用操作系统的 IO 能力,将数据读入内核缓冲区,这期间用户线程阻塞
  2. 从内核态切换回用户态,将数据从内核缓冲区读入用户缓冲区,这期间 CPU 参与拷贝
  3. 调用 write() 方法,将数据从用户缓冲区写入 Socket 缓冲区,CPU 参与拷贝
  4. 向网卡写入数据,Java 不具备 IO 能力,又需要从用户态切换至内核态,调用操作系统的 IO 能力,这期间用户线程阻塞

我们可以看到用户态与内核态的切换发生了 3 次,数据拷贝了 4 次

NIO 优化
在这里插入图片描述
Java 可以使用 DirectByteBuf 将堆外内存映射到 JVM 内存中来使用,这块内存不受垃圾回收影响,所以内存地址固定,有助于 IO 读写,减少了一次数据拷贝

在这里插入图片描述
还可以使用 FileChannel 的 transferFrom 和 transferTo() 两个方法拷贝数据,进一步优化,底层采用的是 Linux 2.1 之后提供的 sendFile() 方法

  1. Java 调用 transferTo() 方法后,从用户态切换至内核态,将数据读入内核缓冲区,不需要使用 CPU
  2. 数据从内核缓冲区传输到 Socket 缓冲,CPU 参与拷贝
  3. 将 Socket 缓冲区的数据写入网卡,不需要使用 CPU

我们减少了一次用户态与内核态的切换

在这里插入图片描述
Linux 2.4 又又进一步进行了优化

  1. Java 调用 transferTo() 方法后,从用户态切换至内核态,将数据读入内核缓冲区,不需要使用 CPU
  2. 只会将一些 offset 和 length 信息写入 Socket 缓冲区,几乎无消耗
  3. 直接将内核缓冲区的数据写入网卡,不需要使用 CPU

我们又减少了一次用户态与内核态的切换,并且两次拷贝的其中一次拷贝几乎无消耗,我们也算减少了一次拷贝

所谓的零拷贝,并不是真正无拷贝,而是不会拷贝数据到 JVM 内存中,零拷贝的优点有

  • 更少的用户态与内核态切换
  • 减少 CPU 占用
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

313YPHU3

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值