本章内容包括
1)客户端和服务器端的Bootstrapping
2)在一个Channel中的Bootstrapping客户端
3)增加ChannelHandler
4)使用ChannelOptions和attributes
已经深入地学习过了ChannelPipeline,ChannelHandler,codec等类,你接下来的可能会问:如何将这些组件组装起来增加到你的应用中去?
答案是:“Bootstrapping”,其实到目前为止,我们已经模糊地接触过几次这个组件了,现在是时候给它下一个明确的定义了,简单地陈述一下,Bootstrapping,顾名思义,就是通过配置可以将你的程序运行起来的意思,也许这个配置的细节比它的概念可能会难一点,特别是启动的是一个网络项目的时候
与Netty框架架构的理念一致,Netty处理Bootstrapping时进行了封装,使其与你的应用解耦,使你的服务器端和客户端都与网络层解耦,不要再注意配置底层实现的细节,你会看见,所有的框架组件连接组合都是在幕后进行的,你会发现Bootstrapping就像你在拼图的时候,缺失的那部分,当你将其放入正确的拼图位置的时候,你的Netty应用的整个蓝图就会完成了
8.1 Bootstrap classes
bootstrapping的类是继承与一个抽象的父类方法,两个具体的实现类,如8.1图所示
可以将这两个具体的实现类想象成服务器端和客户端启动类,这样可以帮助你区分他们所支持的不同的应用功能点,顾名思义,一个服务器端致力于创建一个父类channel来接收来自客户端的连接,并且创建一个子channel用来服务于两者之间的对话,然而对于客户端来说,一个客户端最多接收一个单独的非父类的channel用来进行网络交互,我们也会意识到,这种模式也是使用于无连接类型的传输类型的UDP协议,因为他们不需要为每次的连接创建一个channel
我们之前几个章节学习的一些Netty组件也会在启动的过程中加入进来,他们中的一些成员将会在服务器端和客户端都会被用到,bootstrapping将一些对于所有应用类型的公共实现方法放到了AbstractBootstrap中,而相对应的一些具体的操作放在了Bootstrap和ServerBootstrap中分别用来服务客户端和服务器端
在接下来的章节中,我们将详细地讨论这两个类的细节,我们先从比较简单的Bootstrap类讲起
TIPS:为什么bootstrap的类实现了Cloneable的接口呢?
你有时候需要创建多个channel,这些channel有着相同或者相似的配置,为了在不需要创建一个新的bootstrap实例来为每一个channel一一配置的前提下去支持这种功能模式,AbstractBootstrap就被标记了Cloneable,在一个已经配置好的bootstrap的基础上调用一个clone方法将会返回一个可以立即投入使用的bootstrap实例
请注意,这只是对Bootstrap的EventLoopGroup的浅拷贝,所以EventLoopGroup将在所有的channel中共享,这个是可以接受的,因为大部分的clone的channel它的生命周期经常是很短暂的,一个经典的案例就是创建一个Http请求
一个AbstractBootstrap的完整声明是这样的:
在这个声明中,子类B是父类中的一个类型参数,这样做的好处就是在运行时这个对象的引用可以被返回,这样就可以支持流式编程模式
子类的可以如下定义
8.2 Bootstrapping clients and connectionless protocols
Bootstrap被用在客户端或者使用在无连接的应用中,表8.1给出了这个类的一些概述,这些里面的很多方法是继续于AbstractBootstrap类
下一个小节我们将会一步步地分析客户端的bootstrapping,我们也会讨论在选择可利用的组件实现的时候,兼容性的一些问题
8.2.1 Bootstrapping a client
Bootstrap是用来为客户端创建一个channel并且为应用可以利用无连接的协议,如图8.2所示
下面的代码清单展示了Bootstrap的客户端使用NIO的TCP协议
这个例子很好地展示了流式编程风格,所有的方法除了connect方法之外都是通过前面方法返回的原始对象的引用链式调用的
8.2.2 Channel and EventLoopGroup compatibility
下面的目录是来自于io.netty.channel包的结构,你可以清晰地看见包名与包下对应的类的前缀名是一样的,这些类名是用来关联EventLoopGroup和Channel,并以NIO和OIO两种方式去实现
兼容性是必须去维护的,你不能混合的使用这些不同命名前缀的组件,例如将NioEventLoopGroup和OioSocketChannel混合使用,下面的代码清单展示了这样混合使用的尝试
这个代码块会引起IllegalStateException的异常,因为它混合了不相容的传输方式
TIPS:关于更多的IllegalStateException
在启动bootstrap时,你在调用bind或者connect方法之前,你必须使用如下的方法设置那些启动时的必要参数,例如group方法,channel方法或者channelFactory方法,和handler方法,如果做这些操作失败的话就会抛出IllegalStateException异常,handler方法特别重要,因为它是用来配置ChannelPipeline的
8.3 Bootstrapping servers
我们先列出ServerBootstrap的API,来给我们学习ServerBootstrap时做一个一个知识总览,然后我们就一步步地讲解启动服务器端时的一些小细节,并且讲解一些相关知识点,包括一个特殊的案例:从一个服务端的channel去启动一个客户端
8.3.1 The ServerBootstrap class
表8.2列出了ServerBootstrap的几个方法
下一个章节,我们将讲解server端的bootstrapping的一些细节
8.3.2 Bootstrapping a server
你可能已经看到表8.2中列出的一些方法并没有在表8.1中出现过,例如childHandler(),childAttr(),childOption()方法,这些是典型的支持服务器端操作的方法,具体来说,ServerChannel的实现负责用于创建一个子Channel,这个Channel负责接收一些链接,然后ServerBootstrap用来启动ServerChannel,提供了这些方法来简化一些链接channel的配置任务
图8.3展示了一个ServerBootstrap用bind方法来创建一个ServerChannel的情形,并且ServerChannel用来管理一定数量的子Channel
下面的代码清单实现了图8.3中服务器端启动的具体实现
8.4 Bootstrapping clients from a Channel
假设你的服务器端在需要处理一个客户端请求时,需要将服务器端的角色定位一个第三方的客户端的角色,这种情形是很容易经常发生的,例如代理服务器,这种情况下你需要与一个组织上的已经存在的系统再次进行交互,例如web server或者数据库,在这种情况下,你就需要从ServerChannel中启动一个客户端的Channel
你可以按照小节8.2.1中描述的那样创建一个Bootstrap,但是这并不是一个最有效的解决方案,因为它需要你再次定义另一个EventLoop用来管理新的客户端的channel,这就会需要新增额外的线程,当进行在原有的Channel和客户端channel之间数据交互的时候,就会产生线程之间的上下文切换
一个更好的解决方案就是分享接收的Channel的EventLoop,通过将EventLoop传递给Bootstrap的group方法,因为被分配到同一个EventLoop所有的Channel使用同一个线程,这样就阻止了更多额外进程的创建和我们之前提及的上下文切换的问题,这种分享EventLoop的方案说明图如下8.4图展示:
实现EventLoop共享需要通过调用group方法来设置EventLoop,如下面的代码清单所示:
关于共享EventLoop这个主题我们在这个小节已经讨论过了,这个解决防范反映了我们在编写Netty应用的一个思路和应该遵守的方针:尽可能的重复利用EventLoop来减少创建线程的开销
8.5 Adding multiple ChannelHandlers during a bootstrap
在我们之前的展示的所有代码中,我们在bootstrap处理一个单独的ChannelHandler的时候只调用了handler和childhandler方法,这对于一些简单的应用来说是很高效的,但是这并不能满足我们的一些比较复杂的需求,例如,如果一个应用需要支持多协议,那么就需要多个ChannelHandler了,来替代哪些笨重冗余的一些类
我们之前提及过很多次,你可以将你需要的所有ChannelHandler链式的放入到ChannelPipeline中去,但是如果在启动过程中只能设置一个ChannelHandler时怎么办的?
为了解决这个使用案例,Netty提供了一个特殊的父类ChannelInboundHandlerAdapter
它定义了如下的方法:
这个方法提供了一个简单的途径可以将多个ChannelHandler添加到ChannelPipeline中,你可以简单地通过提供一个ChannelInitializer的具体实现给bootstrap,当EventLoop注册到Channel时,你自定义的initChannel的方法就会被调用,当所有的返回之后,ChannelInitializer实例将会将自己从ChannelPipeline中移除
下面的代码清单定义了一个ChannelInitializerImpl类,且将其注册到bootstrap的childHandler方法上,你将会看到这个复杂操作可以被这么直白易懂地编写出
如果你的应用需要利用多个ChannelHandler,你可以自定义你自己的ChannelInitializer来将其安装到管道中
8.6 Using Netty ChannelOptions and attributes
当为每一个创建的channel一个个手动配置时将会变得非常冗余,幸运的是,我们不需要这么做,相反,你可以用option方法来将ChannelOptions应用到bootstrap中,你提供的一些属性值将会自动应用到bootstrap创建的所有channel中去,ChannelOptions中可以配置一些底层的连接细节例如keep-alive或者超时配置和buffer配置
Netty应用经常需要与一些项目中专属应用交互,Netty中的一些组件可能会被用在Netty项目之外的项目中运用到,例如Channel组件,在这种场景中,一些常用的属性和数据将会变得不可用,Netty提供了AttributeMap抽象,这个集合是由channel和bootstrap类提供的,和AttributeKey<T>这是一个通用的类用来帮助你插入和检索一些属性值,有了这些工具类,你可以在客户和服务端Channel中安全地交互任何形式的数据
思考一下下面的例子,当一个服务器端的应用需要追踪用户和channel之间关系的时候,你可以通过存储一个user的id做为一个channel的属性来实现这个功能,这个简单的小功能可以依据他们的id来路由到他们的客户端信息,或者来关闭客户端连接当channel中长时间没有任何活动的时候
下面的代码清单向你展示了如何使用ChannelOption来配置一个Channel这个属性中存储了一个Integer类型的变量
8.7 Bootstrapping DatagramChannels
先前的bootstrap代码示例都是使用ServerChannel,这是基于TCP的,但是有时候Bootstrap也可以用于无连接的协议,Netty提供了各种各样的DatagramChannel的具体实现来达到这个目的,唯一的不同就是你不用connect方法而是使用bind方法,如下面代码展示:
8.8 Shutdown
Bootstrapping使你的应用进入运行的状态,但是迟早你都需要将应用优雅地关闭,当然,你可以使用JVM处理,让所有的应用退出,但这不能满足“优雅”二字,这两个字代表了可以很干净地去释放所有的资源,其实在关闭Netty应用的时候,并没有太多需要特别关注的,只需要有几点细节需要牢记在心
首先,你需要先关闭EventLoopGroup,这可以处理任何后续的事件和任务,接下来就是释放所有的存活的线程,你只需要调用EventLoopGroup的shutdownGracefully方法,这个方法将会返回一个Future对象,当关闭的操作完成的时候,将会通知Future对象,注意到shutdownGracefully这个方法也是异步操作,所以你需要要么阻塞当前的操作指导关闭操作的完成,要么注册一个监听器来监听Future对象来确定关闭是否完成
下面的代码清单展示了如何优雅地关闭
另外,你可以在调用EventLoopGroup的shutdownGracefully方法之前调用Channel.close方法来清晰地关闭所有存活的channel对象,但是在所有的案例中,请一定要关闭EventLoopGroup对象本身
8.9 Summary
在这个章节中,我们学习了如何启动Netty的客户端和服务器端,包括使用无连接的协议,我们使用了一些具体的案例来讲解,这些案例包括在服务器端的应用去启动一个客户端的channel,还包括在启动过程中如何使用ChannelInitializer来处理安装多个channel
你也了解到在channel中进行配置一些具体的选项,如何使用attribute来将一些附加的信息添加到channel中,最后我们学习到如何优雅地关闭一个应用来按顺序地去释放所有的资源
在下一个章节,我们将学习Netty提供的一些工具,用来测试你自定义的ChannelHandler的实现