icon: edit date: 2022-01-04 category:
CategoryA
CategoryB tag:
tag A
tag B
Netty 4.1.7.Final
Netty 的核心组件
Channel
回调
Future
事件和 ChannelHandler
简单的例子
class AceServer {
companion object {
@JvmStatic
fun main ( args: Array< String> ) {
val boss = NioEventLoopGroup ( )
val worker = NioEventLoopGroup ( 4 )
val bootstrap = ServerBootstrap ( )
. group ( boss, worker)
. channel ( NioServerSocketChannel:: class . java)
. option ( ChannelOption. SO_BACKLOG, 128 )
. childOption ( ChannelOption. SO_KEEPALIVE, true )
. option ( ChannelOption. SO_RCVBUF, 1024 * 10000 )
. option ( ChannelOption. SO_SNDBUF, 1024 * 10000 )
. handler ( LoggingHandler ( LogLevel. INFO) )
. childHandler ( object : ChannelInitializer< SocketChannel> ( ) {
override fun initChannel ( p0: SocketChannel) {
p0. pipeline ( ) . addLast ( StringEncoder ( ) )
p0. pipeline ( ) . addLast ( StringDecoder ( ) )
p0. pipeline ( ) . addLast ( object : SimpleChannelInboundHandler< String> ( ) {
override fun channelRead0 ( p0: ChannelHandlerContext, p1: String? ) {
println ( "收到客户端的消息: $ p1 " )
p0. channel ( ) . writeAndFlush ( p1)
}
} )
}
} )
val server = bootstrap. bind ( 8888 ) . sync ( )
server. addListener {
if ( it. isSuccess)
println ( "netty-im server have been bind in 8888!" )
else
println ( "netty-im server run error!" )
}
server. channel ( ) . closeFuture ( ) . sync ( )
boss. shutdownGracefully ( )
worker. shutdownGracefully ( )
}
}
}
class AceClient {
companion object {
@JvmStatic
fun main ( args: Array< String> ) {
val boss = NioEventLoopGroup ( )
val bootstrap = Bootstrap ( ) . group ( boss)
. channel ( NioSocketChannel:: class . java) . handler ( LoggingHandler ( LogLevel. INFO) )
. option ( ChannelOption. SO_RCVBUF, 1024 * 10000 )
. option ( ChannelOption. SO_SNDBUF, 1024 * 10000 )
. handler ( object : ChannelInitializer< SocketChannel> ( ) {
override fun initChannel ( p0: SocketChannel) {
p0. pipeline ( ) . addLast ( StringEncoder ( ) )
p0. pipeline ( ) . addLast ( StringDecoder ( ) )
p0. pipeline ( ) . addLast ( object : io. netty. channel. ChannelInboundHandlerAdapter ( ) {
override fun channelRead ( ctx: ChannelHandlerContext, msg: Any? ) {
println ( "收到服务端的消息: $ msg " )
}
} )
}
} )
val sync = bootstrap. connect ( "localhost" , 8888 ) . sync ( ) . addListener {
if ( it. isSuccess) println ( "connect success!" )
}
thread {
val scanner = Scanner ( System. `in `)
while ( scanner. hasNextLine ( ) ) {
sync. channel ( ) . writeAndFlush ( scanner. nextLine ( ) )
}
}
sync. channel ( ) . closeFuture ( ) . sync ( )
println ( "结束" )
}
}
}
Channel
Channel—Socket
基本的 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
从应用程序开发人员的角度来看,Netty 的主要组件是 ChannelHandler,它充当了所有处理入站和出站数据的应用程序逻辑的容器。这是可行的,因为 ChannelHandler 的方法是
由网络事件(其中术语“事件”的使用非常广泛)触发的。事实上,ChannelHandler 可专门用于几乎任何类型的动作,例如将数据从一种格式转换为另外一种格式,或者处理转换过程中所抛出的异常。
举例来说,ChannelInboundHandler 是一个你将会经常实现的子接口。这种类型的ChannelHandler 接收入站事件和数据,这些数据随后将会被你的应用程序的业务逻辑所处理。当你要给连接的客户端发送响应时,也可以从 ChannelInboundHandler 冲刷数据。你的应用程序的业务逻辑通常驻留在一个或者多个 ChannelInboundHandler 中。
ChannelPipeline
ChannelPipeline 提供了 ChannelHandler 链的容器,并定义了用于在该链上传播入站和出站事件流的 API。当 Channel 被创建时,它会被自动地分配到它专属的 ChannelPipeline。
ChannelHandler 安装到 ChannelPipeline 中的过程如下所示:
一个ChannelInitializer的实现被注册到了ServerBootstrap中 ①;
当 ChannelInitializer.initChannel()方法被调用时,ChannelInitializer将在 ChannelPipeline 中安装一组自定义的 ChannelHandler;
ChannelInitializer 将它自己从 ChannelPipeline 中移除。
ChannelHandler 是专为支持广泛的用途而设计的,可以将它看作是处理往来 ChannelPipeline 事件(包括数据)的任何代码的通用容器。
Handler 派生的 ChannelInboundHandler 和 ChannelOutboundHandler 接口使得事件流经 ChannelPipeline 是 ChannelHandler 的工作,它们是在应用程序的初始化或者引导阶段被安装的。这些对象接收事件、执行它们所实现的处理逻辑,并将数据传递给链中的下一个 ChannelHandler。它们的执行顺序是由它们被添加的顺序所决定。
:::tip 为什么需要适配器类
有一些适配器类可以将编写自定义的 ChannelHandler 所需要的努力降到最低限度,因为它们提
供了定义在对应接口中的所有方法的默认实现。
下面这些是编写自定义 ChannelHandler 时经常会用到的适配器类:
ChannelHandlerAdapter
ChannelInboundHandlerAdapter
ChannelOutboundHandlerAdapter
ChannelDuplexHandler
:::
SimpleChannelInboundHandler
最常见的情况是,你的应用程序会利用一个 ChannelHandler 来接收解码消息,并对该数据应用业务逻辑。要创建一个这样的 ChannelHandler,你只需要扩展基类 SimpleChannelInboundHandler,其中 T 是你要处理的消息的 Java 类型 。在这个 ChannelHandler 中, 你将需要重写基类的一个或者多个方法,并且获取一个到 ChannelHandlerContext 的引用,这个引用将作为输入参数传递给 ChannelHandler 的所有方法。
在这种类型的 ChannelHandler 中,最重要的方法是 channelRead0(ChannelHandlerContext,T)。除了要求不要阻塞当前的 I/O 线程之外,其具体实现完全取决于你。
引导
Netty 的引导类为应用程序的网络层配置提供了容器,这涉及将一个进程绑定到某个指定的端口,或者将一个进程连接到另一个运行在某个指定主机的指定端口上的进程。
通常来说,我们把前面的用例称作引导一个服务器,后面的用例称作引导一个客户端。虽然这个术语简单方便,但是它略微掩盖了一个重要的事实,即“服务器”和“客户端”实际上表示了不同的网络行为;换句话说,是监听传入的连接还是建立到一个或者多个进程的连接。
面向连接的协议
请记住,严格来说,“连接”这个术语仅适用于面向连接的协议,如 TCP,其保证了两个连接端点之间消息的有序传递
传输
interface Channel
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CkRi9AdQ-1667891439095)(/img/netty/channel.png)]
每个 Channel 都将会被分配一个 ChannelPipeline 和 ChannelConfig。
ChannelConfig 包含了该 Channel 的所有配置设置,并且支持热更新。由于特定的传输可能具有独特的设置,所以它可能会实现一个 ChannelConfig 的子类型。(请参考 ChannelConfig实现对应的 Javadoc。)
由于 Channel 是独一无二的,所以为了保证顺序将 Channel 声明为 java.lang.Comparable 的一个子接口。因此,如果两个不同的 Channel 实例都返回了相同的散列码,那 么 AbstractChannel 中的 compareTo()方法的实现将会抛出一个 Error。
ChannelPipeline 持有所有将应用于入站和出站数据以及事件的 ChannelHandler 实例,这些 ChannelHandler 实现了应用程序用于处理状态变化以及数据处理的逻辑。
ChannelHandler 的典型用途包括:
将数据从一种格式转换为另一种格式
提供异常的通知
提供 Channel 变为活动的或者非活动的通知
提供当 Channel 注册到 EventLoop 或者从 EventLoop 注销时的通知
提供有关用户自定义事件的通知
你也可以根据需要通过添加或者移除ChannelHandler实例来修改ChannelPipeline。
Channel 方法
eventLoop 返回分配给 Channel 的 EventLoop
pipeline 返回分配给 Channel 的 ChannelPipeline
isActive 如果 Channel 是活动的,则返回 true。活动的意义可能依赖于底层的传输。例如,一个 Socket 传输一旦连接到了远程节点便是活动的,而一个 Datagram 传输一旦被打开便是活动的
localAddress 返回本地的 SokcetAddress
remoteAddress 返回远程的 SocketAddress
write 将数据写到远程节点。这个数据将被传递给 ChannelPipeline,并且排队直到它被冲刷
flush 将之前已写的数据冲刷到底层传输,如一个 Socket
writeAndFlush 一个简便的方法,等同于调用 write()并接着调用 flush()
NIO非阻塞 I/O
NIO 提供了一个所有 I/O 操作的全异步的实现。它利用了自 NIO 子系统被引入 JDK 1.4 时便可用的基于选择器的 API。
选择器背后的基本概念是充当一个注册表,在那里你将可以请求在 Channel 的状态发生变化时得到通知。可能的状态变化有:
新的 Channel 已被接受并且就绪;
Channel 连接已经完成;
Channel 有已经就绪的可供读取的数据;
Channel 可用于写数据
选择器运行在一个检查状态变化并对其做出相应响应的线程上,在应用程序对状态的改变做出响应之后,选择器将会被重置,并将重复这个过程。
OP_ACCEPT 请求在接受新连接并创建 Channel 时获得通知
OP_CONNECT 请求在建立一个连接时获得通知
OP_READ 请求当数据已经就绪,可以从 Channel 中读取时获得通知
OP_WRITE 请求当可以向 Channel 中写更多的数据时获得通知。这处理了套接字缓冲区被完全填满时的情况,这种情况通常发生在数据的发送速度比远程节点可处理的速度更快的时候
ByteBuf
特点
Netty 的数据处理 API 通过两个组件暴露——abstract class ByteBuf 和 interface ByteBufHolder。
下面是一些 ByteBuf API 的优点:
ByteBuf 的使用模式
堆缓冲区
最常用的 ByteBuf 模式是将数据存储在 JVM 的堆空间中。这种模式被称为支撑数组(backing array),它能在没有使用池化的情况下提供快速的分配和释放。这种方式。
ByteBuf heapBuf = . . . ;
if ( heapBuf. hasArray ( ) ) {
byte [ ] array = heapBuf. array ( ) ;
int offset = heapBuf. arrayOffset ( ) + heapBuf. readerIndex ( ) ;
int length = heapBuf. readableBytes ( ) ;
handleArray ( array, offset, length) ;
}
:::tip 注意
当 hasArray()方法返回 false 时,尝试访问支撑数组将触发一个 Unsupported OperationException。这个模式类似于 JDK 的 ByteBuffer 的用法。
:::
直接缓冲区
直接缓冲区是另外一种 ByteBuf 模式。我们期望用于对象创建的内存分配永远都来自于堆中,但这并不是必须的——NIO 在 JDK 1.4 中引入的 ByteBuffer 类允许 JVM 实现通过本地调用来分配内存。这主要是为了避免在每次调用本地 I/O 操作之前(或者之后)将缓冲区的内容复制到一个中间缓冲区(或者从中间缓冲区把内容复制到缓冲区)。
ByteBuffer的Javadoc①明确指出:“直接缓冲区的内容将驻留在常规的会被垃圾回收的堆之外。”这也就解释了为何直接缓冲区对于网络数据传输是理想的选择。如果你的数据包含在一个在堆上分配的缓冲区中,那么事实上,在通过套接字发送它之前,JVM将会在内部把你的缓冲区复制到一个直接缓冲区中。
直接缓冲区的主要缺点是,相对于基于堆的缓冲区,它们的分配和释放都较为昂贵。如果你正在处理遗留代码,你也可能会遇到另外一个缺点:因为数据不是在堆上,所以你不得不进行一次复制。
显然,与使用支撑数组相比,这涉及的工作更多。因此,如果事先知道容器中的数据将会被作为数组来访问,你可能更愿意使用堆内存。
ByteBuf directBuf = . . . ;
if ( ! directBuf. hasArray ( ) ) {
int length = directBuf. readableBytes ( ) ;
byte [ ] array = new byte [ length] ;
directBuf. getBytes ( directBuf. readerIndex ( ) , array) ;
handleArray ( array, 0 , length) ;
}
复合缓冲区
第三种也是最后一种模式使用的是复合缓冲区,它为多个 ByteBuf 提供一个聚合视图。在这里你可以根据需要添加或者删除 ByteBuf 实例,这是一个 JDK 的 ByteBuffer 实现完全缺失的特性。
Netty 通过一个 ByteBuf 子类——CompositeByteBuf——实现了这个模式,它提供了一个将多个缓冲区表示为单个合并缓冲区的虚拟表示。
:::warning 警告
CompositeByteBuf 中的 ByteBuf 实例可能同时包含直接内存分配和非直接内存分配。如果其中只有一个实例,那么对 CompositeByteBuf 上的 hasArray()方法的调用将返回该组件上的 hasArray()方法的值;否则它将返回 false。
:::
使用 ByteBuffer 的复合缓冲区模式
ByteBuffer [ ] message = new ByteBuffer [ ] { header, body } ;
ByteBuffer message2 =
ByteBuffer . allocate ( header. remaining ( ) + body. remaining ( ) ) ;
message2. put ( header) ;
message2. put ( body) ;
message2. flip ( ) ;
使用 CompositeByteBuf 的复合缓冲区模式
CompositeByteBuf messageBuf = Unpooled . compositeBuffer ( ) ;
ByteBuf headerBuf = . . . ;
ByteBuf bodyBuf = . . . ;
messageBuf. addComponents ( headerBuf, bodyBuf) ;
messageBuf. removeComponent ( 0 ) ;
for ( ByteBuf buf : messageBuf) {
System . out. println ( buf. toString ( ) ) ;
}
访问 CompositeByteBuf 中的数据
CompositeByteBuf compBuf = Unpooled . compositeBuffer ( ) ;
int length = compBuf. readableBytes ( ) ;
byte [ ] array = new byte [ length] ;
compBuf. getBytes ( compBuf. readerIndex ( ) , array) ;
handleArray ( array, 0 , array. length) ;
需要注意的是,Netty使用了CompositeByteBuf来优化套接字的I/O操作,尽可能地消除了由JDK的缓冲区实现所导致的性能以及内存使用率的惩罚。
随机访问索引
如同在普通的 Java 字节数组中一样,ByteBuf 的索引是从零开始的:第一个字节的索引是0,最后一个字节的索引总是 capacity() - 1。
ByteBuf buffer = . . . ;
for ( int i = 0 ; i < buffer. capacity ( ) ; i++ ) {
byte b = buffer. getByte ( i) ;
System . out. println ( ( char ) b) ;
}
需要注意的是,使用那些需要一个索引值参数的方法(的其中)之一来访问数据既不会改变readerIndex 也不会改变 writerIndex。如果有需要,也可以通过调用 readerIndex(index)或者 writerIndex(index)来手动移动这两者。
顺序访问索引
虽然 ByteBuf 同时具有读索引和写索引,但是 JDK 的 ByteBuffer 却只有一个索引,这也就是为什么必须调用 flip()方法来在读模式和写模式之间进行切换的原因。下图展示了ByteBuf 是如何被它的两个索引划分成 3 个区域的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MoTGSJQX-1667891439096)(/img/netty/bytebuf.png)]
可丢弃字节
通过调用 discardReadBytes()方法,可以丢弃它们并回收空间。这个分段的初始大小为 0,存储在 readerIndex 中,会随着 read 操作的执行而增加(get*操作不会移动 readerIndex)。
下图中所展示的缓冲区上调用discardReadBytes()方法后的结果。可以看到,可丢弃字节分段中的空间已经变为可写的了。注意,在调用discardReadBytes()之后,对可写分段的内容并没有任何的保证。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-noP3oxjt-1667891439097)(/img/netty/bytebuf-gc.png)]
可读字节
ByteBuf 的可读字节分段存储了实际数据。新分配的、包装的或者复制的缓冲区的默认的readerIndex 值为 0。任何名称以 read 或者 skip 开头的操作都将检索或者跳过位于当前readerIndex 的数据,并且将它增加已读字节数。
如果被调用的方法需要一个 ByteBuf 参数作为写入的目标,并且没有指定目标索引参数,那么该目标缓冲区的 writerIndex 也将被增加,例如:
readBytes(ByteBuf dest);
如果尝试在缓冲区的可读字节数已经耗尽时从中读取数据,那么将会引发一个 IndexOutOfBoundsException;
ByteBuf buffer = . . . ;
while ( buffer. isReadable ( ) ) {
System . out. println ( buffer. readByte ( ) ) ;
}
可写字节
可写字节分段是指一个拥有未定义内容的、写入就绪的内存区域。新分配的缓冲区的writerIndex 的默认值为 0。任何名称以 write 开头的操作都将从当前的 writerIndex 处开始写数据,并将它增加已经写入的字节数。如果写操作的目标也是 ByteBuf,并且没有指定源索引的值,则源缓冲区的 readerIndex 也同样会被增加相同的大小。这个调用如下所示:
writeBytes(ByteBuf dest);
如果尝试往目标写入超过目标容量的数据,将会引发一个IndexOutOfBoundException
ByteBuf buffer = . . . ;
while ( buffer. writableBytes ( ) >= 4 ) {
buffer. writeInt ( random. nextInt ( ) ) ;
}
上述代码是一个用随机整数值填充缓冲区,直到它空间不足为止的例子。writeableBytes()方法在这里被用来确定该缓冲区中是否还有足够的空间。
索引管理
JDK 的 InputStream 定义了 mark(int readlimit)和 reset()方法,这些方法分别被用来将流中的当前位置标记为指定的值,以及将流重置到该位置。
同样,可以通过调用 markReaderIndex()、markWriterIndex()、resetWriterIndex()和 resetReaderIndex()来标记和重置 ByteBuf 的 readerIndex 和 writerIndex。这些和InputStream 上的调用类似,只是没有 readlimit 参数来指定标记什么时候失效。
也可以通过调用 readerIndex(int)或者 writerIndex(int)来将索引移动到指定位置。试 图将任何一个索引设置到一个无效的位置都将导致一个 IndexOutOfBoundsException。
可以通过调用 clear()方法来将 readerIndex 和 writerIndex 都设置为 0。注意,这并不会清除内存中的内容。图 5-5(重复上面的图 5-3)展示了它是如何工作的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vza5Hgyj-1667891439097)(/img/netty/bytebuf-index.png)]
调用 clear()比调用 discardReadBytes()轻量得多,因为它将只是重置索引而不会复制任何的内存。
查找操作
在ByteBuf中有多种可以用来确定指定值的索引的方法。最简单的是使用indexOf()方法。较复杂的查找可以通过那些需要一个ByteBufProcessor
ByteBufProcessor针对一些常见的值定义了许多便利的方法。假设你的应用程序需要和所谓的包含有以NULL结尾的内容的Flash套接字
作为参数的方法达成。这个接口只定义了一个方法:
boolean process(byte value)
它将检查输入值是否是正在查找的值。
ByteBufProcessor针对一些常见的值定义了许多便利的方法。假设你的应用程序需要和所谓的包含有以NULL结尾的内容的Flash套接字集成;
调用forEachByte(ByteBufProcessor.FIND_NUL)将简单高效地消费该 Flash 数据,因为在处理期间只会执行较少的边界检查。
ByteBuf buffer = . . . ;
int index = buffer. forEachByte ( ByteBufProcessor . FIND_CR) ;
派生缓冲区
派生缓冲区为 ByteBuf 提供了以专门的方式来呈现其内容的视图。这类视图是通过以下方法被创建的:
duplicate();
slice();
slice(int, int);
Unpooled.unmodifiableBuffer(…);
order(ByteOrder);
readSlice(int)。
每个这些方法都将返回一个新的 ByteBuf 实例,它具有自己的读索引、写索引和标记索引。其内部存储和 JDK 的 ByteBuffer 一样也是共享的。这使得派生缓冲区的创建成本是很低廉的,但是这也意味着,如果你修改了它的内容,也同时修改了其对应的源实例,所以要小心。
:::tip ByteBuf复制
如果需要一个现有缓冲区的真实副本,请使用 copy()或者 copy(int, int)方法。不同于派生缓冲区,由这个调用所返回的 ByteBuf 拥有独立的数据副本。
:::
Charset utf8 = Charset . forName ( "UTF-8" ) ;
ByteBuf buf = Unpooled . copiedBuffer ( "Netty in Action rocks!" , utf8) ;
ByteBuf sliced = buf. slice ( 0 , 15 ) ;
System . out. println ( sliced. toString ( utf8) ) ;
buf. setByte ( 0 , ( byte ) 'J' ) ;
assert buf. getByte ( 0 ) == sliced. getByte ( 0 ) ;
Charset utf8 = Charset . forName ( "UTF-8" ) ;
ByteBuf buf = Unpooled . copiedBuffer ( "Netty in Action rocks!" , utf8) ;
ByteBuf copy = buf. copy ( 0 , 15 ) ;
System . out. println ( copy. toString ( utf8) ) ;
buf. setByte ( 0 , ( byte ) 'J' ) ;
assert buf. getByte ( 0 ) != copy. getByte ( 0 ) ;
读/写操作
正如我们所提到过的,有两种类别的读/写操作:
get()和 set()操作,从给定的索引开始,并且保持索引不变;
read()和 write()操作,从给定的索引开始,并且会根据已经访问过的字节数对索 引进行调整。
下面列举了最常用的 get()方法。完整列表请参考对应的 API 文档。
get操作
getBoolean(int) 返回给定索引处的 Boolean 值
getByte(int) 返回给定索引处的字节
getUnsignedByte(int) 将给定索引处的无符号字节值作为 short 返回
getMedium(int) 返回给定索引处的 24 位的中等 int 值
getUnsignedMedium(int) 返回给定索引处的无符号的 24 位的中等 int 值
getInt(int) 返回给定索引处的 int 值
getUnsignedInt(int) 将给定索引处的无符号 int 值作为 long 返回
getLong(int) 返回给定索引处的 long 值
getShort(int) 返回给定索引处的 short 值
getUnsignedShort(int) 将给定索引处的无符号 short 值作为 int 返回
getBytes(int, …) 将该缓冲区中从给定索引开始的数据传送到指定的目的地
set操作
setBoolean(int, boolean) 设定给定索引处的 Boolean 值
setByte(int index, int value) 设定给定索引处的字节值
setMedium(int index, int value) 设定给定索引处的 24 位的中等 int 值
setInt(int index, int value) 设定给定索引处的 int 值
setLong(int index, long value) 设定给定索引处的 long 值
setShort(int index, int value) 设定给定索引处的 short 值
Charset utf8 = Charset . forName ( "UTF-8" ) ;
ByteBuf buf = Unpooled . copiedBuffer ( "Netty in Action rocks!" , utf8) ;
System . out. println ( ( char ) buf. getByte ( 0 ) ) ;
int readerIndex = buf. readerIndex ( ) ;
int writerIndex = buf. writerIndex ( ) ;
buf. setByte ( 0 , ( byte ) 'B' ) ;
System . out. println ( ( char ) buf. getByte ( 0 ) ) ;
assert readerIndex == buf. readerIndex ( ) ;
assert writerIndex == buf. writerIndex ( ) ;
read()操作
其作用于当前的 readerIndex 或 writerIndex。这些方法将用于从 ByteBuf 中读取数据,如同它是一个流。
readBoolean() 返回当前 readerIndex 处的 Boolean,并将 readerIndex 增加 1
readByte() 返回当前 readerIndex 处的字节,并将 readerIndex 增加 1
readUnsignedByte() 将当前 readerIndex 处的无符号字节值作为 short 返回,并将readerIndex 增加 1
readMedium() 返回当前 readerIndex 处的 24 位的中等 int 值,并将 readerIndex增加 3
readUnsignedMedium() 返回当前 readerIndex 处的 24 位的无符号的中等 int 值,并将readerIndex 增加 3
readInt() 返回当前 readerIndex 的 int 值,并将 readerIndex 增加 4
readUnsignedInt() 将当前 readerIndex 处的无符号的 int 值作为 long 值返回,并将readerIndex 增加 4
readLong() 返回当前 readerIndex 处的 long 值,并将 readerIndex 增加 8
readShort() 返回当前 readerIndex 处的 short 值,并将 readerIndex 增加 2
readUnsignedShort() 将当前 readerIndex 处的无符号 short 值作为 int 值返回,并将readerIndex 增加 2
readBytes(ByteBuf | byte[] destination,int dstIndex [,int length])将当前 ByteBuf 中从当前 readerIndex 处开始的(如果设置了,length 长度的字节)数据传送到一个目标 ByteBuf 或者 byte[],从目标的 dstIndex 开始的位置。本地的 readerIndex 将被增加已经传输的字节数
write操作
writeBoolean(boolean) 在当前 writerIndex 处写入一个 Boolean,并将 writerIndex 增加 1
writeByte(int) 在当前 writerIndex 处写入一个字节值,并将 writerIndex 增加 1
writeMedium(int) 在当前 writerIndex 处写入一个中等的 int 值,并将 writerIndex增加 3
writeInt(int) 在当前 writerIndex 处写入一个 int 值,并将 writerIndex 增加 4
writeLong(long) 在当前 writerIndex 处写入一个 long 值,并将 writerIndex 增加 8
writeShort(int) 在当前 writerIndex 处写入一个 short 值,并将 writerIndex 增加 2
writeBytes(source ByteBuf |byte[] [,int srcIndex,int length])从当前 writerIndex 开始,传输来自于指定源(ByteBuf 或者 byte[])的数据。如果提供了 srcIndex 和 length,则从 srcIndex 开始读取,并且处理长度为 length 的字节。当前 writerIndex 将会被增加所写入的字节数
Charset utf8 = Charset . forName ( "UTF-8" ) ;
ByteBuf buf = Unpooled . copiedBuffer ( "Netty in Action rocks!" , utf8) ;
System . out. println ( ( char ) buf. readByte ( ) ) ;
int readerIndex = buf. readerIndex ( ) ;
int writerIndex = buf. writerIndex ( ) ;
buf. writeByte ( ( byte ) '?' ) ;
assert readerIndex == buf. readerIndex ( ) ;
assert writerIndex != buf. writerIndex ( ) ;
更多的操作
isReadable() 如果至少有一个字节可供读取,则返回 true
isWritable() 如果至少有一个字节可被写入,则返回 true
readableBytes() 返回可被读取的字节数
writableBytes() 返回可被写入的字节数
capacity() 返回 ByteBuf 可容纳的字节数。在此之后,它会尝试再次扩展直到达到 maxCapacity()
maxCapacity() 返回 ByteBuf 可以容纳的最大字节数
hasArray() 如果 ByteBuf 由一个字节数组支撑,则返回 true
array() 如果 ByteBuf 由一个字节数组支撑则返回该数组;否则,它将抛出一个UnsupportedOperationException 异常
ByteBufHolder 接口
除了实际的数据负载之外,我们还需要存储各种属性值。HTTP 响应便是一个很好的例子,除了表示为字节的内容,还包括状态码、cookie 等。
为了处理这种常见的用例,Netty 提供了 ByteBufHolder。ByteBufHolder 也为 Netty 的高级特性提供了支持,如缓冲区池化,其中可以从池中借用 ByteBuf,并且在需要时自动释放。
ByteBufHolder 只有几种用于访问底层数据和引用计数的方法。下表列出了它们(这里不包括它继承自 ReferenceCounted 的那些方法)。
content() 返回由这个 ByteBufHolder 所持有的 ByteBuf
copy() 返回这个 ByteBufHolder 的一个深拷贝,包括一个其所包含的 ByteBuf 的非共享拷贝
duplicate() 返回这个 ByteBufHolder 的一个浅拷贝,包括一个其所包含的 ByteBuf 的共享拷贝
ByteBuf 分配
按需分配:ByteBufAllocator 接口
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2R1iZIaY-1667891439097)(/img/netty/bytebuf-allocator.png)]
可以通过 Channel(每个都可以有一个不同的 ByteBufAllocator 实例)或者绑定到ChannelHandler 的 ChannelHandlerContext 获取一个到 ByteBufAllocator 的引用。
Channel channel = . . . ;
ByteBufAllocator allocator = channel. alloc ( ) ;
. . . .
ChannelHandlerContext ctx = . . . ;
ByteBufAllocator allocator2 = ctx. alloc ( ) ;
Netty提供了两种ByteBufAllocator的实现:PooledByteBufAllocator和UnpooledByteBufAllocator。前者池化了ByteBuf的实例以提高性能并最大限度地减少内存碎片。此实现使用了一种称为jemalloc的已被大量现代操作系统所采用的高效方法来分配内存。后者的实现不池化ByteBuf实例,并且在每次它被调用时都会返回一个新的实例。
虽然Netty默认 使用了PooledByteBufAllocator,但这可以很容易地通过ChannelConfig API或者在引导你的应用程序时指定一个不同的分配器来更改。
Unpooled 缓冲区
可能某些情况下,你未能获取一个到 ByteBufAllocator 的引用。对于这种情况,Netty 提供了一个简单的称为 Unpooled 的工具类,它提供了静态的辅助方法来创建未池化的 ByteBuf实例。下表列举了这些中最重要的方法。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BUFpy5Pt-1667891439098)(/img/netty/bytebuf-unpolled.png)]
ByteBufUtil 类
ByteBufUtil 提供了用于操作 ByteBuf 的静态的辅助方法。因为这个 API 是通用的,并且和池化无关,所以这些方法已然在分配类的外部实现。
这些静态方法中最有价值的可能就是 hexdump()方法,它以十六进制的表示形式打印ByteBuf 的内容。这在各种情况下都很有用,例如,出于调试的目的记录 ByteBuf 的内容。十六进制的表示通常会提供一个比字节值的直接表示形式更加有用的日志条目,此外,十六进制的版本还可以很容易地转换回实际的字节表示。
另一个有用的方法是 boolean equals(ByteBuf, ByteBuf),它被用来判断两个 ByteBuf实例的相等性。如果你实现自己的 ByteBuf 子类,你可能会发现 ByteBufUtil 的其他有用方法。
引用计数
引用计数是一种通过在某个对象所持有的资源不再被其他对象引用时释放该对象所持有的资源来优化内存使用和性能的技术。Netty 在第 4 版中为 ByteBuf 和 ByteBufHolder 引入了引用计数技术,它们都实现了 interface ReferenceCounted。
引用计数背后的想法并不是特别的复杂;它主要涉及跟踪到某个特定对象的活动引用的数量。一个 ReferenceCounted 实现的实例将通常以活动的引用计数为 1 作为开始。只要引用计数大于 0,就能保证对象不会被释放。当活动引用的数量减少到 0 时,该实例就会被释放。注意,虽然释放的确切语义可能是特定于实现的,但是至少已经释放的对象应该不可再用了。
引用计数对于池化实现(如 PooledByteBufAllocator)来说是至关重要的,它降低了内存分配的开销。下面代码展示了相关的示例。
Channel channel = . . . ;
ByteBufAllocator allocator = channel. alloc ( ) ;
. . . .
ByteBuf buffer = allocator. directBuffer ( ) ;
assert buffer. refCnt ( ) == 1 ;
ByteBuf buffer = . . . ;
boolean released = buffer. release ( ) ;
试图访问一个已经被释放的引用计数的对象,将会导致一个 IllegalReferenceCountException。
注意,一个特定的(ReferenceCounted 的实现)类,可以用它自己的独特方式来定义它的引用计数规则。例如,我们可以设想一个类,其 release()方法的实现总是将引用计数设为零,而不用关心它的当前值,从而一次性地使所有的活动引用都失效。
:::warning 谁负责释放
一般来说,是由最后访问(引用计数)对象的那一方来负责将它释放。
:::
Channel 和 Pipeline
Channel 的生命周期
Interface Channel 定义了一组和 ChannelInboundHandler API 密切相关的简单但功能强大的状态模型,下表列出了 Channel 的这 4 个状态。
ChannelUnregistered Channel 已经被创建,但还未注册到 EventLoop
ChannelRegistered Channel 已经被注册到了 EventLoop
ChannelActive Channel 处于活动状态(已经连接到它的远程节点)。它现在可以接收和发送数据了
ChannelInactive Channel 没有连接到远程节点
Channel 的状态模型
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hRMhh59I-1667891439098)(/img/netty/channel-life.png)]
handlerAdded 当把 ChannelHandler 添加到 ChannelPipeline 中时被调用
handlerRemoved 当从 ChannelPipeline 中移除 ChannelHandler 时被调用
exceptionCaught 当处理过程中在 ChannelPipeline 中有错误产生时被调用
Netty 定义了下面两个重要的 ChannelHandler 子接口:
ChannelInboundHandler——处理入站数据以及各种状态变化;
ChannelOutboundHandler——处理出站数据并且允许拦截所有的操作。
ChannelInboundHandler 接口
ChannelInboundHandler 的方法
channelRegistered 当 Channel 已经注册到它的 EventLoop 并且能够处理 I/O 时被调用
channelUnregistered 当 Channel 从它的 EventLoop 注销并且无法处理任何 I/O 时被调用
channelActive 当 Channel 处于活动状态时被调用;Channel 已经连接/绑定并且已经就绪
channelInactive 当 Channel 离开活动状态并且不再连接它的远程节点时被调用
channelReadComplete 当Channel上的一个读操作完成时被调用 ①
channelRead 当从 Channel 读取数据时被调用
ChannelWritability-Changed当 Channel 的可写状态发生改变时被调用。用户可以确保写操作不会完成得太快(以避免发生 OutOfMemoryError)或者可以在 Channel 变为再次可写时恢复写入。可以通过调用 Channel 的 isWritable()方法来检测Channel 的可写性。与可写性相关的阈值可以通过 Channel.config().setWriteHighWaterMark()和 Channel.config().setWriteLowWaterMark()方法来设置
userEventTriggered 当 ChannelnboundHandler.fireUserEventTriggered()方法被调用时被调用,因为一个 POJO 被传经了 ChannelPipeline
:::warning 警告
当某个 ChannelInboundHandler 的实现重写 channelRead()方法时,它将负责显式地释放与池化的 ByteBuf 实例相关的内存。
:::
@Sharable
public class DiscardHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead ( ChannelHandlerContext ctx, Object msg) {
ReferenceCountUtil . release ( msg) ;
}
}
继承SimpleChannelInboundHandler的Handler可以自动释放资源
public class SimpleDiscardHandler extends SimpleChannelInboundHandler < Object > {
@Override
public void channelRead0 ( ChannelHandlerContext ctx,
Object msg) {
}
}
由于 SimpleChannelInboundHandler 会自动释放资源,所以你不应该存储指向任何消息的引用供将来使用,因为这些引用都将会失效。
ChannelOutboundHandler 接口
ChannelOutboundHandler 的方法
bind(ChannelHandlerContext,SocketAddress,ChannelPromise)当请求将 Channel 绑定到本地地址时被调用
connect(ChannelHandlerContext,SocketAddress,SocketAddress,ChannelPromise)当请求将 Channel 连接到远程节点时被调用
disconnect(ChannelHandlerContext,ChannelPromise)当请求将 Channel 从远程节点断开时被调用
close(ChannelHandlerContext,ChannelPromise) 当请求关闭 Channel 时被调用
deregister(ChannelHandlerContext,ChannelPromise)当请求将 Channel 从它的 EventLoop 注销时被调用
read(ChannelHandlerContext) 当请求从 Channel 读取更多的数据时被调用
flush(ChannelHandlerContext) 当请求通过 Channel 将入队数据冲刷到远程节点时被调用
write(ChannelHandlerContext,Object,ChannelPromise)当请求通过 Channel 将数据写到远程节点时被调用
ChannelPromise 与ChannelFuture ChannelOutboundHandler中的大部分方法都需要一个ChannelPromise参数,以便在操作完成时得到通知。ChannelPromise是ChannelFuture的一个子类,其定义了一些可写的方法,如setSuccess()和setFailure(),从而使ChannelFuture不可变。
ChannelHandler 适配器
你可以使用 ChannelInboundHandlerAdapter 和 ChannelOutboundHandlerAdapter类作为自己的 ChannelHandler 的起始点。这两个适配器分别提供了 ChannelInboundHandler和 ChannelOutboundHandler 的基本实现。通过扩展抽象类 ChannelHandlerAdapter,它们获得了它们共同的超接口 ChannelHandler 的方法。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8egB1goo-1667891439098)(/img/netty/channel-adapter.png)]
ChannelHandlerAdapter 还提供了实用方法 isSharable()。如果其对应的实现被标注为 Sharable,那么这个方法将返回 true,表示它可以被添加到多个 ChannelPipeline中。
ChannelInboundHandlerAdapter 和 ChannelOutboundHandlerAdapter 中所提供的方法体调用了其相关联的 ChannelHandlerContext 上的等效方法,从而将事件转发到了 ChannelPipeline 中的下一个 ChannelHandler 中。
你要想在自己的 ChannelHandler 中使用这些适配器类,只需要简单地扩展它们,并且重写那些你想要自定义的方法。
释放资源
ReferenceCountUtil . release ( msg) ;
ChannelPipeline 接口
如果你认为ChannelPipeline是一个拦截流经Channel的入站和出站事件的ChannelHandler 实例链,那么就很容易看出这些 ChannelHandler 之间的交互是如何组成一个应用程序数据和事件处理逻辑的核心的。
每一个新创建的 Channel 都将会被分配一个新的 ChannelPipeline。这项关联是永久性的;Channel 既不能附加另外一个 ChannelPipeline,也不能分离其当前的。在 Netty 组件的生命周期中,这是一项固定的操作,不需要开发人员的任何干预。
根据事件的起源,事件将会被 ChannelInboundHandler 或者 ChannelOutboundHandler扩展了ChannelInboundandlerAdapter 通过调用 ReferenceCountUtil.release()方法释放资源
扩展了ChannelOutboundHandlerAdapter通过使用 ReferenceCountUtil.realse(…)方法释放资源通知 ChannelPromise数据已经被处理了
处理。随后,通过调用 ChannelHandlerContext 实现,它将被转发给同一超类型的下一个ChannelHandler。
:::tip ChannelHandlerContext
ChannelHandlerContext使得ChannelHandler能够和它的ChannelPipeline以及其他的ChannelHandler 交互。 ChannelHandler 可以通知其所属的 ChannelPipeline 中的下一 个ChannelHandler,甚至可以动态修改它所属的ChannelPipeline。
:::
下图展示了一个典型的同时具有入站和出站 ChannelHandler 的 ChannelPipeline 的布局,并且印证了我们之前的关于 ChannelPipeline 主要由一系列的 ChannelHandler 所组成的说法。ChannelPipeline 还提供了通过 ChannelPipeline 本身传播事件的方法。如果一个入站事件被触发,它将被从 ChannelPipeline 的头部开始一直被传播到 Channel Pipeline 的尾端。
在下图中,一个出站 I/O 事件将从 ChannelPipeline 的最右边开始,然后向左传播。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mabXmjUs-1667891439099)(/img/netty/channel-context.png)]
:::tip ChannelPipeline 相对论
从事件途经 ChannelPipeline 的角度来看,ChannelPipeline 的头部和尾端取决于该事件是入站的还是出站的。然而 Netty 总是将 ChannelPipeline 的入站口作为头部,而将出站口作为尾端。
当你完成了通过调用 ChannelPipeline.add*()方法将入站处理器(ChannelInboundHandler)和出站处理器( ChannelOutboundHandler )混合添加到 ChannelPipeline 之后,每一个ChannelHandler 从头部到尾端的顺序位置正如同我们方才所定义它们的一样。因此,如果上图中的处理器(ChannelHandler)从左到右进行编号,那么第一个被入站事件看到的 ChannelHandler 将是1,而第一个被出站事件看到的 ChannelHandler 将是 5。
:::
在 ChannelPipeline 传播事件时,它会测试 ChannelPipeline 中的下一个 ChannelHandler 的类型是否和事件的运动方向相匹配。如果不匹配,ChannelPipeline 将跳过该ChannelHandler 并前进到下一个,直到它找到和该事件所期望的方向相匹配的为止。(当然,ChannelHandler 也可以同时实现 ChannelInboundHandler 接口和 ChannelOutboundHandler 接口。)
修改 ChannelPipeline
ChannelHandler 可以通过添加、删除或者替换其他的 ChannelHandler 来实时地修改ChannelPipeline 的布局。(它也可以将它自己从 ChannelPipeline 中移除。)这是 ChannelHandler 最重要的能力之一。
AddFirstaddBefore addAfteraddLast 将一个ChannelHandler 添加到ChannelPipeline 中
remove 将一个ChannelHandler 从ChannelPipeline 中移除
replace 将 ChannelPipeline 中的一个 ChannelHandler 替换为另一个 ChannelHandler
ChannelPipeline pipeline = . . ;
FirstHandler firstHandler = new FirstHandler ( ) ;
pipeline. addLast ( "handler1" , firstHandler) ;
pipeline. addFirst ( "handler2" , new SecondHandler ( ) ) ;
pipeline. addLast ( "handler3" , new ThirdHandler ( ) ) ;
. . .
pipeline. remove ( "handler3" ) ;
pipeline. remove ( firstHandler) ;
pipeline. replace ( "handler2" , "handler4" , new ForthHandler ( ) ) ;
:::tip ChannelHandler 的执行和阻塞
通常 ChannelPipeline 中的每一个 ChannelHandler 都是通过它的 EventLoop(I/O 线程)来处理传递给它的事件的。所以至关重要的是不要阻塞这个线程,因为这会对整体的 I/O 处理产生负面的影响。
但有时可能需要与那些使用阻塞 API 的遗留代码进行交互。对于这种情况,ChannelPipeline 有一些接受一个 EventExecutorGroup 的 add()方法。如果一个事件被传递给一个自定义的 EventExecutorGroup,它将被包含在这个 EventExecutorGroup 中的某个 EventExecutor 所处理,从而被从该Channel 本身的 EventLoop 中移除。对于这种用例,Netty 提供了一个叫 DefaultEventExecutorGroup 的默认实现。
:::
ChannelPipeline 的用于访问 ChannelHandler 的操作
get 通过类型或者名称返回 ChannelHandler
context 返回和 ChannelHandler 绑定的 ChannelHandlerContext
names 返回 ChannelPipeline 中所有 ChannelHandler 的名称
触发事件
ChannelPipeline 的 API 公开了用于调用入站和出站操作的附加方法。下表列出了入站操作,用于通知 ChannelInboundHandler 在 ChannelPipeline 中所发生的事件。
ChannelPipeline 的入站操作
fireChannelRegistered 调用 ChannelPipeline 中下一个 ChannelInboundHandler 的channelRegistered(ChannelHandlerContext)方法
fireChannelUnregistered 调用 ChannelPipeline 中下一个 ChannelInboundHandler 的channelUnregistered(ChannelHandlerContext)方法
fireChannelActive 调用 ChannelPipeline 中下一个 ChannelInboundHandler 的channelActive(ChannelHandlerContext)方法
fireChannelInactive 调用 ChannelPipeline 中下一个 ChannelInboundHandler 的channelInactive(ChannelHandlerContext)方法
fireExceptionCaught 调用 ChannelPipeline 中下一个 ChannelInboundHandler 的exceptionCaught(ChannelHandlerContext, Throwable)方法
fireUserEventTriggered 调用 ChannelPipeline 中下一个 ChannelInboundHandler 的userEventTriggered(ChannelHandlerContext, Object)方法
fireChannelRead 调用 ChannelPipeline 中下一个 ChannelInboundHandler 的channelRead(ChannelHandlerContext, Object msg)方法fireChannelReadComplete 调用 ChannelPipeline 中下一个 ChannelInboundHandler 的channelReadComplete(ChannelHandlerContext)方法
fireChannelWritabilityChanged调用 ChannelPipeline 中下一个 ChannelInboundHandler 的channelWritabilityChanged(ChannelHandlerContext)方法
ChannelPipeline 的出站操作
bind 将 Channel 绑定到一个本地地址,这将调用 ChannelPipeline 中的下一个ChannelOutboundHandler 的 bind(ChannelHandlerContext, SocketAddress, ChannelPromise)方法
connect 将 Channel 连接到一个远程地址,这将调用 ChannelPipeline 中的下一个ChannelOutboundHandler 的 connect(ChannelHandlerContext, SocketAddress, ChannelPromise)方法
disconnect 将Channel 断开连接。这将调用ChannelPipeline 中的下一个ChannelOutboundHandler 的 disconnect(ChannelHandlerContext, Channel Promise)方法
close 将 Channel 关闭。这将调用 ChannelPipeline 中的下一个 ChannelOutboundHandler 的 close(ChannelHandlerContext, ChannelPromise)方法
deregister 将 Channel 从它先前所分配的 EventExecutor(即 EventLoop)中注销。这将调用 ChannelPipeline 中的下一个 ChannelOutboundHandler 的 deregister(ChannelHandlerContext, ChannelPromise)方法
flush 冲刷Channel所有挂起的写入。这将调用ChannelPipeline中的下一个ChannelOutboundHandler 的 flush(ChannelHandlerContext)方法
write 将消息写入 Channel。这将调用 ChannelPipeline 中的下一个 ChannelOutboundHandler的write(ChannelHandlerContext, Object msg, ChannelPromise)方法。注意:这并不会将消息写入底层的 Socket,而只会将它放入队列中。要将它写入 Socket,需要调用 flush()或者 writeAndFlush()方法
writeAndFlush 这是一个先调用 write()方法再接着调用 flush()方法的便利方法
read 请求从 Channel 中读取更多的数据。这将调用 ChannelPipeline 中的下一个ChannelOutboundHandler 的 read(ChannelHandlerContext)方法
总结
ChannelPipelne 保存了与 Channel 相关联的 ChannelHandler;
ChannelPipeline 可以根据需要,通过添加或者删除 ChannelHandler 来动态地修改;
ChannelPipeline 有着丰富的 API 用以被调用,以响应入站和出站事件。
ChannelHandlerContext 接口
ChannelHandlerContext 代表了 ChannelHandler 和 ChannelPipeline 之间的关联,每当有 ChannelHandler 添加到 ChannelPipeline 中时,都会创建 ChannelHandlerContext。ChannelHandlerContext 的主要功能是管理它所关联的 ChannelHandler 和在同一个 ChannelPipeline 中的其他 ChannelHandler 之间的交互。
ChannelHandlerContext 有很多的方法,其中一些方法也存在于 Channel 和 ChannelPipeline 本身上,但是有一点重要的不同。如果调用 Channel 或者 ChannelPipeline 上的这些方法,它们将沿着整个 ChannelPipeline 进行传播。而调用位于 ChannelHandlerContext上的相同方法,则将从当前所关联的 ChannelHandler 开始,并且只会传播给位于该ChannelPipeline 中的下一个能够处理该事件的 ChannelHandler。
ChannelHandlerContext 的 API
alloc 返回和这个实例相关联的Channel 所配置的 ByteBufAllocator
bind 绑定到给定的 SocketAddress,并返回 ChannelFuture
channel 返回绑定到这个实例的 Channel
close 关闭 Channel,并返回 ChannelFuture
connect 连接给定的 SocketAddress,并返回 ChannelFuture
deregister 从之前分配的 EventExecutor 注销,并返回 ChannelFuture
disconnect 从远程节点断开,并返回 ChannelFuture
executor 返回调度事件的 EventExecutor
fireChannelActive 触发对下一个 ChannelInboundHandler 上的channelActive()方法(已连接)的调用
fireChannelInactive 触发对下一个 ChannelInboundHandler 上的channelInactive()方法(已关闭)的调用
fireChannelRead 触发对下一个 ChannelInboundHandler 上的channelRead()方法(已接收的消息)的调用
fireChannelReadComplete 触发对下一个ChannelInboundHandler上的channelReadComplete()方法的调用
fireChannelRegistered 触发对下一个 ChannelInboundHandler 上的fireChannelRegistered()方法的调用
fireChannelUnregistered 触发对下一个 ChannelInboundHandler 上的fireChannelUnregistered()方法的调用
fireChannelWritabilityChanged 触发对下一个 ChannelInboundHandler 上的fireChannelWritabilityChanged()方法的调用
fireExceptionCaught 触发对下一个 ChannelInboundHandler 上的fireExceptionCaught(Throwable)方法的调用
fireUserEventTriggered 触发对下一个 ChannelInboundHandler 上的fireUserEventTriggered(Object evt)方法的调用
handler 返回绑定到这个实例的 ChannelHandler
isRemoved 如果所关联的 ChannelHandler 已经被从 ChannelPipeline中移除则返回 true
name 返回这个实例的唯一名称
pipeline 返回这个实例所关联的 ChannelPipeline
read 将数据从Channel读取到第一个入站缓冲区;如果读取成功则触发 ①一个channelRead事件,并(在最后一个消息被读取完成后)通 知 ChannelInboundHandler 的 channelReadComplete(ChannelHandlerContext)方法
write 通过这个实例写入消息并经过 ChannelPipeline
writeAndFlush 通过这个实例写入并冲刷消息并经过 ChannelPipeline
:::tip 注意
当使用 ChannelHandlerContext 的 API 的时候,请牢记以下两点:
ChannelHandlerContext 和 ChannelHandler 之间的关联(绑定)是永远不会改变的,所以缓存对它的引用是安全的;
如同我们在本节开头所解释的一样,相对于其他类的同名方法,ChannelHandler Context的方法将产生更短的事件流,应该尽可能地利用这个特性来获得最大的性能。
:::
使用 ChannelHandlerContext
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3Dv0Hl1a-1667891439099)(/img/netty/ChannelHandlerContext.png)]
ChannelHandlerContext ctx = . . ;
Channel channel = ctx. channel ( ) ;
channel. write ( Unpooled . copiedBuffer ( "Netty in Action" , CharsetUtil . UTF_8) ) ;
ChannelHandlerContext ctx = . . ;
ChannelPipeline pipeline = ctx. pipeline ( ) ;
pipeline. write ( Unpooled . copiedBuffer ( "Netty in Action" , CharsetUtil . UTF_8) ) ;
重要的是要注意到,虽然被调用的 Channel 或 ChannelPipeline 上的 write()方法将一直传播事件通过整个 ChannelPipeline,但是在 ChannelHandler 的级别上,事件从一个 ChannelHandler到下一个 ChannelHandler 的移动是由 ChannelHandlerContext 上的调用完成的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Nfcs8bqk-1667891439099)(/img/netty/channel-event.png)]
为什么会想要从 ChannelPipeline 中的某个特定点开始传播事件呢?
为了减少将事件传经对它不感兴趣的 ChannelHandler 所带来的开销。
为了避免将事件传经那些可能会对它感兴趣的 ChannelHandler。
要想调用从某个特定的 ChannelHandler 开始的处理过程,必须获取到在(ChannelPipeline)该 ChannelHandler 之前的 ChannelHandler 所关联的 ChannelHandlerContext。这个 ChannelHandlerContext 将调用和它所关联的 ChannelHandler 之后的ChannelHandler。
ChannelHandlerContext ctx = . . ;
ctx. write ( Unpooled . copiedBuffer ( "Netty in Action" , CharsetUtil . UTF_8) ) ;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C3lfpuq8-1667891439100)(/img/netty/channel-context-event.png)]
消息将从下一个 ChannelHandler 开始流经 ChannelPipeline,绕过了所有前面的 ChannelHandler。
异常处理
处理入站异常
如果在处理入站事件的过程中有异常被抛出,那么它将从它在 ChannelInboundHandler里被触发的那一点开始流经 ChannelPipeline。要想处理这种类型的入站异常,你需要在你的 ChannelInboundHandler 实现中重写下面的方法。
public void exceptionCaught ( ChannelHandlerContext ctx, Throwable cause) throws Exception
public class InboundExceptionHandler extends ChannelInboundHandlerAdapter {
@Override
public void exceptionCaught ( ChannelHandlerContext ctx, Throwable cause) {
cause. printStackTrace ( ) ;
ctx. close ( ) ;
}
}
因为异常将会继续按照入站方向流动(就像所有的入站事件一样),所以实现了前面所示逻辑的 ChannelInboundHandler 通常位于 ChannelPipeline 的最后。这确保了所有的入站异常都总是会被处理,无论它们可能会发生在 ChannelPipeline 中的什么位置。
你应该如何响应异常,可能很大程度上取决于你的应用程序。你可能想要关闭Channel(和连接),也可 能会尝试进行恢复。如果你不实现任何处理入站异常的逻辑(或者没有消费该异常),那么Netty将会记录该异常没有被处理的事实。
ChannelHandler.exceptionCaught()的默认实现是简单地将当前异常转发给ChannelPipeline 中的下一个 ChannelHandler;
如果异常到达了 ChannelPipeline 的尾端,它将会被记录为未被处理;
要想定义自定义的处理逻辑,你需要重写 exceptionCaught()方法。然后你需要决定是否需要将该异常传播出去。
处理出站异常
用于处理出站操作中的正常完成以及异常的选项,都基于以下的通知机制。
每个出站操作都将返回一个 ChannelFuture。注册到 ChannelFuture 的 ChannelFutureListener 将在操作完成时被通知该操作是成功了还是出错了。
几乎所有的 ChannelOutboundHandler 上的方法都会传入一个 ChannelPromise的实例。作为 ChannelFuture 的子类,ChannelPromise 也可以被分配用于异步通知的监听器。但是,ChannelPromise 还具有提供立即通知的可写方法:
ChannelPromise setSuccess ( ) ;
ChannelPromise setFailure ( Throwable cause) ;
添加 ChannelFutureListener 只需要调用 ChannelFuture 实例上的 addListener(ChannelFutureListener)方法,并且有两种不同的方式可以做到这一点。其中最常用的方式是,调用出站操作(如 write()方法)所返回的 ChannelFuture 上的 addListener()方法。
ChannelFuture future = channel. write ( someMessage) ;
future. addListener ( new ChannelFutureListener ( ) {
@Override
public void operationComplete ( ChannelFuture f) {
if ( ! f. isSuccess ( ) ) {
f. cause ( ) . printStackTrace ( ) ;
f. channel ( ) . close ( ) ;
}
}
} ) ;
第二种方式是将 ChannelFutureListener 添加到即将作为参数传递给 ChannelOutboundHandler 的方法的 ChannelPromise。
public class OutboundExceptionHandler extends ChannelOutboundHandlerAdapter {
@Override
public void write ( ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
promise. addListener ( new ChannelFutureListener ( ) {
@Override
public void operationComplete ( ChannelFuture f) {
if ( ! f. isSuccess ( ) ) {
f. cause ( ) . printStackTrace ( ) ;
f. channel ( ) . close ( ) ;
} }
}
) ;
}
}
:::tip ChannelPromise 的可写方法
通过调用 ChannelPromise 上的 setSuccess()和 setFailure()方法,可以使一个操作的状态在 ChannelHandler 的方法返回给其调用者时便即刻被感知到。
:::
EventLoop 线程模型
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-btVYquOT-1667891439100)(/img/netty/execute.png)]
EventLoop 接口
运行任务来处理在连接的生命周期内发生的事件是任何网络框架的基本功能。与之相应的编程上的构造通常被称为事件循环—一个 Netty 使用了 interface io.netty.channel.EventLoop 来适配的术语。
Netty 的 EventLoop 是协同设计的一部分,它采用了两个基本的 API:并发和网络编程。首先,io.netty.util.concurrent 包构建在 JDK 的 java.util.concurrent 包上,用来提供线程执行器。其次,io.netty.channel 包中的类,为了与 Channel 的事件进行交互,扩展了这些接口/类。下图展示了生成的类层次结构。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9BUfF9gH-1667891439100)(/img/netty/event-group.png)]
在这个模型中,一个 EventLoop 将由一个永远都不会改变的 Thread 驱动,同时任务(Runnable 或者 Callable)可以直接提交给 EventLoop 实现,以立即执行或者调度执行。
根据配置和可用核心的不同,可能会创建多个 EventLoop 实例用以优化资源的使用,并且单个EventLoop 可能会被指派用于服务多个 Channel。
需要注意的是,Netty的EventLoop在继承了ScheduledExecutorService的同时,只定义了一个方法,parent()。这个方法,如下面的代码片断所示,用于返回到当前EventLoop实现的实例所属的EventLoopGroup的引用。
public interface EventLoop extends EventExecutor , EventLoopGroup {
@Override
EventLoopGroup parent ( ) ;
}
:::warning 事件/任务的执行顺序
事件和任务是以先进先出(FIFO)的顺序执行的。这样可以通过保证字节内容总是按正确的顺序被处理,消除潜在的数据损坏的可能性。
:::
由 I/O 操作触发的事件将流经安装了一个或者多个ChannelHandler 的 ChannelPipeline。传播这些事件的方法调用可以随后被 ChannelHandler 所拦截,并且可以按需地处理事件。
事件的性质通常决定了它将被如何处理;它可能将数据从网络栈中传递到你的应用程序中,或者进行逆向操作,或者 执行一些截然不同的操作。但是事件的处理逻辑必须足够的通用和灵活,以处理所有可能的用例。因此,在Netty 4 中,所有的I/O操作和事件都由已经被分配给了EventLoop的那个Thread来处理。
使用 EventLoop 调度任务
ScheduledExecutorService 的实现具有局限性,例如,事实上作为线程池管理的一部分,将会有额外的线程创建。如果有大量任务被紧凑地调度,那么这将成为一个瓶颈。Netty 通 过 Channel 的 EventLoop 实现任务调度解决了这一问题。
Channel ch = . . .
ScheduledFuture < ? > future = ch. eventLoop ( ) . schedule (
new Runnable ( ) {
@Override
public void run ( ) {
System . out. println ( "60 seconds later" ) ;
}
} , 60 , TimeUnit . SECONDS) ;
经过 60 秒之后,Runnable 实例将由分配给 Channel 的 EventLoop 执行。如果要调度任务以每隔 60 秒执行一次,请使用 scheduleAtFixedRate()方法。
ScheduledFuture < ? > future = ch. eventLoop ( ) . scheduleAtFixedRate (
new Runnable ( ) {
@Override
public void run ( ) {
System . out. println ( "Run every 60 seconds" ) ;
}
} , 60 , 60 , TimeUnit. Seconds ) ;
用 ScheduledFuture 取消任务
ScheduledFuture < ? > future = ch. eventLoop ( ) . scheduleAtFixedRate ( . . . ) ;
boolean mayInterruptIfRunning = false ;
future. cancel ( mayInterruptIfRunning) ;
线程管理
Netty线程模型的卓越性能取决于对于当前执行的Thread的身份的确定,也就是说,确定它是否是分配给当前Channel以及它的EventLoop的那一个线程。
如果(当前)调用线程正是支撑 EventLoop 的线程,那么所提交的代码块将会被(直接)执行。否则,EventLoop 将调度该任务以便稍后执行,并将它放入到内部队列中。当 EventLoop下次处理它的事件时,它会执行队列中的那些任务/事件。这也就解释了任何的 Thread 是如何与 Channel 直接交互而无需在 ChannelHandler 中进行额外同步的。
注意,每个 EventLoop 都有它自已的任务队列,独立于任何其他的 EventLoop。下图展示了 EventLoop 用于调度任务的执行逻辑。这是 Netty 线程模型的关键组成部分:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6NiJ6s3v-1667891439101)(/img/netty/thread-event.png)]
我们之前已经阐明了不要阻塞当前 I/O 线程的重要性。我们再以另一种方式重申一次:“永远不要将一个长时间运行的任务放入到执行队列中,因为它将阻塞需要在同一线程上执行的任何其他任务。”如果必须要进行阻塞调用或者执行长时间运行的任务,我们建议使用一个专门的EventExecutor。
引导
Bootstrap 类
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WFwaglV7-1667891439101)(/img/netty/bootstrap.png)]
相对于将具体的引导类分别看作用于服务器和客户端的引导来说,记住它们的本意是用来支撑不同的应用程序的功能的将有所裨益。也就是说,服务器致力于使用一个父 Channel 来接受来自客户端的连接,并创建子 Channel 以用于它们之间的通信;而客户端将最可能只需要一个单独的、没有父 Channel 的 Channel 来用于所有的网络交互。(正如同我们将要看到的,这也适用于无连接的传输协议,如 UDP,因为它们并不是每个连接都需要一个单独的 Channel。)
客户端和服务端两种应用程序类型之间通用的引导步骤由 AbstractBootstrap 处理,而特定于客户端或者服务器的引导步骤则分别由 Bootstrap 或 ServerBootstrap 处理。
AbstractBootstrap 类的完整声明是:
public abstract class AbstractBootstrap < B > extends AbstractBootstrap < B , C > , C extends Channel >
public class Bootstrap extends AbstractBootstrap < Bootstrap , Channel >
public class ServerBootstrap extends AbstractBootstrap < ServerBootstrap , ServerChannel >
引导客户端和无连接协议
Bootstrap group(EventLoopGroup) 设置用于处理 Channel 所有事件的 EventLoopGroup
Bootstrap channel(Class) Bootstrap channelFactory( ChannelFactory) channel()方法指定了Channel的实现类。如果该实现类没提供默认的构造函数 ① ,可以通过调用channelFactory()方法来指定一个工厂类,它将会被bind()方法调用
Bootstrap localAddress(SocketAddress)指定 Channel 应该绑定到的本地地址。如果没有指定,则将由操作系统创建一个随机的地址。或者,也可以通过
bind()或者 connect()方法指定 localAddress
Bootstrap option(ChannelOption<\T> option,T value)设置 ChannelOption,其将被应用到每个新创建的Channel 的 ChannelConfig。这些选项将会通过
bind()或者 connect()方法设置到 Channel,不管哪 个先被调用。这个方法在 Channel 已经被创建后再调用将不会有任何的效果。支持的 ChannelOption 取决于使用的 Channel 类型。
Bootstrap attr(Attribute key, T value)指定新创建的 Channel 的属性值。这些属性值是通过bind()或者 connect()方法设置到 Channel 的,具体
取决于谁最先被调用。
Bootstrap handler(ChannelHandler)设置将被添加到 ChannelPipeline 以接收事件通知的ChannelHandler
Bootstrap clone() 创建一个当前 Bootstrap 的克隆,其具有和原始的Bootstrap 相同的设置信息
Bootstrap remoteAddress(SocketAddress)设置远程地址。或者,也可以通过 connect()方法来指定它
ChannelFuture connect() 连接到远程节点并返回一个 ChannelFuture,其将 会在连接操作完成后接收到通知
ChannelFuture bind() 绑定 Channel 并返回一个 ChannelFuture,其将会在绑定操作完成后接收到通知,在那之后必须调用 Channel.connect()方法来建立连接
引导客户端
Bootstrap 类负责为客户端和使用无连接协议的应用程序创建 Channel
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q9SFCom9-1667891439101)(/img/netty/bootstrap-init.png)]
EventLoopGroup group = new NioEventLoopGroup ( ) ;
Bootstrap bootstrap = new Bootstrap ( ) ;
bootstrap. group ( group)
. channel ( NioSocketChannel . class )
. handler ( new SimpleChannelInboundHandler < ByteBuf > ( ) {
@Override
protected void channelRead0 ( ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception {
System . out. println ( "Received data" ) ;
}
} ) ;
ChannelFuture future = bootstrap. connect ( new InetSocketAddress ( "www.manning.com" , 80 ) ) ;
future. addListener ( new ChannelFutureListener ( ) {
@Override
public void operationComplete ( ChannelFuture channelFuture)
throws Exception {
if ( channelFuture. isSuccess ( ) ) {
System . out. println ( "Connection established" ) ;
} else {
System . err. println ( "Connection attempt failed" ) ;
channelFuture. cause ( ) . printStackTrace ( ) ;
}
}
} ) ;
Channel 和 EventLoopGroup 的兼容性
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1KmzN849-1667891439102)(/img/netty/channel-extend.png)]
不兼容的 Channel 和 EventLoopGroup
EventLoopGroup group = new NioEventLoopGroup ( ) ;
Bootstrap bootstrap = new Bootstrap ( ) ;
bootstrap. group ( group)
. channel ( OioSocketChannel . class )
. handler ( new SimpleChannelInboundHandler < ByteBuf > ( ) {
@Override
protected void channelRead0 (
ChannelHandlerContext channelHandlerContext,
ByteBuf byteBuf) throws Exception {
System . out. println ( "Received data" ) ;
}
} ) ;
ChannelFuture future = bootstrap. connect (
new InetSocketAddress ( "www.manning.com" , 80 ) ) ;
future. syncUninterruptibly ( ) ;
这段代码将会导致 IllegalStateException,因为它混用了不兼容的传输。
::: 关于 IllegalStateException 的更多讨论
在引导的过程中,在调用 bind()或者 connect()方法之前,必须调用以下方法来设置所需的组件:
group();
channel()或者 channelFactory();
handler()。
如果不这样做,则将会导致 IllegalStateException。对 handler()方法的调用尤其重要,因为它需要配置好 ChannelPipeline。
:::
引导服务器
ServerBootstrap 类的方法
group 设置 ServerBootstrap 要用的 EventLoopGroup。这个 EventLoopGroup将用于 ServerChannel 和被接受的子 Channel 的 I/O 处理
channel 设置将要被实例化的 ServerChannel 类
channelFactory 如果不能通过默认的构造函数 ①创建Channel,那么可以提供一个ChannelFactory
localAddress 指定 ServerChannel 应该绑定到的本地地址。如果没有指定,则将由操作系统使用一个随机地址。或者,可以通过 bind()方法来指定该 localAddress
option 指定要应用到新创建的 ServerChannel 的 ChannelConfig 的 ChannelOption。这些选项将会通过 bind()方法设置到 Channel。在 bind()方法
被调用之后,设置或者改变 ChannelOption 都不会有任何的效果。所支持的 ChannelOption 取决于所使用的 Channel 类型。
childOption 指定当子 Channel 被接受时,应用到子 Channel 的 ChannelConfig 的ChannelOption。所支持的 ChannelOption 取决于所使用的 Channel 的类
型。
attr 指定 ServerChannel 上的属性,属性将会通过 bind()方法设置给 Channel。在调用 bind()方法之后改变它们将不会有任何的效果
childAttr 将属性设置给已经被接受的子 Channel。接下来的调用将不会有任何的效果
handler 设置被添加到ServerChannel 的ChannelPipeline中的ChannelHandler。更加常用的方法参见 childHandler()
childHandler 设置将被添加到已被接受的子 Channel 的 ChannelPipeline 中的 ChannelHandler。handler()方法和 childHandler()方法之间的区别是:前者所添加的 ChannelHandler 由接受子 Channel 的 ServerChannel 处理,而childHandler()方法所添加的 ChannelHandler 将由已被接受的子 Channel处理,其代表一个绑定到远程节点的套接字
clone 克隆一个设置和原始的 ServerBootstrap 相同的 ServerBootstrap
bind 绑定 ServerChannel 并且返回一个 ChannelFuture,其将会在绑定操作完成后收到通知(带着成功或者失败的结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Tm2uEr6a-1667891439102)(/img/netty/serverBootstrap.png)]
NioEventLoopGroup group = new NioEventLoopGroup ( ) ;
ServerBootstrap bootstrap = new ServerBootstrap ( ) ;
bootstrap. group ( group)
. channel ( NioServerSocketChannel . class )
. childHandler ( new SimpleChannelInboundHandler < ByteBuf > ( ) {
@Override
protected void channelRead0 ( ChannelHandlerContext ctx,
ByteBuf byteBuf) throws Exception {
System . out. println ( "Received data" ) ;
}
} ) ;
ChannelFuture future = bootstrap. bind ( new InetSocketAddress ( 8080 ) ) ;
future. addListener ( new ChannelFutureListener ( ) {
@Override
public void operationComplete ( ChannelFuture channelFuture)
throws Exception {
if ( channelFuture. isSuccess ( ) ) {
System . out. println ( "Server bound" ) ;
} else {
System . err. println ( "Bound attempt failed" ) ;
channelFuture. cause ( ) . printStackTrace ( ) ;
}
}
} ) ;
引导客户端
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g4TVsTUM-1667891439102)(/img/netty/clientBootstrap.png)]
ServerBootstrap bootstrap = new ServerBootstrap ( ) ;
bootstrap
. group ( new NioEventLoopGroup ( ) , new NioEventLoopGroup ( ) )
. channel ( NioServerSocketChannel . class )
. childHandler (
new SimpleChannelInboundHandler < ByteBuf > ( ) {
ChannelFuture connectFuture;
@Override
public void channelActive ( ChannelHandlerContext ctx)
throws Exception {
Bootstrap bootstrap = new Bootstrap ( ) ;
bootstrap. channel ( NioSocketChannel . class ) . handler (
new SimpleChannelInboundHandler < ByteBuf > ( ) {
@Override
protected void channelRead0 (
ChannelHandlerContext ctx, ByteBuf in)
throws Exception {
System . out. println ( "Received data" ) ;
}
} ) ;
bootstrap. group ( ctx. channel ( ) . eventLoop ( ) ) ;
connectFuture = bootstrap. connect (
new InetSocketAddress ( "www.manning.com" , 80 ) ) ;
}
@Override
protected void channelRead0 (
ChannelHandlerContext channelHandlerContext,
ByteBuf byteBuf) throws Exception {
if ( connectFuture. isDone ( ) ) {
}
}
} ) ;
ChannelFuture future = bootstrap. bind ( new InetSocketAddress ( 8080 ) ) ;
future. addListener ( new ChannelFutureListener ( ) {
@Override
public void operationComplete ( ChannelFuture channelFuture)
throws Exception {
if ( channelFuture. isSuccess ( ) ) {
System . out. println ( "Server bound" ) ;
} else {
System . err. println ( "Bind attempt failed" ) ;
channelFuture. cause ( ) . printStackTrace ( ) ;
}
}
} ) ;
引导 DatagramChannel
Bootstrap bootstrap = new Bootstrap ( ) ;
bootstrap. group ( new OioEventLoopGroup ( ) ) . channel (
OioDatagramChannel . class ) . handler (
new SimpleChannelInboundHandler < DatagramPacket > ( ) {
@Override
public void channelRead0 ( ChannelHandlerContext ctx,
DatagramPacket msg) throws Exception {
}
}
) ;
ChannelFuture future = bootstrap. bind ( new InetSocketAddress ( 0 ) ) ;
future. addListener ( new ChannelFutureListener ( ) {
@Override
public void operationComplete ( ChannelFuture channelFuture)
throws Exception {
if ( channelFuture. isSuccess ( ) ) {
System . out. println ( "Channel bound" ) ;
} else {
System . err. println ( "Bind attempt failed" ) ;
channelFuture. cause ( ) . printStackTrace ( ) ;
}
}
} ) ;
关闭
引导使你的应用程序启动并且运行起来,但是迟早你都需要优雅地将它关闭。当然,你也可以让 JVM 在退出时处理好一切,但是这不符合优雅的定义,优雅是指干净地释放资源。关闭 Netty应用程序并没有太多的魔法,但是还是有些事情需要记在心上。
最重要的是,你需要关闭 EventLoopGroup,它将处理任何挂起的事件和任务,并且随后释放所有活动的线程。这就是调用 EventLoopGroup.shutdownGracefully()方法的作用。
这个方法调用将会返回一个 Future,这个 Future 将在关闭完成时接收到通知。需要注意的是,shutdownGracefully()方法也是一个异步的操作,所以你需要阻塞等待直到它完成,或者向所返回的 Future 注册一个监听器以在关闭完成时获得通知。
EventLoopGroup group = new NioEventLoopGroup ( ) ;
Bootstrap bootstrap = new Bootstrap ( ) ;
bootstrap. group ( group)
. channel ( NioSocketChannel . class ) ;
. . .
Future < ? > future = group. shutdownGracefully ( ) ;
future. syncUninterruptibly ( ) ;
或者,你也可以在调用 EventLoopGroup.shutdownGracefully()方法之前,显式地在所有活动的 Channel 上调用 Channel.close()方法。但是在任何情况下,都请记得关闭EventLoopGroup 本身。
编解码器
每个网络应用程序都必须定义如何解析在两个节点之间来回传输的原始字节,以及如何将其和目标应用程序的数据格式做相互转换。这种转换逻辑由编解码器处理,编解码器由编码器和解码器组成,它们每种都可以将字节流从一种格式转换为另一种格式。
如果将消息看作是对于特定的应用程序具有具体含义的结构化的字节序列—它的数据。那么编码器是将消息转换为适合于传输的格式(最有可能的就是字节流);而对应的解码器则是将网络字节流转换回应用程序的消息格式。因此,编码器操作出站数据,而解码器处理入站数据。
解码器
将字节解码为消息——ByteToMessageDecoder 和 ReplayingDecoder;
将一种消息类型解码为另一种——MessageToMessageDecoder。
因为解码器是负责将入站数据从一种格式转换到另一种格式的,所以知道 Netty 的解码器实现了 ChannelInboundHandler 也不会让你感到意外。
什么时候会用到解码器呢?很简单:每当需要为 ChannelPipeline 中的下一个 ChannelInboundHandler 转换入站数据时会用到。此外,得益于 ChannelPipeline 的设计,可以将多个解码器链接在一起,以实现任意复杂的转换逻辑,这也是 Netty 是如何支持代码的模块化以及复用的一个很好的例子。
抽象类 ByteToMessageDecoder
将字节解码为消息(或者另一个字节序列)是一项如此常见的任务,以至于 Netty 为它提供了一个抽象的基类:ByteToMessageDecoder。由于你不可能知道远程节点是否会一次性地发送一个完整的消息,所以这个类会对入站数据进行缓冲,直到它准备好处理。
ByteToMessageDecoder API
decode(ChannelHandlerContext ctx,ByteBuf in,List out)这是你必须实现的唯一抽象方法。decode()方法被调用时将会传入一个包含了传入数据的 ByteBuf,以及一个用来添加解码消息的 List。对这个方法的调用将会重复进行,直到确定没有新的元素被添加到该 List,或者该 ByteBuf 中没有更多可读取的字节时为止。然后,如果该 List 不为空,那么它的内容将会被传递给ChannelPipeline 中的下一个 ChannelInboundHandler
decodeLast(ChannelHandlerContext ctx,ByteBuf in,List out)Netty提供的这个默认实现只是简单地调用了decode()方法。当Channel的状态变为非活动时,这个方法将会被调用一次。可以重写该方法以提供特殊的处理 。
public class ToIntegerDecoder extends ByteToMessageDecoder {
@Override
public void decode ( ChannelHandlerContext ctx, ByteBuf in,
List < Object > out) throws Exception {
if ( in. readableBytes ( ) >= 4 ) {
out. add ( in. readInt ( ) ) ;
}
}
}
虽然 ByteToMessageDecoder 使得可以很简单地实现这种模式,但是你可能会发现,在调用 readInt()方法前不得不验证所输入的 ByteBuf 是否具有足够的数据有点繁琐。在下一节中,下面会有 ReplayingDecoder,它是一个特殊的解码器,以少量的开销消除了这个步骤。
:::warning 编解码器中的引用计数
引用计数需要特别的注意,对于编码器和解码器来说,其过程也是相当的简单:一旦消息被编码或者解码,它就会被 ReferenceCountUtil.release(message)调用自动释放。如果你需要保留引用以便稍后使用,那么你可以调用 ReferenceCountUtil.retain(message)方法。这将会增加该引用计数,从而防止该消息被释放。
:::
抽象类 ReplayingDecoder
ReplayingDecoder扩展了ByteToMessageDecoder类(如代码清单 10-1 所示),使得我们不必调用 readableBytes()方法。它通过使用一个自定义的ByteBuf实现 ,ReplayingDecoderByteBuf,包装传入的ByteBuf实现了这一点,其将在内部执行该调用,这个类的完整声明是:
public abstract class ReplayingDecoder < S > extends ByteToMessageDecoder
类型参数 S 指定了用于状态管理的类型,其中 Void 代表不需要状态管理。下面代码展示了基于 ReplayingDecoder 重新实现的 ToIntegerDecoder。
public class ToIntegerDecoder2 extends ReplayingDecoder < Void > {
@Override
public void decode ( ChannelHandlerContext ctx, ByteBuf in, List < Object > out) throws Exception {
out. add ( in. readInt ( ) ) ;
}
}
和之前一样,从ByteBuf中提取的int将会被添加到List中。如果没有足够的字节可用,这个readInt()方法的实现将会抛出一个Error,其将在基类中被捕获并处理。当有更多的数据可供读取时,该decode()方法将会被再次调用。
请注意 ReplayingDecoderByteBuf 的下面这些方面:
并不是所有的 ByteBuf 操作都被支持,如果调用了一个不被支持的方法,将会抛出一个 UnsupportedOperationException;
ReplayingDecoder 稍慢于 ByteToMessageDecoder
这里有一个简单的准则:如果使用 ByteToMessageDecoder 不会引入太多的复杂性,那么请使用它;否则,请使用 ReplayingDecoder。
:::tip 更多的解码器
下面的这些类处理更加复杂的用例:
io.netty.handler.codec.LineBasedFrameDecoder—这个类在 Netty 内部也有使用,它使用了行尾控制字符(\n 或者\r\n)来解析消息数据;
io.netty.handler.codec.http.HttpObjectDecoder—一个 HTTP 数据的解码器。在 io.netty.handler.codec 子包下面,你将会发现更多用于特定用例的编码器和解码器实现。
更多有关信息参见 Netty 的 Javadoc。
:::
抽象类 MessageToMessageDecoder
public abstract class MessageToMessageDecoder < I > extends ChannelInboundHandlerAdapter
类型参数 I 指定了 decode()方法的输入参数 msg 的类型,它是你必须实现的唯一方法。
MessageToMessageDecoder API
decode(ChannelHandlerContext ctx,I msg,List out) 对于每个需要被解码为另一种格式的入站消息来说,该方法都将会被调用。解码消息随后会被传递给 ChannelPipeline中的下一个 ChannelInboundHandler
在这个示例中,我们将编写一个 IntegerToStringDecoder 解码器来扩展 MessageToMessageDecoder。它的 decode()方法会把 Integer 参数转换为它的 String表示,并将拥有下列签名:
public void decode( ChannelHandlerContext ctx,Integer msg, List out ) throws Exception和之前一样,解码的String将被添加到传出的List中,并转发给下一个ChannelInboundHandler。
public class IntegerToStringDecoder extends MessageToMessageDecoder < Integer > {
@Override
public void decode ( ChannelHandlerContext ctx, Integer msg, List < Object > out) throws Exception {
out. add ( String . valueOf ( msg) ) ;
}
}
:::tip HttpObjectAggregator
有关更加复杂的例子,请研究 io.netty.handler.codec.http.HttpObjectAggregator 类,它扩展了 MessageToMessageDecoder。
:::
TooLongFrameException 类
由于 Netty 是一个异步框架,所以需要在字节可以解码之前在内存中缓冲它们。因此,不能让解码器缓冲大量的数据以至于耗尽可用的内存。为了解除这个常见的顾虑,Netty 提供了TooLongFrameException 类,其将由解码器在帧超出指定的大小限制时抛出。为了避免这种情况,你可以设置一个最大字节数的阈值,如果超出该阈值,则会导致抛出一 个 TooLongFrameException(随后会被 ChannelHandler.exceptionCaught()方法捕获)。然后,如何处理该异常则完全取决于该解码器的用户。某些协议(如 HTTP)可能允许你返回一个特殊的响应。而在其他的情况下,唯一的选择可能就是关闭对应的连接。
下面代码展示了 ByteToMessageDecoder 是如何使用 TooLongFrameException来通知 ChannelPipeline 中的其他 ChannelHandler 发生了帧大小溢出的。需要注意的是,如果你正在使用一个可变帧大小的协议,那么这种保护措施将是尤为重要的。
public class SafeByteToMessageDecoder extends ByteToMessageDecoder {
private static final int MAX_FRAME_SIZE = 1024 ;
@Override
public void decode ( ChannelHandlerContext ctx, ByteBuf in, List < Object > out) throws Exception {
int readable = in. readableBytes ( ) ;
if ( readable > MAX_FRAME_SIZE) {
in. skipBytes ( readable) ;
throw new TooLongFrameException ( "Frame too big!" ) ;
}
. . .
}
}
编码器
编码器实现了 ChannelOutboundHandler,并将出站数据从一种格式转换为另一种格式,和我们方才学习的解码器的功能正好相反。Netty 提供了一组类,用于帮助你编写具有以下功能的编码器:
抽象类 MessageToByteEncoder
MessageToByteEncoder API
encode(ChannelHandlerContext ctx,I msg,ByteBuf out)encode()方法是你需要实现的唯一抽象方法。它被调用时将会传入要被该类编码为 ByteBuf 的(类型为 I 的)出站消息。该 ByteBuf 随后将会被转发给 ChannelPipeline中的下一个 ChannelOutboundHandler
这个类只有一个方法,而解码器有两个。原因是解码器通常需要在Channel 关闭之后产生最后一个消息(因此也就有了 decodeLast()方法)。这显然不适用于编码器的场景——在连接被关闭之后仍然产生一个消息是毫无意义的。
public class ShortToByteEncoder extends MessageToByteEncoder < Short > {
@Override
public void encode ( ChannelHandlerContext ctx, Short msg, ByteBuf out) throws Exception {
out. writeShort ( msg) ;
}
}
Netty 提供了一些专门化的 MessageToByteEncoder,你可以基于它们实现自己的编码器。WebSocket08FrameEncoder 类提供了一个很好的实例。你可以在 io.netty.handler.codec.http.websocketx 包中找到它。
抽象类 MessageToMessageEncoder
MessageToMessageEncoder API
encode(ChannelHandlerContext ctx,I msg,List out)这是你需要实现的唯一方法。每个通过 write()方法写入的消息都将会被传递给 encode()方法,以编码为一个或者多个出站消息。随后,这些出站消息将会被转发给 ChannelPipeline中的下一个 ChannelOutboundHandler
public class IntegerToStringEncoder extends MessageToMessageEncoder < Integer > {
@Override
public void encode ( ChannelHandlerContext ctx, Integer msg, List < Object > out) throws Exception {
out. add ( String . valueOf ( msg) ) ;
}
}
抽象的编解码器类
抽象类 ByteToMessageCodec
让我们来研究这样的一个场景:我们需要将字节解码为某种形式的消息,可能是 POJO,随后再次对它进行编码。ByteToMessageCodec 将为我们处理好这一切,因为它结合了ByteToMessageDecoder 以及它的逆向——MessageToByteEncoder。
任何的请求/响应协议都可以作为使用ByteToMessageCodec的理想选择。例如,在某个SMTP的实现中,编解码器将读取传入字节,并将它们解码为一个自定义的消息类型,如SmtpRequest。而在接收端,当一个响应被创建时,将会产生一个SmtpResponse,其将被编码回字节以便进行传输。
ByteToMessageCodec API
decode(ChannelHandlerContext ctx,ByteBuf in,List)只要有字节可以被消费,这个方法就将会被调用。它将入站ByteBuf 转换为指定的消息格式,并将其转发给ChannelPipeline 中的下一个 ChannelInboundHandler
decodeLast(ChannelHandlerContext ctx,ByteBuf in,List out)这个方法的默认实现委托给了 decode()方法。它只会在Channel 的状态变为非活动时被调用一次。它可以被重写以实现特殊的处理
encode(ChannelHandlerContext ctx,I msg,ByteBuf out)对于每个将被编码并写入出站 ByteBuf 的(类型为 I 的)消息来说,这个方法都将会被调用
抽象类 MessageToMessageCodec
扩展了 MessageToMessageEncoder 以将一种消息格式转换为另外一种消息格式的例子。通过使用 MessageToMessageCodec,我们可以在一个单个的类中实现该转换的往返过程。MessageToMessageCodec 是一个参数化的类,定义如下:
public abstract class MessageToMessageCodec < INBOUND_IN, OUTBOUND_IN>
MessageToMessageCodec 的方法
protected abstract decode(ChannelHandlerContext ctx,INBOUND_IN msg,List out)这个方法被调用时会被传入 INBOUND_IN 类型的消息。它将把它们解码为 OUTBOUND_IN 类型的消息,这些消息将被转发给 ChannelPipeline 中的下一个 ChannelInboundHandler
protected abstract encode(ChannelHandlerContext ctx,OUTBOUND_IN msg,List out)对于每个 OUTBOUND_IN 类型的消息,这个方法都将会被调用。这些消息将会被编码为 INBOUND_IN 类型的消息,然后被转发给 ChannelPipeline 中的下一个ChannelOutboundHandler
decode()方法是将INBOUND_IN类型的消息转换为OUTBOUND_IN类型的消息,而encode()方法则进行它的逆向操作。将INBOUND_IN类型的消息看作是通过网络发送的类型,而将OUTBOUND_IN类型的消息看作是应用程序所处理的类型,将可能有所裨益。
CombinedChannelDuplexHandler 类
正如我们前面所提到的,结合一个解码器和编码器可能会对可重用性造成影响。但是,有一种方法既能够避免这种惩罚,又不会牺牲将一个解码器和一个编码器作为一个单独的单元部署所带来的便利性。CombinedChannelDuplexHandler 提供了这个解决方案,其声明为:
public class CombinedChannelDuplexHandler < I extends ChannelInboundHandler , O extends ChannelOutboundHandler >
这个类充当了 ChannelInboundHandler 和 ChannelOutboundHandler(该类的类型参数 I 和 O)的容器。通过提供分别继承了解码器类和编码器类的类型,我们可以实现一个编解码器,而又不必直接扩展抽象的编解码器类。
public class ByteToCharDecoder extends ByteToMessageDecoder {
@Override
public void decode ( ChannelHandlerContext ctx, ByteBuf in, List < Object > out) throws Exception {
while ( in. readableBytes ( ) >= 2 ) {
out. add ( in. readChar ( ) ) ;
}
}
}
这里的 decode()方法一次将从 ByteBuf 中提取 2 字节,并将它们作为 char 写入到 List中,其将会被自动装箱为 Character 对象。
下面代码包含了CharToByteEncoder,它能将 Character 转换回字节。这个类扩展了 MessageToByteEncoder,因为它需要将 char 消息编码到 ByteBuf 中。这是通过直接写入ByteBuf 做到的。
public class CharToByteEncoder extends MessageToByteEncoder < Character > {
@Override
public void encode ( ChannelHandlerContext ctx, Character msg, ByteBuf out) throws Exception {
out. writeChar ( msg) ;
}
}
public class CombinedByteCharCodec extends CombinedChannelDuplexHandler < ByteToCharDecoder , CharToByteEncoder > {
public CombinedByteCharCodec ( ) {
super ( new ByteToCharDecoder ( ) , new CharToByteEncoder ( ) ) ;
}
}
预置的ChannelHandler
HTTP 解码器、编码器和编解码器
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m4SGEu2D-1667891439102)(/img/netty/http1.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rTrHygzb-1667891439103)(/img/netty/http2.png)]
HttpRequestEncoder 将HttpRequest、HttpContent 和 LastHttpContent 消息编码为字节
HttpResponseEncoder 将HttpResponse、HttpContent 和LastHttpContent 消息编码为字节
HttpRequestDecoder 将字节解码为HttpRequest、HttpContent 和 LastHttpContent 消息
HttpResponseDecoder 将字节解码为HttpResponse、HttpContent 和LastHttpContent 消息
public class HttpPipelineInitializer extends ChannelInitializer < Channel > {
private final boolean client;
public HttpPipelineInitializer ( boolean client) {
this . client = client;
}
@Override
protected void initChannel ( Channel ch) throws Exception {
ChannelPipeline pipeline = ch. pipeline ( ) ;
if ( client) {
pipeline. addLast ( "decoder" , new HttpResponseDecoder ( ) ) ;
pipeline. addLast ( "encoder" , new HttpRequestEncoder ( ) ) ;
} else {
pipeline. addLast ( "decoder" , new HttpRequestDecoder ( ) ) ;
pipeline. addLast ( "encoder" , new HttpResponseEncoder ( ) ) ;
}
}
}
聚合 HTTP 消息
ChannelInitializer 将 ChannelHandler 安装到 ChannelPipeline 中之后,你便可以处理不同类型的 HttpObject 消息了。但是由于 HTTP 的请求和响应可能由许多部分组成,因此你需要聚合它们以形成完整的消息。为了消除这项繁琐的任务,Netty 提供了一个聚合器,它可以将多个消息部分合并为 FullHttpRequest 或者 FullHttpResponse 消息。通过这样的方式,你将总是看到完整的消息内容。
public class HttpAggregatorInitializer extends ChannelInitializer < Channel > {
private final boolean isClient;
public HttpAggregatorInitializer ( boolean isClient) {
this . isClient = isClient;
}
@Override
protected void initChannel ( Channel ch) throws Exception {
ChannelPipeline pipeline = ch. pipeline ( ) ;
if ( isClient) {
pipeline. addLast ( "codec" , new HttpClientCodec ( ) ) ;
} else {
pipeline. addLast ( "codec" , new HttpServerCodec ( ) ) ;
}
pipeline. addLast ( "aggregator" , new HttpObjectAggregator ( 512 * 1024 ) ) ;
}
}
HTTP 压缩
当使用 HTTP 时,建议开启压缩功能以尽可能多地减小传输数据的大小。虽然压缩会带来一些 CPU 时钟周期上的开销,但是通常来说它都是一个好主意,特别是对于文本数据来说。Netty 为压缩和解压缩提供了 ChannelHandler实现,它们同时支持 gzip和 deflate编码。
:::tip HTTP 请求的头部信息
客户端可以通过提供以下头部信息来指示服务器它所支持的压缩格式:
GET /encrypted-area HTTP/1.1
Host: www.example.com
Accept-Encoding: gzip, deflate
然而,需要注意的是,服务器没有义务压缩它所发送的数据。
:::
public class HttpCompressionInitializer extends ChannelInitializer < Channel > {
private final boolean isClient;
public HttpCompressionInitializer ( boolean isClient) {
this . isClient = isClient;
}
@Override
protected void initChannel ( Channel ch) throws Exception {
ChannelPipeline pipeline = ch. pipeline ( ) ;
if ( isClient) {
pipeline. addLast ( "codec" , new HttpClientCodec ( ) ) ;
pipeline. addLast ( "decompressor" ,
new HttpContentDecompressor ( ) ) ;
} else {
pipeline. addLast ( "codec" , new HttpServerCodec ( ) ) ;
pipeline. addLast ( "compressor" ,
new HttpContentCompressor ( ) ) ;
}
}
}
WebSocket
WebSocketFrame 类型
BinaryWebSocketFrame 数据帧:二进制数据
TextWebSocketFrame 数据帧:文本数据
ContinuationWebSocketFrame 数据帧:属于上一个 BinaryWebSocketFrame 或者 TextWeb
SocketFrame 的文本的或者二进制数据
CloseWebSocketFrame 控制帧:一个 CLOSE 请求、关闭的状态码以及关闭的原因
PingWebSocketFrame 控制帧:请求一个 PongWebSocketFrame
PongWebSocketFrame 控制帧:对 PingWebSocketFrame 请求的响应
public class WebSocketServerInitializer extends ChannelInitializer < Channel > {
@Override
protected void initChannel ( Channel ch) throws Exception {
ch. pipeline ( ) . addLast ( new HttpServerCodec ( ) , new HttpObjectAggregator ( 65536 ) ,
new WebSocketServerProtocolHandler ( "/websocket" ) , new TextFrameHandler ( ) , new BinaryFrameHandler ( ) ,
new ContinuationFrameHandler ( ) ) ;
}
public static final class TextFrameHandler extends SimpleChannelInboundHandler < TextWebSocketFrame > {
@Override
public void channelRead0 ( ChannelHandlerContext ctx,
TextWebSocketFrame msg) throws Exception {
}
}
public static final class BinaryFrameHandler extends SimpleChannelInboundHandler < BinaryWebSocketFrame > {
@Override
public void channelRead0 ( ChannelHandlerContext ctx, BinaryWebSocketFrame msg) throws Exception {
}
}
public static final class ContinuationFrameHandler extends SimpleChannelInboundHandler < ContinuationWebSocketFrame > {
@Override
public void channelRead0 ( ChannelHandlerContext ctx, ContinuationWebSocketFrame msg) throws Exception {
}
}
}
空闲的连接和超时
用于空闲连接以及超时的 ChannelHandler
IdleStateHandler 当连接空闲时间太长时,将会触发一个 IdleStateEvent 事件。然后,你可以通过在你的 ChannelInboundHandler 中重写 userEventTriggered()方法来处理该 IdleStateEvent 事件
ReadTimeoutHandler 如果在指定的时间间隔内没有收到任何的入站数据,则抛出一个 ReadTimeoutException 并关闭对应的 Channel。可以通过重写你的ChannelHandler 中的 exceptionCaught()方法来检测该 ReadTimeoutException
WriteTimeoutHandler 如果在指定的时间间隔内没有任何出站数据写入,则抛出一个 WriteTimeoutException 并关闭对应的 Channel 。可以通过重写你的ChannelHandler 的 exceptionCaught()方法检测该 WriteTimeoutException
发送心跳
public class IdleStateHandlerInitializer extends ChannelInitializer < Channel > {
@Override
protected void initChannel ( Channel ch) throws Exception {
ChannelPipeline pipeline = ch. pipeline ( ) ;
pipeline. addLast ( new IdleStateHandler ( 0 , 0 , 60 , TimeUnit . SECONDS) ) ;
pipeline. addLast ( new HeartbeatHandler ( ) ) ;
}
public static final class HeartbeatHandler extends ChannelInboundHandlerAdapter {
private static final ByteBuf HEARTBEAT_SEQUENCE = Unpooled . unreleasableBuffer ( Unpooled . copiedBuffer ( "HEARTBEAT" , CharsetUtil . ISO_8859_1) ) ;
@Override
public void userEventTriggered ( ChannelHandlerContext ctx, Object evt) throws Exception {
if ( evt instanceof IdleStateEvent ) {
ctx. writeAndFlush ( HEARTBEAT_SEQUENCE. duplicate ( ) ) . addListener ( ChannelFutureListener . CLOSE_ON_FAILURE) ;
} else {
super . userEventTriggered ( ctx, evt) ;
}
}
}
}
写大型数据
因为网络饱和的可能性,如何在异步框架中高效地写大块的数据是一个特殊的问题。由于写操作是非阻塞的,所以即使没有写出所有的数据,写操作也会在完成时返回并通知 ChannelFuture。当这种情况发生时,如果仍然不停地写入,就有内存耗尽的风险。所以在写大型数据时,需要准备好处理到远程节点的连接是慢速连接的情况,这种情况会导致内存释放的延迟。让我们考虑下将一个文件内容写出到网络的情况。
NIO的零拷贝特性消除了将文件的内容从文件系统移动到网络栈的复制过程。所有的这一切都发生在 Netty 的核心中,所以应用程序所有需要做的就是使用一个 FileRegion 接口的实现,其在 Netty 的 API 文档中的定义是: “通过支持零拷贝的文件传输的 Channel 来发送的文件区域。”
FileInputStream in = new FileInputStream ( file) ;
FileRegion region = new DefaultFileRegion ( in. getChannel ( ) , 0 , file. length ( ) ) ;
channel. writeAndFlush ( region) . addListener (
new ChannelFutureListener ( ) {
@Override
public void operationComplete ( ChannelFuture future) throws Exception {
if ( ! future. isSuccess ( ) ) {
Throwable cause = future. cause ( ) ;
}
}
} ) ;
这个示例只适用于文件内容的直接传输,不包括应用程序对数据的任何处理。在需要将数据从文件系统复制到用户内存中时,可以使用 ChunkedWriteHandler,它支持异步写大型数据流,而又不会导致大量的内存消耗。
关键是 interface ChunkedInput,其中类型参数 B 是 readChunk()方法返回的类型。Netty 预置了该接口的 4 个实现,如下表中所列出的。每个都代表了一个将由 Chunked WriteHandler 处理的不定长度的数据流。
ChunkedInput 的实现
ChunkedFile 从文件中逐块获取数据,当你的平台不支持零拷贝或者你需要转换数据时使用
ChunkedNioFile 和 ChunkedFile 类似,只是它使用了 FileChannel
ChunkedStream 从 InputStream 中逐块传输内容
ChunkedNioStream 从 ReadableByteChannel 中逐块传输内容
序列化数据
JDK 提供了 ObjectOutputStream 和 ObjectInputStream,用于通过网络对 POJO 的基本数据类型和图进行序列化和反序列化。该 API 并不复杂,而且可以被应用于任何实现了java.io.Serializable 接口的对象。但是它的性能也不是非常高效的。
JDK 序列化
如果你的应用程序必须要和使用了ObjectOutputStream和ObjectInputStream的远程节点交互,并且兼容性也是你最关心的,那么JDK序列化将是正确的选择下表中列出了Netty提供的用于和JDK进行互操作的序列化类。
CompatibleObjectDecoder和使用 JDK 序列化的非基于 Netty 的远程节点进行互操作的解码器
CompatibleObjectEncoder 和使用 JDK 序列化的非基于 Netty 的远程节点进行互操作的编码器
ObjectDecoder 构建于 JDK 序列化之上的使用自定义的序列化来解码的解码器;当没有其他的外部依赖时,它提供了速度上的改进。否则其他的序列化实现更加可取
ObjectEncoder 构建于 JDK 序列化之上的使用自定义的序列化来编码的编码器;当没有其他的外部依赖时,它提供了速度上的改进。否则其他的序列化实现更加可取
WebSocket
添加 WebSocket 支持
在从标准的HTTP或者HTTPS协议切换到WebSocket时,将会使用一种称为升级握手的机制。因此,使用WebSocket的应用程序将始终以HTTP/S作为开始,然后再执行升级。这个升级动作发生的确切时刻特定于应用程序;它可能会发生在启动时,也可能会发生在请求了某个特定的URL之后。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N9aaM1aE-1667891439103)(/img/netty/websocket-logic.png)]
处理 HTTP 请求
首先,我们将实现该处理 HTTP 请求的组件。这个组件将提供用于访问聊天室并显示由连接的客户端发送的消息的网页。下面代码清单给出了这个 HttpRequestHandler 对应的代码,其扩展了 SimpleChannelInboundHandler 以处理 FullHttpRequest 消息。需要注意的是,channelRead0()方法的实现是如何转发任何目标 URI 为/ws 的请求的。
public class HttpRequestHandler extends SimpleChannelInboundHandler < FullHttpRequest > {
private final String wsUri;
private static final File INDEX;
static {
URL location = HttpRequestHandler . class
. getProtectionDomain ( )
. getCodeSource ( ) . getLocation ( ) ;
try {
String path = location. toURI ( ) + "index.html" ;
path = ! path. contains ( "file:" ) ? path : path. substring ( 5 ) ;
INDEX = new File ( path) ;
} catch ( URISyntaxException e) {
throw new IllegalStateException (
"Unable to locate index.html" , e) ;
}
}
public HttpRequestHandler ( String wsUri) {
this . wsUri = wsUri;
}
@Override
public void channelRead0 ( ChannelHandlerContext ctx,
FullHttpRequest request) throws Exception {
if ( wsUri. equalsIgnoreCase ( request. getUri ( ) ) ) {
ctx. fireChannelRead ( request. retain ( ) ) ;
} else {
if ( HttpHeaders . is100ContinueExpected ( request) ) {
send100Continue ( ctx) ;
}
RandomAccessFile file = new RandomAccessFile ( INDEX, "r" ) ;
HttpResponse response = new DefaultHttpResponse (
request. getProtocolVersion ( ) , HttpResponseStatus . OK) ;
response. headers ( ) . set (
HttpHeaders. Names . CONTENT_TYPE,
"text/plain; charset=UTF-8" ) ;
boolean keepAlive = HttpHeaders . isKeepAlive ( request) ;
if ( keepAlive) {
response. headers ( ) . set (
HttpHeaders. Names . CONTENT_LENGTH, file. length ( ) ) ;
response. headers ( ) . set ( HttpHeaders. Names . CONNECTION,
HttpHeaders. Values . KEEP_ALIVE) ;
}
ctx. write ( response) ;
if ( ctx. pipeline ( ) . get ( SslHandler . class ) == null ) {
ctx. write ( new DefaultFileRegion (
file. getChannel ( ) , 0 , file. length ( ) ) ) ;
} else {
ctx. write ( new ChunkedNioFile ( file. getChannel ( ) ) ) ;
}
ChannelFuture future = ctx. writeAndFlush (
LastHttpContent . EMPTY_LAST_CONTENT) ;
if ( ! keepAlive) {
future. addListener ( ChannelFutureListener . CLOSE) ;
}
}
}
private static void send100Continue ( ChannelHandlerContext ctx) {
FullHttpResponse response = new DefaultFullHttpResponse (
HttpVersion . HTTP_1_1, HttpResponseStatus . CONTINUE) ;
ctx. writeAndFlush ( response) ;
}
@Override
public void exceptionCaught ( ChannelHandlerContext ctx, Throwable cause)
throws Exception {
cause. printStackTrace ( ) ;
ctx. close ( ) ;
}
}
如果该 HTTP 请求指向了地址为/ws 的 URI,那么 HttpRequestHandler 将调用 FullHttpRequest 对象上的 retain()方法,并通过调用 fireChannelRead(msg)方法将它转发给下一个 ChannelInboundHandler 。之所以需要调用 retain()方法,是因为调用 channelRead()方法完成之后,它将调用 FullHttpRequest 对象上的 release()方法以释放它的资源。
如果客户端发送了 HTTP 1.1 的 HTTP 头信息 Expect: 100-continue,那么 HttpRequestHandler 将会发送一个 100 Continue 响应。在该 HTTP 头信息被设置之后,HttpRequestHandler 将会写回一个 HttpResponse 给客户端。这不是一个 FullHttpResponse,因为它只是响应的第一个部分。此外,这里也不会调用 writeAndFlush()方法,在结束的时候才会调用。
如果不需要加密和压缩,那么可以通过将 index.html 的内容存储到 DefaultFileRegion 中来达到最佳效率。这将会利用零拷贝特性来进行内容的传输。为此,你可以检查一下,是否有 SslHandler 存在于在 ChannelPipeline 中。否则,你可以使用 ChunkedNioFile。
HttpRequestHandler 将写一个 LastHttpContent 来标记响应的结束。如果没有请求 keep-alive ,那么 HttpRequestHandler 将会添加一个 ChannelFutureListener到最后一次写出动作的 ChannelFuture,并关闭该连接。在这里,你将调用 writeAndFlush()方法以冲刷所有之前写入的消息。
这部分代码代表了聊天服务器的第一个部分,它管理纯粹的 HTTP 请求和响应。接下来,我们将处理传输实际聊天消息的 WebSocket 帧。
:::tip WEBSOCKET 帧
WebSocket 以帧的方式传输数据,每一帧代表消息的一部分。一个完整的消息可能会包含许多帧
:::
处理 WebSocket 帧
WebSocketFrame 的类型
BinaryWebSocketFrame 包含了二进制数据
TextWebSocketFrame 包含了文本数据
ContinuationWebSocketFrame 包含属于上一个BinaryWebSocketFrame或TextWebSocketFrame 的文本数据或者二进制数据
CloseWebSocketFrame 表示一个 CLOSE 请求,包含一个关闭的状态码和关闭的原因
PingWebSocketFrame 请求传输一个 PongWebSocketFrame
PongWebSocketFrame 作为一个对于 PingWebSocketFrame 的响应被发送
我们的聊天应用程序将使用下面几种帧类型:
CloseWebSocketFrame
PingWebSocketFrame
PongWebSocketFrame
TextWebSocketFrame
TextWebSocketFrame 是我们唯一真正需要处理的帧类型。为了符合 WebSocket RFC,Netty 提供了 WebSocketServerProtocolHandler 来处理其他类型的帧。
下面代码展示了我们用于处理 TextWebSocketFrame 的 ChannelInboundHandler,其还将在它的 ChannelGroup 中跟踪所有活动的 WebSocket 连接:
public class TextWebSocketFrameHandler extends SimpleChannelInboundHandler < TextWebSocketFrame > {
private final ChannelGroup group;
public TextWebSocketFrameHandler ( ChannelGroup group) {
this . group = group;
}
@Override
public void userEventTriggered ( ChannelHandlerContext ctx, Object evt) throws Exception {
if ( evt == WebSocketServerProtocolHandler. ServerHandshakeStateEvent . HANDSHAKE_COMPLETE) {
ctx. pipeline ( ) . remove ( HttpRequestHandler . class ) ;
group. writeAndFlush ( new TextWebSocketFrame (
"Client " + ctx. channel ( ) + " joined" ) ) ;
group. add ( ctx. channel ( ) ) ;
} else {
super . userEventTriggered ( ctx, evt) ;
}
}
@Override
public void channelRead0 ( ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
group. writeAndFlush ( msg. retain ( ) ) ;
}
}
TextWebSocketFrameHandler 只有一组非常少量的责任。当和新客户端的 WebSocket握手成功完成之后 ,它将通过把通知消息写到 ChannelGroup 中的所有 Channel 来通知所有已经连接的客户端,然后它将把这个新 Channel 加入到该 ChannelGroup 中 。
如果接收到了 TextWebSocketFrame 消息 ,TextWebSocketFrameHandler 将调用TextWebSocketFrame 消息上的 retain()方法,并使用 writeAndFlush()方法来将它传输给 ChannelGroup,以便所有已经连接的 WebSocket Channel 都将接收到它。
和之前一样,对于 retain()方法的调用是必需的,因为当 channelRead0()方法返回时,TextWebSocketFrame 的引用计数将会被减少。由于所有的操作都是异步的,因此,writeAndFlush()方法可能会在 channelRead0()方法返回之后完成,而且它绝对不能访问一个已经失效的引用。
因为 Netty 在内部处理了大部分剩下的功能,所以现在剩下唯一需要做的事情就是为每个新创建的 Channel 初始化其 ChannelPipeline。为此,我们将需要一个 ChannelInitializer。
初始化 ChannelPipeline
public class ChatServerInitializer extends ChannelInitializer {
private final ChannelGroup group;
public ChatServerInitializer(ChannelGroup group) {
this.group = group;
}
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new ChunkedWriteHandler());
pipeline.addLast(new HttpObjectAggregator(64 * 1024));
pipeline.addLast(new HttpRequestHandler("/ws"));
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
pipeline.addLast(new TextWebSocketFrameHandler(group));
}
}
基于 WebSocket 聊天服务器的 ChannelHandler
HttpServerCodec 将字节解码为 HttpRequest、HttpContent 和 LastHttpContent。并将 HttpRequest、HttpContent 和 LastHttpContent 编码为字节
ChunkedWriteHandler 写入一个文件的内容HttpObjectAggregator 将一个 HttpMessage 和跟随它的多个 HttpContent 聚合为单个 FullHttpRequest 或者 FullHttpResponse(取决于它是被用来处理请求还是响应)。安装了这个之后,ChannelPipeline 中的下一个 ChannelHandler 将只会收到完整的 HTTP 请求或响应
HttpRequestHandler 处理 FullHttpRequest(那些不发送到/ws URI 的请求)
WebSocketServerProtocolHandler 按照 WebSocket 规范的要求,处理 WebSocket 升级握手、PingWebSocketFrame 、 PongWebSocketFrame 和CloseWebSocketFrame
TextWebSocketFrameHandler 处理 TextWebSocketFrame 和握手完成事件
Netty 的 WebSocketServerProtocolHandler 处理了所有委托管理的 WebSocket 帧类型以及升级握手本身。如果握手成功,那么所需的 ChannelHandler 将会被添加到 ChannelPipeline中,而那些不再需要的 ChannelHandler 则将会被移除。
WebSocket 协议升级之前的 ChannelPipeline 的状态图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uMKWQBeX-1667891439103)(/img/netty/ws-init-before.png)]
WebSocket 协议升级完成之后,WebSocketServerProtocolHandler 将会把 HttpRequestDecoder 替换为 WebSocketFrameDecoder,把 HttpResponseEncoder 替换为WebSocketFrameEncoder。为了性能最大化,它将移除任何不再被 WebSocket 连接所需要的ChannelHandler。这也包括了图 12-3 所示的 HttpObjectAggregator 和 HttpRequestHandler。
下图展示了这些操作完成之后的ChannelPipeline。需要注意的是,Netty目前支持 4个版本的WebSocket协议,它们每个都具有自己的实现类。Netty将会根据客户端(这里指浏览器)所支持的版本 ,自动地选择正确版本的WebSocketFrameDecoder和WebSocketFrameEncoder。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ud9FaAYa-1667891439103)(/img/netty/ws-init-after.png)]
引导
public class ChatServer {
private final ChannelGroup channelGroup =
new DefaultChannelGroup ( ImmediateEventExecutor . INSTANCE) ;
private final EventLoopGroup group = new NioEventLoopGroup ( ) ;
private Channel channel;
public ChannelFuture start ( InetSocketAddress address) {
ServerBootstrap bootstrap = new ServerBootstrap ( ) ;
bootstrap. group ( group)
. channel ( NioServerSocketChannel . class )
. childHandler ( createInitializer ( channelGroup) ) ;
ChannelFuture future = bootstrap. bind ( address) ;
future. syncUninterruptibly ( ) ;
channel = future. channel ( ) ;
return future;
}
protected ChannelInitializer < Channel > createInitializer (
ChannelGroup group) {
return new ChatServerInitializer ( group) ;
}
public void destroy ( ) {
if ( channel != null ) {
channel. close ( ) ;
}
channelGroup. close ( ) ;
group. shutdownGracefully ( ) ;
}
public static void main ( String [ ] args) throws Exception {
if ( args. length != 1 ) {
System . err. println ( "Please give port as argument" ) ;
System . exit ( 1 ) ;
}
int port = Integer . parseInt ( args[ 0 ] ) ;
final ChatServer endpoint = new ChatServer ( ) ;
ChannelFuture future = endpoint. start (
new InetSocketAddress ( port) ) ;
Runtime . getRuntime ( ) . addShutdownHook ( new Thread ( ) {
@Override
public void run ( ) {
endpoint. destroy ( ) ;
}
} ) ;
future. channel ( ) . closeFuture ( ) . syncUninterruptibly ( ) ;
}
}
UDP
面向连接的传输(如 TCP)管理了两个网络端点之间的连接的建立,在连接的生命周期内的有序和可靠的消息传输,以及最后,连接的有序终止。相比之下,在类似于 UDP 这样的无连接协议中,并没有持久化连接这样的概念,并且每个消息(一个 UDP 数据报)都是一个单独的传输单元。
此外,UDP 也没有 TCP 的纠错机制,其中每个节点都将确认它们所接收到的包,而没有被确认的包将会被发送方重新传输。
通过类比,TCP 连接就像打电话,其中一系列的有序消息将会在两个方向上流动。相反,UDP 则类似于往邮箱中投入一叠明信片。你无法知道它们将以何种顺序到达它们的目的地,或者它们是否所有的都能够到达它们的目的地。
UDP的这些方面可能会让你感觉到严重的局限性,但是它们也解释了为何它会比TCP快那么多:所有的握手以及消息管理机制的开销都已经被消除了。显然,UDP很适合那些能够处理或者容忍消息丢失的应用程序,但可能不适合那些处理金融交易的应用程序。
UDP 广播
到目前为止,我们所有的例子采用的都是一种叫作单播 的传输模式,定义为发送消息给一个由唯一的地址所标识的单一的网络目的地。面向连接的协议和无连接协议都支持这种模式。
UDP 提供了向多个接收者发送消息的额外传输模式:
多播——传输到一个预定义的主机组;
广播——传输到网络(或者子网)上的所有主机。
本章中的示例应用程序将通过发送能够被同一个网络中的所有主机所接收的消息来演示 UDP 广播的使用。为此,我们将使用特殊的受限广播地址或者零网络地址 255.255.255.255。
发送到这个地址的消息都将会被定向给本地网络(0.0.0.0)上的所有主机,而不会被路由器转发给其他的网络。
消息 POJO: LogEvent
public final class LogEvent {
public static final byte SEPARATOR = ( byte ) ':' ;
private final InetSocketAddress source;
private final String logfile;
private final String msg;
private final long received;
public LogEvent ( String logfile, String msg) {
this ( null , - 1 , logfile, msg) ;
}
public LogEvent ( InetSocketAddress source, long received,
String logfile, String msg) {
this . source = source;
this . logfile = logfile;
this . msg = msg;
this . received = received;
}
public InetSocketAddress getSource ( ) {
return source;
}
public String getLogfile ( ) {
return logfile;
}
public String getMsg ( ) {
return msg;
}
public long getReceivedTimestamp ( ) {
return received;
}
}
编写广播者
在广播者中使用的 Netty 的 UDP 相关类
interface AddressedEnvelope extends ReferenceCounted定义一个消息,其包装了另一个消息并带有发送者和接收者地址。其中 M 是消息类型;A 是地址类型
class DefaultAddressedEnvelope implements AddressedEnvelope提供了 interface AddressedEnvelope的默认实现
class DatagramPacket extends DefaultAddressedEnvelope implements ByteBufHolder扩展了 DefaultAddressedEnvelope 以使 用 ByteBuf 作为消息数据容器
interface DatagramChannel extends Channel扩展了 Netty 的 Channel 抽象以支持 UDP 的多播组管理
class NioDatagramChannnel extends AbstractNioMessageChannel implements DatagramChannel定义了一个能够发送和接收 AddressedEnvelope 消息的 Channel 类型
Netty 的 DatagramPacket 是一个简单的消息容器,DatagramChannel 实现用它来和远程节点通信。类似于在我们先前的类比中的明信片,它包含了接收者(和可选的发送者)的地址以及消息的有效负载本身。
要将 LogEvent 消息转换为 DatagramPacket,我们将需要一个编码器。但是没有必要从头开始编写我们自己的。我们将扩展 Netty 的 MessageToMessageEncoder。
public class LogEventEncoder extends MessageToMessageEncoder < LogEvent > {
private final InetSocketAddress remoteAddress;
public LogEventEncoder ( InetSocketAddress remoteAddress) {
this . remoteAddress = remoteAddress;
}
@Override
protected void encode ( ChannelHandlerContext channelHandlerContext,
LogEvent logEvent, List < Object > out) throws Exception {
byte [ ] file = logEvent. getLogfile ( ) . getBytes ( CharsetUtil . UTF_8) ;
byte [ ] msg = logEvent. getMsg ( ) . getBytes ( CharsetUtil . UTF_8) ;
ByteBuf buf = channelHandlerContext. alloc ( )
. buffer ( file. length + msg. length + 1 ) ;
buf. writeBytes ( file) ;
buf. writeByte ( LogEvent . SEPARATOR) ;
buf. writeBytes ( msg) ;
out. add ( new DatagramPacket ( buf, remoteAddress) ) ;
}
}
public class LogEventBroadcaster {
private final EventLoopGroup group;
private final Bootstrap bootstrap;
private final File file;
public LogEventBroadcaster ( InetSocketAddress address, File file) {
group = new NioEventLoopGroup ( ) ;
bootstrap = new Bootstrap ( ) ;
bootstrap. group ( group) . channel ( NioDatagramChannel . class )
. option ( ChannelOption . SO_BROADCAST, true )
. handler ( new LogEventEncoder ( address) ) ;
this . file = file;
}
public void run ( ) throws Exception {
Channel ch = bootstrap. bind ( 0 ) . sync ( ) . channel ( ) ;
long pointer = 0 ;
for ( ; ; ) {
long len = file. length ( ) ;
if ( len < pointer) {
pointer = len;
} else if ( len > pointer) {
RandomAccessFile raf = new RandomAccessFile ( file, "r" ) ;
raf. seek ( pointer) ;
String line;
while ( ( line = raf. readLine ( ) ) != null ) {
ch. writeAndFlush ( new LogEvent ( null , - 1 ,
file. getAbsolutePath ( ) , line) ) ;
}
pointer = raf. getFilePointer ( ) ;
raf. close ( ) ;
}
try {
Thread . sleep ( 1000 ) ;
} catch ( InterruptedException e) {
Thread . interrupted ( ) ;
break ;
}
}
}
public void stop ( ) {
group. shutdownGracefully ( ) ;
}
public static void main ( String [ ] args) throws Exception {
if ( args. length != 2 ) {
throw new IllegalArgumentException ( ) ;
}
LogEventBroadcaster broadcaster = new LogEventBroadcaster (
new InetSocketAddress ( "255.255.255.255" ,
Integer . parseInt ( args[ 0 ] ) ) , new File ( args[ 1 ] ) ) ;
try {
broadcaster. run ( ) ;
} finally {
broadcaster. stop ( ) ;
}
}
}
编写监视器
目的是把netcat 替换为一个更加完整的事件消费者,我们称之为 LogEventMonitor。
这个程序将:
接收由 LogEventBroadcaster 广播的 UDP DatagramPacket;
将它们解码为 LogEvent 消息;
将 LogEvent 消息写出到 System.out。
和之前一样,该逻辑由一组自定义的 ChannelHandler 实现,我们将扩展 MessageToMessageDecoder。下图描绘了 LogEventMonitor 的 ChannelPipeline,并且展示了 LogEvent 是如何流经它的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JlrBiLiM-1667891439104)(/img/netty/log-monitor.png)]
public class LogEventDecoder extends MessageToMessageDecoder < DatagramPacket > {
@Override
protected void decode ( ChannelHandlerContext ctx,
DatagramPacket datagramPacket, List < Object > out) throws Exception {
ByteBuf data = datagramPacket. content ( ) ;
int idx = data. indexOf ( 0 , data. readableBytes ( ) ,
LogEvent . SEPARATOR) ;
String filename = data. slice ( 0 , idx)
. toString ( CharsetUtil . UTF_8) ;
String logMsg = data. slice ( idx + 1 ,
data. readableBytes ( ) ) . toString ( CharsetUtil . UTF_8) ;
LogEvent event = new LogEvent ( datagramPacket. sender ( ) ,
System . currentTimeMillis ( ) , filename, logMsg) ;
out. add ( event) ;
}
}
第二个 ChannelHandler 的工作是对第一个 ChannelHandler 所创建的 LogEvent 消息执行一些处理。在这个场景下,它只是简单地将它们写出到 System.out。在真实世界的应用程序中,你可能需要聚合来源于不同日志文件的事件,或者将它们发布到数据库中。下面代码展示了 LogEventHandler,其说明了需要遵循的基本步骤。
public class LogEventHandler
extends SimpleChannelInboundHandler < LogEvent > {
@Override
public void exceptionCaught ( ChannelHandlerContext ctx,
Throwable cause) throws Exception {
cause. printStackTrace ( ) ;
ctx. close ( ) ;
}
@Override
public void channelRead0 ( ChannelHandlerContext ctx,
LogEvent event) throws Exception {
StringBuilder builder = new StringBuilder ( ) ;
builder. append ( event. getReceivedTimestamp ( ) ) ;
builder. append ( " [" ) ;
builder. append ( event. getSource ( ) . toString ( ) ) ;
builder. append ( "] [" ) ;
builder. append ( event. getLogfile ( ) ) ;
builder. append ( "] : " ) ;
builder. append ( event. getMsg ( ) ) ;
System . out. println ( builder. toString ( ) ) ;
}
}
LogEventHandler 将以一种简单易读的格式打印 LogEvent 消息,包括以下的各项
以毫秒为单位的被接收的时间戳;
发送方的 InetSocketAddress,其由 IP 地址和端口组成;
生成 LogEvent 消息的日志文件的绝对路径名;
实际上的日志消息,其代表日志文件中的一行。
public class LogEventMonitor {
private final EventLoopGroup group;
private final Bootstrap bootstrap;
public LogEventMonitor ( InetSocketAddress address) {
group = new NioEventLoopGroup ( ) ;
bootstrap = new Bootstrap ( ) ;
bootstrap. group ( group)
. channel ( NioDatagramChannel . class )
. option ( ChannelOption . SO_BROADCAST, true )
. handler ( new ChannelInitializer < Channel > ( ) {
@Override
protected void initChannel ( Channel channel)
throws Exception {
ChannelPipeline pipeline = channel. pipeline ( ) ;
pipeline. addLast ( new LogEventDecoder ( ) ) ;
pipeline. addLast ( new LogEventHandler ( ) ) ;
}
} )
. localAddress ( address) ;
}
public Channel bind ( ) {
return bootstrap. bind ( ) . syncUninterruptibly ( ) . channel ( ) ;
}
public void stop ( ) {
group. shutdownGracefully ( ) ;
}
public static void main ( String [ ] main) throws Exception {
if ( args. length != 1 ) {
throw new IllegalArgumentException (
"Usage: LogEventMonitor " ) ;
}
LogEventMonitor monitor = new LogEventMonitor (
new InetSocketAddress ( Integer . parseInt ( args[ 0 ] ) ) ) ;
try {
Channel channel = monitor. bind ( ) ;
System . out. println ( "LogEventMonitor running" ) ;
channel. closeFuture ( ) . sync ( ) ;
} finally {
monitor. stop ( ) ;
}
}
}
声明
该文章摘抄于何品译的书籍《Netty实战》,仅作为个人的重点笔记和分享使用。
你可能感兴趣的:(spring,boot,spring,java)
SpringBoot整合WebSocket服务
码云217
spring boot websocket 后端
1、前言2、开始使用2.1、加入Maven依赖2.2(推荐方式)使用WebSocketConfigurer方式创建WebSocket端点(二选一)2.2.1创建一个测试WebSocket处理器2.2.2开启WebSocket并且注册WebSocket处理器2.3使用ServerEndpoint方式创建WebSocket端点(二选一)2.3.1创建一个测试WebSocket处理器2.3.2开启Web
JavaScript 类型转换的意外
神明木佑
javascript 开发语言 ecmascript
在JavaScript中,类型转换是将一个数据类型转换为另一个数据类型的过程。它可以是显式的,即通过使用特定的转换函数或操作符来实现,也可以是隐式的,即由JavaScript引擎自动完成。以下是JavaScript中的一些常见类型转换规则:字符串转换:使用String()函数或toString()方法可以将其他类型的值转换为字符串类型。varnum=42;varstr=String(num);//
【Kafka专栏 12】实时数据流与任务队列的较量 :Kafka与RabbitMQ有什么不同
夏之以寒
夏之以寒-kafka专栏 kafka rabbitmq 数据流 任务队列
作者名称:夏之以寒作者简介:专注于Java和大数据领域,致力于探索技术的边界,分享前沿的实践和洞见文章专栏:夏之以寒-kafka专栏专栏介绍:本专栏旨在以浅显易懂的方式介绍Kafka的基本概念、核心组件和使用场景,一步步构建起消息队列和流处理的知识体系,无论是对分布式系统感兴趣,还是准备在大数据领域迈出第一步,本专栏都提供所需的一切资源、指导,以及相关面试题,立刻免费订阅,开启Kafka学习之旅!
java设计模式单件模式_Head First设计模式(5):单件模式
weixin_39822493
java设计模式单件模式
更多的可以参考我的博客,也在陆续更新inghttp://www.hspweb.cn/单件模式确保一个类只有一个实例,并提供一个全局访点。例子:学生的学号生成方案,是在学生注册后,通过录入学生的基本信息,包括入学学年、学院、专业、班级等信息后,保存相应的资料后自动生成的。学号生成器的业务算法为:入学学年(2位)+学院代码(2位)+专业代码(2位)+班级代码(2位)+序号(2位)1.目录image2.
基于Java+Spring+vue+element实现旅游信息管理平台系统
网顺技术团队
成品程序项目 java spring vue.js spring boot 课程设计
基于Java+Spring+vue+element实现旅游信息管理平台系统作者主页网顺技术团队欢迎点赞收藏⭐留言文末获取源码联系方式查看下方微信号获取联系方式承接各种定制系统精彩系列推荐精彩专栏推荐订阅不然下次找不到哟Java毕设项目精品实战案例《1000套》感兴趣的可以先收藏起来,还有大家在毕设选题,项目以及论文编写等相关问题都可以给我留言咨询,希望帮助更多的人文章目录基于Java+Spring
JavaScript之BOM编程
qq_39095899
前端知识入门 javascript
BOM编程什么是BOM?BrowerObjectModel(浏览器对象模型,)关闭浏览器窗口、打开一个新的浏览器窗口、后退、前进、浏览器地址栏上的地址等,都是BOM编程BOM和DOM的区别与联系?BOM的顶级对象是:windowDOM的顶级对象是:document实际上BOM是包括DOM的!1、BOM编程中,window对象是顶级对象,代表浏览器窗口2、window有open和close方法,可以
JVM直接内存详解
fengdongnan
jvm 开发语言 java
直接内存学习JVM内存结构部分时遇到的最后一部分,直接内存。虽然和其他堆栈等不是核心部分,但其类似缓存的特点和与GC相关的特性显得有点特殊,比较好奇这个高速缓存有没有实际开发使用场景,所以写这篇博客记录直接内存的相关知识点与使用场景。概念直接内存(DirectMemory)是操作系统内存和Java内存共用的一片内存区域读写性能高,常见于NIO操作作为数据缓存区可以通过ByteBuffer.allo
Vue+Jest 单元测试
arron4210
前端 vue 单元测试 vue
新到一个公司,要求单元测试覆盖率达50%以上,我们都是后补的单测,其实单测的意义是根据需求提前写好,驱动开发,代替手动测试。然鹅这只是理想。。。这里总结一下各种遇到的单测场景挂载组件,调用elementui,mock函数```javascriptdescribe('页面验证',()=>{constwrapper=getVue({component:onlineFixedPrice,callback
聊聊这两年学习slam啃过的书!
3D视觉工坊
3D视觉从入门到精通 定位 编程语言 人工智能 机器学习 slam
入坑2年多,零七零八买了7、8本书,正好最近研一的新师弟让我来推荐几本,那么,独乐乐不如众乐乐,我就来巴拉巴拉一下我买的这些书吧。以下测评,仅代表个人观点,与书的作者无关(狗头保命)1、【C++PrimerPlus】嗯~这个灰常灰常厚的c++书是我买的第一本书,也是我所有书里除了java最厚的一本(java买了就没看),But,这本巨厚的c++我竟然翻完了!!!当年,年轻的我以为,看完这本书,我就
盘点时下最流行的十大编程语言优缺点,附2024年5月最新的编程语言排行榜单
嵌入式软件测试开发
IT杂谈 python 开发语言 c语言 c++ c# java javascript
文章目录前言一、Python二、C三、C++四、Java五、C#六、JavaScript七、VisualBasic八、Go九、SQL十、Fortran总结前言TIOBE公布了2024年5月最新的编程语言排行榜,本次的亮点是Fortran这个编程界的元老级语言,竟然在沉寂20多年后,再次闯入榜单的Top10。前10名分别是Python、C、C++、Java、C#、JavaScript、VisualB
【HeadFirst系列之HeadFirst设计模式】第5天之工厂模式:比萨店的秘密武器,轻松搞定对象创建!
工一木子
HeadFirst系列 HeadFirst设计模式 笔记 设计模式 工厂模式
工厂模式:比萨店的秘密武器,轻松搞定对象创建!大家好,今天我们来聊聊设计模式中的工厂模式。如果你曾经为对象的创建感到头疼,或者觉得代码中到处都是new关键字,那么工厂模式就是你的救星!本文基于《HeadFirst设计模式》的工厂模式章节,带你从比萨店的故事中轻松掌握工厂模式的精髓,附上Java代码示例,让你彻底理解并爱上它!1.简单工厂模式:比萨店的起步故事背景小明开了一家比萨店,刚开始只有两种比
HtML之JavaScript BOM编程
录大大i
前端 HTML JavaScript javascript html 前端
HtML之JavaScriptBOM编程windowhistory历史location地址栏document浏览器打开的.html文档consoleF12开发者工具的控制台screen屏幕navigator浏览器软件本身(历史原因一直沿用)sessionStorage会话级存储localStorage持久级存储window对象APIwindow对象的属性APIhistory窗口的访问历史locat
结构型-代理模式(Proxy Pattern)
babstyt
设计模式 代理模式 java 设计模式 后端
什么是代理模式由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。Java中的代理按照代理类生成时机不同又分为静态代理和动态代理。静态代理代理类在编译期就生成,而动态代理代理类则是在Java运行时动态生成。动态代理分为JDK代理和CGLib代理两种。结构抽象主题(Subject)类:通过接口或抽象类声明真
【YashanDB 知识库】kettle 同步大表提示 java 内存溢出
数据库
【问题分类】数据导入导出【关键字】数据同步,kettle,数据迁移,java内存溢出【问题描述】kettle同步大表提示ERROR:couldnotcreatethejavavirtualmachine!【问题原因分析】java内存溢出【解决/规避方法】①增加JVM的堆内存大小。编辑Spoon.bat,增加堆大小到2GB,如:if"%PENTAHO_DI_JAVA_OPTIONS%"==""set
SpringBoot 版本 与 Spring 与JDK 版本 与 maven 兼容问题
wei198621
maven_gradle Spring maven spring boot spring
转自大牛https://blog.csdn.net/wangqing84411433/article/details/90634603SpringBoot版本SpringFrameworkjdk版本maven版本1.2.0版本之前63.01.2.04.1.3+63.2+1.2.14.1.3+73.2+1.2.34.1.5+73.2+1.3.44.2.6+73.2+1.3.64.2.7+73.2+1
JavaScript&ES6----数组去重的多种方法
方法一---双层for循环利用双层for循环,前一个循环前一项,后一个循环后一项,两两比对,如果发现重复的就用splice()属性,把重复的元素从数组arr中删除letarr=[2,5,1,5,3,2,'hello','1',4]letunique=(arr)=>{//第一层for循环循环数组前一项for(i=0;i{//声明一个新数组letnewArr=[];for(i=0;i{//声明一个新数
基于Selenium实现简单的任务流程操作
一个有女朋友的程序员
小工具 selenium java 责任链模式
文章目录概要技术介绍SeleniumWebDriverManager开始编码先导入对应的依赖初始化WebDriver建立流程链创建抽象节点初始化流程使用Selenium小结概要前段时间同事让我帮他老婆写一个可以自动操作浏览器办理业务的小程序,一开始是想着在网上找一找有没有的RPA软件(公司里用过金智维RPA,感觉自己比较熟悉),但是莫得找到,就只能自己用Java试一试了这里我选择Selenium来
Java文件加密
听风说起雨
android java
一、意义加密Java文件的主要目的是增加代码的安全性和保护知识产权。下面是一些加密Java文件的意义:防止代码泄露:加密Java文件可以减少源代码被非法访问、盗取或泄露的风险。特别是在开发商或个人希望保护其知识产权和商业机密时,加密可以有效防止未授权的访问。提高代码安全性:加密Java文件可以增加对恶意攻击的抵抗能力。通过加密,攻击者将难以理解和修改源代码,使得他们难以发现漏洞和进行攻击。防止反编
美团一面:说说synchronized的实现原理?
K&&K
面试 java
在Java中,synchronized是用于实现线程同步的关键字,其底层实现原理涉及对象头、监视器锁(Monitor)以及锁升级机制。以下是详细解析:1.对象头与MarkWord每个Java对象在内存中由三部分组成:对象头(Header):存储对象的元数据,包括锁状态、GC分代年龄等。实例数据(InstanceData):对象的成员变量。对齐填充(Padding):确保对象内存对齐。MarkWor
String...和String[]区别
码农张3
Java基础 java
publicstaticStringget(Stringurl,String...params){}类型后面三个点(String…),是从Java5开始,Java语言对方法参数支持一种新写法,叫可变长度参数列表,其语法就是类型后跟…,表示此处接受的参数为0到多个Object类型的对象,或者是一个Object[]。例如我们有一个方法叫做test(String…strings),那么你还可以写方法te
RSA加密解密
码农张3
Java基础 java 后端
packagecom.coder.common.utils.security;importorg.apache.commons.codec.binary.Base64;importjavax.crypto.Cipher;importjava.security.*;importjava.security.interfaces.RSAPrivateKey;importjava.security.int
40个JS常用使用技巧案例
javascript
大家好,我是V哥。在日常开发中,我们经常会使用JS解决页面的交互,在JS使用过程V哥总结了40个小技巧,分享给大家,废话不多说,马上开干。先赞再看后评论,腰缠万贯财进门。JS常用技巧案例以下是40个常用的JavaScript使用技巧,包含案例代码和解释:1.数组去重constarr=[1,2,2,3,4,4,5];constuniqueArr=[...newSet(arr)];console.lo
Vue中虚拟DOM的全面解析
七公子77
vue vue.js 前端 javascript
一、虚拟DOM的核心概念虚拟DOM(VirtualDOM)是一个轻量级的JavaScript对象,它是对真实DOM的抽象表示。在Vue中,组件模板会被编译成虚拟DOM树,通过Diff算法对比新旧虚拟DOM,计算出最小化的DOM操作,最终批量更新真实DOM。二、为什么需要虚拟DOM?1.直接操作DOM的问题性能瓶颈:DOM操作是浏览器中最昂贵的操作之一,频繁操作会导致性能下降。手动优化困难:开发者需
基于JAVA的象棋游戏的设计与实现
Python数据分析与机器学习
算法设计 java 青少年编程 c 开发语言 游戏 数据结构 算法
目录摘要第1章绪论1.1研究意义1.2研究目标第2章系统分析2.1相关技术和理论2.1.1开发环境2.1.2Java介绍2.1.3VSCode介绍2.2需求分析第3章设计与实现3.1程序流程图设计3.2游戏设计3.3棋盘棋子实现3.3.1基本数据结构——位棋盘3.3.2位棋盘的作用及初始化3.4功能实现3.4.1悔棋功能3.4.2认输3.5走棋和吃子规则实现3.6平台网络链接研究及实现第4章平台测
Spring Boot项目Jar包加密详解
一休哥助手
java spring boot jar
目录引言Jar包加密的基础知识为什么需要加密Jar包Jar包加密的基本原理常用的Jar包加密工具ProGuardJavaguardJavaAgent
如何使用 Java 读取本地文件并转换为 MultipartFile 对象
火皇405
java 开发语言 tomcat spring spring boot
在许多JavaWeb应用中,我们经常会遇到将本地文件上传至服务器或其他系统的需求。在这种场景下,MultipartFile对象非常常用,用来表示HTTP请求中的文件。在本文中,我将演示如何编写代码来读取本地文件并将其转换为自定义的MultipartFile对象。1.基本需求为了将本地文件读取并转换为MultipartFile,我们需要完成以下任务:读取文件的内容和类型。构建MultipartFil
JAVA:享元模式(Flyweight Pattern)的技术指南
拾荒的小海螺
设计模式 java 享元模式 开发语言
1、简述享元模式(FlyweightPattern)是一种结构型设计模式,旨在通过共享对象来减少内存的使用和对象的创建,从而提高程序的性能。设计模式样例:https://gitee.com/lhdxhl/design-pattern-example.git2、什么是享元模式享元模式通过共享对象来支持大量细粒度对象的复用,避免对象的大量创建。它通过将可共享的状态外部化,把内在状态封装到享元对象中,从
阿里云盘资源分享-java300集
cylar-gg
资源分享 Java java
01.java300集阿里云盘分享提取码:0r8v点击链接保存,或者复制本段内容,打开「阿里云盘」APP,无需下载极速在线查看,视频原画倍速播放。阿里云盘分享https://www.aliyundrive.com/s/KAHfhBS2x88https://www.aliyundrive.com/s/KAHfhBS2x88
使用RabbitMQ实现异步消息处理与解耦:Spring Boot整合实践
MarkerHub
java-rabbitmq rabbitmq spring boot 分布式 后端
前后端微服务商城项目,手把手教学!在现代应用架构中,异步消息处理和解耦是提高系统性能、扩展性和维护性的关键技术手段。RabbitMQ作为一种流行的消息队列中间件,提供了可靠、高效、灵活的消息传递功能,广泛应用于分布式系统和微服务架构中。本文将带你走进SpringBoot与RabbitMQ的整合实践,帮助你实现异步消息处理和系统解耦。通过这一过程,你将能够轻松地实现消息传递的异步化,提升系统的响应速
vue中子组件和子组件之间怎么通信_Vue.js中父子组件之间通信方法实例详解
weixin_39774445
Vue.js中父子组件之间通信方法实例详解发布于2020-11-27|复制链接摘记:本文实例讲述了vuejs中父子组件之间通信方法。分享给大家供大家参考,具体如下:一、父组件向子组件传递消息```javascript//Parent.vue..本文实例讲述了vuejs中父子组件之间通信方法。分享给大家供大家参考,具体如下:一、父组件向子组件传递消息```javascript//Parent.vue
jdk tomcat 环境变量配置
Array_06
java jdk tomcat
Win7 下如何配置java环境变量
1。准备jdk包,win7系统,tomcat安装包(均上网下载即可)
2。进行对jdk的安装,尽量为默认路径(但要记住啊!!以防以后配置用。。。)
3。分别配置高级环境变量。
电脑-->右击属性-->高级环境变量-->环境变量。
分别配置 :
path
&nbs
Spring调SDK包报java.lang.NoSuchFieldError错误
bijian1013
java spring
在工作中调另一个系统的SDK包,出现如下java.lang.NoSuchFieldError错误。
org.springframework.web.util.NestedServletException: Handler processing failed; nested exception is java.l
LeetCode[位运算] - #136 数组中的单一数
Cwind
java 题解 位运算 LeetCode Algorithm
原题链接:#136 Single Number
要求:
给定一个整型数组,其中除了一个元素之外,每个元素都出现两次。找出这个元素
注意:算法的时间复杂度应为O(n),最好不使用额外的内存空间
难度:中等
分析:
题目限定了线性的时间复杂度,同时不使用额外的空间,即要求只遍历数组一遍得出结果。由于异或运算 n XOR n = 0, n XOR 0 = n,故将数组中的每个元素进
qq登陆界面开发
15700786134
qq
今天我们来开发一个qq登陆界面,首先写一个界面程序,一个界面首先是一个Frame对象,即是一个窗体。然后在这个窗体上放置其他组件。代码如下:
public class First { public void initul(){ jf=ne
Linux的程序包管理器RPM
被触发
linux
在早期我们使用源代码的方式来安装软件时,都需要先把源程序代码编译成可执行的二进制安装程序,然后进行安装。这就意味着每次安装软件都需要经过预处理-->编译-->汇编-->链接-->生成安装文件--> 安装,这个复杂而艰辛的过程。为简化安装步骤,便于广大用户的安装部署程序,程序提供商就在特定的系统上面编译好相关程序的安装文件并进行打包,提供给大家下载,我们只需要根据自己的
socket通信遇到EOFException
肆无忌惮_
EOFException
java.io.EOFException
at java.io.ObjectInputStream$PeekInputStream.readFully(ObjectInputStream.java:2281)
at java.io.ObjectInputStream$BlockDataInputStream.readShort(ObjectInputStream.java:
基于spring的web项目定时操作
知了ing
java Web
废话不多说,直接上代码,很简单 配置一下项目启动就行
1,web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="h
树形结构的数据库表Schema设计
矮蛋蛋
schema
原文地址:
http://blog.csdn.net/MONKEY_D_MENG/article/details/6647488
程序设计过程中,我们常常用树形结构来表征某些数据的关联关系,如企业上下级部门、栏目结构、商品分类等等,通常而言,这些树状结构需要借助于数据库完成持久化。然而目前的各种基于关系的数据库,都是以二维表的形式记录存储数据信息,
maven将jar包和源码一起打包到本地仓库
alleni123
maven
http://stackoverflow.com/questions/4031987/how-to-upload-sources-to-local-maven-repository
<project>
...
<build>
<plugins>
<plugin>
<groupI
java IO操作 与 File 获取文件或文件夹的大小,可读,等属性!!!
百合不是茶
类 File
File是指文件和目录路径名的抽象表示形式。
1,何为文件:
标准文件(txt doc mp3...)
目录文件(文件夹)
虚拟内存文件
2,File类中有可以创建文件的 createNewFile()方法,在创建新文件的时候需要try{} catch(){}因为可能会抛出异常;也有可以判断文件是否是一个标准文件的方法isFile();这些防抖都
Spring注入有继承关系的类(2)
bijian1013
java spring
被注入类的父类有相应的属性,Spring可以直接注入相应的属性,如下所例:1.AClass类
package com.bijian.spring.test4;
public class AClass {
private String a;
private String b;
public String getA() {
retu
30岁转型期你能否成为成功人士
bijian1013
成长 励志
很多人由于年轻时走了弯路,到了30岁一事无成,这样的例子大有人在。但同样也有一些人,整个职业生涯都发展得很优秀,到了30岁已经成为职场的精英阶层。由于做猎头的原因,我们接触很多30岁左右的经理人,发现他们在职业发展道路上往往有很多致命的问题。在30岁之前,他们的职业生涯表现很优秀,但从30岁到40岁这一段,很多人
【Velocity四】Velocity与Java互操作
bit1129
velocity
Velocity出现的目的用于简化基于MVC的web应用开发,用于替代JSP标签技术,那么Velocity如何访问Java代码.本篇继续以Velocity三http://bit1129.iteye.com/blog/2106142中的例子为基础,
POJO
package com.tom.servlets;
public
【Hive十一】Hive数据倾斜优化
bit1129
hive
什么是Hive数据倾斜问题
操作:join,group by,count distinct
现象:任务进度长时间维持在99%(或100%),查看任务监控页面,发现只有少量(1个或几个)reduce子任务未完成;查看未完成的子任务,可以看到本地读写数据量积累非常大,通常超过10GB可以认定为发生数据倾斜。
原因:key分布不均匀
倾斜度衡量:平均记录数超过50w且
在nginx中集成lua脚本:添加自定义Http头,封IP等
ronin47
nginx lua csrf
Lua是一个可以嵌入到Nginx配置文件中的动态脚本语言,从而可以在Nginx请求处理的任何阶段执行各种Lua代码。刚开始我们只是用Lua 把请求路由到后端服务器,但是它对我们架构的作用超出了我们的预期。下面就讲讲我们所做的工作。 强制搜索引擎只索引mixlr.com
Google把子域名当作完全独立的网站,我们不希望爬虫抓取子域名的页面,降低我们的Page rank。
location /{
java-3.求子数组的最大和
bylijinnan
java
package beautyOfCoding;
public class MaxSubArraySum {
/**
* 3.求子数组的最大和
题目描述:
输入一个整形数组,数组里有正数也有负数。
数组中连续的一个或多个整数组成一个子数组,每个子数组都有一个和。
求所有子数组的和的最大值。要求时间复杂度为O(n)。
例如输入的数组为1, -2, 3, 10, -4,
Netty源码学习-FileRegion
bylijinnan
java netty
今天看org.jboss.netty.example.http.file.HttpStaticFileServerHandler.java
可以直接往channel里面写入一个FileRegion对象,而不需要相应的encoder:
//pipeline(没有诸如“FileRegionEncoder”的handler):
public ChannelPipeline ge
使用ZeroClipboard解决跨浏览器复制到剪贴板的问题
cngolon
跨浏览器 复制到粘贴板 Zero Clipboard
Zero Clipboard的实现原理
Zero Clipboard 利用透明的Flash让其漂浮在复制按钮之上,这样其实点击的不是按钮而是 Flash ,这样将需要的内容传入Flash,再通过Flash的复制功能把传入的内容复制到剪贴板。
Zero Clipboard的安装方法
首先需要下载 Zero Clipboard的压缩包,解压后把文件夹中两个文件:ZeroClipboard.js
单例模式
cuishikuan
单例模式
第一种(懒汉,线程不安全):
public class Singleton { 2 private static Singleton instance; 3 pri
spring+websocket的使用
dalan_123
一、spring配置文件
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.or
细节问题:ZEROFILL的用法范围。
dcj3sjt126com
mysql
1、zerofill把月份中的一位数字比如1,2,3等加前导0
mysql> CREATE TABLE t1 (year YEAR(4), month INT(2) UNSIGNED ZEROFILL, -> day
Android开发10——Activity的跳转与传值
dcj3sjt126com
Android开发
Activity跳转与传值,主要是通过Intent类,Intent的作用是激活组件和附带数据。
一、Activity跳转
方法一Intent intent = new Intent(A.this, B.class); startActivity(intent)
方法二Intent intent = new Intent();intent.setCla
jdbc 得到表结构、主键
eksliang
jdbc 得到表结构、主键
转自博客:http://blog.csdn.net/ocean1010/article/details/7266042
假设有个con DatabaseMetaData dbmd = con.getMetaData(); rs = dbmd.getColumns(con.getCatalog(), schema, tableName, null); rs.getSt
Android 应用程序开关GPS
gqdy365
android
要在应用程序中操作GPS开关需要权限:
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
但在配置文件中添加此权限之后会报错,无法再eclipse里面正常编译,怎么办?
1、方法一:将项目放到Android源码中编译;
2、方法二:网上有人说cl
Windows上调试MapReduce
zhiquanliu
mapreduce
1.下载hadoop2x-eclipse-plugin https://github.com/winghc/hadoop2x-eclipse-plugin.git 把 hadoop2.6.0-eclipse-plugin.jar 放到eclipse plugin 目录中。 2.下载 hadoop2.6_x64_.zip http://dl.iteye.com/topics/download/d2b
如何看待一些知名博客推广软文的行为?
justjavac
博客
本文来自我在知乎上的一个回答:http://www.zhihu.com/question/23431810/answer/24588621
互联网上的两种典型心态:
当初求种像条狗,如今撸完嫌人丑
当初搜贴像条犬,如今读完嫌人软
你为啥感觉不舒服呢?
难道非得要作者把自己的劳动成果免费给你用,你才舒服?
就如同 Google 关闭了 Gooled Reader,那是
sql优化总结
macroli
sql
为了是自己对sql优化有更好的原则性,在这里做一下总结,个人原则如有不对请多多指教。谢谢!
要知道一个简单的sql语句执行效率,就要有查看方式,一遍更好的进行优化。
一、简单的统计语句执行时间
declare @d datetime ---定义一个datetime的变量set @d=getdate() ---获取查询语句开始前的时间select user_id
Linux Oracle中常遇到的一些问题及命令总结
超声波
oracle linux
1.linux更改主机名
(1)#hostname oracledb 临时修改主机名
(2) vi /etc/sysconfig/network 修改hostname
(3) vi /etc/hosts 修改IP对应的主机名
2.linux重启oracle实例及监听的各种方法
(注意操作的顺序应该是先监听,后数据库实例)
&nbs
hive函数大全及使用示例
superlxw1234
hadoop hive函数
具体说明及示例参 见附件文档。
文档目录:
目录
一、关系运算: 4
1. 等值比较: = 4
2. 不等值比较: <> 4
3. 小于比较: < 4
4. 小于等于比较: <= 4
5. 大于比较: > 5
6. 大于等于比较: >= 5
7. 空值判断: IS NULL 5
Spring 4.2新特性-使用@Order调整配置类加载顺序
wiselyman
spring 4
4.1 @Order
Spring 4.2 利用@Order控制配置类的加载顺序
4.2 演示
两个演示bean
package com.wisely.spring4_2.order;
public class Demo1Service {
}
package com.wisely.spring4_2.order;
public class