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)
vue中子组件和子组件之间怎么通信_Vue.js中父子组件之间通信方法实例详解
weixin_39774445
Vue.js中父子组件之间通信方法实例详解发布于2020-11-27|复制链接摘记:本文实例讲述了vuejs中父子组件之间通信方法。分享给大家供大家参考,具体如下:一、父组件向子组件传递消息```javascript//Parent.vue..本文实例讲述了vuejs中父子组件之间通信方法。分享给大家供大家参考,具体如下:一、父组件向子组件传递消息```javascript//Parent.vue
SQL语句建表范例分析(1)
梅子专栏
数据库 数据库 mysql sql
建表语句如下:首先应该有个数据库:createdatabaseboot1;useboot1;即在选中了库的情况下,执行下面的语句,创建出以下表:auth_operationauth_roleauth_role_operationauth_useri_articlei_filei_log表关系如图所示:/*NavicatMySQLDataTransferSourceServer:127.0.0.1S
大学生HTML期末大作业——HTML+CSS+JavaScript学校网站(成都大学)
无·糖
Web前端期末大作业 html 课程设计 css 大学生 前端 javascript 大学生大作业
HTML+CSS+JS【学校网站】网页设计期末课程大作业web前端开发技术web课程设计网页规划与设计文章目录一、网站题目二、网站描述三、网站介绍四、网站效果五、️网站代码六、️如何学习进步七、☠️更多干货文章目录一、网站题目学校网站(成都大学)6页含JQ二、网站描述总结了一些学生网页制作的经验:一般的网页需要融入以下知识点:div+css布局、浮动、定位、高级css、表格、表单及验证、js轮
js数据类型全解析,怎么区分呢?
IT木昜
大白话前端面试题 javascript 前端
在JavaScript里,数据类型就像是不同类型的“小盒子”,每个“小盒子”装的数据都有自己的特点,区分它们能帮助我们在编程时正确处理数据。下面用大白话给你讲讲常见的数据类型以及区分方法,还会配上代码示例。基本数据类型数字(number):就是我们平常说的各种数字,像整数1、2、3,小数3.14等都属于这个类型。在JavaScript里,它还能表示特殊值,比如NaN(表示不是一个数字,像0除以0的
js作用域和闭包,原理与用途?
IT木昜
大白话前端面试题 javascript 开发语言 ecmascript
js作用域和闭包,原理与用途作用域原理作用域可以理解为变量和函数的“活动范围”。在JavaScript里,变量和函数并不是在任何地方都能被访问的,它们只能在特定的范围内起作用,这个范围就是作用域。JavaScript中有全局作用域和函数作用域,ES6还引入了块级作用域。全局作用域:全局作用域是最外层的作用域,在全局作用域中声明的变量和函数可以在代码的任何地方被访问。就好像整个城市是一个全局作用域,
js原型和原型链,到底咋回事?
IT木昜
大白话前端面试题 javascript 前端
js原型和原型链,到底咋回事?js原型的原理在JavaScript里,每个对象都有一个“隐藏的小伙伴”,这个“小伙伴”就是原型。可以把原型想象成一个模板或者一个仓库,对象能从它这个“小伙伴”那里借用一些属性和方法。当你访问一个对象的某个属性或方法时,JavaScript会先在这个对象本身找,如果没找到,就会去它的原型里找。代码示例//定义一个构造函数functionPerson(name){thi
Java部署机器学习模型:方案二(基于DJL)
iiilloi
机器学习 spring spring boot
DJL(DeepJavaLibrary)是由亚马逊公司开发的一款开源的深度学习框架,它旨在为Java开发人员提供一个简单而强大的API,使得在Java中使用深度学习变得更加容易。DJL有以下几个方面优势:支持多个底层引擎DJL支持多个底层引擎,包括MXNet、TensorFlow和PyTorch等。这使得DJL可以在多个平台上使用,包括Java、Android、iOS和RaspberryPi等。易
【YashanDB 知识库】虚拟机重启后启动 YMP 报错
数据库
【标题】虚拟机重启后启动YMP报错【关键字】YMP启动报错【问题描述】使用ymp.sh启动ymp时,报8093端口错误【问题原因分析】8093端口为yasom使用端口,说明yasom进程未启动。【解决/规避方法】启动yasom进程和yasagent进程yasbootprocessyasomstart-cympyasbootprocessyasagentstart-cymp如果报yasboot无法识
专栏简介:从入门到精通 JavaScript 1000例实战开发
小蘑菇二号
入门到精通 JavaScript 1000例实战开发 JavaScript 开发语言
目录专栏简介:从入门到精通JavaScript1000例实战开发专栏特色目标受众学习收获专栏目录:从入门到精通JavaScript1000例实战开发第一部分:JavaScript基础篇第二部分:JavaScript核心进阶篇第三部分:前端框架与库篇第四部分:高级实战篇第五部分:前沿技术篇附录专栏简介:从入门到精通JavaScript1000例实战开发本专栏旨在为开发者提供一套系统化的学习路径,帮助
Java 日期时间格式化标准
骑个小蜗牛
Java java
文章目录Java日期时间格式化符号ISO8601中的日期时间ISO8601标准的定义ISO8601日期时间格式周数年份ISO8601中的周数年份Java中的周数年份Java跨年日期格式化BUG注意事项Java日期时间格式化符号JDK官网截图:格式化符号梳理:符号描述符号用法示例G公历纪元G:公历纪元AD(公元)、BC(公元前)y年份yy:两位年份yyyy:四位年份242024Y年份(ISO周数所在
2后端JAVA:下载数据库数据到EXCEL表格?代码
simplesin
bug处理 数据库 java excel
这是运用kimi,cursor写出来的后端基础增删改查代码心得系列。将从如何用cursor写出代码,修改cursor写出来的代码,连接数据库,上传下载代码等等方面展开。第一集:后端JAVA:Cursor与kimi如何结合?Cursor写出的代码出现哪些bug?-CSDN博客下载代码:/***下载数据库数据*总情况:将数据库中的合同信息导出为Excel文件*@paramresponseHTTP响应对
AI时代Java 项目中生成 gRPC 接口文档,smart-doc 仍然是你的最佳选择!
javagrpc
前言在现代Java项目开发中,尤其是基于微服务架构的系统,gRPC已成为一种流行的远程过程调用(RPC)框架。它通过高效的二进制协议和多语言支持,极大地简化了服务间通信。然而,随着项目的复杂度增加,维护gRPC接口文档变得越来越困难。尽管目前有许多AI工具可以帮助生成代码文档,但在Java项目中生成gRPC接口文档时,smart-doc仍然是最优解。为什么这么说?我们将在本文中详细探讨。smart
EJB构件
Wlq0415
系统架构 系统架构
EJB(EnterpriseJavaBeans)构件是Java平台企业版(JavaEE)中用于构建分布式企业级应用程序的核心组件。EJB提供了一种简化的方式,使得开发者可以创建可移植的、安全的、可伸缩的、事务性的、并发的组件,这些组件可以运行在支持JavaEE的服务器上。EJB构件主要可以分为以下三类:会话Bean(SessionBean):会话Bean用于实现业务逻辑,可以分为有状态会话Bean
Spring全面详解:架构体系演变及其入门(学习总结)
小夕Coding
大数据系列 spring java 大数据 javabean spring boot
文章目录架构体系演变背景单一应用架构垂直应用架构分布式服务架构流动计算架构入门配置文件yaml语法快速入门基本格式要求对象数组常量一些特殊符号使用yaml进行配置注解学习反射获取注解配置文件优先级存放目录Application属性文件,按优先级排序,位置高的将覆盖位置读取顺序web开发嵌入式Servlet容器支持扫描Servlet,过滤器和listeners1、springboot整合servle
KindEditor - 支持word上传的富文本编辑器
M_Snow
javascript vue.js html5
1.4.2之后官方并没有做功能的改动,1.4.2在word复制这块没有bug,其他版本会出现手动无法转存的情况本文使用的后台是Java。前端为Jsp(前端都一样,后台如果语言不通得自己做Base64编码解码)因为公司业务需要支持IE8,网上其实有很多富文本框,效果都很好。例如www.wangEditor.com但试了一圈都不支持IE8。所以回到Ueditor,由于官方没有维护,新的neuditor
java架构师面试核心问题
wespten
Spring全家桶 微信小程序 Java全栈开发 java架构师面试核心问题
java架构师面试核心问题源码与视屏讲解同步,但都不是最重要的(具体实现)最重要的是快速过书,然后回退一遍画圈,重思想一些特别的实现只要是源码能发布就发布。视屏讲解里的和源码配合发布,不是源码的不发布。第一部分:java的高质量编码与性能优化详解对象包装器与自动装箱对象包装器类是不可变的,一旦构造了包装器对象,包含在包装器中的内容不会改变。因为Java方法都是值传递的,所以不能使用这些包装器类创建
Maven在Eclipse中的使用指南
froginwe11
开发语言
Maven在Eclipse中的使用指南引言Maven是一个强大的项目管理和构建自动化工具,它能够简化Java项目的构建过程。Eclipse则是一款流行的集成开发环境(IDE),广泛应用于Java开发。本文将详细介绍如何在Eclipse中配置和使用Maven,帮助您更高效地管理Java项目。Maven概述Maven的核心概念Maven使用项目对象模型(ProjectObjectModel,简称POM
KindEditor 实现ctrl+v粘贴图片并上传、word粘贴带图片
Mr_Zang666
word图片一键粘贴 word servlet java
这种方法是servlet,编写好在web.xml里配置servlet-class和servlet-mapping即可使用后台(服务端)java服务代码:(上传至ROOT/lqxcPics文件夹下)配置web.xmlindex.jsp前端(页面)测试代码:WordPaster-jsp-ueditor-1.2.6.0这里写你的初始化内容varpasterMgr=newWordPasterManager
Java-自动拆箱/装箱/缓存/效率/String和包装类相互转换
我荔枝呢!
java 开发语言
为什么基本类型需要包装类?泛型与集合支持问题:基本数据类型在使用上虽然方便、简单且高效,但像泛型以及集合元素的存储等场景并不支持基本数据类型,而包装类可以解决这个问题,使其能更好地融入到一些需要对象类型的机制中。面向对象思维:基本数据类型不符合面向对象编程的思维方式,包装类把基本数据类型“包装”成了对象形式,更契合面向对象的编程模式。提供实用方法:包装类提供了很多实用的方法,例如Integer类中
@ApiModel
weixin_51687565
python 开发语言
@ApiModel是Swagger(现在通常被称为OpenAPI)中的一个注解,用于在RESTfulWeb服务中描述API的模型。Swagger允许开发人员为RESTfulWeb服务生成、描述、调用和可视化API文档。通过使用Swagger的注解(如@ApiModel),开发人员可以自动地生成这些文档,而无需手动编写和维护它们。@ApiModel注解通常用于Java类的顶部,这些类代表API响应或
在Qt6 QML中集成JavaScript
Quz
QML javascript qt
目录简述1.JavaScript类型2.QML对JavaScript类型的扩展2.1提供全局对象和辅助方法2.2支持动态对象创建2.3增强的导入机制2.4严格的作用域和类型检查2.5与QML属性和信号的深度集成2.6支持异步编程2.7类型转换和数据绑定3.JavaScript资源导入3.1内联JavaScript3.2外部JavaScript文件4.JavaScript表达式和属性绑定总结简述Qt
java八股文之Redis
qq_45923849
java redis 开发语言
1.Rdis常见的使用场景缓存分布式锁(redision,setnx)计数器保存token消息队列延迟队列2.说明一下缓存雪崩,缓存穿透和缓存击穿以及解决方式1.缓存雪崩定义:缓存雪崩指的是当大量的缓存数据同时失效,或者Redis服务器突然宕机,导致后端数据库突然承受大量请求的压力,从而可能导致数据库崩溃的情况。解决:1.分散缓存过期时间:给缓存设置不同的过期时间,避免同时失效。2.使用Rdis集
SpringBoot自动配置过程中的注册配置类的源码
一个儒雅随和的男子
spring spring boot 后端 java
SpringBoot的自动配置是其核心特性之一,通过条件化配置和约定优于配置的设计思想,自动加载和注册所需的配置类。以下从源码角度详细解析自动配置过程中注册配置类的关键步骤:一、自动配置的核心流程启动类标注@SpringBootApplication:该注解组合了@EnableAutoConfiguration,触发自动配置逻辑。加载spring.factories文件:从所有依赖的META-IN
java http远程调用接口下载文件
《小书生》
java杂谈 java
远程调用http接口下载文件,接口返回流一、将文件保存本地publicStringhttpDownload(StringhttpUrl){try{URLurl=newURL(httpUrl);//filePath文件地址,fileName文件名Filefile=newFile(filePath,fileName);FileUtils.copyURLToFile(url,file);}catch(I
SpringBoot3.0.3集成Redisson3.20.0
青阳科技
SpringBoot3.0.3 spring boot redis 分布式
一、概述使用SpringBoot构建分布式应用程序时,常常需要实现分布式锁、分布式限流、分布式任务调度和分布式缓存等功能,这些功能都可以通过Redis来实现。而Redisson是一个优秀的Redis客户端,它提供了丰富的分布式功能,可以帮助我们更方便地实现这些功能。那么,SpringBoot集成Redisson后我们可以实现哪些功能?1.分布式锁在分布式系统中,多个节点可能会同时对同一个资源进行修
springboot整合redisson实现分布式锁及布隆过滤器
lncy1991
工作记录 redis 过滤器 spring
下载安装redisson整合maven添加依赖使用的版本是3.13,因为springboot使用的是2.1.3版本,因此按照官方上的提示,移除redisson-spring-data-22org.springframework.bootspring-boot-starter-data-redisorg.redissonredisson-spring-boot-starterorg.redisson
Spring Boot ShardingJDBC分库分表(草稿)
dashalen
SpringBoot spring boot 数据库 后端
ShardingJDBC分库分表1.Maven引用org.apache.shardingspheresharding-jdbc-spring-boot-starter4.1.1org.springframework.bootspring-boot-starter-data-jpamysqlmysql-connector-java2.数据库和表格数据库*****_ch*****_hk*****_us
JavaSE基础知识点记录 08章 面向对象编程(高级)
老CCC
Java SE基础 java 开发语言
目录8-1static修饰成员变量与方法8-2单例的设计模式1、饿汉式2、懒汉式8-3main()的理解8-4类的成员之四:代码块1、静态代码块2、非静态代码块8-5实例变量赋值位置与赋值顺序8-6final关键字的使用8-7abstract关键字修饰类、方法8-8接口的理解与基本语法接口的实现8-9jdk8,jdk9中接口的新特性8-10类的成员之五:内部类1、成员内部类2、局部内部类8-11枚
Web后端 Tomcat服务器
小安同学iter
Web后端 前端 tomcat 服务器
一TomcatWeb服务器介绍:Tomcat是一个开源的JavaServlet容器和Web服务器,由Apache软件基金会开发。它实现了JavaServlet和JavaServerPages(JSP)技术,用于运行JavaWeb应用程序。Tomcat轻量、易于配置,常作为开发和部署JavaWeb应用的首选服务器。1Web服务器对HTTP协议操作进行封装,简化了web程序的开发。部署web项目,对外
Web 后端 请求与响应
小安同学iter
Web后端 maven java tomcat intellij-idea
一请求响应1.请求(Request)客户端向服务器发送的HTTP请求,通常包含以下内容:请求行:HTTP方法(GET/POST等)、请求的URL、协议版本。请求头(Headers):客户端信息(如浏览器类型、支持的语言)、Cookie、内容类型等。请求体(Body):POST/PUT请求时携带的数据(如表单参数、JSON)。在JavaWeb中的处理:Servlet通过HttpServletRequ
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