Netty基础知识

一、Netty简介

Netty 是基于 Java NIO 的异步事件驱动的网络应用框架,使用 Netty 可以快速开发网络应用,Netty 提供了高层次的抽象来简化 TCP 和 UDP 服务器的编程,但是你仍然可以使用底层的 API。

  • 高并发:Netty 是一款基于 NIO(Nonblocking IO,非阻塞IO)开发的网络通信框架,对比于 BIO(Blocking I/O,阻塞IO),他的并发性能得到了很大提高。
  • 传输快:Netty 的传输依赖于零拷贝特性,尽量减少不必要的内存拷贝,实现了更高效率的传输。
  • 封装好:Netty 封装了 NIO 操作的很多细节,提供了易于使用调用接口。

在这里插入图片描述

二、Netty线程模型

Netty 线程模型是典型的 Reactor 模型结构,其中常用的 Reactor 线程模型有三种,分别为:Reactor单线程模型Reactor多线程模型Reactor主从多线程模型

Netty 的线程模型并非固定不变,通过在启动辅助类中创建不同的 EventLoopGroup 实例并通过适当的参数配置,可以支持上述三种 Reactor 线程模型。

1、Reactor 单线程模型

所有的 IO 操作都在同一个 NIO 线程上面完成。读取通信对端的请求或向通信对端发送消息请求或者应答消息。

由于 Reactor 模式使用的是非阻塞 IO,理论上一个线程可以独立处理所有 IO 相关的操作。

在一些小容量应用场景下,可以使用单线程模型。但是对于高负载、大并发的应用场景却不合适,主要原因如下:

  • 一个NIO线程同时处理成百上千的连接,性能上无法支撑,即便NIO线程的CPU负荷达到100%,也无法满足海量消息的编码、解码、读取和发送。
  • 当NIO线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超时,超时之后往往会进行重发,这更加重了NIO线程的负载,最终会导致大量消息积压和处理超时,成为系统的性能瓶颈。
  • 可靠性问题:一旦NIO线程意外跑飞,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障。

注意,Redis的请求处理也是单线程模型,为什么Redis的性能会如此之高呢 ?
因为Redis的读写操作基本都是内存操作,并且Redis协议比较简洁,序列化/反序列化耗费性能更低。
在这里插入图片描述
在这里插入图片描述

EventLoopGroup bossGroup = new NioEventLoopGroup(1);
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup)...

2、Reactor 多线程模型

Rector 多线程模型与单线程模型最大的区别就是有一组 NIO 线程处理 IO 操作

一个NIO线程处理Accept。该线程可以处理多个连接事件,一个连接的事件只能属于一个Acceptor线程。

网络 IO 操作等由一个 NIO 线程池负责,线程池可以采用标准的 JDK 线程池实现,它包含一个任务队列和 N 个可用的线程,由这些 NIO 线程负责消息的读取、解码、编码和发送。

在绝大多数场景下,Reactor 多线程模型可以满足性能需求。但是,在个别特殊场景中,一个 NIO 线程负责监听和处理所有的客户端连接可能会存在性能问题。例如并发百万客户端连接,或者服务端需要对客户端握手进行安全认证,但是认证本身非常损耗性能。在这类场景下,单独一个 Acceptor 线程可能会存在性能不足的问题。
在这里插入图片描述
在这里插入图片描述

EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
 .channel(NioServerSocketChannel.class)
 ...

bossGroup 中只有一个线程,workerGroup 中默认的线程数是 CPU 核心数* 2,那么就对应 Reactor 的多线程模型。

3、Reactor主从多线程模型

主从 Reactor 线程模型的特点是:服务端用于接收客户端连接的不再是 1 个单独的 NIO 线程,而是一个独立的 NIO 线程池。

Acceptor 接收到客户端 TCP 连接请求处理完成后,将新创建的 SocketChannel 注册到 IO 线程池(sub reactor 线程池)的某个 IO 线程上,由它负责 SocketChannel 的读写和编解码工作。

Acceptor 线程池仅仅只用于客户端的登录、握手和安全认证,一旦链路建立成功,就将链路注册到subReactor 线程池的 IO 线程上,由 IO 线程负责后续的 IO 操作。
在这里插入图片描述
在这里插入图片描述

EventLoopGroup bossGroup = new NioEventLoopGroup(4);
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
 .channel(NioServerSocketChannel.class)
 ...

4、Netty中主从多线程模型的实现

在Netty 的服务器端的 acceptor 阶段,没有使用到多线程, 因此上面的主从多线程模型在 Netty 的实现是有误的

服务器端的 ServerSocketChannel 只绑定到了 bossGroup 中的一个线程,因此在调用 Java NIO 的 Selector.select 处理客户端的连接请求时,实际上是在一个线程中的,所以对只有一个服务的应用来说,bossGroup 设置多个线程是没有什么作用的,反而还会造成资源浪费。

至于 Netty 中的 bossGroup 为什么使用线程池,在 stackoverflow 找到一个对于此问题的讨论 。

the creator of Netty says multiple boss threads are useful if we share NioEventLoopGroup between different serverbootstraps(如果在不同的serverbootstraps之间共享NioEventLoopGroup,那么多个boss线程是有用的

所以通常可以将 BossEventLoopGroup 的线程数参数为 1。

彻底搞懂 netty 线程模型

Netty实战入门详解——让你彻底记住什么是Netty(看不懂你来找我)

5、Netty线程模型实践

(1) 时间可控的简单业务直接在 I/O 线程上处理

时间可控的简单业务直接在 I/O 线程上处理,如果业务非常简单,执行时间非常短,不需要与外部网络交互、访问数据库和磁盘,不需要等待其它资源,则建议直接在业务 ChannelHandler 中执行,不需要再启业务的线程或者线程池。避免线程上下文切换,也不存在线程并发问题。

(2) 复杂和时间不可控业务建议投递到后端业务线程池统一处理

复杂度较高或者时间不可控业务建议投递到后端业务线程池统一处理,对于此类业务,不建议直接在业务 ChannelHandler 中启动线程或者线程池处理,建议将不同的业务统一封装成 Task,统一投递到后端的业务线程池中进行处理。过多的业务ChannelHandler 会带来开发效率和可维护性问题,不要把 Netty 当作业务容器,对于大多数复杂的业务产品,仍然需要集成或者开发自己的业务容器,做好和Netty 的架构分层。

(3) 业务线程避免直接操作 ChannelHandler

业务线程避免直接操作 ChannelHandler,对于 ChannelHandler,IO 线程和业务线程都可能会操作,因为业务通常是多线程模型,这样就会存在多线程操作ChannelHandler。为了尽量避免多线程并发问题,建议按照 Netty 自身的做法,通过将操作封装成独立的 Task 由 NioEventLoop 统一执行,而不是业务线程直接操作,相关代码如下所示:
在这里插入图片描述
彻底搞懂 netty 线程模型

三、Netty中的主要组件

1、NioEventLoopGroup 和 EventLoop

当系统在运行过程中,如果频繁的进行线程上下文切换,会带来额外的性能损耗。多线程并发执行某个业务流程,业务开发者还需要时刻对线程安全保持警惕,哪些数据可能会被并发修改,如何保护?这不仅降低了开发效率,也会带来额外的性能损耗。

为了解决上述问题,Netty采用了串行化设计理念,从消息的读取、编码以及后续 ChannelHandler 的执行,始终都由一个 IO 线程 EventLoop 负责,这就意味着整个流程不会进行线程上下文的切换,数据也不会面临被并发修改的风险。

EventLoopGroup 是一组 EventLoop 的抽象,一个 EventLoopGroup 当中会包含一个或多个 EventLoop,EventLoopGroup 提供 next 接口,可以从一组 EventLoop 里面按照一定规则获取其中一个 EventLoop 来处理任务。

在 Netty 服务器端编程中我们需要 BossEventLoopGroupWorkerEventLoopGroup 两个 EventLoopGroup 来进行工作。

BossEventLoopGroup 通常是一个单线程的 EventLoop,EventLoop 维护着一个注册了 ServerSocketChannel 的 Selector 实例,EventLoop 的实现涵盖 IO 事件的分离和分发(Dispatcher),EventLoop 的实现充当 Reactor 模式中的分发(Dispatcher)的角色。

所以通常可以将 BossEventLoopGroup 的线程数参数为 1。

BossEventLoop 只负责处理连接,故开销非常小,连接到来,马上按照策略将 SocketChannel 转发给 WorkerEventLoopGroup,WorkerEventLoopGroup 会由 next 选择其中一个 EventLoop 来将这个SocketChannel 注册到其维护的 Selector 并对其后续的 IO 事件进行处理。

ChannelPipeline 中的每一个 ChannelHandler 都是通过它的 EventLoop(I/O 线程)来处理传递给它的事件的。所以至关重要的是不要阻塞这个线程,因为这会对整体的 I/O 处理产生严重的负面影响。

2、Channel、ChannelPipeline、ChannelHandler、ChannelHandlerContext

Netty 中的 Channel 是框架自己定义的一个通道接口,Netty 实现的客户端 NIO 套接字通道是 NioSocketChannel,提供的服务器端 NIO 套接字通道是 NioServerSocketChannel

当服务端和客户端建立一个新的连接时, 一个新的 Channel 将被创建,同时它会被自动地分配到它专属的 ChannelPipeline

ChannelPipeline 是一个拦截流经 Channel 的入站和出站事件的 ChannelHandler 实例链,并定义了用于在该链上传播入站和出站事件流的 API。

ChannelHandlerContext 代表了 ChannelHandler 和 ChannelPipeline 之间的关联,每当有 ChannelHandler 添加到 ChannelPipeline 中时,都会创建 ChannelHandlerContext。

ChannelHandlerContext 的主要功能是管理它所关联的 ChannelHandler 和在同一个 ChannelPipeline 中的其他 ChannelHandler 之间的交互。事件从一个 ChannelHandler 到下一个 ChannelHandler 的移动是由 ChannelHandlerContext 上的调用完成的。

在这里插入图片描述

ChannelHandler分为 ChannelInBoundHandlerChannelOutboundHandler 两种。

  1. 如果一个入站 IO 事件被触发,这个事件会从第一个开始依次通过 ChannelPipeline中的 ChannelInBoundHandler,先添加的先执行。
  2. 若是一个出站 I/O 事件,则会从最后一个开始依次通过 ChannelPipeline 中的 ChannelOutboundHandler,后添加的先执行。
  3. 通过调用在 ChannelHandlerContext 中定义的事件传播方法传递给最近的 ChannelHandler

在 ChannelPipeline 传播事件时,它会测试 ChannelPipeline 中的下一个 ChannelHandler 的类型是否和事件的运动方向相匹配。如果某个ChannelHandler不能处理则会跳过,并将事件传递到下一个ChannelHandler,直到它找到和该事件所期望的方向相匹配的为止。

ChannelPipeline p = ...;
p.addLast("1", new InboundHandlerA());
p.addLast("2", new InboundHandlerB());
p.addLast("3", new OutboundHandlerA());
p.addLast("4", new OutboundHandlerB());
p.addLast("5", new InboundOutboundHandlerX());

当一个事件进入 inbound 时 handler 的顺序是 1,2,3,4,5;当一个事件进入 outbound 时,handler 的顺序是 5,4,3,2,1。在这个最高准则下,ChannelPipeline 跳过特定 ChannelHandler 的处理:

  • 3,4 没有实现 ChannelInboundHandler,因而一个 inbound 事件的处理顺序是 1,2,5。
  • 1,2 没有实现 ChannelOutBoundhandler,因而一个 outbound 事件的处理顺序是 5,4,3。
  • 5 同时实现了 ChannelInboundHandler 和 channelOutBoundHandler,所以它同时可以处理 inbound 和 outbound 事件。

(3)Future、ChannelFuture

在Netty中所有的IO操作都是异步的,不能立刻得知消息是否被正确处理,但是可以过一会等它执行完成或者直接注册一个监听,具体的实现就是通过Future和ChannelFuture,他们可以注册一个监听,当操作执行成功或失败时监听会自动触发注册的监听事件。

 @Override
 public void channelRead(ChannelHandlerContext ctx,  GoodByeMessage msg) {
     ChannelFuture future = ctx.channel().close();
     future.addListener(new ChannelFutureListener() {
         public void operationComplete(ChannelFuture future) {
             // Perform post-closure operation
             // ...
         }
     });
 }

serverBootstrap.bind(port).addListener(future -> {
    if (future.isSuccess()) {
            System.out.println(new Date() + ": 端口[" + port + "]绑定成功!");
        } else {
            System.err.println("端口[" + port + "]绑定失败!");
        }
    });

Netty实战入门详解——让你彻底记住什么是Netty(看不懂你来找我)

四、TCP粘包和拆包解决方案

TCP的粘包和拆包问题往往出现在基于TCP协议的通讯中,比如RPC框架、Netty等。
在这里插入图片描述

1、TCP发生粘包、拆包的原因

因为TCP协议数据传输是基于字节流的,不包含消息、数据包等概念,没有边界。而操作系统在发送TCP数据时,会通过缓冲区来进行优化。

如果一次请求发送的数据量比较小,没达到缓冲区大小,TCP则会将多个请求合并为同一个请求进行发送,这就形成了粘包问题。

如果一次请求发送的数据量比较大,超过了缓冲区大小,TCP就会将其拆分为多次发送,这就是拆包。

TCP粘包原因

发送方原因:

TCP默认使用Nagle算法(主要作用:减少网络中报文段的数量),而Nagle算法主要做两件事:

  • 只有上一个分组得到确认,才会发送下一个分组
  • 收集多个小分组,在一个确认到来时一起发送

Nagle算法造成了发送方可能会出现粘包问题

Nagle 算法是指发送方发送的数据不会立即发出,而是先放在缓冲区,,等缓存区满了再发出。发送完一批数据后, 会等待接收方对这批数据的回应,然后再发送下一批数据。

Nagle 算法适用于发送方需要发送大批量数据,并且接收方会及时作出回应的场合,这种算法通过减少传输数据的次数来提高通信效率。

对于发送方造成的粘包问题,可以通过关闭Nagle算法来解决,使用TCP_NODELAY选项来关闭算法。

接收方原因:

TCP接收到数据包时,并不会马上交到应用层进行处理,或者说应用层并不会立即处理。实际上,TCP将接收到的数据包保存在接收缓存里,然后应用程序主动从缓存读取收到的分组。这样一来,如果TCP接收数据包到缓存的速度大于应用程序从缓存中读取数据包的速度,多个包就会被缓存,应用程序就有可能读取到多个首尾相接粘到一起的包。

TCP拆包原因

  • 要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。
  • 待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。

2、UDP中没有粘包、拆包的原因

UDP则是面向消息传输的,是有保护消息边界的,接收方一次只接受一条独立的信息,所以不存在粘包问题。

UDP不存在粘包问题,是由于UDP发送的时候,没有经过Negal算法优化,不会将多个小包合并一次发送出去。另外,在UDP协议的接收端,采用了链式结构来记录每一个到达的UDP包,这样接收端应用程序一次recv只能从socket接收缓冲区中读出一个数据包。也就是说,发送端send了几次,接收端必须recv几次(无论recv时指定了多大的缓冲区)。

TCP粘包及拆包详解

3、应用层常见解决方案

对于粘包和拆包问题,常见的解决方案有四种:

  • 发送端将每个包都封装成固定的长度,比如100字节大小。如果不足100字节可通过补0或空等进行填充到指定长度;
  • 发送端在每个包的末尾使用固定的分隔符,例如\r\n。如果发生拆包需等待多个包发送过来之后再找到其中的\r\n进行合并;例如,FTP协议;
  • 将消息分为头部和消息体,头部中保存整个消息的长度,只有读取到足够长度的消息之后才算是读到了一个完整的消息;
  • 通过自定义协议进行粘包和拆包的处理。

4、Netty中的解决方案

Netty对解决粘包和拆包的方案做了抽象,提供了一些解码器(Decoder)来解决粘包和拆包的问题。如:

  • LineBasedFrameDecoder:以行为单位进行数据包的解码;
  • DelimiterBasedFrameDecoder:添加特殊分隔符报文来分包
  • FixedLengthFrameDecoder:以固定长度进行数据包的解码;
  • LengthFieldBasedFrameDecoder:基于长度域拆包,适用于消息头包含消息长度的协议(最常用);

基于Netty进行网络读写的程序,可以直接使用这些Decoder来完成数据包的解码。对于高并发、大流量的系统来说,每个数据包都不应该传输多余的数据(所以补齐的方式不可取),LengthFieldBasedFrameDecoder更适合这样的场景。

面试题:聊聊TCP的粘包、拆包以及解决方案

5、代码例子

《精通并发与Netty》学习笔记(14 - 解决TCP粘包拆包(二)Netty自定义协议解决粘包拆包)

五、Netty零拷贝

1、零拷贝概念

(1)数据传输:传统方法

在这里插入图片描述

从图中可以看出文件经历了4次拷贝过程:

  1. 数据从磁盘读取到内核的read buffer
  2. 数据从内核缓冲区拷贝到用户缓冲区
  3. 数据从用户缓冲区拷贝到内核的socket buffer
  4. 数据从内核的socket buffer拷贝到网卡接口(硬件)的缓冲区

其中2、3两次复制是多余的。

(2)数据传输:零拷贝方法

JDK NIO中的的transferTo() 方法可以实现零拷贝操作,其实现依赖于操作系统底层的sendFile()方法。
在这里插入图片描述

  1. 将文件拷贝到kernel buffer中;(DMA引擎将文件内容copy到内核缓存区)
  2. 向socket buffer中追加当前要发送的数据在kernel buffer中的位置和偏移量;
  3. 根据socket buffer中的位置和偏移量直接将kernel buffer的数据copy到网卡设备(protocol engine)中;

netty深入理解系列-Netty零拷贝的实现原理

2、Netty零拷贝的实现

Netty的 Zero-coyp 完全是在用户态(Java 层面)的, 它的 Zero-copy 的更多的是偏向于 优化数据操作 这样的概念.

(1)避免数据流经用户空间 ----> 通过FileRegion调用FileChannel.transferTo()

Netty在这一层对零拷贝实现就是FileRegion类的transferTo()方法,我们可以不提供buffer完成整个文件的发送,不再需要开辟buffer循环读写。

(2)避免数据从JVM Heap到C Heap的拷贝----->ByteBuf直接内存

在JVM层面,每当程序需要执行一个I/O操作时,都需要将数据先从JVM管理的堆内存复制到使用C malloc()或类似函数分配的Heap内存中才能够触发系统调用完成操作,这部分内存站在Java程序的视角来看就是堆外内存,但是以操作系统的视角来看其实都属于进程的堆区,OS并不知道JVM的存在,都是普通的用户程序。

这样一来JVM在I/O时永远比使用native语言编写的程序多一次数据复制,这是所有基于VM的编程语言都绕不开的问题,而且是纯粹的人为多增加了一个步骤。

为什么不直接使用JVM堆区数据的地址而是要复制一下呢?

原因很简单,虚拟机只是一个用户程序,它本身并没有直接访问硬件的能力,因此所有的I/O操作都需要借助于系统调用来实现。在Linux系统中,与I/O相关的read()和write()系统调用,都需要传入一个指针,指向在程序中分配的一片内存区域起始地址。然后操作系统会将数据填入这片区域或者从这片区域中读出数据。

这里如果直接使用JVM堆中对应byte[]类型的地址的话,会有两个无法解决的问题:

  1. Java中的对象实际的内存布局跟C是不一样的,不同的JVM可能有不同的实现,byte[]的首地址可能只是个对象头,并不是真实的数据;
  2. 垃圾收集器的存在使得JVM会经常移动对象的位置,这样同一个对象的真实内存地址随时都有可能发生变化,JVM知道地址变了,但是操作系统不知道。

Netty中对零拷贝思想的第二处实现,就是在适当的位置直接使用堆外内存(直接内存),从而避免了数据从JVM Heap到C Heap的拷贝。

(3)减少数据在用户空间的多次拷贝---->CompositeByteBuf

在写代码时有很多时候会将数据多次移动来实现一些功能,比如在Netty中我们可能会先将ByteBuf中的字节数据读到自己开辟的一处byte[]中再遍历处理,这样就多了一次数据的复制。有时候可能需要将多个ByteBuf组合起来使用才能完成某些业务逻辑,这样就需要再开辟一个更大的字节数组将所有ByteBuf都复制过来。

  • Netty 提供了 CompositeByteBuf 类, 它可以将多个 ByteBuf 合并为一个逻辑上的 ByteBuf, 避免了各个 ByteBuf 之间的拷贝。
  • 通过 wrap 操作, 我们可以将 byte[] 数组、ByteBuf、ByteBuffer等包装成一个 Netty ByteBuf 对象, 进而避免了拷贝操作。
  • ByteBuf 支持 slice 操作, 因此可以将 ByteBuf 分解为多个共享同一个存储区域的 ByteBuf, 避免了内存的拷贝。

Netty对零拷贝(Zero Copy)三个层次的实现

(4)代码例子

ByteBuf header = ...
ByteBuf body = ...

//进行了两次拷贝
ByteBuf allBuf = Unpooled.buffer(header.readableBytes() + body.readableBytes());
allBuf.writeBytes(header);
allBuf.writeBytes(body);

//零拷贝
CompositeByteBuf compositeByteBuf = Unpooled.compositeBuffer();
compositeByteBuf.addComponents(true, header, body);

//对CompositeByteBuf 进行了封装
ByteBuf allByteBuf = Unpooled.wrappedBuffer(header, body);

注意addComponents(boolean increaseWriterIndex, ByteBuf... buffers) 来添加两个 ByteBuf, 其中第一个参数是 true, 表示当添加新的 ByteBuf 时, 自动递增 CompositeByteBufwriteIndex

如果调用的是compositeByteBuf.addComponents(header, body);那么其实 compositeByteBuf 的 writeIndex 仍然是0,此时不可能从 compositeByteBuf 中读取到数据,。

byte[] bytes = ...

//进行了拷贝
ByteBuf byteBuf = Unpooled.buffer();
byteBuf.writeBytes(bytes);

//零拷贝,进行了包装。此时ByteBuf 和 bytes[] 共用一个存储空间, 对 bytes[] 的修改会反映到 ByteBuf 中.
ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes);

public static ByteBuf wrappedBuffer(byte[] array)
public static ByteBuf wrappedBuffer(byte[] array, int offset, int length)

public static ByteBuf wrappedBuffer(ByteBuffer buffer)
public static ByteBuf wrappedBuffer(ByteBuf buffer)

public static ByteBuf wrappedBuffer(byte[]... arrays)
public static ByteBuf wrappedBuffer(ByteBuf... buffers)
public static ByteBuf wrappedBuffer(ByteBuffer... buffers)

public static ByteBuf wrappedBuffer(int maxNumComponents, byte[]... arrays)
public static ByteBuf wrappedBuffer(int maxNumComponents, ByteBuf... buffers)
public static ByteBuf wrappedBuffer(int maxNumComponents, ByteBuffer... buffers)
public ByteBuf slice();
public ByteBuf slice(int index, int length);

ByteBuf byteBuf = ...
ByteBuf header = byteBuf.slice(0, 5);

不带参数的 slice 方法等同于 buf.slice(buf.readerIndex(), buf.readableBytes()) 调用, 即返回 buf 中可读部分的切片。 而 slice(int index, int length) 方法可以设置不同的参数来获取到 buf 不同区域的切片。

对于 Netty ByteBuf 的零拷贝(Zero Copy) 的理解

六、代码例子

1、服务端代码

public class HttpServer {
    public static void main(String[] args) {
        //构造两个线程组
        EventLoopGroup bossrGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            //服务端启动辅助类
            ServerBootstrap bootstrap = new ServerBootstrap();
            
            bootstrap.group(bossGroup, workerGroup)
            .channel(NioServerSocketChannel.class)// 设置channel类型为NIO类型
            .option(ChannelOption.SO_BACKLOG, 1024)// 设置连接配置参数
            .childOption(ChannelOption.SO_KEEPALIVE, true)
            .childOption(ChannelOption.TCP_NODELAY, true)
            .childHandler(new ChannelInitializer<NioSocketChannel>() {// 配置入站、出站事件handler
                @Override
                protected void initChannel(NioSocketChannel ch) {
                	ChannelPipeline pipeline = ch.pipeline();
                    // 配置入站、出站事件channel
                    pipeline.addLast(new HttpClientCodec());
                   	pipeline.addLast(new HttpObjectAggregator(65536));
                   	pipeline.addLast("httpServerHandler", new HttpServerChannelHandler());
                    pipeline.addLast(...);
                }
             });
            ChannelFuture future = bootstrap.bind(8080).sync();
            //等待服务端口关闭
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            // 优雅退出,释放线程池资源
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

自定义的ChannelHandler

public class HttpServerChannelHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
 
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {
 
        ctx.channel().remoteAddress();
 
        FullHttpRequest request = msg;
 
        System.out.println("请求方法名称:" + request.method().name());
 
        System.out.println("uri:" + request.uri());
        ByteBuf buf = request.content();
        System.out.print(buf.toString(CharsetUtil.UTF_8));
 
 
        ByteBuf byteBuf = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8);
        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, byteBuf);
        response.headers().add(HttpHeaderNames.CONTENT_TYPE, "text/plain");
        response.headers().add(HttpHeaderNames.CONTENT_LENGTH, byteBuf.readableBytes());
 
        ctx.writeAndFlush(response);
    }
}

HttpClientCodecHttpServerCodec 是Netty自带的Http编解码组件 。
HttpServerCodecHttpRequestDecoderHttpResponseEncoder 的组合,因为在处理 Http 请求时这两个类是经常使用的,所以 Netty 直接将他们合并在一起更加方便使用。用于服务端。

HttpClientCodecHttpRequestEecoderHttpResponseDncoder 的组合,用于客户端

pipeline.addLast("httpServerCodec", new HttpServerCodec())
等价于
pipeline.addLast("httpResponseEndcoder", new HttpResponseEncoder());
pipeline.addLast("HttpRequestDecoder", new HttpRequestDecoder());

Http请求由请求头和请求体组成。一次http请求并不是通过一次对话完成的,中间可能有很次的连接。

Netty 提供了FullHttpRequestFullHttpResponse类,包含请求和响应的所有信息,

Netty 提供了一个 HttpObjectAggregator 类,这个类的作用是将请求合并为一个 FullHttpRequest 或 FullHttpResponse。

2、客户端代码

public class HttpClient {

   public static void main(String[] args) throws Exception {
       String host = "127.0.0.1";
       int port = 8080;

       EventLoopGroup group = new NioEventLoopGroup();
       try {
           Bootstrap b = new Bootstrap();
           b.group(group)
           .channel(NioSocketChannel.class)
           .handler(new ChannelInitializer<SocketChannel>() {
               @Override
               public void initChannel(SocketChannel ch) throws Exception {
                   ChannelPipeline pipeline = ch.pipeline();
                   pipeline.addLast(new HttpClientCodec());
                   pipeline.addLast(new HttpObjectAggregator(65536));
                   pipeline.addLast(new HttpClientHandler());
               }
           });
           // 启动客户端.
           ChannelFuture f = b.connect(host, port).sync();
           f.channel().closeFuture().sync();
       } finally {
           group.shutdownGracefully();
       }
   }
}

自定义的ChannelHandler

public class HttpClientHandler extends SimpleChannelInboundHandler<FullHttpResponse> {
 
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        URI uri = new URI("http://127.0.0.1:8080");
        String msg = "Are you ok?";
        FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET,
                uri.toASCIIString(), Unpooled.wrappedBuffer(msg.getBytes("UTF-8")));
 
        // 构建http请求
//        request.headers().set(HttpHeaderNames.HOST, "127.0.0.1");
//        request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
        request.headers().set(HttpHeaderNames.CONTENT_LENGTH, request.content().readableBytes());
        // 发送http请求
        ctx.channel().writeAndFlush(request);
    }
 
    @Override
    public void channelRead0(ChannelHandlerContext ctx, FullHttpResponse msg) {
        FullHttpResponse response = msg;
        response.headers().get(HttpHeaderNames.CONTENT_TYPE);
        ByteBuf buf = response.content();
        System.out.println(buf.toString(io.netty.util.CharsetUtil.UTF_8));
    }
}

当连接建立时,channelActive 方法会被调用。在方法中构建了一个 FullHttpRequest 对象,并且通过 writeAndFlush 方法将请求发送出去。

channelRead0 方法用于处理服务端返回的响应,打印服务端返回给客户端的信息。

Netty实战入门详解——让你彻底记住什么是Netty(看不懂你来找我)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值