Asynchronous by design
Callbacks
Callbacks are a technique often used with asynchronous processing. A callback is passed to the
method and executes after the method completes.
Sometimes using futures can feel ugly because you need to check the state of the Future in intervals to see if it
is completed yet, whereas with a callback you re notified directly after it s done.
A ByteBuffer can
either be allocated on the heap or directly, which means it s stored outside of the Heap-
Space. Usually, using a direct buffer is faster when passing it to the channel, but the
allocation/deallocation costs are higher.
A channel represents a connection to an entity capable of performing IO operations such as
a file or a socket.
A selector is a NIO component that determines if one or more channels are ready for
reading and/or writing, thus a single select selector can be used to handle multiple
connections, alleviating the need for the thread-per-connection model.
3. When channels are registered, you call the Selector.select() method to block until
one of these events occurs.
4. When the method unblocks, you can obtain all of the SelectionKey instances (which
hold the reference to the registered channel and to selected Ops) and do something.
NIO.2 handles threading and the creation of the so-called event loop for you.
Squashing the famous epoll bug
while (true) {
int selected = selector.select(); #1
Set<SelectedKeys> readyKeys = selector.selectedKeys();
Iterator iterator = readyKeys.iterator(); #2
while (iterator.hasNext()) { #3
...
... #4
}
}
http://my.oschina.net/flashsword/blog/162936
Callbacks
Callbacks are a technique often used with asynchronous processing. A callback is passed to the
method and executes after the method completes.
Sometimes using futures can feel ugly because you need to check the state of the Future in intervals to see if it
is completed yet, whereas with a callback you re notified directly after it s done.
A ByteBuffer can
either be allocated on the heap or directly, which means it s stored outside of the Heap-
Space. Usually, using a direct buffer is faster when passing it to the channel, but the
allocation/deallocation costs are higher.
A channel represents a connection to an entity capable of performing IO operations such as
a file or a socket.
A selector is a NIO component that determines if one or more channels are ready for
reading and/or writing, thus a single select selector can be used to handle multiple
connections, alleviating the need for the thread-per-connection model.
3. When channels are registered, you call the Selector.select() method to block until
one of these events occurs.
4. When the method unblocks, you can obtain all of the SelectionKey instances (which
hold the reference to the registered channel and to selected Ops) and do something.
NIO.2 handles threading and the creation of the so-called event loop for you.
Squashing the famous epoll bug
while (true) {
int selected = selector.select(); #1
Set<SelectedKeys> readyKeys = selector.selectedKeys();
Iterator iterator = readyKeys.iterator(); #2
while (iterator.hasNext()) { #3
...
... #4
}
}
http://my.oschina.net/flashsword/blog/162936
不使用netty,直接基于NIO编写网络程序,你需要做什么(以Server端TCP连接为例,这里我们使用Reactor模型):
- 监听端口,建立Socket连接
- 建立线程,处理内容
- 读取Socket内容,并对协议进行解析
- 进行逻辑处理
- 回写响应内容
- 如果是多次交互的应用(SMTP、FTP),则需要保持连接多进行几次交互
- 关闭连接
建立线程是一个比较耗时的操作,同时维护线程本身也有一些开销,所以我们会需要多线程机制,幸好JDK已经有很方便的多线程框架了,这里我们不需要花很多心思。
此外,因为TCP连接的特性,我们还要使用连接池来进行管理:
- 建立TCP连接是比较耗时的操作,对于频繁的通讯,保持连接效果更好
- 对于并发请求,可能需要建立多个连接
- 维护多个连接后,每次通讯,需要选择某一可用连接
- 连接超时和关闭机制
想想就觉得很复杂了!实际上,基于NIO直接实现这部分东西,即使是老手也容易出现错误,而使用Netty之后,你只需要关注逻辑处理部分就可以了。
这里我们引用Netty的example包里的一个例子,一个简单的EchoServer,它接受客户端输入,并将输入原样返回。其主要代码如下:
public void run() {
// Configure the server.
ServerBootstrap bootstrap = new ServerBootstrap(
new NioServerSocketChannelFactory(
Executors.newCachedThreadPool(),
Executors.newCachedThreadPool()));
// Set up the pipeline factory.
bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
public ChannelPipeline getPipeline() throws Exception {
return Channels.pipeline(new EchoServerHandler());
}
});
// Bind and start to accept incoming connections.
bootstrap.bind(new InetSocketAddress(port));
}
这里
EchoServerHandler
是其业务逻辑的实现者,大致代码如下:
public class EchoServerHandler extends SimpleChannelUpstreamHandler {
@Override
public void messageReceived(
ChannelHandlerContext ctx, MessageEvent e) {
// Send back the received message to the remote peer.
e.getChannel().write(e.getMessage());
}
}
抓住几个重点对象,理解其领域概念及设计思想,从而理清其脉络,相当于打通了任督二脉,以后的阅读就不再困难了。
理解Netty的关键点在哪呢?我觉得,除了NIO的相关知识,另一个就是事件驱动的设计思想。什么叫事件驱动?我们回头看看
EchoServerHandler
的代码,其中的参数:public void messageReceived(ChannelHandlerContext ctx, MessageEvent e)
,MessageEvent就是一个事件。这个事件携带了一些信息,例如这里e.getMessage()
就是消息的内容,而EchoServerHandler
则描述了处理这种事件的方式。一旦某个事件触发,相应的Handler则会被调用,并进行处理。这种事件机制在UI编程里广泛应用,而Netty则将其应用到了网络编程领域。
在Netty里,所有事件都来自
ChannelEvent
接口,这些事件涵盖监听端口、建立连接、读写数据等网络通讯的各个阶段。而事件的处理者就是ChannelHandler
,这样,不但是业务逻辑,连网络通讯流程中底层的处理,都可以通过实现ChannelHandler
来完成了。事实上,Netty内部的连接处理、协议编解码、超时等机制,都是通过handler完成的。当博主弄明白其中的奥妙时,不得不佩服这种设计!
下图描述了Netty进行事件处理的流程。
Channel
是连接的通道,是ChannelEvent的产生者,而ChannelPipeline
可以理解为ChannelHandler的集合。合:开启Netty源码之门
理解了Netty的事件驱动机制,我们现在可以来研究Netty的各个模块了。Netty的包结构如下:
org
└── jboss
└── netty
├── bootstrap 配置并启动服务的类
├── buffer 缓冲相关类,对NIO Buffer做了一些封装
├── channel 核心部分,处理连接
├── container 连接其他容器的代码
├── example 使用示例
├── handler 基于handler的扩展部分,实现协议编解码等附加功能
├── logging 日志
└── util 工具类
在这里面,
channel
和handler
两部分比较复杂。我们不妨与Netty官方的结构图对照一下,来了解其功能。
具体的解释可以看这里:http://netty.io/3.7/guide/#architecture。图中可以看到,除了之前说到的事件驱动机制之外,Netty的核心功能还包括两部分:
- Zero-Copy-Capable Rich Byte Buffer零拷贝的Buffer。为什么叫零拷贝?因为在数据传输时,最终处理的数据会需要对单个传输层的报文,进行组合或者拆分。NIO原生的ByteBuffer要做到这件事,需要对ByteBuffer内容进行拷贝,产生新的ByteBuffer,而Netty通过提供Composite(组合)和Slice(切分)两种Buffer来实现零拷贝。这部分代码在
org.jboss.netty.buffer
包中。 - Universal Communication API统一的通讯API。因为Java的Old I/O和New I/O,使用了互不兼容的API,而Netty则提供了统一的API(
org.jboss.netty.channel.Channel
)来封装这两种I/O模型。这部分代码在org.jboss.netty.channel
包中。
此外,Protocol Support功能通过handler机制实现。
http://my.oschina.net/flashsword/blog/164237
buffer中文名又叫缓冲区,按照维基百科的解释,是"在数据传输时,在内存里开辟的一块临时保存数据的区域”。它其实是一种化同步为异步的机制,可以解决数据传输的速率不对等以及不稳定的问题。
根据这个定义,我们可以知道涉及I/O(特别是I/O写)的地方,基本会有buffer的存在。就Java来说,我们非常熟悉的Old I/O–
InputStream
&OutputStream
系列API,基本都是在内部使用到了buffer。Java课程老师就教过,必须调用OutputStream.flush()
,才能保证数据写入生效!
而NIO中则直接将buffer这个概念封装成了对象,其中最常用的大概是ByteBuffer了。于是使用方式变为了:将数据写入Buffer,flip()一下,然后将数据读出来。于是,buffer的概念更加深入人心了!
When & Where: TCP/IP协议与buffer
TCP/IP协议是目前的主流网络协议。它是一个多层协议,最下层是物理层,最上层是应用层(HTTP协议等),而在Java开发中,一般只接触TCP以上,即传输层和应用层的内容。这也是Netty的主要应用场景。
TCP报文有个比较大的特点,就是它传输的时候,会先把应用层的数据项拆开成字节,然后按照自己的传输需要,选择合适数量的字节进行传输。什么叫"自己的传输需要”?首先TCP包有最大长度限制,那么太大的数据项肯定是要拆开的。其次因为TCP以及下层协议会附加一些协议头信息,如果数据项太小,那么可能报文大部分都是没有价值的头信息,这样传输是很不划算的。因此有了收集一定数量的小数据,并打包传输的Nagle算法(这个东东在HTTP协议里会很讨厌,Netty里可以用setOption(“tcpNoDelay”, true)关掉它)。