引言

《跟闪电侠学Netty》 并不是集体接触的第一本Netty书籍,但集体更举荐读者把它作为作为第一本Netty入门的书籍。

《Netty In Action》 不同,这本书间接从Netty入门程序代码开始引入Netty框架,前半部分教你如何用Netty搭建繁难的通信零碎,整体难度比拟低,后半局部间接从服务端源码、客户端源码、ChannelPipeline开始介绍,和前半部分割裂较为重大。

相较于入门的程序,源码剖析毫无疑问是比拟有干货的局部,然而和后面入门程序相比有点学完了99乘法表就让你去做微积分的卷子一样,如果Netty应用陌生源码局部解说必定是非常难懂的,所以更倡议只看前半截。

集体比拟举荐这本书吃透Netty编写的简略通信“我的项目”之后,间接去看《Netty In Action》做一个更为零碎的深刻和根底坚固。等《Netty In Action》看明确之后,再回过头来看《跟闪电侠学Netty》的源码剖析局部。

抛开源码剖析局部,这本书是“我奶奶都能学会”的优良入门书籍,用代码实战加解说形式学起来轻松印象粗浅。

开篇入门局部先不引入我的项目,这里先对于过来JDK的网络IO模型作为引子介绍为什么咱们须要用Netty,学习Netty带来的益处等。

思维导图

Netty 依赖版本(4.1.6.Final)

本书应用的Netty版本为 4.1.6,为了防止前面浏览源码的时候产生误解,倡议以此版本为基准。

    <dependency>        <groupId>io.netty</groupId>        <artifactId>netty-all</artifactId>        <version>4.1.6.Final</version>    </dependency>

JDK 原生编程模型

到目前为止,JDK一共实现了三种网络IO编程模型:BIO、NIO和AIO。遗憾的是这三种模型不仅产生的距离时间跨度大,并且由三组齐全不同编程格调的开发人员设计API,不同编程模型和设计思路之间的切换十分复杂,开发者的学习老本也比拟大。

针对这些问题,咱们间接理解Netty如何对立这些模型以及如何升高并发编程的开发难度,咱们先对过来的JDK网络IO编程模型做一个理解。

洗衣机洗衣服例子理清了解阻塞非阻塞,同步异步概念

在理解JDK的网络IO模型之前,比方得先理解绕不过的阻塞非阻塞同步异步的概念。

同步和异步指的是工作之间是否须要期待其它工作实现或者期待某个事件的产生。如果一个工作必须期待另一个工作实现能力继续执行,那么这两个工作就是同步的;如果一个工作能够间接继续执行而无需期待另一个工作的实现,那么这两个工作就是异步的。

阻塞和非阻塞指的是工作在期待后果时是否会始终占用CPU资源。如果一个工作在期待后果时会始终占用CPU资源,那么这个工作就是阻塞的;如果一个工作在期待后果时不会占用CPU资源,那么这个工作就是非阻塞的。

这里给一个生存中洗衣服的例子帮忙齐全没有理解过这些概念的读者加深印象,这个例子来源于某个网课,集体感觉非常贴切和易懂就拿过去用了。

同步阻塞

了解:

洗衣服丢到洗衣机,全程看着洗衣机洗完,洗好之后晾衣服。

类比 :

  • 申请接口
  • 期待接口返回后果,两头不能做其余事件。
  • 拿到后果解决数据

剖析:
同步:全程看着洗衣机洗完。
阻塞:期待洗衣机洗好衣服之后跑过来晾衣服。

同步非阻塞

了解:

把衣服丢到洗衣机洗,而后回客厅做其余事件,定时看看洗衣机是不是洗完了,洗好后再去晾衣服。(期待期间你能够做其余事件,比方用电脑刷剧看视频)。

这种模式相似日常生活洗衣机洗衣服。

类比:

  • 申请接口。
  • 期待期间切换到其余工作,然而须要定期察看接口是否有回送数据。
  • 拿到后果解决数据。

剖析:

和阻塞形式的最大区别是不须要始终盯着洗衣机,期间能够抽空干其余的事件。

同步:期待洗衣机洗完这个事件没有实质变动,洗好衣服之后还是要跑过来晾衣服。
非阻塞:拿到衣服之前能够干别的事件,只不过须要每次隔一段时间查看能不能拿到洗好的衣服。

异步阻塞

了解:

把衣服丢到洗衣机洗,而后看着洗衣机洗完,洗好后再去晾衣服(没这个状况,简直没这个说法,能够疏忽)。

类比:

  • 申请接口,不须要关怀后果。
  • 客户端能够抽空干其余事件,然而非得期待接口返回后果
  • 拿到服务端的处理结果

剖析:

难以描述,简直不存在这种说法。

异步非阻塞

了解:

把衣服丢到洗衣机洗,而后回客厅做其余事件,洗衣机洗好后会主动去晾衣服,晾实现后放个音乐通知你洗好衣服并晾好了

类比 :

  • 申请接口,此时客户端能够继续执行代码。
  • 服务端筹备并且解决数据,在解决实现之后在适合的工夫告诉客户端
  • 客户端收到服务端解决实现的后果。

剖析:
异步:洗衣机本人不仅把衣服洗好了还帮咱们把衣服晾好了。
非阻塞:拿到“衣服”后果之前能够干别的事件。

留神异步非阻塞状况下,“咱们”看待洗衣服这件事件的“态度”齐全变了。

BIO 编程模型

BIO叫做阻塞IO模型,在阻塞IO模型中两个工作之间须要期待响应后果,利用过程须要期待内核把整个数据筹备好之后能力开始进行解决。

BIO是入门网络编程的第一个程序,从JDK1.0开始便存在了,存在于java.net包当中。上面的程序也是入门Tomcat源码的根底程序。

Java实现代码

在BIO的实现代码中,服务端通过accept始终阻塞期待直到有客户端连贯。首先是服务端代码。

public static void main(String[] args) throws IOException {      ServerSocket serverSocket = new ServerSocket(8000);        // 承受连贯      new Thread(() -> {          while (true) {              // 1. 阻塞获取连贯              try {                  Socket socket = serverSocket.accept();                    // 2. 为每一个新连贯应用一个新线程                  new Thread(() -> {                      try {                          int len;                          byte[] data = new byte[1024];                          InputStream inputStream = socket.getInputStream();                          // 字节流读取数据                          while ((-1 != (len = inputStream.read()))) {                              System.err.println(new String(data, 0, len));                          }                      } catch (IOException ioException) {                          ioException.printStackTrace();                      }                  }).start();              } catch (IOException e) {                  e.printStackTrace();                }          }      }).start();  }

较为外围的局部是serverSocket.accept()这一串代码,会导致服务端阻塞期待客户端的连贯申请,即便没有连贯也会始终阻塞。

服务端启动之后会监听8000端口,期待客户端连贯,此时须要始终占用CPU资源,获取到客户端连贯之将会开拓一个新的线程独自为客户端提供服务。

而后是客户端代码。

public static void main(String[] args) {      new Thread(()->{          try {              Socket socket = new Socket("127.0.0.1", 8000);              while (true){                  socket.getOutputStream().write((new Date() + ":"+ "hellow world").getBytes(StandardCharsets.ISO_8859_1));                  try {                      Thread.sleep(2000);                  } catch (InterruptedException e) {                      e.printStackTrace();                  }              }          } catch (IOException ioException) {              ioException.printStackTrace();          }      }).start();  }

客户端的外围代码如下,通过建设Socket和服务端建设连贯。

Socket socket = new Socket("127.0.0.1", 8000);
Connected to the target VM, address: '127.0.0.1:5540', transport: 'socket'

客户端启动之后会距离两秒发送数据给服务端,服务端收到申请之后打印客户端传递的内容。

Connected to the target VM, address: '127.0.0.1:5548', transport: 'socket'Disconnected from the target VM, address: '127.0.0.1:5548', transport: 'socket'Process finished with exit code 130

优缺点剖析

传统的IO模型有如下优缺点:

  • 长处

    • 实现简略 。
    • 客户端较少状况下运行良好。
  • 毛病

    • 每次连贯都须要一个独自的线程。
    • 单机单核心线程上下文切换代价微小 。
    • 数据读写只能以字节流为单位。
    • while(true) 死循环十分节约CPU资源 。
    • API 艰涩难懂,对于编程人员须要思考十分多的内容。

论断:

在传统的IO模型中,每个连贯创立胜利之后都须要一个线程来保护,每个线程蕴含一个while死循环,那么1w个连贯对应1w个线程,继而1w个while死循环。

单机是不可能实现同时撑持1W个线程的,然而在客户端连贯数量较少的时候,这种形式效率很高并且实现非常简单。

NIO 编程模型

NIO 编程模型是 JDK1.4 呈现的全新API,它实现的是同步非阻塞IO编程模型。以上面的模型为例,第二阶段仍然须要期待后果之后被动解决数据,次要的区别在第一阶段(红线局部)轮询的时候能够干别的事件,只需屡次调用查看是否有数据能够开始读取。

Java 实现代码

NIO编程模型中,新来一个连贯不再创立一个新的线程,而是能够把这条连贯间接绑定到某个指定线程

概念上了解NIO并不难,然而要写出JDK的NIO编程模板代码却不容易。

public static void main(String[] args) throws IOException {      Selector serverSelector = Selector.open();        Selector clientSelector = Selector.open();        new Thread(() -> {            try {                // 对应IO编程中服务端启动                ServerSocketChannel listenerChannel = ServerSocketChannel.open();                listenerChannel.socket().bind(new InetSocketAddress(8000));                listenerChannel.configureBlocking(false);                listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);                while (true) {                    // 监测是否有新的连贯,这里的1指的是阻塞的工夫为1ms                    if (serverSelector.select(1) > 0) {                        Set<SelectionKey> set = serverSelector.selectedKeys();                        Iterator<SelectionKey> keyIterator = set.iterator();                        while (keyIterator.hasNext()) {                            SelectionKey key = keyIterator.next();                            if (key.isAcceptable()) {                                try {                                    // (1) 每来一个新连贯,不须要创立一个线程,而是间接注册到clientSelector                                    SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();                                    clientChannel.configureBlocking(false);                                    clientChannel.register(clientSelector, SelectionKey.OP_READ);                                } finally {                                    keyIterator.remove();                                }                            }                        }                    }                }            } catch (IOException ignored) {            }        }).start();        new Thread(() -> {            try {                while (true) {                    // (2) 批量轮询是否有哪些连贯有数据可读,这里的1指的是阻塞的工夫为1ms                    if (clientSelector.select(1) > 0) {                        Set<SelectionKey> set = clientSelector.selectedKeys();                        Iterator<SelectionKey> keyIterator = set.iterator();                        while (keyIterator.hasNext()) {                            SelectionKey key = keyIterator.next();                            if (key.isReadable()) {                                try {                                    SocketChannel clientChannel = (SocketChannel) key.channel();                                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);                                    // (3) 读取数据以块为单位批量读取                                    clientChannel.read(byteBuffer);                                    byteBuffer.flip();                                    System.out.println(Charset.defaultCharset().newDecoder().decode(byteBuffer)                                            .toString());                                } finally {                                    keyIterator.remove();                                    key.interestOps(SelectionKey.OP_READ);                                }                            }                        }                    }                }            } catch (IOException ignored) {            }        }).start();}

下面的代码不须要过多纠结,NIO的代码模板的确非常复杂,咱们能够把下面的两个线程看作是两个传送带,第一条传送带只负责接管内部的连贯申请,收到申请数据之后间接丢给第二条传送带解决。第二条传送带收到工作之后进行解析和解决,最初把后果返回即可。

书中并没有给NIO的客户端案例,然而有意思的是Netty的客户端启动连贯代码能够完满连接JDK的NIO Server服务端,从这一点上能够发现Netty的NIO编程模型实际上就是对于JDK NIO模型的改进和优化。

PS:后续篇章的源码浏览能够看到Netty和JDK的API的关系密不可分。
public static void main(String[] args) throws InterruptedException {      Bootstrap bootstrap = new Bootstrap();      NioEventLoopGroup eventExecutors = new NioEventLoopGroup();      // 疏导器疏导启动      bootstrap.group(eventExecutors)              .channel(NioSocketChannel.class)              .handler(new ChannelInitializer<Channel>() {                  @Override                  protected void initChannel(Channel channel) throws Exception {                      channel.pipeline().addLast(new StringEncoder());                  }              });        // 建设通道      Channel channel = bootstrap.connect("127.0.0.1", 8000).channel();        while (true){          channel.writeAndFlush(new Date() + " Hello world");          Thread.sleep(2000);      }  }

Netty无论是客户端启动还是服务端启动都会打印一堆日志,上面是客户端启动日志。

14:42:24.020 [main] DEBUG i.n.buffer.PooledByteBufAllocator - -Dio.netty.allocator.cacheTrimIntervalMillis: 014:42:24.020 [main] DEBUG i.n.buffer.PooledByteBufAllocator - -Dio.netty.allocator.useCacheForAllThreads: false14:42:24.020 [main] DEBUG i.n.buffer.PooledByteBufAllocator - -Dio.netty.allocator.maxCachedByteBuffersPerChunk: 102314:42:24.027 [main] DEBUG io.netty.buffer.ByteBufUtil - -Dio.netty.allocator.type: pooled14:42:24.027 [main] DEBUG io.netty.buffer.ByteBufUtil - -Dio.netty.threadLocalDirectBufferSize: 014:42:24.027 [main] DEBUG io.netty.buffer.ByteBufUtil - -Dio.netty.maxThreadLocalCharBufferSize: 1638414:42:24.052 [main] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.maxCapacityPerThread: 409614:42:24.052 [main] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.ratio: 814:42:24.052 [main] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.chunkSize: 3214:42:24.052 [main] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.blocking: false14:42:24.060 [nioEventLoopGroup-2-1] DEBUG io.netty.buffer.AbstractByteBuf - -Dio.netty.buffer.checkAccessible: true14:42:24.060 [nioEventLoopGroup-2-1] DEBUG io.netty.buffer.AbstractByteBuf - -Dio.netty.buffer.checkBounds: true14:42:24.060 [nioEventLoopGroup-2-1] DEBUG i.n.util.ResourceLeakDetectorFactory - Loaded default ResourceLeakDetector: io.netty.util.ResourceLeakDetector@310af49Disconnected from the target VM, address: '127.0.0.1:13875', transport: 'socket'Process finished with exit code 130

客户端连贯之后会距离2S向服务端推送以后工夫。

Connected to the target VM, address: '127.0.0.1:13714', transport: 'socket'Tue Apr 11 14:42:24 CST 2023 Hello worldTue Apr 11 14:42:26 CST 2023 Hello worldTue Apr 11 14:42:28 CST 2023 Hello world

JDK的NIO针对BIO的改进点

NIO模型工作上有了“分工”的细节,即两个Selector,一个负责承受新连贯,另一个负责解决连贯传递的数据。

比照BIO模型一个连贯就调配一个线程的策略,NIO模型的策略是让所有的连贯注册过程变为由一个Selector实现,Selector会定期轮询查看哪个客户端连贯能够接入,如果能够接入就注册到以后的Selector,后续遇到数据读取只须要轮询一个Selector就行了。

线程资源受限问题通过Selector将每个客户端的while(true) 转为只有一个 while(true) 死循环得以解决,它的“副作线程用”是线程的缩小间接带来了切换效率的晋升。不仅如此NIO还提供了面向Buffer的缓存 ByteBuffer,进步读写效率,挪动指针任意读写。

JDK的NIO编程模型毛病

看起来无非就是代码简单了一点,其实NIO模型看起来也“还不错”?

NO!NO!NO!JDK的NIO实际上还有很多其余问题:

    1. API简单难用,须要了解十分多的底层概念 。(尤其是臭名远扬的 ByteBuffer)
    1. JDK没有线程模型,用户须要本人设计底层NIO模型。
    1. 自定义协定也要拆包 。
    1. JDK的NIO是因为Epoll实现的,底层存在空轮询的BUG
    1. 自行实现NIO模型会存在很多问题。
    1. 编程人员的编程程度档次不齐,集体定制的NIO模型难以通用,替换性也很差。
基于以上种种问题,Netty 通通都有解决方案。

简略介绍AIO

JDK的AIO不是很成熟,AIO底层仍然因为Epoll的遗留问题存在臭名远扬的空轮询BUG,这里并不举荐读者应用JDK的AIO进行编程。

Java AIO 的外围在于两个要害类:AsynchronousSocketChannelAsynchronousServerSocketChannel

AsynchronousSocketChannel 实现异步套接字通信,能够让咱们在不同的客户端连贯之间切换,而无需创立新的线程或线程池。

AsynchronousServerSocketChannel 则用于异步地监听客户端的连贯申请。

Java 实现代码

这里用ChatGPT生成了一段JDK的AIO代码,为了更好了解顺带让它把正文一块生成了。

public class AIOServer {          public static void main(String[] args) throws IOException {          // 创立一个 ExecutorService,用于解决异步操作的线程池          ExecutorService executor = Executors.newFixedThreadPool(10);          // 创立一个 AsynchronousChannelGroup,将线程池与该 Channel 组关联          AsynchronousChannelGroup channelGroup = AsynchronousChannelGroup.withThreadPool(executor);            // 创立 AsynchronousServerSocketChannel,并绑定到指定地址和端口          final AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open(channelGroup);          InetSocketAddress address = new InetSocketAddress("localhost", 12345);          serverSocketChannel.bind(address);            System.out.println("Server started on port " + address.getPort());            // 调用 accept 办法接管客户端连贯,同时传入一个 CompletionHandler 解决连贯后果          serverSocketChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {              // 当连贯胜利时会调用 completed 办法,传入客户端的 SocketChannel 实例作为参数              @Override              public void completed(AsynchronousSocketChannel clientSocketChannel, Object attachment) {                  // 持续承受下一个客户端连贯,并解决以后客户端的申请                  serverSocketChannel.accept(null, this);                  handleClient(clientSocketChannel);              }                // 当连贯失败时会调用 failed 办法,传入异样信息作为参数              @Override              public void failed(Throwable exc, Object attachment) {                  System.out.println("Error accepting connection: " + exc.getMessage());              }          });            // 在主线程中期待,避免程序退出          while (true) {              try {                  Thread.sleep(Long.MAX_VALUE);              } catch (InterruptedException e) {                  break;              }          }      }        private static void handleClient(AsynchronousSocketChannel clientSocketChannel) {          ByteBuffer buffer = ByteBuffer.allocate(1024);          // 读取客户端发送的数据,同时传入一个 CompletionHandler 解决读取后果          clientSocketChannel.read(buffer, null, new CompletionHandler<Integer, Object>() {              // 当读取胜利时会调用 completed 办法,传入读取到的字节数和附件对象(此处不须要)              @Override              public void completed(Integer bytesRead, Object attachment) {                  if (bytesRead > 0) {                      // 将 Buffer 翻转,以便进行读取操作                      buffer.flip();                      byte[] data = new byte[bytesRead];                      buffer.get(data, 0, bytesRead);                      String message = new String(data);                      System.out.println("Received message: " + message);                      // 向客户端发送数据                      clientSocketChannel.write(ByteBuffer.wrap(("Hello, " + message).getBytes()));                      buffer.clear();                      // 持续读取下一批数据,并传入以后的 CompletionHandler 以解决读取后果                      clientSocketChannel.read(buffer, null, this);                  } else {                      try {                          // 当客户端敞开连贯时,敞开该 SocketChannel                        clientSocketChannel.close();                      } catch (IOException e) {                          System.out.println("Error closing client socket channel: " + e.getMessage());                      }                  }              }                // 当读取失败时会调用 failed 办法,传入异样信息和附件对象(此处不须要)              @Override              public void failed(Throwable exc, Object attachment) {                  System.out.println("Error reading from client socket channel: " + exc.getMessage());              }          });      }      }

AIO 编程模型优缺点

长处

并发性高、CPU利用率高、线程利用率高 。

毛病

不适宜轻量级数据传输,因为过程之间频繁的通信在追错、治理,资源耗费上不是很可观。

实用场景

对并发有需要的重量级数据传输。

从下面的代码也能够看出,AIO的API和NIO又是截然不同的写法,为了不持续减少学习老本,这里点到为止,不再深刻AIO编程模型的局部了,让咱们持续回到Netty,理解Netty的编程模型。

应用Netty 带来的益处

  • Netty不须要理解过多概念
  • 底层IO模型随便切换
  • 自带粘包拆包的问题解决
  • 解决了空轮询问题
  • 自带协定栈,反对通用协定切换
  • 社区沉闷,各种问题都有解决方案
  • RPC、消息中间件实际,健壮性极强

网络IO通信框架过程

一个网络IO通信框架从客户端发出请求到承受到后果,根本蕴含了上面这8个操作:

    1. 解析指令
    1. 构建指令对象
    1. 编码
    1. 期待响应
    1. 解码
    1. 翻译指令对象
    1. 解析指令
    1. 执行

上面来看看Netty的编程模型。

Netty 启动模板代码(重要)

通过下面一长串的铺垫,当初来到整体Netty的代码局部:

服务端

首先是服务端代码:

 public static void main(String[] args) {        ServerBootstrap serverBootstrap = new ServerBootstrap();        NioEventLoopGroup boos = new NioEventLoopGroup();        NioEventLoopGroup worker = new NioEventLoopGroup();        serverBootstrap                .group(boos, worker)                .channel(NioServerSocketChannel.class)                .childHandler(new ChannelInitializer<NioSocketChannel>() {                    protected void initChannel(NioSocketChannel ch) {                        ch.pipeline().addLast(new StringDecoder());                        ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {                            @Override                            protected void channelRead0(ChannelHandlerContext ctx, String msg) {                                System.out.println(msg);                            }                        });                    }                })                .bind(8000);    }

初学Netty的时候可能没有NIO的教训,所以咱们简略做个类比:

NioEventLoopGroup boos = new NioEventLoopGroup();NioEventLoopGroup worker = new NioEventLoopGroup();

能够间接看作

Selector serverSelector = Selector.open();Selector clientSelector = Selector.open();

其中boss负责解决连贯,worker负责读取申请和解决数据。两者的工作模式也是相似的,boss就像是老板负责“接单”,worker 打工仔负责接管单子的内容而后开始打工干活。

客户端

客户端的启动代码如下。

public static void main(String[] args) throws InterruptedException {      Bootstrap bootstrap = new Bootstrap();      NioEventLoopGroup eventExecutors = new NioEventLoopGroup();      // 疏导器疏导启动      bootstrap.group(eventExecutors)              .channel(NioSocketChannel.class)              .handler(new ChannelInitializer<Channel>() {                  @Override                  protected void initChannel(Channel channel) throws Exception {                      channel.pipeline().addLast(new StringEncoder());                  }              });        // 建设通道      Channel channel = bootstrap.connect("127.0.0.1", 8000).channel();        while (true){          channel.writeAndFlush(new Date() + " Hello world");          Thread.sleep(2000);      }    }

客户端的代码中的NioEventLoopGroup理论对应了main函数独自开启的线程。下面的代码能够完满的代替调JDK的NIO、AIO、BIO 的API,学习老本大大降低,Netty为使用者做了大量的“筹备”工作,提供了很多"开箱即用"的性能,十分不便。

Netty的服务端和客户端的入门程序代码是剖析源码的开始,这部分代码须要有较深的印象。

问题

摘录局部Netty入门级别的八股。

Linux网络编程中的五种I/O模型

关键点:

不同的角度了解IO模型的概念会有变动。留神本局部站在用户程序和内核的网络IO交互的角度了解的。

权威:

  • RFC规范
  • 书籍 《UNIX Network Programming》(中文名《UNIX网络编程-卷一》)第六章。

上面局部总结自:《UNIX Network Programming》(中文名《UNIX网络编程-卷一》)

1)阻塞式I/O

留神原书中阻塞式I/O给出的例子是UDP而不是TCP的例子。recvfrom 函数能够看作是零碎调用,在阻塞I/O模型中,recvfrom 的零碎调用要期待内核把数据从内核态拷贝到用户的缓冲池或者产生谬误的时候(比方信号中断)才进行返回。recvfrom 收到数据之后再执行数据处理。

2)非阻塞式I/O

recvfrom 的零碎调用会在设置非阻塞的时候,会要求内核在无数据的时候返回谬误,所以后面三次都是谬误调用,在第四次调用之后此时recvfrom轮询到数据,于是开始失常的期待内核把数据复制到用户过程缓存。

此处轮询的定义为:对于描述符进行recvfrom循环调用,会减少CPU的开销。留神非阻塞的轮询不肯定要比阻塞期待要强,有时候甚至会有无意义的开销反而不如阻塞。

3)I/O复用(select,poll,epoll...)

I/O多路复用是阻塞在select,epoll这样的零碎调用,没有阻塞在真正的I/O零碎调用如recvfrom。过程受阻于select,期待可能多个套接口中的任一个变为可读

IO多路复用最大的区别是应用两个零碎调用(select和recvfrom)。Blocking IO(BIO)只调用了一个零碎调用(recvfrom)。

select/epoll 外围是能够同时解决多个 connection,然而这并不一定晋升效率,连接数不高的话性能不肯定比多线程+阻塞IO好。然而连接数比拟宏大之后会有显著的差距。

多路复用模型中,每一个socket都须要设置为non-blocking,否则是无奈进行elect的。

listenerChannel.configureBlocking(false);这个设置的意义就在于此。

4)信号驱动式I/O(SIGIO)

信号驱动的劣势是期待数据报到之前过程不被阻塞,主循环能够继续执行,期待信号到来即可,留神这里有可能是数据曾经筹备好被解决,或者数据复制实现能够筹备读取。

信号驱动IO 也是同步模型,尽管能够通过信号的形式缩小交互,然而零碎调用过程当中仍然须要进行期待,内核也仍然是告诉何时开启一个IO操作,和后面介绍的IO模型比照发现劣势并不显著。

5)异步I/O(POSIX的aio_系列函数)

外围: Future-Listener机制

  • IO操作分为两步

    • 发动IO申请,期待数据筹备(Waiting for the data to be ready)
    • 理论的IO操作,将数据从内核拷贝到过程中(Copying the data from the kernel to the process)

前四种IO模型都是同步IO操作,次要的区别在于第一阶段解决形式,而他们的第二阶段是一样的:在数据从内核复制到利用缓冲区期间(用户空间),过程阻塞于recvfrom调用或者select() 函数。 异步I/O模型外在这两个阶段都要(自行)解决。

阻塞IO和非阻塞IO区别在于第一步,发动IO申请是否会被阻塞,如果阻塞直到实现那么就是传统的阻塞IO,如果不阻塞,那么就是非阻塞IO。

同步IO和异步IO的区别就在于第二个步骤是否阻塞,如果理论的IO读写阻塞申请过程,那么就是同步IO,因而阻塞IO、非阻塞IO、IO复用、信号驱动IO都是同步IO,如果不阻塞,而是操作系统帮你做完IO操作再将后果返回给你,那么就是异步IO

异步IO模型十分像是咱们日常点外卖,咱们时不时看看配送进度就是在“轮询”,当外卖员把外卖送到指定地位打电话告诉咱们去拿即可。

交互几个外围点

再次强调是针对用户程序和内核的网络IO交互角度了解的。

  • 阻塞非阻塞说的是线程的状态(重要)
  • 同步和异步说的是音讯的告诉机制(重要)
     - 同步须要被动读写数据,异步是不须要被动读写数据
     - 同步IO和异步IO是针对用户应用程序和内核的交互

为什么Netty应用NIO而不是AIO?

  1. Netty不看重Windows上的应用,在Linux零碎上,AIO的底层实现仍应用EPOLL,没有很好实现AIO,因而在性能上没有显著的劣势,而且被JDK封装了一层不容易深度优化
  2. Netty整体架构是reactor模型, 而AIO是proactor模型, 混合在一起会十分凌乱,把AIO也革新成reactor模型看起来是把epoll绕个弯又绕回来
  3. AIO还有个毛病是接收数据须要事后调配缓存, 而不是NIO那种须要接管时才须要调配缓存, 所以对连贯数量十分大但流量小的状况, 内存节约很多
  4. Linux上AIO不够成熟,解决回调后果速度跟不到解决需要,比方外卖员太少,顾客太多,供不应求,造成处理速度有瓶颈(待验证)。

论断

Netty整体架构是reactor模型,采纳epoll机制,所以往深的说,还是IO多路复用模式,所以也可说netty是同步非阻塞模型(看的档次不一样),只不过理论是异步IO。

Netty 利用场景理解么?

  • 作为 RPC 框架的网络通信工具。分布式系统之间的服务器通信能够应用Netty实现,尽管是Java编写的框架,然而性能十分靠近 C 和C++ 执行效率。
  • 作为 RPC 框架的网络通信工具,Netty自身就是
  • 音讯队列:比方赫赫有名的RocketMq底层齐全依赖Netty,编程人员不须要很强的并发编程功底也能够疾速上手和保护代码。
  • 实现一个即时通讯零碎:正好和本书利用场景重合了。

介绍Netty

简短介绍

Netty是一个高性能异步NIO编程模型的网络编程框架。它提供了简略易用的API,能够疾速地开发各种网络应用程序,如客户端、服务器和协定实现等。同时,Netty还具备良好的可扩展性和灵活性,反对多种传输协定和编解码器。

略微简单一点

Netty是由JBOSS提供的一个java开源框架, 是业界最风行的NIO框架,整合了多种协定( 包含FTP、SMTP、HTTP等各种二进制文本协定)的实现教训,精心设计的框架,在多个大型商业我的项目中失去充沛验证。 1)API应用简略 2)成熟、稳固 3)社区沉闷 有很多种NIO框架 如mina 4)通过大规模的验证(互联网、大数据、网络游戏、电信通信行业)。

总结

  • 开篇简略介绍了JDK的BIO、NIO和AIO,三者不仅呈现时间跨度大,三个团队编写,和JDK的IO编程一样艰涩难懂和不好用,开发人员须要花大量事件学习底层细节。
  • 用洗衣机的例子,了解网络编程模型的重要概念:同步、非同步、阻塞、非阻塞。从入门的角度来看,同步和异步能够认为是否是由客户端被动获取数据,而阻塞和非阻塞则是客户端是否须要拿到后果进行解决,两者是相辅相成的。
  • Netty 编程模型对立了JDK的编程模型,升高了学习老本,同时效率比原生JDK更高,并且解决了NIO 中的空轮询问题。
  • Netty 底层实际上和JDK的网络编程模型密切相关,从案例代码能够看到Netty的客户端API代码能够间接往NIO的Server发送数据。
  • 补充书中没有介绍的AIO编程模型,用ChatGPT 生成的代码简略易懂。
  • 最初补充无关Netty的问题。

写在最初

开篇局部补充了书中没介绍的一些网络编程模型的基本概念,以及在最初关联了些相干书籍的知识点和,最初顺带演绎了一些八股问题,当然最为重要的局部是相熟Netty的入门程序代码。

开篇入门篇到此就完结了,如果内容形容有误,欢送评论或者私信留言。

参考

  • 《跟闪电侠学Netty》开篇:Netty是什么? - 简书 (jianshu.com)
  • 网课专栏:《高并发系列之百万连贯Netty实战课程》

Netty 书籍举荐

  • 《Netty权威指南》
  • 《Netty进阶之路》