Netty基础知识

目录

一、Netty是什么

Netty是由JBOSS提供的一个java开源框架。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。Netty整合了网络编程、多线程处理和并发等多个领域的只是,极大地简化了网络开发的流程。

Netty是一个NIO客户端服务器框架,它能够快速和轻松地开发网络应用程序,如协议服务器和客户端。它极大地简化了TCP和UDP套接字服务器等网络编程。"快速和轻松"并不意味着结果应用程序将受到可维护性或性能问题的困扰。Netty的设计非常谨慎,它使用了许多协议,如FTP、SMTP、HTTP和各种二进制和基于文本的遗留协议。因此,Netty成功地找到了一种方法,可以在不妥协的情况下实现开发、性能、稳定性和灵活性。

二、Netty出现的原因

2.1 传统的BIO

早期Java API只支持由本地系统套接字库提供的阻塞函数,即同步阻塞式BIO(blocking I/O)。

BIOServer


BIOClient


在BIOServer中,ServerSocket上的accept方法会一直阻塞到一个连接建立,随后返回一个新的Socket用于客户端和服务器之间的通信。该ServerSocket将继续监听传入的连接。readLine方法也会阻塞,知道有一个由换行符或者回车符结尾的字符串被读取。

BufferReader和BufferWriter都衍生自Socket的输入输出流。前者从一个字符输入流中读取文本,后者打印对象的格式化表示到文本输出流。

一个Socket就是一个连接,若要管理多个并发客户端,需要为每一个客户端Socket创建一个新的Thread。

BIO的缺点在于:

  1. 在任何时候都有可能有大量的线程处于休眠状态,只是等待输入或者输出数据就绪,浪费资源

  2. 需要为每一个线程的调用栈分配内存,浪费内存资源

  3. 当线程数量过多时,上下文的切换就会造成大量不必要的开销

2.2 NIO

NIO,即非阻塞IO: Non-blocking I/O,是在JDK1.4中引入的,位于java.nio包中。

Java NIO的核心是Channel,Buffer和Selector。

2.2.1 Channel

Java NIO的通道类似流,但又有些不同:

  • 既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的。

  • 通道可以异步地读写。

  • 通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入。

既可以通道读取数据到缓冲区,从缓冲区写入数据到通道。

2.2.2 Buffer

Java NIO中的Buffer用于和NIO通道进行交互。如你所知,数据是从通道读入缓冲区,从缓冲区写入到通道中的。

缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。

2.2.3 Selector

Selector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接。

2.3 AIO

AIO是异步IO的缩写,虽然NIO在网络操作中,提供了非阻塞的方法,但是NIO的IO行为还是同步的。对于NIO来说,我们的业务线程是在IO操作准备好时,得到通知,接着就由这个线程自行进行IO操作,IO操作本身是同步的。但是对

AIO来说,则更加进了一步,它不是在IO准备好时再通知线程,而是在IO操作已经完成后,再给线程发出通知。因此AIO是不会阻塞的,此时我们的业务逻辑将变成一个回调函数,等待IO操作完成后,由系统自动触发。

本文重点是介绍NIO中的Netty框架的构成和使用,暂不考虑AIO。

三、Netty的使用场景

想要基于NIO设计一个非阻塞的服务器仍然是一件很困难的事,特别是在高负载下可靠并有效地处理I/O调度是一项非常繁琐且容易出错的事情。

开发出高质量的NIO程序并不是一件简单的事情,除去NIO固有的复杂性和BUG不谈,作为一个NIO服务端需要能够处理网络的闪断、客户端的重复接入、客户端的安全认证、消息的编解码、半包读写等等,如果你没有足够的NIO编程经验积累,一个NIO框架的稳定往往需要半年甚至更长的时间。更为糟糕的是一旦在生产环境中发生问题,往往会导致跨节点的服务调用中断,严重的可能会导致整个集群环境都不可用,需要重启服务器,这种非正常停机会带来巨大的损失。

从可维护性角度看,由于NIO采用了异步非阻塞编程模型,而且是一个IO线程处理多条链路,它的调试和跟踪非常麻烦,特别是生产环境中的问题,我们无法有效调试和跟踪,往往只能靠一些日志来辅助分析,定位难度很大。

Netty把复杂的网络编程封装成了简单的api,同时提供了对各种通信协议的兼容。HTTP(s)、SSL、TCP、UDP等等。

过对Netty的分析,我们将它的优点总结如下:

1)      API使用简单,开发门槛低;

2)      功能强大,预置了多种编解码功能,支持多种主流协议;

3)      定制能力强,可以通过ChannelHandler对通信框架进行灵活的扩展;

4)      性能高,通过与其它业界主流的NIO框架对比,Netty的综合性能最优;

5)      成熟、稳定,Netty修复了已经发现的所有JDK NIO BUG,业务开发人员不需要再为NIO的BUG而烦恼;

6)      社区活跃,版本迭代周期短,发现的BUG可以被及时修复,同时,更多的新功能会被加入;

7)      经历了大规模的商业应用考验,质量已经得到验证。在互联网、大数据、网络游戏、企业应用、电信软件等众多行业得到成功商用,证明了它可以完全满足不同行业的商业应用。

四、Netty的基础组件

Netty的核心组件主要包括Channel、回调、Future、事件和ChannelHandler。围绕着这些核心组件还有一部分衍生组件用于对核心组件进行拼装或使用的基础组件,ChannelPipeline、EventLoop、ChannelGroup、EventLoopGroup、ChannelInitializer、Encoder和Decoder等等。

4.1 通信信道

4.1.1 Channel

Channel是NIO的一个基本构造。它代表一个到可进行I/O操作实体的开放连接。Channel的可用I/O操作包括读、写、连接、绑定等等。

Channel能为用户提供以下信息

  1. 该Channel当前的状态,主要包括open、connected、closed

  2. 该Channel的配置项信息,在ChannelConfig类中

  3. 该Channel支持的I/O操作

  4. 处理所有跟该Channel相关的I/O事件和请求的ChannelPipeline

Netty的所有的IO操作都是异步的,调用诸如read,write方法后,并不保证IO操作完成,但会返回一个凭证ChannelFuture,在IO操作成功,取消或失败后会记录在该凭证中,即Future。

Channel是分层的。一个Channel可以有一个父级,这取决于它是如何创建的,例如,一个SocketChannel是被ServerSocketChannel接收的连接,那么该ServerChannel就是它的父级。

Channel从处理事件上分类,可以分为ServerChannel和非ServerChannel两大类,其中ServerChannel负责接收传入连接尝试,并创建子通道来对它们进行处理。

4.1.2 ChannelGroup

ChannelGroup是Netty提供的一个线程安全的Channel集合。该接口继承Set接口,提供了对Channel的各种批量操作。用户可以使用ChannelGroup对Channel进行业务分组。关闭的Channel会自动从ChannelGroup中

移除,用户无需担心集合中Channel的生命周期。一个Channel可以归属于多个ChannelGroup。

4.1.2.1 利用ChannelGroup广播消息

ChannelGroup的write/writeAndFlush即对它管理的所有Channel进行消息广播。我们可以利用group分组来进行业务上的广播区分。

ChannelGroup recipients = 
         new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); 
 recipients.add(channelA); 
 recipients.add(channelB); 
 .. 
 recipients.write(Unpooled.copiedBuffer( 
         "Service will shut down for maintenance in 5 minutes.", 
         CharsetUtil.UTF_8));

4.1.2.3 使用ChannelGroup简化shutdown过程

如果ChannelGroup中同时又ServerChannel和非ServerChannel,那么group中所有的I/O消息都会先在ServerChannel上执行,再去其他Channel中执行。

这个规则在用户一次性关闭服务器时非常有用

ChannelGroup allChannels = 
         new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); 
 
 public static void main(String[] args) throws Exception { 
     ServerBootstrap b = new ServerBootstrap(..); 
     ... 
     b.childHandler(new MyHandler()); 
 
     // Start the server 
     b.getPipeline().addLast("handler", new MyHandler()); 
     Channel serverChannel = b.bind(..).sync(); 
     allChannels.add(serverChannel); 
 
     ... Wait until the shutdown signal reception ... 
 
     // Close the serverChannel and then all accepted connections. 
     allChannels.close().awaitUninterruptibly(); 
 } 
 
 public class MyHandler extends ChannelInboundHandlerAdapter { 
      @Override 
     public void channelActive(ChannelHandlerContext ctx) { 
         // closed on shutdown. 
         allChannels.add(ctx.channel()); 
         super.channelActive(ctx); 
     } 
 }

4.2 异步执行结果和回调

4.2.1 Future

如上所说,Netty的所有IO操作都是异步的,只会返回一个Future。JDK预置了interface java.util.concurrent.Future,但是对于这种Future的检查较为繁琐。所以Netty提供了它自己的实现--ChannelFuture。以及针对ChannelFuture的处理方法:回调。

4.2.2 回调

ChannelFuture提供了几种额外的方法,使得我们能够注册一个或者多个ChannelFutureListener实例进行监听和回调。监听器的回调方法将在对应的操作完成时被调用,然后监听器可以判断该操作是成功完成了还

是出错了,如果是后者,我们可以检索产生的Throwable。简而言之,由ChannelFutureListener提供的通知机制消除了手动检查Future是否完成的必要。


4.3 事件和事件处理器

4.3.1 Event

Netty使用不同的事件来通知我们状态的改变或是操作的状态。这使得我们能够基于已经发生的事件来触发适当的动作。这些工作可能是: 记录日志,数据转换,流控制,应用程序逻辑。

Netty是一个网络编程框架,所以事件是按照它们入站或者出站的数据流的相关性进行分类的。可能由入站数据或者相关的状态变更而出发的事件包括:

  1. 连接被激活或者失活

  2. 数据读取

  3. 用户事件

  4. 错误事件

出站事件是未来将会触发的某个动作的操作结果,这些动作包括

  1. 打开或者关闭到远程节点的连接

  2. 将数据写到或者冲刷到套接字。

4.3.2 ChannelHandler

ChannelHandler是用于处理事件的处理器抽象。以应用程序开发人员的角度来看,Netty的主要组件是ChannelHandler,它充当了所有处理入站和出站数据的应用程序逻辑的容器。

每个ChannelHandler都会和一个ChannelHandlerContext共同出现。ChannelHandlerContext会记录这个ChannelHandler所归属的ChannelPipeline。通过ChannelHandlerContext的关联关系,ChannelHandler可以

向上或是向下传递事件、动态修改ChannelPipeline,或是通过AttributeKeys来存储特定指向handler的数据。

注意,当一个ChannelHandler归属于多个ChannelPipeline时,它会有多个ChannelHandlerContext,这种情况下对应的ChannelHandler必须要使用@Sharable注解标注;否则,试图将它添加到多个ChannelPipeline

会触发异常。显而易见,为了安全地被用于多个并发的Channel,这样的ChannelHandler必须是线程安全的。

Netty定义了下面两个重要的ChannelHandler子类

  1. ChannelInboundHandler--处理入站数据以及各种状态变化,即负责往网络中写入数据。

  2. ChannelOutboundHandler--处理出站数据并且允许拦截所有的操作,即负责处理从网络中接收到的数据。

ChannelHandler、ChannelInboundHandler和ChannelOutboundHandler都是handler的抽象,Netty提供了ChannelInboundHandler和ChannelOutboundHandler的基本实现,即ChannelHandlerAdapter适配器。

  1. ChannelHandlerAdapter ChannelHandler的实现抽象类

  2. ChannelInboundHandlerAdapter ChannelInboundHandler的基本实现,继承了ChannelHandlerAdapter,它的各种方法中仅仅是调用了ChannelHandlerContext进行简单的转发

  3. ChannelOutboundHandlerAdapter ChannelOutboundHandler的基本实现,继承了ChannelHandlerAdapter,它的各种方法中仅仅是调用了ChannelHandlerContext进行简单的转发

ChannelInboundHandler针对一些入站数据事件和对应Channel状态变化提供了对应的方法,如下

方法

描述

channelRegistered

当Channel已经注册到它的EventLoop并且能够处理I/O时调用

channelUnregistered

当Channel已经从它的EventLoop注销并且无法处理任何I/O时调用

channelActive

当Channel处于活动状态时被调用;Channel已经连接/绑定并且已经就绪

channelInactive

当Channel离开活动状态并且不再连接它的远程节点时被调用

channelReadComplete

当Channel上的一个读操作完成时被调用

channelRead

当从Channel读取数据时被调用

channelWritabilityChanged

当Channel的可写状态发生改变时被调用。用户可以确保写操作不会完成得太快(以避免发生OutOfMemoryError)或者可以在Channel变为再次可写时恢复写入。可以通过调用Channel的isWritable()方法来检测Channel的可写性。与可写性相关的阈值可以通过Channel.config().setWriteHighWaterMark()和setWriteLowWaterMark()方法来设置

userEventTriggered

当ChannelInboundHandler.fireUserEventTriggered()方法被调用时使用,因为一个POJO被传经了ChannelPipeline

ChannelOutboundHandler的一个强大功能是可以按需推迟操作或者事件,这使得可以通过一些复杂的方法来处理请求。例如,如果到远程节点的写入被暂停了,那么你可以推迟冲刷操作并在稍后继续。

方法

描述

bind

当请求将Channel绑定到本地地址时被调用

connect

当请求将Channel连接到远程节点时被调用

disconnect

当请求将Channel从远程节点断开时被调用

close

当请求关闭Channel时被调用

deregister

当请求将Channel从它的EventLoop注销时被调用

read

当请求从Channel读取更多的数据时被调用

flush

当请求从Channel将入队数据重刷到远程节点时被调用

write

当请求通过Channel将数据写到远程节点时调用

4.3.3 ChannelInitializer

在稍后的引导类章节介绍中,我们能看到Netty为用户提供了一种简略的方式来配置网络应用程序中的各种组件,包括Handler、Channel类型、配置文件等等。引导类中提供了两个方法来进行ChannelHandler的配置,handler和childHandler

方法可以为Channel配置一个对应的处理器。但对于一个复杂的网络应用程序或是一个必须要支持多种协议的应用程序将会有很多按功能细分的ChannelHandler,而不是一个把全部功能都整合起来的复杂handler。用户可以通过ChannelPipeline来将

多个ChannelHandler链接起来,但是,该如何在只能接受一个Handler的引导类方法中做到这一点呢?

针对这个问题,Netty提供了一个特殊的ChannelInboundHandlerAdapter子类ChannelInitializer


ChannelInitializer提供了一个initChannel方法。


该方法提供了一种将多个ChannelHandler添加到一个ChannelPipeline中的简便方法。用户只需要简单地向启动类Bootstrap或是ServerBootstrap的实例提供你的ChannelInitializer即可。并且一旦

Channel被注册到它的EventLoop之后,就会调用用户自己实现的initChannel方法。在该方法返回之后,ChannelInitializer的实例将会从ChannelPipeline中移除它自己。


4.3.4 ChannelHandlerContext

ChannelHandlerContext代表了ChannelHandler和ChannelPipeline之间的关联,每当有ChannelHandler添加到ChannelPipeline中时,都会创建ChannelHandlerContext。ChannelHandlerContext的主要功能是管理

它所关联的ChannelHandler和在同一个ChannelPipeline中的其他ChannelHandler之间的交互。

ChannelHandlerContext的引用可以单独保存,延迟调用,甚至在handler以外的其他线程中调用。

ChannelHandlerContext中的AttributeKey允许用户存储和读取和handler和它的context相关的状态信息。

注意,一个ChannelHandler实例可以被加到多个ChannelPipeline中,这意味着一个ChannelHandler实例可以拥有多个ChannelHandlerContext。

ChannelHandlerContext有很多方法,其中一些方法也存在于Channel和ChannelPipeline中,但是有一点重要的不同,如果调用Channel或者ChannelPipeline上的这些方法,它们将沿着整个ChannelPipeline进行传

播。而调用位于ChannelHandlerContext上的相同方法,则将从当前所关联的ChannelHandler开始,并且只会传播给位于该ChannelPipeline中的下一个能够处理该事件的ChannelHandler。

4.3.5 ChannelPipeline

每个Channel都有它自身的pipeline,ChannelPipeline是在Channel创建时自动创建的。Channel有且只有一个ChannelPipeline,但可能会有多个ChannelHandler。

每个Channel中都可能会有一系列的ChannelHandler来处理或拦截该Channel的入站事件或出站事件。ChannelPipeline实现了一种拦截过滤器模式,来让用户可以完全控制事件的处理机制以及pipeline中

ChannelHandler的交流机制。

ChanelPipeline是一个拦截流经Channel的入站和出站事件的ChannelHandler实例链。每一个新创建的Channel都将会被分配一个新的ChannelPipeline。这项关联是永久性的,Channel既不能附加另外一个

ChannelPipeline,也不能分离当前的。ChannelPipeline还提供了一个通过自身传递事件的方法。

ChannelPipeline可以添加、删除或者替换其他的ChannelPipeline,从而实时修改ChannelPipeline的布局。

名称

描述

addFirst

addBefore

addAfter

addLast

将ChannelHandler添加到ChannelPipeline中

remove

移除

replace

替换

ChannelPipeline还可以通过一些方法来触发链条中的ChannelHandler或是Channel进行事件处理。

4.4 事件处理和线程模型

4.4.1 EventLoop

前文中我们提到过Netty是事件驱动的,Netty使用不同的事件来通知我们状态的改变或是操作的状态。ChannelHandler是用于处理事件的处理器抽象。

EventLoop定义了Netty的核心抽象,用于处理连接的生命周期中所发生的事件。

Channel、EventLoop、Thread以及EventLoopGroup之间的关系。

  1. 一个EventLoopGroup包含一个或者多个EventLoop

  2. 一个EventLoop在它的生命周期内只和一个Thread绑定

  3. 所有由EventLoop处理的I/O事件都将在它专有的Thread绑定

  4. 一个Channel在它的生命周期内只注册于一个EventLoop

  5. 一个EventLoop可能会被分配给一个或多个Channel

4.4.2 EventLoopGroup

Netty使用事件驱动来传递数据和状态消息。

4.4.3 线程模型

Netty是按照Reactor模型来进行设计和实现的。reactor设计模式,是一种基于事件驱动的设计模式。在事件驱动的应用中,将一个或多个客户的服务请求分离(demultiplex)和调度(dispatch)给应用程

序。在事件驱动的应用中,同步地、有序地处理同时接收的多个服务请求。       

reactor模式与观察者模式有点像。不过,观察者模式与单个事件源关联,而反应器模式则与多个事件源关联 。当一个主体发生改变时,所有依属体都得到通知。观察者模式具有一个事件队列,而

Reactor模式并没有。

Reactor模型又分为三种模式

  1. 单线程模型

  2. 多线程模型

  3. 主从线程模型

4.4.3.1 Reactor-单线程模型


所谓单线程, 即 acceptor 处理和 handler 处理都在一个线程中处理. 这个模型的坏处显而易见: 当其中某个 handler 阻塞时, 会导致其他所有的 client 的 handler 都得不到执行, 并且更严重的是, handler 的

阻塞也会导致整个服务不能接收新的 client 请求(因为 acceptor 也被阻塞了). 因为有这么多的缺陷, 因此单线程Reactor 模型用的比较少.

4.4.3.2 Reactor-多线程模型

Reactor 的多线程模型与单线程模型的区别就是 acceptor 是一个单独的线程处理, 并且有一组特定的 NIO 线程来负责各个客户端连接的 IO 操作. Reactor 多线程模型如下:


Reactor 多线程模型 有如下特点:

  • 有专门一个线程, 即 Acceptor 线程用于监听客户端的TCP连接请求.

  • 客户端连接的 IO 操作都是由一个特定的 NIO 线程池负责. 每个客户端连接都与一个特定的 NIO 线程绑定, 因此在这个客户端连接中的所有 IO 操作都是在同一个线程中完成的.

  • 客户端连接有很多, 但是 NIO 线程数是比较少的, 因此一个 NIO 线程可以同时绑定到多个客户端连接中.

4.4.3.3 Reactor-主从线程模型

一般情况下, Reactor 的多线程模式已经可以很好的工作了, 但是我们考虑一下如下情况: 如果我们的服务器需要同时处理大量的客户端连接请求或我们需要在客户端连接时, 进行一些权限的检查, 那么单线程

的 Acceptor 很有可能就处理不过来, 造成了大量的客户端不能连接到服务器.Reactor 的主从多线程模型就是在这样的情况下提出来的, 它的特点是: 服务器端接收客户端的连接请求不再是一个线程, 而是由一

个独立的线程池组成. 它的线程模型如下:


4.4.3.4 Netty中的Reactor模式

在Netty中,EventLoop对应一个Thread,EventLoopGroup来管理多个EventLoop,有点类似于线程池的功能。在引导程序启动时,我们可以选择设置用于接受请求以及处理事件的EventLoopGroup,实

际上,EventLoopGroup的设置就对应了Reactor模式中的不同形式。

当Server端接收连接和处理事件共享同一个EventLoopGroup,且EventLoopGroup的数量是1时,和Reactor模式中的单线程模式一致。所有的连接接受和数据处理都由这唯一一个线程来进行处理。

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

当Server端接收连接使用一个线程,处理事件使用多个线程时,对应Reactor模式中的多线程模式。

如下图,引导程序中group方法,支持接收两个EventLoopGroup参数,其中第一个用于接收连接并创建子Channel,第二个用于处理事件。

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

注意,Netty不支持以线程池的形式接受连接,因为无法实现Reactor的主从线程模型。

4.5 编解码器

网络只将数据看作是原始的字节序列,然而,我们的应用程序则会把这些字节组织成有意义的信息。在数据和网络字节流之间做相互转换是最常见的编程任务之一。

将应用程序的数据转换为网络格式,以及将网络格式转换为应用程序的数据的组件分别叫做编码器和解码器。同时具有这两种功能的单一组件叫做编解码器。Netty提供了一系列用来创建所有这些编码器、

解码器以及编解码器的工具,从专门为知名协议(如HTTP以及Base64)预构建的类。

编码器和解码器只是一种特定形式的ChannelHandler,这里就不再详细描述。下面仅枚举出一些Netty已提供的编解码器组件。

4.5.1 Encoder

MessageToByteEncoder

MessageToMessageEncoder

4.5.2 Decoder

ByteToMessageDecoder

ReplayingDecoder

MessageToMessageDecoder

4.6 引导程序

在深入地学习了ChannelPipeline、ChannelHandler和EventLoop之后,我们接下来可能会产生疑问“如何将这些部分组织起来,成为一个实际可运行的应用程序呢”。Netty已经为我们提供了简易的使用入

口:引导类AbstractBootstrap。引导类提供了链式操作方法,用户可以通过引导类的链式调用完成启动一个Netty server/client的全部过程。

引导类AbstractBootstrap是一个抽象类,根据Netty中server和client所承担的责任不同,具有两个具体实现子类Bootstrap和ServerBootstrap。server致力于使用一个父Channel来接受来自客户端的连接,

并创建子Channel以用于它们之间的通信;而client最可能只需要一个单独的,没有父Channel的Channel来用于所有的网络交互。

引导类在使用时必须保证Channel和EventLoopGroup的兼容性,Netty对BIO和NIO都实现了对应的Channel和EventLoopGroup的实现,在使用过程中不能跨协议使用。

4.6.1 ServerBootstrap

引导服务器ServerBootstrap可以为accept过程和事件处理过程分配两个不同的EventLoopGroup。它在调用bind方法时会创建一个ServerChannel,由这个ServerChannel来接收连接,并创建和管理子

Channel以用于I/O事件处理。

4.6.2 Bootstrap

Bootstrap类用于引导客户端,整体操作和ServerBootstrap类似,但所提供的方法存在一些server和client的差异。例如server需要进行bind绑定本地端口,client需要进行connect连接远程server

4.6.3 从Channel引导客户端

4.6.4 在引导过程中添加多个ChannelHandler

参考 4.3.3 ChannelInitializer

五、Netty的使用样例

内部链接,暂不共享

猜你喜欢

转载自blog.csdn.net/junerli/article/details/79952213