Netty知识汇总
这篇主要是阅读Netty In Action这本书的一些有关Netty的知识点汇总
历史
阻塞io
一个线程用来处理一个连接,连接的创建和操作连接都是阻塞的
使用方法
try {
// 监听本地的8000端口
ServerSocket serverSocket = new ServerSocket(8000);
// 阻塞等待连接
Socket clientSocket = serverSocket.accept();
BufferedReader inputStream = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter writer = new PrintWriter(clientSocket.getOutputStream(), true);
String request;
// 阻塞等待输入
while((request = inputStream.readLine()) != null) {
if ("Done".equals(request)) {
break;
}
System.out.println(request);
}
} catch (IOException e) {
e.printStackTrace();
}
缺点
- 同时只能处理一个请求,如果需要处理多个请求,需要创建多个线程
- 大量的线程处于等待连接或者等待输入的状态,造成线程资源浪费
- 每个线程都的调用栈都需要分配内存,一方面物理机的内存限制了线程数量,另一方面,大量线程之间的线程切换带来大量开销
NIO
NIO使用一个Selector来管理多个Socket,每个Socket注册感兴趣的事件,通过事件通知的方式来确定哪些Socket可以进行处理
因此可以用一个线程来管理多个Socket
NIO有新旧两个版本,旧的版本是在jdk4中引入的,新的版本是在jdk7中引入的
使用
老的NIO
ServerSocketChannel serverChannel = ServerSocketChannel.open();
ServerSocket ss = serverChannel.socket();
InetSocketAddress address = new InetSocketAddress(8000);
ss.bind(address);
serverChannel.configureBlocking(false);
Selector selector = Selector.open();
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
try {
selector.select();
} catch (IOException ex) {
ex.printStackTrace();
// handle in a proper way
break;
}
Set readyKeys = selector.selectedKeys();
Iterator iterator = readyKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = (SelectionKey) iterator.next();
iterator.remove();
try {
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel)
key.channel();
SocketChannel client = server.accept();
System.out.println("Accepted connection from " +
client);
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_WRITE
SelectionKey.OP_READ, ByteBuffer.allocate(100));
}
if (key.isReadable()) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer output = (ByteBuffer) key.attachment();
client.read(output);
}
if (key.isWritable()) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer output = (ByteBuffer) key.attachment();
output.flip();
client.write(output);
output.compact();
}
} catch (IOException ex) {
key.cancel();
try {
key.channel().close();
} catch (IOException cex) {
}
}
}
}
新的NIO
在新的NIO中引入了CompletionHandler,当操作完成时,对应的CompletionHandler会被调用
新的NIO相较老的NIO来说,最大的不同是通过使用CompletionHandler来注册回调,不需要检查当前哪些事件准备好了
当事件准备好了,对应的CompletionHandler会自动调用
public void nioTest() throws Exception{
final AsynchronousServerSocketChannel serverChannel =
AsynchronousServerSocketChannel.open();
InetSocketAddress address = new InetSocketAddress(8000);
serverChannel.bind(address);
final CountDownLatch latch = new CountDownLatch(1);
serverChannel.accept(null, new
CompletionHandler<AsynchronousSocketChannel, Object>() {
@Override
public void completed(final AsynchronousSocketChannel channel,
Object attachment) {serverChannel.accept(null, this);
ByteBuffer buffer = ByteBuffer.allocate(100);
channel.read(buffer, buffer,
new EchoCompletionHandler(channel));
}
@Override
public void failed(Throwable throwable, Object attachment) {
try {
serverChannel.close();
} catch (IOException e) {
// ingnore on close
} finally {
latch.countDown();
}
}
});
try {
latch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private final class EchoCompletionHandler implements
CompletionHandler<Integer, ByteBuffer> {
private final AsynchronousSocketChannel channel;
EchoCompletionHandler(AsynchronousSocketChannel channel) {
this.channel = channel;
}
@Override
public void completed(Integer result, ByteBuffer buffer) {
buffer.flip();
channel.write(buffer, buffer, new CompletionHandler<Integer,
ByteBuffer>() { #6
@Override
public void completed(Integer result, ByteBuffer buffer) {
if (buffer.hasRemaining()) {
channel.write(buffer, buffer, this); #7
} else {
buffer.compact();
channel.read(buffer, buffer,
EchoCompletionHandler.this); #8
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
try {
channel.close();
} catch (IOException e) {
// ingnore on close
}
}
});
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
try {
channel.close();
} catch (IOException e) {
// ingnore on close
}
}
}
优点
- 一个线程管理多个Socket,避免创建大量线程,减少了内存的占用以及线程切换的开销
- 当没有io操作时,线程可以做其他工作
缺点
- 不容易使用
- 跨平台性较差,相同的代码可能在linux上运行正常,但是在windows上运行异常
- NIO2.0只在jdk7及以上版本支持
- NIO中的数据容器ByteBuffer不支持拓展
- linux上的epoll bug会导致Selector空循环,从而导致cpu利用率100%
Netty
使用
服务端
public class EchoServer {
private final int port;
public EchoServer(int port) {
this.port = port;
}
public void start() throws Exception{
EchoServerHandler handle = new EchoServerHandler();
// 指定如何处理事件,比如连接的建立,发送数据,接收数据
EventLoopGroup group = new NioEventLoopGroup();
try {
// 使用Bootstrap来配置服务端
ServerBootstrap b = new ServerBootstrap();
b.group(group)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
// 这里有两种写法,一种是当前这种,那么多个channel会共享这个handle
// 如果使用当前这种写法,Handler上面必须使用@Sharable注解,否则多个连接会报错
socketChannel.pipeline().addLast(handle);
// 如果每个channel使用自己的handler,不和其他channel共享handler,那么使用下面这种写法
//socketChannel.pipeline().addLast(new EchoServerHandler());
}
});
// 阻塞等待绑定成功
ChannelFuture f = b.bind(port).sync();
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully().sync();
}
}
/**
* 服务端业务逻辑,对输入数据进行处理
* 将接收到的数据,写回客户端
*/
@Sharable
class EchoServerHandler extends ChannelInboundHandlerAdapter {
private AtomicInteger count = new AtomicInteger(0);
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("server channel active");
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println(count);
count.addAndGet(1);
ByteBuf buf = (ByteBuf) msg;
System.out.println(String.format("server receivecd:%s", buf.toString(CharsetUtil.UTF_8)));
// 将接收到的数据写回客户端,此时数据并没有flush
ctx.writeAndFlush(Unpooled.copiedBuffer("server received", CharsetUtil.UTF_8));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println(String.format("exception caught:%s", cause));
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
// flush之前写的数据
// 并且在完成之后关闭连接
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
.addListener(ChannelFutureListener.CLOSE);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("server channel inactive");
}
}
public static void main(String[] args) throws Exception{
new EchoServer(8000).start();
}
}
客户端
public class EchoClient {
private final String host;
private final int port;
public EchoClient(String host, int port) {
this.host = host;
this.port = port;
}
public void start() throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
// 配置客户端
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new EchoClientHandler());
}
});
// 连接服务器,ChannelFuture代表连接结果
ChannelFuture f = b.connect(host, port).sync();
// 连接结果注册回调,当连接建立成功或者失败时会执行回调
f.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (future.isSuccess()) {
System.out.println("Connection established");
} else {
System.out.println("Connection failed");
}
}
});
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully().sync();
}
}
class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("channel active");
// 当连接建立时,向服务端发送消息
String sendMessage = "netty rocks";
ctx.writeAndFlush(Unpooled.copiedBuffer("netty rocks", CharsetUtil.UTF_8));
//ctx.writeAndFlush("netty rocks");
}
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception {
// 接收到的信息可能会分批到达,因此一次调用入参中的数据可能只属于一次发送信息的一部分
System.out.println("client received" +byteBuf.toString(CharsetUtil.UTF_8));
}
}
public static void main(String[] args) throws Exception{
new EchoClient("localhost", 8000).start();
}
}
核心组件
Bootstrap
Bootstrap用来对服务端或者客户端进行配置,比如指定EventLoopGroup,Channel,Handler等
Handler
当指定的事件触发时,Handler就会被调用,Handler中主要是我们的业务逻辑
ChannelInitializer
ChannelInitializer用来添加Handler
EventLoop
EventLoop用来处理io操作,一个EventLoop用来处理多个channel
EventLoopGroup中包含多个EventLoop
Channle
Channel一般情况下就对应一个socket连接,但是也可以对应任何具有io操作的事物
ChannelFuture
不管是建立连接,还是读写数据,在netty中都是异步执行的
当执行了某些操作之后,这些操作并不会立即执行,会在之后的某个时间点执行
一些场景下需要知道操作是否完成,因此netty提供了ChannelFuture
ChannelFuture是一个特殊的Future,可以在其上注册一些listener,从而当操作执行完成时,执行这些回调
原理
EventLoop ChannelThread的关系
当一个新的连接建立请求到来时,会从EventLoopGroup中选择一个EventLoop,针对这个连接创建一个Channel,注册到这EventLoop上
一方面,EventLoop和一个线程绑定;另一方面,该Channel的所有事件的处理都交给这个EventLoop
所以我们在开发业务逻辑的时候,并不需要进行同步
Client Bootstrap和Server Bootstrap
不管是在客户端还是服务端使用netty,都需要使用Bootstrap来进行配置
主要有以下两个不同
- 客户端的Bootstrap指定连接到哪个地址的哪个端口;服务端的Bootstrap则指定监听本地的哪个端口
- 客户端的Bootstrap使用一个EventLoopGroup;服务端的Bootstrap使用两个EventLoopGroup,其中一个负责处理连接建立请求,针对建立好的请求,创建一个Channel注册到另外一个EventLoopGroup上
服务端之所以使用两个EventLoopGroup,是因为如果使用一个group同时处理连接建立和其他事件,当有大量的连接正在进行处理时,线程都被这些任务所占用,导致无法接收新的连接,从而导致连接超时
Transport
Netty提供了很多内置的Transport实现
这里重点看下NIO和OIO
NIO
OIO
对比OIO和NIO
吞吐量
吞吐量方面,OIO要好于NIO
原因是NIO的实现机制会导致其处理事件会有一定的延迟,当事件发生时,需要花费一定的时间来通知selector
并发度
并发度方面,NIO要好于OIO
因为在NIO中一个线程对应一个selector,一个selector可以管理多个连接;而在OIO中,一个线程对应一个连接
因此在相同的物理资源条件下,NIO可以处理比OIO更多的连接
文件传输
文件传输方面,比如用来实现ftp服务器,NIO要好于OIO
因为NIO有零拷贝,如果不需要对文件进行操作,只是将文件返回给客户端,NIO会直接将文件从文件系统写入到网络堆栈中,从而避免了将数据从内核空间拷贝到用户空间的过程
不同场景下的选择
Buffer
Buffer类型
因为jdk中的ByteBuffer不易使用,所以netty设计了自己的buffer,用来存储数据
常见的buffer有以下几个类型:
- Heap Buffer:将数据存储在jvm的堆内存中。(1)分配空间和回收空间速度更快(2)可以直接访问底层存放数据的数组
- Direct Buffer:将数据存储在堆外内存中。(1)当需要使用的数据需要通过socket来进行传输时,Direct Buffer是比较好的选择,因为如果不使用direct buffer,jvm会在发送数据之前将数据拷贝到一个direct buffer(2)分配和回收空间代价比较大,但是池化可以解决这个问题(3)因为数据并没有在jvm堆中,因此不能直接访问底层存放数据的数组
- Composite Buffer:混合Buffer,可以包含多种类型的Buffer。(1)可以向其中添加多个Buffer,在此之上提供统一的视图(2)因为底层的buffer有多种类型,因此不能直接返回数组
操作
Buffer一共提供了两种方式来访问存储的字节:随机访问和顺序访问
read和get write和set
在ByteBuf中存在两类方法来读取数据:read和get,同样也存在两类方法类写数据:write和set
区别如下:
- read读取完数据之后,会增加readerIndex,而get操作并不会修改readerIndex
- write在写入数据之后,会增加writerIndex,而set操作并不会修改writerIndex
随机访问
ByteBuf buffer = ...;
for (int i = 0;i < buffer.capacity();i++) {
byte b = buffer.getByte(i);
System.out.println(b);
}
顺序访问
Buffer提供了两个指针来表示当前的读取位置和写入位置
discardable bytes
这个区域的字节代表用户已经访问过,可以进行丢弃,当调用discardReadBytes时,会清空这部分的数据,并且通过内存拷贝的方式将后面尚未读取的数据移动到该区域,因此会影响性能
ChannelHandler
ChannelPipeline
- ChannelPipeline中包含多个ChannelHandler,每个Handler包含自己的处理逻辑
- 每个ChannelPipeline和一个Channel绑定
- 当从连接中读取数据时,会按照InboundHandler添加到handlerPipeline中的顺序,从前往后执行
- 当向连接中写入数据是,会按照OutboundHandler添加到handlerPipeline中的顺序,从后往前执行
- 可以在运行的过程中动态修改pipeline
ChannelHandlerContext
每次向ChannelPipeline中添加一个新的ChannelHandler,都会创建一个新的ChannelHandlerContext然后关联上该ChannelHandler
ChannelHandlerContext可以使当前ChannelHandler和其他ChannelHandler进行交互
一个ChannelHandler可以添加到多个ChannelPipeline中,每当ChannelHandler添加到一个ChannelPipeline时,就会新生成一个ChannlelHandlerContext和该ChannelHandler进行绑定
当一个ChannelHandler添加到多个ChannelPipeline时,必须使用@Sharable注解
ChannelInboundInvoker和ChannelOutboundInvoker
ChannelInboundInvoker接口具有触发inbound事件的能力
ChannelOutboundInvoker接口具有触发outbound事件的能力
ChannelHandlerContext和ChannelPipeline同时继承了ChannelInboundInvoker和ChannelOutboundInvoker
因此都具有触发inbound和outbound事件的能力
但是两者的表现形式不同,通过ChannelPipeline触发的事件会由ChannelPipeline中包含的所有ChannelHandler执行,而通过ChannlerHandlerContext触发的事件,则是从最近的ChannelHandler开始执行
Channel状态模型
状态 | 描述 |
---|---|
chanelUnregistered | channel已经被创建,但是没有注册到EventLoop |
chanelRegistered | channel已经被创建,并且已经注册到EventLoop |
chanelActive | channel已经连接到远端,可以接收和发送消息 |
chanelInactive | channel和远端断开连接 |
ChannelHandler
一共有两种ChannelHandler:
- ChannelInboundHandler
用来接收数据和响应状态的改变 - ChannelOutboundHandler
用来发送数据
ChannleInboundHandler
ChannelInboundHandlerAdapter提供了ChannelInboundHandler的简单实现,每种事件触发,并不会对这个事件进行任何处理,只是简单地调用下一个handler来进行处理;另外在其channelRead方法中,用户必须手动释放byteBuf,否则会造成内存泄露
SimpleChannelInboundHandler则在ChannelInboundHandlerAdapter的基础上提供了自动释放byteBuf的功能,只需要在channelRead0方法中编写读取数据的处理逻辑,当该方法执行完毕之后,会自动释放对应的byteBuf
ChannelOutboundHandler
ChannelOutboundHandlerAdapter提供了ChannelOutboundHanler的简单实现,每种事件触发,并不会对这个事件进行任何处理,只是简单地调用下一个handler来进行处理
Codec
Codec定义了如何将字节转换为指定格式的数据以及如何将指定格式的数据转换为字节
Codec主要分如下两个部分:
- Decoder
将字节转换成指定格式的数据,用在inbound中 - Encoder
将指定格式的数据转换为字节,用在outbound中
Decoder
Decoder主要用在inbound中,用来将数据从一种格式转换为另外一种格式,是一个ChannelInboundHandler
因此Decoder根据源格式和目标格式,可以分为如下3类
- byte -> message
- message -> message
- message -> byte
ByteToMessageDecoder
netty针对byte->message的场景,提供了抽象类ByteToMessageDecoder
主要需要实现下面这个方法
- decode byte->message的逻辑所在
下面看一个将字节转换为整数的例子
public class ByteToIntMessageDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
// 判断输入中可读字节个数是否多于4个
if (in.readableBytes() >= 4) {
// 从输入中读取一个整数
out.add(in.readInt());
}
}
}
上面的例子可以看出比较繁琐的一点是,每次从输入中读取数据的时候,都需要判断输入中是否有足够的字节
MessageToMessageDecoder
当需要将一种格式的message转换成另外一种格式的message时,netty提供了MessageToMessageDecoder
MessageToMessageDecoder
和ByteToMessageDecoder类似,也需要实现decode方法
下面看一个将整数转换为字符串的例子
public class IntToStringDecoder extends MessageToMessageDecoder<Integer> {
@Override
protected void decode(ChannelHandlerContext ctx, Integer msg, List<Object> out) throws Exception {
out.add(msg.toString());
}
}
Encoder
Encoder主要作用于outbound,根据源类型和目标类型的不同,主要分如下两种类别:
- message -> message
- message -> byte
MessageToByteEncoder
当需要进行message->byte转换时,netty提供了MessageToByteEncoder,我们只需要继承这个类,然后实现encode方法即可
下面看一个将整数转换为byte的encoder的实现
public class IntToByteEncoder extends MessageToByteEncoder<Integer> {
@Override
protected void encode(ChannelHandlerContext ctx, Integer msg, ByteBuf out) throws Exception {
out.writeInt(msg);
}
}
MessageToMessageEncoder
当需要进行message->message转换时,netty提供了MessageToMessageEncoder,同样,只需要实现encode方法即可
下面看一个将整数转换为字符串的例子
public class IntToStringMessageEncoder extends MessageToMessageEncoder<Integer> {
@Override
protected void encode(ChannelHandlerContext ctx, Integer msg, List<Object> out) throws Exception {
out.add(msg.toString());
}
}
组合
通常会有将encoder和decoder组合在一起,一起添加到pipeline中共同左右的需求
有两种方式来实现组合:
- 继承ChannelDuplexHandler
继承ChannelDuplexHandler然后实现decode decodeLast encode方法 - 继承CombinedChannelDuplexHandler
继承CombinedChannelDuplexHandler,然后将encoder和decoder作为泛型传进去
Bootstrap
Bootstrap用来对客户端和服务端进行配置并且启动
ServerBootstrap用来对服务端进行配置
Bootstrap用来对客户端进行配置
Client Boostrap
EventLoopGroup group = new NioEventLoopGroup();
try {
// 配置客户端
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.remoteAddress(new InetSocketAddress(host, port))
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new EchoClientHandler());
}
});
// 连接服务器,ChannelFuture代表连接结果
ChannelFuture f = b.connect();
// 连接结果注册回调,当连接建立成功或者失败时会执行回调
f.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (future.isSuccess()) {
System.out.println("Connection established");
} else {
System.out.println("Connection failed");
}
}
});
} finally {
group.shutdownGracefully().sync();
}
Server Bootstrap
Server Bootstrap和Client Bootstrap的api相似,只是多了很多个child开头的方法
这是因为在服务端有两种channel:ServerChannel和ChildChannel
- ServerChannel负责接收新的连接,然后为每个连接创建ChildChannel
- 每个ChildChannel代表一个建立的连接
不以child开头的方法是用来配置ServerChannel的,以child开头的方法是用来配置ChildChannel的
// 指定如何处理事件,比如连接的建立,发送数据,接收数据
EventLoopGroup group = new NioEventLoopGroup();
try {
// 使用Bootstrap来配置服务端
ServerBootstrap b = new ServerBootstrap();
b.group(group)
.channel(NioServerSocketChannel.class)
.localAddress(new InetSocketAddress(port))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new EchoServerHandler());
}
});
// 阻塞等待绑定成功
ChannelFuture f = b.bind().sync();
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully().sync();
}
测试
为了方便测试,Netty提供了EmbeddedChannel类,该类主要提供了以下几个方法:
- writeInbound
- readInbound
- writeOutbound
- readOutbound
以outbound举例,当调用writeOutbound时,会沿着channelPipeline的outbound方向传播,当由channelPipeline上的所有ChannnelOutboundHandler处理之后,可以调用readOutbound来读取处理之后的结果,从而进行验证
public class FixedLengthFrameDecoder extends ByteToMessageDecoder {
private int frameLength;
public FixedLengthFrameDecoder(int frameLength) {
if (frameLength <= 0) {
throw new IllegalArgumentException("frame length must be positive");
}
this.frameLength = frameLength;
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
while(in.readableBytes() >= frameLength) {
ByteBuf buf = in.readBytes(frameLength);
out.add(buf);
}
}
}
public class FixedLengthFrameDecodeTest {
@Test
public void testFrameDecode() {
ByteBuf buf = Unpooled.buffer();
for (int i = 0;i < 9;i ++) {
buf.writeByte(i);
}
ByteBuf input = buf.copy();
EmbeddedChannel channel = new EmbeddedChannel(new FixedLengthFrameDecoder(3));
Assert.assertTrue(channel.writeInbound(input));
System.out.println(buf.refCnt());
Assert.assertTrue(channel.finish());
Assert.assertEquals(buf.readBytes(3), channel.readInbound());
Assert.assertEquals(buf.readBytes(3), channel.readInbound());
Assert.assertEquals(buf.readBytes(3), channel.readInbound());
Assert.assertNull(channel.readInbound());
}
}
线程模型
每个任务一个线程
介绍
最简单的线程模型就是每来一个任务,就新创建一个线程来执行这个任务
优点
简单
缺点
效率低,频繁地创建线程和销毁线程,会带来大量开销
线程池
介绍
线程执行完任务之后,不立即销毁,而是放到线程池中进行复用
优点
避免频繁地创建线程和销毁线程
缺点
- 管理资源和上下文切换会带来开销
- 开发有难度
事件循环
介绍
在一个循环中处理事件
优点
- 简化开发,不需要关心同步
类继承结构
使用
Channel ch = ...;
Future<?> future = ch.eventLoop().execute(new Runnable() {
public void run() {
// do something
}
})
一个channel会被绑定到一个eventloop,该channel相关的操作都会在这个eventloop中执行
可以通过channel来获取其绑定的eventloop
channel事件的执行
当通过channel来向其绑定的eventloop来提交一个任务时,会判断当前线程和eventloop绑定的线程是否是同一个
如果是同一个则立即执行,如果不是同一个会加入到eventloop的队列中,等下次遍历到的时候再执行
以此来保证不管在哪个线程通过channel来执行操作,最终都会交给其绑定的eventloop来进行处理,保证由同一个线程来执行channel的所有事件
任务调度
Channel ch = ...;
ScheduledFuture<?> future = ch.eventLoop().schedule(
new Runnable() {
public void run() {
// do something
}
}
, 60, TimeUnit.SECONDS);
netty中的任务调度是对论文”Hashed and hierarchical timing wheels: Data structures to efficiently implement timer facility",不会100%保证任务会准时调度
eventloop的执行可以简化成如下几个步骤:
- 提交一个延时任务
- 任务被插入到eventloop的调度任务队列中
- eventloop检测当前是否有可以处理的任务
- 如果有任务,立即执行并且从队列中移除任务
- 继续检测是否有任务,重复第4步
IO线程分配
使用非阻塞传输层,比如NIO,一个线程会管理多个连接,一个连接对应一个channel
使用阻塞传输层,比如OIO,一个线程只管理一个连接,一个连接对应一个channel