Netty——异步和事件驱动
Java网络编程
阻塞I/O
1 | ServerSocket serverSocket = new ServerSocket(portNumber); //创建一个新的ServerSocket,用以监听指定端口上的连接请求 |
要点:
- ServerSocket上的accept()方法将会一直阻塞到一个连接建立,随后返回一个新的Socket用于客户端和服务器之间的通信。该ServerSocket将继续监听传入的连接;
- BufferedReader和PrintWriter都衍生自Socket的输入输出流。前者从一个字符输入流中读取文本,后者打印对象的格式化的表示到文本输出流;
- readLine()方法将会阻塞,直到在while内一个由换行符或回车符结尾的字符串被读取。
缺点:
- 在任何时候都可能有大量的线程处于休眠状态,只是等待输入或者输出数据就绪,这可能算是一种资源浪费;
- 需要为每个线程的调用栈都分配内存,其默认值大小区间为64KB到1MB,具体取决于操作系统;
- 即使JVM在物理上可以支持非常大数量的线程,但是远在到达该极限之前,上下文切换所带来的的开销就会带来麻烦。
NIO
class java.nio.channels.Selector
是Java的非阻塞I/O实现的关键。它使用了事件通知API以去诶定在一组非阻塞套接字中有哪些已经就绪能够进行I/O相关的操作。因为可以在任何的时间检查任意的读操作或者写操作的完成状态,如图所示,一个单一的线程便可以处理多个并发的连接。
总体而言,与阻塞I/O模型相比,这种模型提供了更好的资源管理:
- 使用较少的线程便可以处理许多连接,因此也减少了内存管理和上下文切换所带来的开销;
- 当没有I/O操作需要处理的时候,线程也可以被用于其他任务
Netty
异步与事件驱动
本质上,一个即是异步的又是事件驱动的系统会表现出一种特殊的、对我们来说极具价值的行为:它可以以任意的顺序响应在任意的时间点产生的事件。
这种能力对于实现最高级别的可伸缩性至关重要,定义为:“一种系统、网络或者进程在需要处理的工作不断增长时,可以通过某种可行的方式或扩大它的处理能力适应这种增长的能力。”
异步和可伸缩性之间的联系:
- 非阻塞网络调用使得我们可以不必等待一个操作的完成。完全异步的I/O正是基于这个特性构建的,并且更进一步:异步方法会立即返回,并且在它完成时,会直接或者在稍后的某个时间点通知用户;
- 选择器使得我们能够通过较少的线程便可监视许多连接上的事件;
Netty的核心组件
Channel
Channel是Java NIO的一个基本构造,它代表一个到实体(如一个硬件设备、一个文件、一个网络套接字或者一个能够执行一个或者多个不同的I/O操作的程序组件)的开放连接,如读操作和写操作。可以把Channel看作是传入(入站)或者传出(出站)数据的载体。因此,它可以被打开或者被关闭,连接或者断开连接。
回调
一个回调其实就是一个方法,一个指向已经被提供给另外一个方法的方法的引用。这使得后者可以在适当的时候调用前者。
Future
Future提供了另一种在操作完成时通知应用程序的方式。这个对象可以看作是一个异步操作的结果的占位符;它将在未来的某个时刻完成,并提供对其结果的访问。
JDK预置了interface java.util.concurrent.Future
,但是其所提供的实现,只允许手动检查对应的操作是否已经完成,或者一直阻塞知道它完成。这是非常繁琐的,所以Netty提供了它自己的实现——ChannelFuture,用于执行异步操作的时候使用。
ChannelFuture提供了几种额外的方法,这些方法使得我们能够注册一个或者多个ChannelFutureListener实例。监听器的回调方法OperationComplete(),将会在对应的操作完成时被调用。然后监听器可以判断该操作是成功地完成了还是出错了。如果是后者,我们可以检索产生的Throwable。简而言之,由ChannelFutureListener提供的通知机制消除了手动检查对应的操作是否完成的必要。
每个Netty的出站I/O操作都将返回一个ChannelFuture;也就是说,它们都不会阻塞,完全是异步和事件驱动的。
1 | private static void nettyTest(){ |
事件和ChannelHandler
Netty使用不同的事件来通知我们状态的改变或者是操作的状态。这些动作可能是:
- 记录日志;
- 数据转换;
- 流控制;
- 应用程序逻辑。
Netty是一个网络编程框架,所以事件是按照它们与入站或出站数据流的相关性进行分类的。可能由入站数据或者相关的状态更改而触发的事件包括:
- 连接已被激活或者连接失活;
- 数据读取;
- 用户事件;
- 错误事件。
出站事件是未来将会触发的某个动作的操作结果,包括:
- 打开或关闭到远程节点的连接;
- 将数据写到或者冲刷到套接字。
Netty应用程序初探
所有的Netty服务器都需要以下两部分:
- 至少一个ChannelHandler——该组件实现了服务器对从客户端接收的数据的处理,即它的业务逻辑;
- 引导——这是配置服务器的启动代码。至少,它会将服务器绑定到它要监听连接请求的端口上。
编写Echo服务器
ChannelHandler与业务逻辑
1 | // 标示一个ChannelHandler可以被多个Channel安全地共享 .Sharable |
如果不捕获异常,会发生什么呢?
每个Channel都拥有一个与之相关联的ChannelPipeline,其持有一个ChannelHandler的实例链。在默认的情况下,ChannelHandler会把对它的方法的调用转发给链中的下一个ChannelHandler。因此,如果exceptionCaught()方法没有被该链中的某处实现,那么所接收的异常将会被传递到ChannelPipeline的尾端并被记录。为此,你的应用程序应该提供至少一个实现了exceptionCaught()方法的ChannelHandler。
关键点:
- 针对不同类型的事件来调用ChannelHandler;
- 应用程序通过实现或者扩展ChannelHandler来挂钩到事件的生命周期,并且提供自定义的应用程序逻辑;
- 在架构上,ChannelHandler有助于保持业务逻辑与网络处理代码的分离。这简化了开发过程,因为代码必须不断地演化以响应不断变化的需求。
引导服务器
1 | package com.louris.springboot.utils; |
编写Echo客户端
Echo客户端将会:
(1)连接到服务器;
(2)发送一个或者多个消息;
(3)对于每个消息,等待并接收从服务器发回的相同的消息;
(4)关闭连接;
通过ChannelHandler实现客户端逻辑
1 | package com.louris.springboot.netty.client; |
每当接收数据时,都会调用channelRead0()方法,需要注意,由服务器发送的消息可能会被分块接收。也就是说,如果服务器发送了5字节,那么不能保证这5字节会被一次性接收。即使是对于这么少量的数据,channelRead0()方法也可能会被调用两次,第一次使用一个持有3字节的ByteBuf(Netty的字节容器),第二次使用一个持有2字节的ByteBuf。
SimpleChannelInboundHandler
与ChannelInboundHandler
:
为何客户端与服务端使用不同?这和两个因素的相互作用有关;业务逻辑如何处理消息以及Netty如何管理资源:
- 在客户端,当channelRead0()方法完成时,你已经有了传入消息,并且已经处理完它了。当该方法返回时,SimpleChannelInBoundHandler负责释放指向保存该消息的ByteBuf的内存引用。
- 在服务端EchoServerHandler汇总,你仍然需要将传入消息会送给发送者,而write()操作是异步的,直到channelRead()方法返回后可能仍然没有完成。为此,EchoServerHandler扩展了ChannelInboundHandlerAdapter,其在这个时间点上不会释放消息。
- 消息在EchoServerHandler的channelReadComplete()方法中,当writeAndFlush()方法被调用时被释放。
引导客户端
1 | package com.louris.springboot.netty.client; |
SpringBoot启动测试
服务器端
1 | package com.louris.springboot.configs; |
1 | package com.louris.springboot; |
客户端
1 | package com.louris.springboot.configs; |
1 | package com.louris.springboot; |
Netty的组件和设计
Channel、EventLoop和ChannelFuture
- Channel——Socket
- EventLoop——控制流、多线程处理、并发
- ChannelFuture——异步通知
Channel接口
基本的I/O操作(bind()、connect()、read()和write())依赖于底层网络传输所提供的原语。在基于Java的网络编程中,其基本的构造是class Socket。Netty的Channel接口所提供的API,大大降低了直接使用Socket类的复杂性。此外,Channel也是拥有许多预定义、专门化实现的广泛类层次结构的根,下面是一个简短的部分清单:
- EmbeddedChannel
- LocalServerChannel
- NioDatagramChannel
- NioSctpChannel
- NioSocketChannel
EventLoop接口
EventLoop定义了Netty的核心抽象,用于处理连接的生命周期中所发生的事件。关系如图所示:
- 一个EventLoopGroup包含一个或多个EventLoop
- 一个EventLoop在它的生命周期内只和一个Thread绑定
- 所有由EventLoop处理的I/O事件都将在它专有的Thread上被处理
- 一个Channel在它的生命周期内只注册一个EventLoop
- 一个EventLoop可能会被分配给一个或多个Channel
注意,在这种设计中,一个给定Channel的I/O操作都是由相同的Thread执行的,实际上消除了对于同步的需要。
ChannelFuture接口
Netty中所有的I/O操作都是异步的。因为一个操作可能不会立即返回,所以需要一种用于在之后的某个时间点确定其结果的方法。为此,Netty提供了ChannelFuture接口,其addListener()方法注册了一个ChannelFutureListener,以便在某个操作完成时(无论是否成功)得到通知。
ChannelHandler和ChannelPipeline
ChannelHandler接口
从应用程序开发人员的角度来看,Netty的主要组件是ChannelHandler,它充当了所有处理入站和出站数据的应用程序逻辑的容器。这是可行的,因为ChannelHandler的方法是由网络事件触发的。事实上,ChannelHandler可专门用于几乎任何类型的动作,例如将数据从一种格式转换为另外一种格式,或者处理转换过程中所抛出的异常。
ChannelPipeline接口
ChannelPipeline提供了ChannelHandler链的容器,并定义了用于在该链上传播入站和出站事件流的API。当Channel被创建时,它会被自动地分配到它专属的ChannelPipeline。
ChannelHandler类的层次结构:
ChannelHandler是专为支持广泛的用于而设计的,可以将它看作是处理往来ChannelPipeline事件(包括数据)的任何代码的通用容器。使得事件刘静ChannelPipeline是ChannelHandler的工作,它们是在应用程序的初始化或者引导阶段被安装的。这些对象接收事件、执行它们所实现的处理逻辑,并将数据传递给链中的下一个ChannelHandler。它们的执行顺序是由它们被添加的顺序所决定的。实际上,而被称为ChannelPipeline的是这些ChannelHandler的编排顺序。
Netty应用程序中入站和出站数据流之间的区别:
当ChannelHandler被添加到ChannelPipeline时,它将会被分配到一个ChannelHandlerContext,其代表了ChannelHandler和ChannelPipeline之间的绑定。虽然这个对象可以被用于获取底层的Channel,但是它主要还是被用于写出站数据。
在Netty中,有两种发送消息的方式:
- 直接写到Channel中:导致消息从ChannelPipeline的尾端开始流动;
- 写到和ChannelHandler相关联的ChannelHandlerContext对象中:导致消息从ChannelPipeline中的下一个ChannelHandler开始流动。
ChannelHandlerAdapter
Netty以适配类的形式提供了大量默认的ChannelHandler实现,其旨在简化应用程序处理逻辑的开发过程。这些适配器类及它们的子类将自动执行这个操作,所以可以只重写那些想要特殊处理的方法和事件。
常见适配器类:
- ChannelHandlerAdapter
- ChannelInboundHandlerAdapter
- ChannelOutboundHandlerAdapter
- ChannelDuplexHandler
编码器和解码器
当通过Netty发送或者接收一个消息的时候,就将会发生一次数据转换。入站消息会被解码;也就是说,从字节转换为另一种格式,通常是一个Java对象。如果是出站消息,则会发生相反方向的转换:它将从它的当前格式被编码为字节。这两种方向的转换的原因很简单:网络数据总是一系列的字节。
- 这些基类的名称类似于ByteToMessageDecoder或MessageToByteEncoder。
- 所有由Netty提供的编码器/解码器适配器类都实现了ChannelOutboundHandler或者ChannelInboundHandler接口。
- 对于入站数据来说,channelRead方法/事件已经被重写了。对于每个从入站Channel读取的消息,这个方法都将会被调用。随后,它将调用由预置解码器所提供的decode()方法,并将已解码的字节转发给ChannelPipeline中的下一个ChannelInboundHandler。
- 出栈消息的模式是相反方向:编码器将消息转换为字节,并将它们转发给下一个ChannelOutboundHandler。
抽象类SimpleChannelInboundHandler
最常见的情况是利用一个ChannelHandler来接收解码消息,并对该数据应用业务逻辑。要创建一个这样的ChannelHandler,只需要扩展基类SimpleChannelInboundHandler
在这种类型的ChannelHandler中,最重要的方法是channelRead0(ChannelHandlerContext, T)。除了要求不要阻塞当前的I/O线程之外,其具体实现完全取决于用户。
引导
Netty的引导类为应用程序的网络层配置提供了容器,这涉及将一个进程绑定到某个指定的端口,或者将一个进程连接到另一个运行在某个指定主机的指定端口上的进程。
两种类型的引导:
- 一种用于客户端:Bootstrap
- 一种用于服务器:ServerBootstrap
类别 | Bootstrap | ServerBootstrap |
---|---|---|
网络编程中的作用 | 连接到远程主机和端口 | 绑定到一个本地端口 |
EventLoopGroup的数目 | 1 | 2 |
服务器需要2个EventLoopGroup的原因是服务器需要两组不同的Channel:
- 第一组将只包含一个ServerChannel,代表服务器自身的已绑定到某各本地端口的正在监听的套接字;
- 第二组将包含所有已创建的用来处理传入客户端连接(对于每个服务器已经接受的连接都有一个)的Channel。
与ServerChannel相关联的EventLoopGroup将分配一个负责为传入连接请求创建Channel的EventLoop。一旦连接被接受,第二个EventLoopGroup就会给它的Channel分配一个EventLoop。
传输
案例研究:传输迁移
传统JDK API的BIO和NIO
BIO
1 | package com.louris.springboot.utils; |
BIO可以处理中等数量的并发客户端。但是随着应用程序变得流行起来,会发现它并不能很好地伸缩到支撑成千上万的并发连入连接。
NIO(基于Selector的IO多路复用)
1 | package com.louris.springboot.utils; |
通过Netty使用BIO和NIO
BIO
1 | package com.louris.springboot.utils; |
NIO
1 | package com.louris.springboot.utils; |
可以看到两者的API相同,改动很小,无论选用哪一种传输的实现,代码都仍然几乎不受影响。在所有情况下,传输的实现都依赖于interface Channel、ChannelPipeline和ChannelHandler。
传输API
传输API的核心是interface Channel,它被用于所有的I/O操作。Channel类的层次结构如下:
- 每个Channel都将会被分配一个ChannelPipeline和ChannelConfig;
- ChannelConfig包含了该Channel的所有配置设置,并且支持热更新。由于特定的传输可能具有独特的设置,所以它可能会实现一个ChnanelConfig的子类型;
- 由于Channel是独一无二的,所以为了保证顺序将Channel声明为java.lang.Comparable的一个子接口。因此,如果两个不同的Channel实例都返回了相同的散列码,那么AbstractChannel中的compareTo()方法的实现将会抛出一个Error;
- ChannelPipeline持有所有将应用于入站和出站数据以及事件的ChannelHandler实例,这些ChannelHandler实现了应用程序用于处理状态变化以及数据处理的逻辑。
ChannelHandler的典型用途包括:
- 将数据从一种格式转换为另一种格式;
- 提供异常的通知;
- 提供Channel变为活动的或者非活动的通知;
- 提供当Channel注册到EventLoop或者从EventLoop注销时的通知;
- 提供有关用户自定义事件的通知
拦截过滤器:ChannelPipeline实现了一种常见的设计模式——拦截过滤器(Intercepting Filter)。UNIX管道是另一个熟悉的例子:多个命令被链接在一起,其中一个命令的输出端将连接到命令行中下一个命令的输入端。
除了访问所分配的ChannelPipeline和ChannelConfig之外,也可以利用Channel的其他方法:
方法名 | 描述 |
---|---|
eventLoop | 返回分配给Channel的EventLoop |
pipeline | 返回分配给Channel的ChannelPipeline |
isActive | 如果Channel是活动的,则返回true。活动的意义可能依赖于底层的传输。例如一个Socket传输一旦连接到远程节点便是活动的,而一个Datagram传输一旦被打开便是活动的 |
localAddress | 返回本地的SocketAddress |
remoteAddress | 返回远程的SocetAddress |
write | 将数据写到远程节点。这个数据将被传递给ChannelPipeline,并且排队直到它被冲刷 |
flush | 将之前已写的数据冲刷到底层传输,如一个Socket |
writeAndFlush | 一个简便的方法,等同于调用write()并接着调用flush() |
写数据并冲刷到远程节点
1 | Channel channel = ... |
Netty的Channel实现是线程安全的,因此可以存储一个到Channel的引用,并且每当需要向远程节点写数据时,都可以使用它,即使当时许多线程都在使用它。多线程写数据的例子如下,需要注意消息将会被保证按顺序发送。
1 | final Channel channel = ... |
内置的传输
Netty内置了一些可开箱即用的传输。因为并不是它们所有的传输都支持每一种协议,所以必须选择一个和应用程序所使用的协议相容的传输。
名称 | 包 | 描述 |
---|---|---|
NIO | io.netty.channel.socket.nio | 使用java.nio.channels包作为基础——基于选择器的方式 |
Epoll | io.netty.channel.epoll | 由JNI驱动的epoll()和非阻塞IO。这个传输支持只有在Linux上可用的多种特性,如SO_RESEPORT,比NIO传输更快,而且是完全非阻塞的 |
OIO | io.netty.channel.socket.ioi | 使用java.net包作为基础——使用阻塞流 |
Local | io.netty.channel.local | 可以在VM内部通过管道进行通信的 |
NIO——非阻塞I/O
NIO提供了一个所有I/O操作的全异步的实现。它利用了自NIO子系统被引入JDK1.4时便可用的基于选择器的API。
选择器背后的基本概念是充当一个注册表,在那里将可以请求在Channel的状态发生变化时得到通知。可能的状态变化有:
- 新的Channel已被接受并且就绪;
- Channel连接已经完成;
- Channel有已经就绪的可供读取的数据;
- Channel可用于写数据。
选择器运行在一个检查状态变化并对其做出响应的线程上,在应用程序对状态的改变做出响应之后,选择器将会被重置,并将重复这个过程。
下表代表了由class java.nio.channels.SelectionKey定义的位模式。这些位模式可以组合起来定义一组应用程序正在请求通知的状态变化集。
名称 | 描述 |
---|---|
OP_ACCEPT | 请求在接受新连接并创建Channel时获得通知 |
OP_CONNECT | 请求在建立一个连接时获得通知 |
OP_READ | 请求当数据已经就绪,可以从Channel中读取时获取通知 |
OP_WRITE | 请求当可以向Channel中写更多的数据时获得通知。这处理了套接字缓冲区被完全填满时的情况,这种情况通常发生在数据的发送速度比远程节点可处理的速度更快的时候 |
非阻塞I/O流程图:
Epoll——用于Linux的本地非阻塞传输
Netty的NIO传输基于Java提供的异步/非阻塞网络编程的通用抽象。虽然这保证了Netty的非阻塞API可以在任何平台上使用,但它也包含了相应的限制,因为JDK为了在所有系统上提供相同的功能,必须做出妥协。
Linux作为高性能网络编程的平台,其重要性与日俱增,这催生了大量先进特性的开发,其中包括epoll——一个高度可扩展的I/O事件通知特性。这个API自Linux内核版本2.5.44(2022)被引入,提供了比旧的POSIX selector和poll系统调用更好的性能,同时现在也是Linux上非阻塞网络编程的事实标准。Linux JDK NIO API使用了这些epoll调用。
Netty为Linux提供了一组NIO API,其以一种和它本身的设计更加一致的方式使用epoll,并且以一种更加轻量的方式使用中断。高负载下,它的性能要优于JDK的NIO实现。
JDK的实现是水平触发,而Netty的(默认的)是边沿触发。
1 | package com.louris.springboot.utils; |
OIO——旧的阻塞I/O
Netty的OIO传输实现代表了一种折中:它可以通过常规的传输API使用,但是由于它是建立在java.net包的阻塞实现之上的,所以它不是异步的。但是,它仍然非常适合于某些用途。
在Java.net API中,我们通常会有一个用于接受到达正在监听的ServerSocket的新连接的线程。会创建一个新的和远程节点进行交互的套接字,并且分配一个新的用于处理相应通信流量的线程。这是必需的,因为某个指定套接字上的任何I/O操作在任意的时间点上都可能会阻塞。使用单个线程来处理多个套接字,很容易导致一个套接字上的阻塞操作也捆绑了所有其他的套接字。
Netty不同,Netty利用了SO_TIMEOUT这个Socket标志,它指定了等待一个I/O操作完成的最大毫秒数。如果操作在指定的时间间隔内没有完成,则将会抛出一个SocketTimeoutException。Netty将捕获这个异常并继续处理循环。在EventLoop下一次运行时,它将再次尝试。这实际上也是类似于Netty这样的异步框架能够支持OIO的唯一方式。
OIO流程图:
用于JVM内部通信的Local传输
Netty提供了一个Local传输,用于在同一个JVM中运行的客户端和服务器之间的异步通信。同样,这个传输也支持所有Netty传输实现都共同的API。
在这个传输中,和服务器Channel相关联的SocketAddress并没有绑定物理网络地址;相反,只要服务器还在运行,它就会被存储在注册表里,并在Channel关闭时注销。因为这个传输并不接受真正的网络流量,所以它并不能够和其他传输实现进行互操作。因此,客户端希望连接到(在同一个JVM中)使用了这个传输的服务器端时也必须使用它。除了这个限制,它的使用方式和其他传输一模一样。
Embedded传输
Netty提供了一种额外的传输,使得你可以将一组ChannelHandler作为帮助器类嵌入到其他的ChannelHandler内部。通过这种方式,你将可以扩展一个ChannelHandler的功能,而又不需要修改其内部代码。其关键是一个EmbeddedChannel的具体的Channel的实现。
传输的用例
传输 | TCP | UDP | SCTP | UDT |
---|---|---|---|---|
NIO | × | × | × | × |
Epoll(仅Linux) | × | × | - | - |
OIO | × | × | × | × |
应用程序的需求 | 推荐的传输 |
---|---|
非阻塞代码库或者一个常规的起点 | NIO(或者在Linux上使用epoll) |
阻塞代码库 | OIO |
在同一个JVM内部的通信 | Local |
测试ChannelHandler的实现 | Embedded |
ByteBuf
Java NIO提供了ByteBuffer作为它的字节容器,但是这个类使用起来过于复杂,而且也有些繁琐。
Netty的数据容器为ByteBuf,一个强大的实现,既解决了JDK API的局限性,又为网络应用程序的开发者提供了更好的API。
ByteBuf的API
- abstract class ByteBuf
- interface ByteBufHolder
ByteBuf API的优点:
- 它可以被用户自定义的缓冲区类型扩展;
- 通过内置的符合缓冲区类型实现了透明的零拷贝;
- 容量可以按需增长(类似于JDK的StringBuilder);
- 在读和写这两种模式之间切换不需要调用ByteBuffer的flip()方法;
- 读和写使用了不同的索引;
- 支持方法的链式调用;
- 支持引用计数;
- 支持池化;
ByteBuf类
如何工作
ByteBuf维护了两个不同的索引:一个用于读取,一个用于写入。
- readerIndex
- writerIndex
ByteBuf的使用模式
堆缓冲区
最常用的ByteBuf模式是将数据存储在JVM的堆空间中。这种模式被称为支撑数组(backing array),它能在没有使用池化的情况下提供快速的分配和释放。这种方式,非常适合于有遗留的数据需要处理的情况。
1 | ByteBuf heapBuf = Unpooled.buffer(100); // 堆缓冲区 |
直接缓冲区
- 直接缓冲区使用的是物理内存,避免在每次调用本地I/O操作之前(或者之后)将缓冲区的内容复制到一个中间缓冲区(或者从中间缓冲区把内容复制到缓冲区);
- 直接缓冲区的内容将驻留在常规的会被垃圾回收的堆之外;
- 直接缓冲区的主要缺点是,相对于基于堆的缓冲区,它们的分配和释放都较为昂贵;另外如果处理遗留代码,因为数据不在堆上,不得不进行一次复制。
1 | ByteBuf directBuf = Unpooled.directBuffer(); // 直接缓冲区 |
复合缓冲区
- 复合缓冲区为多个ByteBuf提供一个聚合视图,在这里可以根据需要添加或者删除ByteBuf实例;
- Netty通过一个ByteBuf子类——CompositeByteBuf——实现了这个模式,它提供了一个将多个缓冲区表示为单个合并缓冲区的虚拟表示;
- CompositeByteBuf中的ByteBuf实例可能同时包含直接内存分配和非直接内存分配:如果其中只有一个实例,那么对CompositeByteBuf上的hasArray()方法的调用将返回该组件上的hasArray()方法的值;否则它将返回false;
- 可以使得多个消息重用相同的消息主题,不用每个消息都重新分配缓冲区;
- Netty使用CompositeByteBuf来优化套接字的I/O操作,尽可能地消除了由JDK的缓冲区实现所导致的性能以及内存使用率的惩罚。
1 | CompositeByteBuf messageBuf = Unpooled.compositeBuffer(); |
字节级操作
随机访问索引
1 | ByteBuf buffer = Unpooled.buffer(); // Unpooled.directBuffer(); |
顺序访问索引
可丢弃字节
可丢弃字节的分段包含了已经被读过的字节。通过调用discardReadBytes()
方法,可以丢弃它们并回收空间。这个分段的初试大小为0,存储在readerIndex中,会随着read操作的执行而增加(get方法不会移动readerIndex)。
- readerIndex(index)或writerIndex(index)可以手动移动索引。
丢弃已读字节之后的ByteBuf:
可读字节
- ByteBuf的可读字节分段存储了实际数据。新分配的、包装的或者复制的缓冲区的默认的readerIndex值为0。任何名称以
read
或者skip
开头的操作都将检索或者跳过位于当前readerIndex的数据,并且将它增加已读字节数。 - 如果被调用的方法需要一个ByteBuf参数作为写入的目标,并且没有指定目标索引参数,那么该目标缓冲区的writerIndex也将被增加,例如
readBytes(ByteBuf des)
; - 如果尝试在缓冲区的可读字节数已经耗尽时从中读取数据,那么将会引发一个
IndexOutOfBoundsException
。
1 | ByteBuf buffer = Unpooled.buffer(); // Unpooled.directBuffer(); |
可写字节
- 可写字节分段是指一个拥有未定义内容的、写入就绪的内存区域。新分配的缓冲区的writerIndex的默认值为0;
- 任何名称以write开头的操作都将从当前的writerIndex处开始写数据,并将它增加已经写入的字节数。如果写操作的目标也是ByteBuf,并且没有指定源索引的值,则源缓冲区的readerIndex也同样会被增加相同的大小;
1 | ByteBuf buffer = Unpooled.buffer(); // Unpooled.directBuffer(); |
索引管理
- JDK的InputStream定义了mark(int readlimit)和reset()方法,这些方法分别被用来将流中的当前位置标记为指定的值,以及将流重置到该位置;
- 同样,可以通过调用
markReaderIndex()
、markWriterIndex()
、resetWriterIndex()
和resetReaderIndex()
来标记和重置ByteBuf的readerIndex和writerIndex; - 通过调用
readerIndex(int)
或者writerIndex(int)
来将索引移动到指定位置;视图将任何一个索引设置到一个无效的位置都将导致一个IndexOutOfBoundsException
; - 可以通过调用
clear()
方法来将readerIndex核writerIndex都设置为0.注意,这并不会清除内存中的内容; - 调用
clear()
比调用discardReadBytes()
轻量得多,因为它将只是重置索引而不会复制任何的内存。
clear()方法被调用前:
clear()方法被调用后:
查找操作
在ByteBuf中有多重可以用来确定指定值的索引的方法:
- 最简单的是使用
indexOf()
方法; - 较复杂的查找可以通过那些需要一个
ByteBufProcessor
作为参数的方法打成;这个接口只定义了一个方法boolean process(byte value)
; - 它将检查输入值是否是正在查找的值
1 | ByteBuf buffer = Unpooled.buffer(); // Unpooled.directBuffer(); |
派生缓冲区
派生缓冲区为ByteBuf提供了以专门的方式来呈现其内容的视图。这类视图是通过以下方法被创建的:
- duplicate();
- slice();
- slice(int, int);
- UnpooledunmodifiableBuffer(…);
- order(ByteOrder);
- readSlice(int);
每个这些方法都将返回一个新的ByteBuf实例,它具有自己的读索引、写索引和标记索引,共享的,如果修改了内容,也同时修改了其对应的源实例。
ByteBuf复制:如果需要一个现有缓冲区的真实副本,请使用copy(...)
方法。不同于派生缓冲区,由这个调用所返回的ByteBuf拥有独立的数据副本。
对ByteBuf进行切片:
1 | Charset utf8 = Charset.forName("UTF-8"); |
复制一个ByteBuf:
1 | Charset utf8 = Charset.forName("UTF-8"); |
读/写操作
get()和set操作,从给定索引开始,并且保持索引不变:
read()和write()操作,从给定的索引开始,并且会根据已经访问的字节数对索引进行调整;
getBoolean(int)
、getByte(int)
、getInt(int)
等setBoolean(int)
、setByte(int index, int value)
等readBoolean()
、readByte()
,readLong()
等writeBoolean(boolean)
、writeByte(int)
、writeInt(int)
等
更多的操作
名称 | 描述 |
---|---|
isReadable() | 如果至少有一个字节可供读取,则返回true |
isWritable() | 如果至少有一个字节可被写入,则返回true |
resableBytes() | 返回可被读取的字节数 |
writableBytes() | 返回可被写入的字节数 |
capacity() | 返回ByteBuf可容纳的字节数。在此之后,它会尝试再次扩展直到达到maxCapacity() |
maxCapacity() | 返回ByteBuf可以容纳的最大字节数 |
hasArray() | 如果ByteBuf由一个字节数组支撑,则返回true |
array() | 如果ByteBuf由一个字节数组支撑则返回该数组;否则,它将抛出一个UnsupportedOperationException异常 |
ByteBufHolder接口
除了实际的数据负载之外,还需要存储各种属性值。为此,Netty提供了ByteBufHolder,其也为Netty的高级特性提供了支持,如缓冲区池化,其中可以从池中借用ByteBuf,并且在需要自动释放。
名称 | 描述 |
---|---|
content() | 返回由这个ByteBufHolder所持有的ByteBuf |
copy() | 返回这个ByteBufHolder的一个深拷贝,包括一个其所包含的ByteBuf的非共享拷贝 |
duplicate() | 返回这个ByteBufHolder的一个浅拷贝,包括一个其所包含的ByteBuf的共享拷贝 |
ByteBuf分配
管理ByteBuf实例的不同方式。
按需分配:ByteBufAllocator接口
1 | Channel channel = ...; |
Netty提供的两种ByteBufAllocator的实现:
- PooledByteBufAllocator(默认):池化了ByteBuf的实例以提高性能并最大限度地减少内存碎片。此实现使用了一种称为jemalloc的已被大量现代操作系统所采用的高效方法来分配内存。
- UnpooledByteBufAllocator:实现不池化的ByteBuf实例,并且在每次它被调用时都会返回一个新的实例。
Unpooled缓冲区
可能某些情况下,未能获得一个到ByteBufAllocator的引用。对于这种情况,Netty提供Unpooled的工具类。
具体方法见API
ByteBufUtil类
- ByteBufUtil提供了用于操作ByteBuf的静态的辅助方法。因为这个API是通用的,并且和池化无关,所以这些方法已然在分配类的外部实现。
- 最优价值的可能是hexdump()方法,它以十六进制的表示形式打印ByteBuf的内容。这在各种情况下都很有用,例如,出于调试的目的记录ByteBuf的内容。十六进制的表示通常会提供一个比字节值的直接表示形式更加有用的日志条目,此外,十六进制的版本还可以很容易地转换回实际的字节表示;
- 另一个有用的方法是boolean equals(ByteBuf, ByteBuf),它被用来判断两个ByteBuf实例的相等性。
引用计数
- Netty为ByteBuf和ByteBufHolder引入了引用计数计数,它们都实现了interface ReferenceCounted。
- 引用计数对于池化实现来说至关重要,它降低了内存分配的开销。
1 | Channel channel = ...; |
ChannelHandler和ChannelPipeline
ChannelHandlerInboundHandler接口
释放消息资源
- 当某各ChannelInboundHandler的实现重写channelRead()方法时,它将负责显示地释放与池化的ByteBuf实例相关的内存。
- Netty将使用WARN级别的日志消息记录未释放的资源,使得可以非常简单地在代码中发现违规的实例。
1 | package com.louris.springboot.utils; |
- 由于SimpleChannelInboundHandler会自动释放资源,所以不应该存储指向任何消息的引用供将来使用,因为这些引用都将会失效。
1 | package com.louris.springboot.utils; |
资源管理
每当通过调用ChannelInboundHandler.channelRead()或者ChannelOutboundHandler.write()方法来处理数据时,都需要确保没有任何的资源泄漏。Netty使用引用计数来处理池化的ByteBuf。所以在完全使用完某个ByteBuf后,调整其引用计数是很重要的。
为了帮助你诊断潜在的(资源泄漏)问题,Netty提供了class ResourceLeakDetector,它将对应用程序的缓冲区分配做大约1%的采样来检测内存泄漏。
Netty定义了4种泄漏检测级别
级别 | 描述 |
---|---|
DISABLED | 禁用泄漏检测。只有在详尽的测试之后才应设置为这个值 |
SIMPLE | 使用1%的默认采样率检测并报告任何发现的泄漏。这是默认级别,适合绝大部分的情况 |
ADVANCED | 使用默认的采样率,报告所发现的任何的泄漏以及对应的消息被访问的位置 |
PARANOID | 类似于ADVANCED,但是其将会对每次(对消息的)访问都进行采样。这对性能将会有很大的影响,应该只在调试阶段使用 |
防止泄漏
入站
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15package com.louris.springboot.utils;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.ReferenceCountUtil;
.Sharable
public class DiscardHandler extends ChannelInboundHandlerAdapter {
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//super.channelRead(ctx, msg);
ReferenceCountUtil.release(msg);
}
}出站
不仅要释放资源,还要通知ChannelPromise。否则可能会出现ChannelFutureListener收不到消息已经被处理了的通知的情况。
1 | package com.louris.springboot.utils; |
ChannelHandlerContext接口
ChannelHandlerContext代表了ChannelHandler和ChannelPipeline之间的关联,每当有ChannelHandler添加到ChannelPipeline中时,都会创建ChannelHandlerContext。ChannelHandlerContext的主要功能时管理它所关联的ChannelHandler都在同一个ChannelPipeline中的其他ChannelHandler之间的交互。
使用ChannelHandlerContext
1 | ChannelHandlerContext ctx = ...; |
1 | ChannelHandlerContext ctx = ...; |
1 | ChannelHandlerContext ctx = ...; |
缓存ChannelHandlerContext的引用
1 | package com.louris.springboot.utils; |
共享ChannelHandler
- 因为一个ChannelHandler可以从属于多个ChannelPipeline,所以它也可以绑定到多个ChannelHandlerContext实例;
- 对于这种用法旨在多个ChannelPipeline中共享一个ChannelHandler,对应的需要注解@Sharable;
- 否则会触发异常。
- 为了安全地被用于多个并发的Channel,所以这样的ChannelHandler必须是线程安全的。
1 | package com.louris.springboot.utils; |
错误用法
1 | package com.louris.springboot.utils; |
异常处理
处理入站异常
- ChannelHandler.exceptionCaught()的默认实现是简单地将当前异常转发给ChannelPipeline中的下一个ChannelHandler;
- 如果异常到达了ChannelPipeline的尾端,它将会被记录为未被处理;
- 要想定义自定义的处理逻辑,需要重写exceptionCaught()方法。然后需要决定是否需要将该异常传播出去。
1 | package com.louris.springboot.utils; |
处理出站异常
用于处理出站操作中的正常完成以及异常的选项,都基于以下的通知机制:
- 每个出站操作都返回一个ChannelFuture。注册到ChannelFuture的ChannelFutureListener将在操作完成时被通知该操作是成功了还是出错了;
- 几乎所有的ChannelOutboundHandler上的方法都会传入一个ChannelPromise的实例。作为ChannelFuture的子类,ChannelPromise也可以被分配用于异步通知的监听器。但是,ChannelPromise还具有提供立即通知的可写方法。
- 方法一
1 | Channel channel = ... |
- 方法二
1 | package com.louris.springboot.utils; |
EventLoop和线程模型
任务调度
JDK的任务调度API
1 | ScheduledExecutorService executor = Executors.newScheduledThreadPool(10); |
使用EventLoop调度任务
1 | Channel ch = ... |
引导
引导客户端
1 | EventLoopGroup group = new NioEventLoopGroup(); |
Channel和EventLoopGroup的兼容性
- Nio前缀与Oio前缀的不能混淆使用
引导服务器
常用方式
1 | NioEventLoopGroup group = new NioEventLoopGroup(); |
从Channel引导到客户端
- 假设你的服务器正在处理一个客户端的请求,这个请求需要它充当第三方系统的客户端;
- 当一个应用程序(如一个代理服务器)必须要和组织现有的系统(如Web服务或者数据库)集成时,就可能发生这种情况;
- 在这种情况下,将需要从已经被接受的子Channel中引导一个客户端Channel;
- 可以按照前一节创建新的Bootstrap实例,但是这并不是最高效的解决方案,因为它将要求你为每个新创建的客户端Channel定义另一个EventLoop。这会产生额外的线程,以及在已被接受的子Channel和客户端Channel之间交换数据时不可避免的上下文切换;
- 更好的解决方案是:通过将已被接受的子Channel的EventLoop传递给Bootstrap的group()方法来共享该EventLoop。因为分配给EventLoop的所有Channel都使用同一个线程,所以这避免了额外的线程创建。
1 | ServerBootstrap bootstrap = new ServerBootstrap(); |
在引导过程中添加多个ChannelHandler
1 | ServerBootstrap bootstrap = new ServerBootstrap(); |
使用Netty的ChannelOption和属性
1 | final AttributeKey<Integer> id = AttributeKey.newInstance("ID"); // 创建一个AttributeKey以标识该属性 |
引导DatagramChannel
- 前面的引导代码都是基于TCP协议的SocketChannel,但是Bootstrap类也可以被用于无连接的协议。
- 为此,Netty提供了各种DatagramChannel的实现;
- 唯一的区别是,不再调用connect()方法,而是只调用bind()方法
1 | Bootstrap bootstrap = new Bootstrap(); |
关闭
- 最重要的是,需要关闭EventLoopGroup,它将处理任何挂起的事件和任务,并且随后释放所有活动的线程:EventLoopGroup.shutdownGracefully()方法的作用;
- shutdownGracefully()也是一个异步的操作,所以需要阻塞等待直到它完成,或者向所返回的Future注册一个监听器以在关闭完成时获得通知;
- 也可以在调用shutdowngracefully()方法之前,显示地在所有活动的Channel上调用Channel.close()方法。
- 当时在任何情况下,都得记得关闭EventLoopGroup本身
1 | EventLoopGroup group = new NioEventLoopGroup(); |
单元测试
EmbeddedChannel概述
名称 | 职责 |
---|---|
writeInbound(Object… msgs) | 将入站消息写到EmbeddedChannel中。如果可以通过readInbound()方法从EmbeddedChannel中读取数据,则返回true |
readInbound() | 从EmbeddedChannel中读取一个入站消息。任何返回的东西都穿越了整个ChannelPipeline。如果没有任何可供读取的,则返回null |
writeOutbound(Object… msgs) | 将出站消息写到EmbeddedChannel中。如果现在可以通过readOutbound()方法从EmbeddedChannel中读取到什么东西,则返回true |
readOutbound() | 从EmbeddedChannel中读取一个出站消息。任何返回的东西都穿越了整个ChannelPipeline。如果没有任何可供读取的,则返回null |
finish() | 将EmbeddedChannel标记为完成,并且如果可被读取的入站数据或者出站数据,则返回true。这个方法还将会调用EmbeddedChannel上的close()方法 |
使用EmbeddedChannel测试ChannelHandler
测试入站消息
1 | package com.louris.springboot.utils; |
1 | package com.louris.springboot.utils; |
测试出站消息
1 | package com.louris.springboot.utils; |
1 | package com.louris.springboot.utils; |
测试异常处理
1 | package com.louris.springboot.utils; |
1 | package com.louris.springboot.utils; |
编解码器框架
- 编解码器:每个网络应用程序都必须定义如何解析在两个节点之间来回传输的原始字节,以及如何将其和目标应用程序的数据格式做相互转换。这种转换逻辑由编解码器处理,编解码器由编码器和解码器组成,它们每种都可以将字节流从一种格式转换为另一种格式;
- 编码器是将消息转换为适合用于传输的格式(最有可能的就是字节流);
- 解码器是将网络字节流转换回应用程序的消息个事;
- 因此,编码器操作出栈数据,而解码器处理入站数据。
解码器
ByteToMessageDecoder
1 | package com.louris.springboot.utils; |
编解码器中的引用计数:
- 一旦消息被解码或者编码,它就会被ReferenceCountUtil.release(message) 调用自动释放;
- 如果需要保留引用以便稍后使用,那么可以调用ReferenceCountUtil.retain(message)方法,这将会增加该引用计数,从而防止该消息被释放。
ReplayingDecoder
1 | package com.louris.springboot.utils; |
- 如果没有足够的字节可用,这个readInt()方法的实现将抛出一个Error,其将在基类中被捕获并处理;
- 当有更多的数据可供读取时,该decode()方法将会被再次调用;
- 并不是所有的ByteBuf操作都被支持,如果调用了一个不被支持的方法,将会抛出一个UnsupportedOperationException;
- ReplayingDecoder稍慢于ByteToMessageDecoder。
更多的解码器
- LineBasedFrameDecoder;
- HttpObjectDecoder;
- io.netty.handler.codec子包下面,将会发现更多用于特定用例的编码器和解码器实现。
MessageToMessageDecoder
1 | package com.louris.springboot.utils; |
TooLongFrameException
1 | package com.louris.springboot.utils; |
编码器
- 将消息编码为字节
- 将消息编码为消息
MessageToByteEncoder
1 | package com.louris.springboot.utils; |
MessageToMessageEncoder
1 | package com.louris.springboot.utils; |
抽象编解码器类
- 在同一个类中管理入站和出战数据和消息的转换
抽象类ByteToMessageCodec
- 它结合了ByteToMessageDecoder以及它的逆向——MessageToByteEncoder;
- 任何的请求/响应协议都可以作为使用ByteToMessageCodec的理想选择
抽象类MessageToMessageCodec
1 | package com.louris.springboot.utils; |
CombinedChannelDuplexHandler类
- 结合一个解码器和编码器可能会对可重用性造成影响;
- CombinedChannelDuplexHandler类即能避免这种惩罚,又不会牺牲将一个解码器和一个编码器作为一个单独的单元部署所带来的的便利性
1 | package com.louris.springboot.utils; |
1 | package com.louris.springboot.utils; |
1 | package com.louris.springboot.utils; |
预置的ChannelHandler和编解码器
通过SSL/TLS保护Netty应用程序
- 为了支持SSL/TLS,Java提供了javax.net.ssl包,它的SSLContext和SSLEngine类使得实现解密和加密相当简单直接;
- Netty通过一个名为SslHandler的ChannelHandler实现利用了这个API,其中SslHandler在内部使用SSLEngine来完成实际的工作。
1 | package com.louris.springboot.utils; |
构建基于Netty的HTTP/HTTPS应用程序
HTTP解编码器
1 | package com.louris.springboot.utils; |
聚合HTTP消息
- 由于HTTP的请求和响应可能由许多部分组成,它可以将多个消息部分合并为FullHttpRequest或者FullHttpResponse消息。
- 由于消息分段需要被缓冲,直到可以转发一个完整的消息给下一个ChannelInboundHandler,所以这个操作有轻微的开销。其带来的好处便是不不必关心消息碎片了。
1 | package com.louris.springboot.utils; |
HTTP压缩
- 当使用HTTP时,建议开启压缩功能以尽可能多地减少传输数据的大小;
- 虽然压缩会带来一些CPU时钟周期上的开销,但是通常来说它都是一个好主意,特别是对于文本数据来说;
- Netty同时支持gzip和deflate编码的压缩实现。
1 | GET /encrypted-area HTTP/1.1 |
1 | package com.louris.springboot.utils; |
使用HTTPS
1 | package com.louris.springboot.utils; |
WebSocket
- WebSocket在客户端和服务器之间提供了真正的双向数据交换;
- WebSocketFrame分为数据帧和控制帧;
名称 | 描述 |
---|---|
BinaryWebSocketFrame | 数据帧:二进制数据 |
TextWebSocketFrame | 数据帧:文本数据 |
ContinuationWebSocketFrame | 数据帧:属于上一个BinaryWebSocketFrame或者TextWebSocketFrame的文本的或者二进制数据 |
CloseWebSocketFrame | 控制帧:一个CLOSE请求、关闭的状态码以及关闭的原因 |
PingWebSocketFrame | 控制帧:请求一个PongWebSocketFrame |
PongWebSocketFrame | 控制帧:对PingWebSocketFrame请求的响应 |
1 | package com.louris.springboot.utils; |
解码基于分隔符的协议和基于长度的协议
在使用Netty的过程中,将会遇到需要解码器的基于分隔符和帧长度的协议。
基于分隔符的协议
名称 | 描述 |
---|---|
DelimiterBasedFrameDecoder | 使用任何由用户提供的分隔符来提取帧的通用解码器 |
LineBasedFrameDecoder | 提取由行尾符(\n或者\r\n)分隔的帧的解码器。这个解码器比上面这个更快 |
行尾分隔符示例
1 | package com.louris.springboot.utils; |
- 如果使用除了行尾符之外的分隔符分隔的帧,那么可以以类似的方式使用DelimiterBasedFrameDecoder,只需要将特定的分隔符序列指定到其构造函数即可。
自定义空格分隔符
1 | package com.louris.springboot.utils; |
基于长度的协议
名称 | 描述 |
---|---|
FixedLengthFrameDecoder | 提取在调用构造函数时指定的定长帧 |
LengthFieldBasedFrameDecoder | 根据编码进帧头部中的长度值提取帧;该字段的偏移量以及长度在构造函数中指定 |
1 | package com.louris.springboot.utils; |
写大型数据
- 因为网络饱和的可能性,如何在异步框架中高效地写大块的数据是一个特殊的问题;
- 由于写操作时非阻塞的,所以即使没有写出所有的数据,写操作也会在完成时返回并通知ChannelFuture;
- 当这种情况发生时,如果仍然不停地写入,就有内存耗尽的风险;
- 所以在写大型数据时,需要准备好处理到远程节点的连接是慢速连接的情况,这种情况会导致内存释放的延迟。
零拷贝特性:
- 所有这一切都发生在Netty的核心中,所以应用程序所有需要做的就是使用一个FileRegion接口的实现,其在Netty的API文档中的定义是:“通过支持零拷贝的文件传输的Channel来发送的文件区域”
以下代码展示了如何通过从FileInputStream创建一个DefaultFileRegion,并将其写入Channel,从而利用零拷贝特性来传输一个文件的内容:
1 | File file = ... |
当Channel的状态变为活动时,WriteStreamHandler将会逐块地把来自文件中的数据作为ChunkedStream写入。数据在传输之前将会由SslHandler加密。
1 | package com.louris.springboot.utils; |
序列化数据
- JDK提供了ObjectOutputStream和ObjectInputStream,用于通过网络对POJO的的基本数据类型和图进行序列化和反序列化;
- 该API并不复杂,而且可以被应用于任何实现了java.io.Serializable接口的对象;
- 但是它的性能也不是非常搞笑的。
JDK序列化
JDK序列化编码器
名称 | 描述 |
---|---|
CompatibleObjectDecoder | 和使用JDK序列化的非基于Netty的远程节点进行互操作的解码器 |
CompatibleObjectEncoder | 和使用JDK序列化的非基于Netty的远程节点进行互操作的编码器 |
ObjectDecoder | 构建于JDK序列化之上的使用自定义的序列化来解码的解码器;当没有其他的外部依赖时,它提供了速度上的改进。否则其他的序列化实现更加可取 |
ObjectEncoder | 构建于JDK序列化之上的使用自定义的序列化来编码的编码器;当没有其他的外部依赖时,它提供了速度上的改进。否则其他的序列化实现更加可取 |
使用JBoss Marshalling进行序列化
- 如果可以自由地使用外部依赖,那么JBoss Marshalling将是个理想的选择:它比JDK序列化最多快3倍,而且也更加紧凑;
- Netty通过下表所示的两组解码器/编码器对为Boss Marshalling提供了支持。
- 第一组兼容只使用JDK序列化的远程节点;
- 第二组提供了最大的性能,适用于和使用JBoss Marshalling的远程节点一起使用
名称 | 描述 |
---|---|
CompatibleMarshallingDecoder | 与只使用JDK序列化的远程节点兼容 |
CompatibleMarshallingEncoder | |
MarshallingDecoder | 适用于使用JBoss Marshalling的节点。这些类必须一起使用 |
MarshallingEncoder |
1 | package com.louris.springboot.utils; |
通过Protocol Buffers序列化
名称 | 描述 |
---|---|
ProtobufDecoder | 使用protobuf对消息进行解码 |
ProtobufEncoder | 使用protobuf对消息进行编码 |
ProtobufVarint32FrameDecoder | 根据消息中的Google Protocol Buffers的”Base 128 Varints”整型长度字段值动态地分割所接收的ByteBuf |
ProtobufVarint32LengthFieldPrepender | 向ByteBuf前追加一个Google Protocal Buffers的”Base 128 Varints”整型的长度字段值 |
添加依赖
1 | <dependency> |
1 | package com.louris.springboot.utils; |
WebSocket
添加WebSocket支持
处理HTTP请求
- 如果地址为/ws的URI被访问,那么服务器将会处理WebSocket升级
1 | package com.louris.springboot.utils; |
处理WebSocket帧
1 | package com.louris.springboot.utils; |
初始化ChannelPipeline
1 | package com.louris.springboot.utils; |
引导
1 | package com.louris.springboot.utils; |
加密
1 | package com.louris.springboot.utils; |
1 | package com.louris.springboot.utils; |
使用UDP广播事件
消息POJO:LogEvent
1 | package com.louris.springboot.utils; |
编写广播者
1 | package com.louris.springboot.utils; |
1 | package com.louris.springboot.utils; |
编写监听器
1 | package com.louris.springboot.utils; |
1 | package com.louris.springboot.utils; |
1 | package com.louris.springboot.utils; |