netty学习笔记

官方文档:https://netty.io/3.8/guide/

文档里的例子举的不错,循序渐进、深入浅出。

公司里一个老项目用的是netty 3.x 版本,索性就拿3.x 版本来学习了。

1. 前言

1.1 问题

现如今,人们使用通用应用程序或库进行通信。例如,我们经常用Http client库从web server获取信息,并经由web服务发起远程过程调用(RPC)。

然而,通用协议或其实现不能很好地扩展。就像我们不会用通用HTTP server去传大文件、email信息和诸如财经消息、多媒体游戏数据这样的近实时消息。这需要高度优化的专用协议实现。例如,你可能想实现一个HTTP server,它专门为AJAX聊天应用、流媒体、大文件做过优化。你甚至可能想设计和实现全新协议以适应你的需求。

另一种不可避免的情况是,你不得不处理旧的专有协议,去和旧系统互操作。这种情况下最重要的是,在不牺牲最终程序的稳定性和性能的条件下,实现该协议的速度。

1.2 解决方案

Netty致力于提供异步事件驱动的网络应用框架,以快速开发可维护、可扩展、高性能的服务器和客户端。

换句话说,Netty是一个NIO客户端服务器框架,可以快速开发网络应用程序。它极大地简化、流水线化了诸如TCP、UDP套接字之类的网络编程。

快速易上手,不意味着损失程序的可维护性和性能。Netty经过精心设计,结合了许多协议(FTP、SMTP、HTTP以及基于二进制、文本的协议)的实现经验。Netty成功找到了轻松开发与性能、稳定性、灵活性之间的平衡。

Netty有其他一些竞品,你可能想知道Netty与众不同之处。答案是Netty的底层哲学。Netty旨在从一开始就在API和实现方面为你提供最舒适的体验。这不是有形的东西,但是你会意识到,当你阅读本指南并把玩Netty时,这种哲学将使你愉悦。

2. 入门

本章将通过简单是示例介绍Netty的核心构造,以使你快速入门。之后,你将能够立即在Netty基础上编写客户端和服务器。

如果你更喜欢自上而下地学习新事物,你可以先看第三章,再回头看第二章。

2.1 入门准备

跑通本章示例的最低要求有两个:Netty(3.x)以及 jdk 1.5(或以上)。

阅读时,你可能对本章介绍的类有更多疑问。如果想进一步了解,请参考API文档。

2.2 Discard Server

世界上最简单的协议不是“Hello World”,而是“DIACARD”。它是这样一个协议,丢弃任何收到的数据,没有任何响应。

实现“DIACARD”协议,唯一要做的事情就是忽略所有收到的数据。直接上handler实现,处理Netty产生的IO事件。

package org.jboss.netty.example.discard;
public class DiscardServerHandler extends SimpleChannelHandler { // 1 @Override public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) { // 2 } @Override public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) { // 3 e.getCause().printStackTrace(); Channel ch = e.getChannel(); ch.close(); } }

(1) DiscardServerHandler继承SimpleChannelHandler,后者是ChannelHandler的一个实现。SimpleChannelHandler提供多种事件处理方法,供你覆写。现在,继承SimpleChannelHandler就足够了,啥都不用你实现。

(2) 这里覆写了messageReceived方法。每当从客户端接收到新数据时,该方法以MessageEvent实参调用,参数包含了接收到的数据。本例中,通过空实现来忽略接收到的数据。

(3) 当Netty由于网络IO错误抛异常,或者某个handler实现处理event抛异常的时候,exceptionCaught方法会被调用。大部分场景,应该打个日志、关闭相关channel,当然也可以不同。比如,你想关闭连接之前,发送个带错误码的response之类的。

截止目前,一切看起来都不错。我们已经实现了DISCARD server的前半部分。剩下的就是写个main方法,启动server。

package org.jboss.netty.example.discard;

import java.net.InetSocketAddress;
import java.util.concurrent.Executors;

public class DiscardServer {

    public static void main(String[] args) throws Exception {
        ChannelFactory factory =
            new NioServerSocketChannelFactory( // 4
                    Executors.newCachedThreadPool(),
                    Executors.newCachedThreadPool());

        ServerBootstrap bootstrap = new ServerBootstrap(factory); // 5

        bootstrap.setPipelineFactory(new ChannelPipelineFactory() { // 6
            public ChannelPipeline getPipeline() {
                return Channels.pipeline(new DiscardServerHandler());
            }
        });

        bootstrap.setOption("child.tcpNoDelay", true); // 7
        bootstrap.setOption("child.keepAlive", true);

        bootstrap.bind(new InetSocketAddress(8080)); // 8
    }
}

(4) ChannelFactory是创建和管理Channel及相关资源的工厂。它处理所有IO请求并执行IO以生成ChannelEvents。Netty提供多种ChannelFactory实现。本例中我们想实现一个服务端应用,因此选用了NioServerSocketChannelFactory。另外要声明的是,它并没有创建IO线程。它会从你在构造函数中指定的线程池中获取线程,以使你更好地控制线程管理方式,例如带安全管理的服务。

(5) ServerBootstrap是设置服务器的辅助类。你可以设置server直接使用一个Channel。但请注意,这是一个繁琐的过程,大多数情形下,你不需要这么做。

(6) 此处配置了ChannelPipelineFactory。每当服务器接受一个新连接,这个指定的ChannelPipelineFactory就会创建一个新的ChannelPipeline。新的pipeline会包含DiscardServerHandler。随着应用程序变得复杂,你可能会往pipeline里添加更多的handlers,并最终将此(实现了ChannelPipelineFactory接口的)匿名类提取到顶级类中。

(7) 可以设置针对特定Channel实现的参数。我们写了一个TCP/IP服务,所以允许设置socket选项,如tcpNoDelay、keepAlive等。请注意所有选项都添加了“child.”前缀。这意味着,这些选项会被用在accepted channels,而不是ServerSocketChannel。设置ServerSocketChannel的选项,要这么写:

bootstrap.setOption("reuseAddress", true);

(8) 准备出发。剩下的事情是绑定端口、启动服务。这里我们绑定了本机所有网卡的8080端口。bind方法可以多次调用,传不同的address。

恭喜你!你已经在Netty的基础上,完成了你的第一个服务。

2.3 细查Received Data

既然我们已经写了一个服务器,就要测试下它是否工作。最简单的测试方式是使用telnet命令。比如,你可以在终端输入“telnet localhost 8080”,随便打点什么。

然而,我们就敢说服务工作正常吗?它是个discard服务,所以我们没法知道。你什么响应也get不到。为了证明它可以工作,我们对服务稍加修改,打印接收到的数据。

我们已经知道数据到达时,会生成MessageEvent,继而messageReceived函数会被调用。那我们就在messageReceived方法里加点代码吧。

@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
    ChannelBuffer buf = (ChannelBuffer) e.getMessage(); // 9
    while(buf.readable()) {
        System.out.println((char) buf.readByte());
        System.out.flush();
    }
}

(9) 假设socket里传输的消息类型总是ChannelBuffer是安全的。在Netty里,ChannelBuffer是储存字节序列的基本数据结构。和NIO ByteBuffer很像,但是更简单灵活。例如,Netty允许创建复合ChannelBuffer以组合多个ChannelBuffer,这样可以减少内存拷贝。尽管它与NIO ByteBuffer非常相似,但强烈建议你参考API手册。学习如何正确使用ChannelBuffer是轻松使用Netty的关键步骤。

再运行一遍telnet命令,你会看到服务打印了接收到的数据。

Discard服务的整个源代码就在 org.jboss.netty.example.discard 包里。

2.4 Echo Server

目前,我们已经不做响应的前提下,把接收到的数据消费掉了。但作为一个服务器,毕竟是要有响应的。通过实现ECHO协议(收到什么,就原封不动发回去),来学习一下如何给client发送response消息吧。

它和前面实现的DISCARD唯一的区别就是把接收数据发回去,而不是仅仅打印出来。因此,改改messageReceived方法就够了。

@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
    Channel ch = e.getChannel(); // 10
    ch.write(e.getMessage());
}

(10) ChannelEvent对象有关联Channel的引用。这里,返回的Channel代表了接收到MessageEvent的连接。我们可以获取到这个Channel,调用write方法去写点什么给远端。

再运行一遍telnet命令,你会看到服务把你发过去的内容原封不动发回来了。

Echo服务的整个源代码就在 org.jboss.netty.example.echo 包里。

2.5 Time Server

本节要实现的是TIME协议。和之前的例子不同,它发送一个32-bit整数,不接受任何请求,发送完消息就断开连接。本例中你将学到如何构造、发送一个消息,以及发送完成后关闭连接。

因为我们要在连接建立以后立刻发送消息、忽略接收到的数据,所以这次就不能用messageReceived了。而是要覆写channelConnected方法。下面是实现代码:

package org.jboss.netty.example.time;

public class TimeServerHandler extends SimpleChannelHandler {

    @Override
    public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) { // 11
        Channel ch = e.getChannel();
        
        ChannelBuffer time = ChannelBuffers.buffer(4); // 12
        time.writeInt((int) (System.currentTimeMillis() / 1000L + 2208988800L));
        
        ChannelFuture f = ch.write(time); // 13
        
        f.addListener(new ChannelFutureListener() { // 14
            public void operationComplete(ChannelFuture future) {
                Channel ch = future.getChannel();
                ch.close();
            }
        });
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) {
        e.getCause().printStackTrace();
        e.getChannel().close();
    }
}

(11) 前面解释过,连接建立的时候,channelConnected方法会被调起来。我们就写一个代表当前时间的32-bit整数进去。

(12) 为发送一条新消息,需要分配一个新buffer来容纳这条消息。要写32-bit整数,因此需要ChannelBuffer的容量是4字节。ChannelBuffers辅助类用于分配新buffer。除了buffer方法,ChannelBuffers提供了许多ChannelBuffer相关的有用方法。详细信息,请参考API文档。

另一方面,对ChannelBuffer使用static import是个好主意。

import static org.jboss.netty.buffer.ChannelBuffers.*;
...
ChannelBuffer  dynamicBuf = dynamicBuffer(256);
ChannelBuffer ordinaryBuf = buffer(1024);

(13) 像往常一样,来写一下constructed message。

不过,等一下,flip哪去了?在NIO里,我们发送消息之前,不是要调用ByteBuffer.flip()吗?ChannelBuffer没有这样的一个方法,因为它有两个指针:一个读、一个写。向ChannelBuffer写数据的时候,写偏移增加,但读偏移保持不变。读偏移和写偏移分别代表了消息的开始、结束位置。

相反,NIO buffer没有提供一个简洁的方法来定位消息的起止,只能调用flip方法。忘了调用flip方法,你就会有麻烦,会发出错误的数据。Netty世界里没有这种错误,因为它为不同的操作类型提供了不同的索引。你会发现它让你的生活变得简单——没有flippling out的生活。

另外值得说的一点,write方法返回一个ChannelFuture。ChannelFuture代表一个还没有发生的IO操作。它意味着任何请求的操作可能还没有执行,因为在Netty里,所有的操作都是异步的。例如,下面的代码可能会在发送消息之前就把连接关闭了:

Channel ch = ...;
ch.write(message);
ch.close();

因此,你需要在write方法返回的ChannelFuture通知你写操作完成时,再调用close方法。请注意,close方法也不是立刻关闭连接,而是也返回了一个ChannelFuture。

(14) 写请求完成后,我们如何被通知到呢?很简单,给返回的ChannelFuture添加一个ChannelFutureListener。这里我们创建了一个匿名ChannelFutureListener,在操作完成以后关闭Channel。

或者,你也可以用一个预定义的listener来简化代码:

f.addListener(ChannelFutureListener.CLOSE);

测试我们的Time服务是否正常工作,可以用Unix命令“rdate”:

$ rdate -o <port> -p <host>

port是你在main方法里指定的端口号,host是localhost。

2.6 Time Client

不像DISCARD和ECHO服务,我们需要一个client来解析TIME协议。本节,我们讨论如何确保服务正常工作,学习如何用Netty写一个client。

用Netty写server还是client,最大也是唯一的区别是需要不同的Bootstrap和ChannelFactory。请看如下代码:

package org.jboss.netty.example.time;

import java.net.InetSocketAddress;
import java.util.concurrent.Executors;

public class TimeClient {

    public static void main(String[] args) throws Exception {
        String host = args[0];
        int port = Integer.parseInt(args[1]);

        ChannelFactory factory =
            new NioClientSocketChannelFactory( // 15
                    Executors.newCachedThreadPool(),
                    Executors.newCachedThreadPool());

        ClientBootstrap bootstrap = new ClientBootstrap(factory); // 16

        bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
            public ChannelPipeline getPipeline() {
                return Channels.pipeline(new TimeClientHandler());
            }
        });
        
        bootstrap.setOption("tcpNoDelay", true); // 17
        bootstrap.setOption("keepAlive", true);

        bootstrap.connect(new InetSocketAddress(host, port)); // 18
    }
}

(15) NioClientSocketChannelFactory,取代NioServerSocketChannelFactory,创建client-side Channel。

(16) ClientBootstrap对应ServerBootstrap。

(17) 请注意这里没有“child.”前缀。Client端SocketChannel没有parent。

(18) 调用connect方法,而不是bind方法。

如你所见,和server端的启动没啥大区别。ChannelHandler实现呢?接收一个32-bit整数,翻译成可读格式,打印时间,然后关闭连接:

package org.jboss.netty.example.time;

import java.util.Date;

public class TimeClientHandler extends SimpleChannelHandler {

    @Override
    public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
        ChannelBuffer buf = (ChannelBuffer) e.getMessage();
        long currentTimeMillis = buf.readInt() * 1000L;
        System.out.println(new Date(currentTimeMillis));
        e.getChannel().close();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) {
        e.getCause().printStackTrace();
        e.getChannel().close();
    }
}

看起来很简单,和server端例子没啥区别。然而,这个handler偶尔会罢工,抛出 IndexOutOfBoundsException。我们下节讨论具体原因。

2.7 处理流式传输

2.7.1 Socket Buffer警告

TCP/IP这种基于流式的传输,会将接收到的数据存储在socket buffer里。不幸的是,buffer里存的不是packet序列,而是字节序列。意味着,你发两个独立的包,OS不会把它们当成两个独立的包,而是当成一堆字节。因此,不能保证你读到的和远端写的一模一样。例如,假设TCP/IP协议栈收到3个packets:

+-----+-----+-----+
| ABC | DEF | GHI |
+-----+-----+-----+

由于流式协议的一般属性,很有可能你在程序里读到的是如下的分段形式:

+----+-------+---+---+
| AB | CDEFG | H | I |
+----+-------+---+---+

因此,接收端,不管它是server还是client,应该将接收到的数据整理到一个或多个有意义的帧中,以使应用程序逻辑易于理解。在上面的示例中,接收数据应该以下面的格式分帧:

+-----+-----+-----+
| ABC | DEF | GHI |
+-----+-----+-----+
2.7.2 解决方案一

现在我们回到TIME client的例子。遇到了同样的问题,32-bit整数是一个很小的数据量,不太可能被频繁分段。然而,问题是有这个可能性,而且可能性会随着流量的增大而增大。

一种简单的解决方案是创建一个内部累积buffer,等4个字节都到齐。下面的TimeClientHandler是修改过的版本:

package org.jboss.netty.example.time;

import static org.jboss.netty.buffer.ChannelBuffers.*;

import java.util.Date;

public class TimeClientHandler extends SimpleChannelHandler {

    private final ChannelBuffer buf = dynamicBuffer(); // 19

    @Override
    public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
        ChannelBuffer m = (ChannelBuffer) e.getMessage();
        buf.writeBytes(m); // 20
        
        if (buf.readableBytes() >= 4) { // 21
            long currentTimeMillis = buf.readInt() * 1000L;
            System.out.println(new Date(currentTimeMillis));
            e.getChannel().close();
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) {
        e.getCause().printStackTrace();
        e.getChannel().close();
    }
}

(19) 动态缓冲(ChannelBuffer),需要的时候可以增加容量。在你不知道消息长度的时候,很有用。

(20) 首先,所有收到的数据都被累积到buf。

(21) 然后,handler必须检查数据是否足够(本例中是4字节),进行实际的业务逻辑。否则,Netty会在更多数据到达的时候,再次调用messageReceived。终究,4字节是会被集齐的。

2.7.3 解决方案二

尽管解决方案一已经解决了TIME client遇到的问题,但修改后的handler代码并不是那么整洁。假设一个更复杂的协议来了,很多字段,有的字段还是变长的。你的ChannelHandler实现很快就会变得不可维护。

你可能已经注意到,可以给ChannelPipeline添加不止一个ChannelHandler,因此你可以把整个ChannelHandler拆成多个模块,以降低程序复杂度。例如,你可以把TimeClientHandler拆成两个handler:

  • TimeDecoder处理碎片问题
  • 最开始的那个简单版本TimeClientHandler

很幸运,Netty提供了一个可扩展的类,可以帮助你开箱即用地编写第一个类:

package org.jboss.netty.example.time;

public class TimeDecoder extends FrameDecoder { // 22

    @Override
    protected Object decode(
            ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer) { // 23
            
        if (buffer.readableBytes() < 4) {
            return null; // 24
        }
        
        return buffer.readBytes(4); // 25
    }
}

(22) FrameDecoder是ChannelHandler的一个实现,可以轻松处理碎片化问题。

(23) 新数据一到,FrameDecoder就调用decode方法,而且内部维护了一个累积buffer。

(24) 如果返回null,代表数据还不够。当有足够数据的时候,FrameDecoder会被再次调用。

(25) 如果返回非null值,意味着decode方法已经成功decode了一条message。FrameDecoder将忽略内部累积buffer的读部分。请记住,你不需要decode多条消息。FrameDecoder将持续调用decode,直到它返回null。

既然有了另外一个handler要插入pipeline,我们需要修改一下TimeClient里的ChannelPipelineFactory实现:

bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
    public ChannelPipeline getPipeline() {
    return Channels.pipeline(
            new TimeDecoder(),
            new TimeClientHandler());
    }
});

如果你是个爱冒险的人,你可能想尝试下ReplayingDecoder,它可以更大程度上简化decoder。那么你就得参考API手册了。

package org.jboss.netty.example.time;

public class TimeDecoder extends ReplayingDecoder<VoidEnum> {

    @Override
    protected Object decode(
            ChannelHandlerContext ctx, Channel channel,
            ChannelBuffer buffer, VoidEnum state) {
            
        return buffer.readBytes(4);
    }
}

此外,Netty提供了系列开箱即用的解码器,使你轻松实现大多数协议,避免最终以一个单体的、不可维护的处理逻辑实现而告终。

  • org.jboss.netty.example.factorial 二进制协议
  • org.jboss.netty.example.telnet 文本协议

2.8 POJO vs. ChannelBuffer

到目前为止,我们看到的例子都是以ChannelBuffer作为协议的主要数据结构的。本节,我们改进TIME协议的client、server实现,用POJO替换ChannelBuffer。

使用POJO的优势在于:通过分离从ChannelBuffer中提取信息的代码,你的程序将更易于维护、易于重用。在TIME client和server例子中,我们只读了32-bit整数,用ChannelBuffer还不是什么大问题。当你实现现实世界中的一个协议的时候,你会发现这中分离是必须的。

首先,我们定义一个新的类型,叫UnixTime。

package org.jboss.netty.example.time;

import java.util.Date;

public class UnixTime {
    private final int value;
    
    public UnixTime(int value) {
        this.value = value;
    }
    
    public int getValue() {
        return value;
    }
    
    @Override
    public String toString() {
        return new Date(value * 1000L).toString();
    }
}

现在我们可以修改TimeDecoder,返回一个UnixTime,而不是ChannelBuffer了。

@Override
protected Object decode(
        ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer) {
    if (buffer.readableBytes() < 4) {
        return null;
    }

    return new UnixTime(buffer.readInt()); // 26
}

(26) FrameDecoder和ReplayingDecoder允许你返回任意类型的实例。如果他们被限制成只返回ChannelBuffer,那我们还得加一个类型转换的handler。

配合这个新版decoder,TimeClientHandler将不再使用ChannelBuffer:

@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
    UnixTime m = (UnixTime) e.getMessage();
    System.out.println(m);
    e.getChannel().close();
}

简单、优雅,是吧?Server端可以用上同样的技术。更新下TimeServerHandler:

@Override
public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) {
    UnixTime time = new UnixTime(System.currentTimeMillis() / 1000);
    ChannelFuture f = e.getChannel().write(time);
    f.addListener(ChannelFutureListener.CLOSE);
}

现在缺的就是一个encoder,把UnixTime转回ChannelBuffer。这要比写decoder容易的多,因为不用处理packet碎片了。

package org.jboss.netty.example.time;
    
import static org.jboss.netty.buffer.ChannelBuffers.*;

public class TimeEncoder extends SimpleChannelHandler {

    public void writeRequested(ChannelHandlerContext ctx, MessageEvent e) { // 27
        UnixTime time = (UnixTime) e.getMessage();
        
        ChannelBuffer buf = buffer(4);
        buf.writeInt(time.getValue());
        
        Channels.write(ctx, e.getFuture(), buf); // 28
    }
}

(27) Encoder覆写writeRequested方法,拦截写请求。请注意这里的MessageEvent参数和messageReceived里指定的参数类型一致,但它们的解释不同。ChannelEvent既可以是上游事件,又可以是下游事件,取决于事件的流向。例如,当调用messageReceived时,MessageEvent是上游事件;当调用writeRequested时,MessageEvent是下游事件。请参考API文档,详查上下游事件的区别。

(28) 一旦完成了POJO到ChannelBuffer的转换,你应该把这个新buffer转发给ChannelPipeline里的先前的ChannelDownstreamHandler。Channels提供了多种辅助方法来生成、发送ChannelEvent。本例中,Channels.write()方法创建一个MessageEvent,发送给ChannelPipeline里的先前的ChannelDownstreamHandler。

另一方面,静态导入Channels是个好主意:

import static org.jboss.netty.channel.Channels.*;
...
ChannelPipeline pipeline = pipeline();
write(ctx, e.getFuture(), buf);
fireChannelDisconnected(ctx);

最后剩下的任务是在Server端的ChannelPipeline里插入TimeEncoder,这留作一个小练习吧。

2.9 程序退出

如果运行TimeClient,你会注意到应用程序并不退出,什么也不做,hang在那里。观察call stack,你会发现几个IO线程在运行。想要关闭IO线程、整个程序优雅退出,你需要释放ChannelFactory分配的资源。

关闭一个典型网络应用的进程有下面三步:

  1. 关闭所有server socket,如果有的话
  2. 关闭所有non-server socket(如client socket、accepted socket),如果有的话
  3. 释放所有ChannelFactory使用的资源

在TimeClient上应用上面的三步,TimeClient.main()会通过关闭client连接、释放ChannelFactory使用的资源来优雅退出。

package org.jboss.netty.example.time;

public class TimeClient {
    public static void main(String[] args) throws Exception {
        ...
        ChannelFactory factory = ...;
        ClientBootstrap bootstrap = ...;
        ...
        ChannelFuture future = bootstrap.connect(...); // 29
        future.awaitUninterruptibly(); // 30
        if (!future.isSuccess()) {
            future.getCause().printStackTrace(); // 31
        }
        future.getChannel().getCloseFuture().awaitUninterruptibly(); // 32
        factory.releaseExternalResources(); // 33
    }
}

(29) ClientBootstrap的connect方法返回ChannelFuture,它会通知连接尝试成功还是失败。它还有一个指向Channel的引用(和这个连接相关联)。

(30) 等待返回的ChannelFuture决定连接成功还是失败。

(31) 失败了,打印下失败原因。如果连接没成功、也没被取消,ChannelFuture的getCause方法将返回失败原因。

(32) 现在连接尝试结束了,我们需要等待Channel的closeFuture,直到连接被关闭。每个Channel都有自己的closeFuture,以便于在关闭是获得通知并执行某些操作。

即便连接尝试失败了,closeFuture也会被通知,因为连接失败,Channel自动关闭。

(33) 在这个节点,所有连接关闭。剩下的就是释放ChannelFactory使用的资源。简单地调用releaseExternalResources()。所有资源包括NIO选择子、线程池都会关闭。

关闭client挺简单,那么关闭server呢?你需要解绑端口,关闭所有accepted连接。需要一个数据结构来记录active连接列表,这不是个小任务。幸好,我们有ChannelGroup的解决方案。

ChannelGroup是Java 集合API的一个特殊扩展,代表了一组开放(open)的Channel。如果一个Channel加入了某ChannelGroup,这个Channel在关闭之后,会被自动移出ChannelGroup。你也可以对同组的Channel批量执行某操作。例如,程序退出前关闭组内所有Channel。

为记录追踪所有open的socket,你需要修改TimeServerHandler,把新开的Channel加到全局ChannelGroup(TimerServer.allChannels)里:

@Override
public void channelOpen(ChannelHandlerContext ctx, ChannelStateEvent e) {
    TimeServer.allChannels.add(e.getChannel()); // 34
}

(34) 是的,ChannelGroup是线程安全的。

现在所有active Channel都被自动管理起来了,关闭server就像关闭client那么容易了:

package org.jboss.netty.example.time;

public class TimeServer {

    static final ChannelGroup allChannels = new DefaultChannelGroup("time-server"); // 35

    public static void main(String[] args) throws Exception {
        ...
        ChannelFactory factory = ...;
        ServerBootstrap bootstrap = ...;
        ...
        Channel channel = bootstrap.bind(...); // 36
        allChannels.add(channel); // 37
        waitForShutdownCommand(); // 38
        ChannelGroupFuture future = allChannels.close(); // 39
        future.awaitUninterruptibly();
        factory.releaseExternalResources();
    }
}

(35) DefaultChannelGroup需要一个名字作为构造函数参数。group-name单纯就是为了做个区分。

(36) bind方法返回一个server端Channel,绑定一个指定的本地地址。在这个Channel上调用close方法,会使其与本地地址解绑。

(37) 任何类型的Channel都可以加到ChannelGroup里,不管它是server端、client端、accepted的。因此,关闭server时,可以一揽子关闭Channel。

(38) waitForShutdownCommand()是一种等待关闭信号的假想(imaginary)的方法。你可以等一个特权client的消息或者JVM shutdown hook。

(39) 你可以在一个ChannelGroup的所有Channel上执行相同操作。这种情况下,关闭所有Channel,意味着绑定的server端Channel全都解绑、accepted connections全都异步关闭。为通知这些异步关闭动作什么时候完成,返回了一个ChannelGroupFuture,作用和ChannelFuture差不多。

2.10 总结

本章,我们快速浏览了Netty,并演示了如何在Netty上编写功能全面的网络应用程序。

接下来的章节将会有更多的细节信息。强烈建议看一下org.jboss.netty.example包里的例子。

我们的社区也等着你的问题和idea,在你的反馈之下,Netty将持续改进。

3. 架构概览

TODO

3.1 数据结构

3.2 异步IO API

3.3 基于事件模型的拦截链模式

3.4 用于快速开发的高级组件

4. FAQ

FAQ节选自StackOverflow。

4.1 何时可以写下游数据

只要你有指向Channel(或ChannelHandlerContext)的引用,你就可以在任何地方、任何线程调用Channel.write()(或者Channels.write())。

当你通过调用Channel.write()或者调用ChannelHandlerContext.sendDownstream(MessageEvent)来触发writeRequested事件时,writeRequested()会被调用。

参考:https://stackoverflow.com/questions/3222134/how-does-downstream-events-work-in-jbosss-netty

4.2 怎样融合我的“阻塞”应用代码和“非阻塞”NioServerSocketChannelFactory

TODO

4.3 假设多个事件可能同时发生,是否需要同步我的 handler 代码

你的ChannelUpstreamHandler会被在同一个线程(例如一个IO线程)里顺序调用,因此一个handler不必担心在前一个上游事件处理完以前,就被新上游事件调起。

然而,下游事件可能由多个线程同时触发。如果你的ChannelDownstreamHandler访问了共享资源或者存储了状态信息,你需要做同步。

参考:https://stackoverflow.com/questions/8655674/is-netty-receive-events-concurrency-how-about-downstream-and-upsream-events

4.4 如何在同一条Channel的handlers之间传递数据

使用ChannelLocal。

// Declare
public static final ChannelLocal<int> data = new ChannelLocal<int>();

// Set
data.set(e.getChannel(), 1);

// Get
int a = data.get(e.getChannel());

参考:https://stackoverflow.com/questions/8449663/usage-of-nettys-channellocal

posted @ 2021-01-08 15:22  不写诗的诗人小安  阅读(214)  评论(0编辑  收藏  举报