本章主要内容
启动器对于Netty应用客户端和服务端都适用。都是为了方便组合我们前面讨论过的组件,如Channel,ChannelHandler,编解码器等等。启动器也提供了相应的机制连接这些组件并让它们在后台工作。
本章会讲述如何将Netty下面这些组件组合起来发挥作用:
Netty提供了两种不同类型的启动器。第一种主要用于服务端的Channel,它接收新连接并创建子Channel服务新连接。第二种主要用于客户端类似的Channel,它不接受新连接,所有的处理过程都在父Channel中完成。客户端类型的Channel看起来挺费解的,它不仅仅指客户端应用,还包括没有子Channel的无连接传输。
例如它有一个应用场景就是为DatagramChannel服务的。这个Channel类型就是用于UDP协议的传输,也就是无连接的传输。因为UDP协议就是不需要连接的,和TCP是不同的。不需要连接的传输协议,就是无连接。这种无连接的传输,只需要一个Channel就可以处理所有的数据,不需要通过父子Channel来处理。
上面说的这两种启动器都继承了同一个父类AbstractBootstrap,请看下图。
前面的章节我们将了很多关于客户端和服务端的组件,基本都看着都差不多。为了客户端与服务端有一个通用的关系,所以Netty提供了AbstractBootstrap。通过这个共同的父类,本章讨论的客户端与服务端启动器不需要太多重复的代码和逻辑就可以达到完美的复用性。
大部分时候很多Channel的设置都是一样或类似的。为了替代每个Channel去创建一个新的启动器,所以AbstractBootstrap实现了Cloneable接口。也就是说深拷贝一个配置好的启动器可以直接重用,不需要再去执行那些配置API。Netty唯一允许浅拷贝的就是启动器的EventLoopGroup,所以拷贝重用的Channel之间会共享EventLoopGroup。这样做好处还是蛮大的,比如很多Channel的存活时间都很短,例如HTTP的请求。
首先我们先来研究Bootstrap和ServerBootstrap。先从Bootstrap,因为它比ServerBootstrap要简单一些。
首先来看看启动器提供了哪些方法。
名称 |
描述 |
group(...) |
使用启动器设置EventLoopGroup,EventLoopGroup主要用来服务Channel的IO操作 |
channel(...) |
设置Channel使用的类 |
channelFactory(...) |
如果Channel类没有无参构造函数,可以使用这个来解决 |
localAddress(...) |
Channel绑定的本地地址,如果不指定,会随机选择一个,也可以使用bind(...) 或connect(...)设置这个本地地址 |
option(...) |
设置Channel的配置,应用于ChannelConfig。bind或connect方法执行时会设置这些配置, 当bind或connect方法执行完后再去修改这些配置不会起作用 |
attr(...) |
设置Channel的属性,和上面的option(...)方法类似,在bind或connect方法执行时设置 |
handler(...) |
添加ChannelHandler |
clone() |
复制启动器 |
remoteAddress(...) |
设置连接的远程地址,也可以在使用connect(...)方法的时候指定 |
connect(...) |
连接远程地址会返回一个ChannelFuture。连接完成之后会获得通知。结果可能 成功也可能失败 |
bind(...) |
绑定Channel,也会返回ChannelFuture。绑定操作完成之后会获得通知 |
下面我们来学习一下如何启动一个客户端,然后会给出一个例子给大家参考。
启动客户端有不同的方法,下面来介绍一下它们如何工作的。
Bootstrap负责启动客户端或无连接类型的应用,Channel会在bind(...)或connect(...)方法执行后创建。
Bootstrap会在指定bind方法或connect方法后创建Channel,下面的代码展示了如何使用NIO TCP协议启动客户端。
//创建一个Bootstrap实例
Bootstrap bootstrap = new Bootstrap();
//指定EventLoopGroup注册Channel
bootstrap.group(new NioEventLoopGroup())
//指定Channel类型为NioSocketChannel
.channel(NioSocketChannel.class)
//添加ChannelHandler
.handler(new SimpleChannelInboundHandler() {
@Override
protected void channeRead0(ChannelHandlerContext channelHandlerContext,
ByteBuf byteBuf) throws Exception {
System.out.println("Reveived data");
byteBuf.clear();
}
});
//使用配置好的启动器连接远程地址
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();
}
}
});
可以看到,处理bind或connect方法,Bootstrap其他方法返回的都是自己的引用,这样就会有比较著名的链式方法代码。这和我们前面学到的ByteBuf的API比较类似,这也是Netty统一风格API的一个体现。
Channel实现类和所配置的EventLoopGroup或EventLoop需要正确兼容。哪个Channel适配哪个EventLoopGroup可以在API文档里面看到。有一个规则就是能兼容的EventLoopGroup和Channel都在同一个包中。例如,你可以看到NioEventLoop,NioEventLoopGroup和NioServerSocketChannel都在同一个包中。注意它们都有一个“Nio”的前缀。当你使用其他前缀的Channel时,例如“Oio”前缀就不行,OioEventLoopGroup和NioServerSocketChannel就不能兼容使用。
EventLoop是指定给Channel处理所有操作的,返回ChannelFuture的方法都是在EventLoop中执行的,而EventLoop是其所在的线程中执行的。EventLoopGroup包含一个或多个EventLoop,它主要是用来分配EventLoop给Channel的。
如果你使用不兼容的EventLoopGroup和Channel,程序就会失败。
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(new NioEventLoopGroup())
.channel(OioSocketChannel.class)
.handler(new SimpleChannelInboundHandler() {
@Override
protected void channelRead0(
ChannelHandlerContext channelHandlerContext,
ByteBuf byteBuf) throws Exception {
System.out.println("Reveived data");
byteBuf.clear();
}
});
ChannelFuture future = bootstrap.connect(
new InetSocketAddress("www.manning.com", 80));
future.syncUninterruptibly();
启动上面的客户端,就会抛出IllegalStateException异常。
Exception in thread "main" java.lang.IllegalStateException: incompatible
event loop type: io.netty.channel.nio.NioEventLoop
at
io.netty.channel.AbstractChannel$AbstractUnsafe.register(AbstractChannel.java
:571)
at
io.netty.channel.SingleThreadEventLoop.register(SingleThreadEventLoop.java:57
)
at
io.netty.channel.MultithreadEventLoopGroup.register(MultithreadEventLoopGroup
.java:48)
at
io.netty.bootstrap.AbstractBootstrap.initAndRegister(AbstractBootstrap.java:2
98)
at io.netty.bootstrap.Bootstrap.doConnect(Bootstrap.java:133)
at io.netty.bootstrap.Bootstrap.connect(Bootstrap.java:115)
at
com.manning.nettyinaction.chapter9.InvalidBootstrapClient.bootstrap(InvalidBo
otstrapClient.java:30)
at
com.manning.nettyinaction.chapter9.InvalidBootstrapClient.main(InvalidBootstr
apClient.java:36)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at
sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.
java:43)
at java.lang.reflect.Method.invoke(Method.java:601)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:120)
所以构建启动器实例的时候要注意可能出错的地方。除了上面的这个不兼容问题,还有其他情况也会导致抛出IllegalArgumentException。例如在执行bind(…)或connect(…)方法前,没有执行那些重要配置的方法,如下:
前面我们学习了怎么启动客户端或无连接型的应用,接下来该学习如何启动服务端应用了。你将会发现它和客户端很像,而且有一些逻辑基本是一样的。这一小节就是学习服务端启动器提供的方法和如何启动服务端。
首先看看ServerBootstrap提供了哪些方法。
名称 |
描述 |
group(...) |
也是设置EventLoopGroup |
channel(...) |
设置Channel实习类 |
channelFactory(...) |
Channel没有无参构造函数使用此方法 |
localAddress(...) |
设置本地地址 |
option(...) |
设置ServerChannel的ChannelConfig |
childOption(...) |
设置接收的新Channel的ChannelConfig |
attr(...) |
设置ServerChannel的属性 |
childAttr(...) |
设置接收的新Channel的属性 |
handler(...) |
添加ChannelHandler到ServerChannel的ChannelPipeline中,这个一般不用 |
childHandler(...) |
添加ChannelHandler到新接收的Channel的ChannelPipeline中,这个很常用很重要 |
clone() |
克隆ServerBootstrap实例 |
bind(...) |
服务端绑定到本地地址和端口上 |
下一小节将会介绍如何启动服务端,并给个例子参考。
通过上面的表格可以看出,服务端启动器的方法很多都和客户端启动器的方法一致。
只有一个不同的地方,就是ServerBootstrap不仅有handler(...),attr(...)和option(...)这些方法,还有相应的以“child”开头的方法。ServerBootstrap提供“child”开头的方法就是为了负责子Channel的创建与配置,ServerChannel主要负责接收新连接然后创建对应的子Channel,如下图。
记住“child”开头的方法只对子Channel有效。
为了更深入学习ServerBootstrap,我们来尝试一个使用NioServerSocketChannel的例子。NioServerChannel负责接收新连接并创建NioSocketChannel实例。
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(new NioEventLoopGroup(), new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new SimpleChannelInboundHandler() {
@Override
protected void channelRead0(ChannelHandlerContext ctx,
ByteBuf byteBuf) throws Exception {
System.out.println("Reveived data");
byteBuf.clear();
}
});
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();
}
}
});
有的时候我们需要在一个Channel中启动一个新的Netty客户端,例如,需要编写一个代理或从其他系统获取数据。当你的应用需要配合已经存在的系统时经常就需要从系统系统获取数据。例如可能在Netty应用里面认证然后直接去查询数据库。
遇到上面的情况你可以创建一个新的Bootstrap然后启动客户端,这样做也可能达到目的,但并不是最好的解决办法;因为这个时候你需要使用另一个EventLoop去处理这个新创建的客户端Channel,如果你还需要在新接收的Channel和新创建的客户端Channel之间交换数据,就必须在两个线程之间交换数据,这就会引发一系列的问题。
幸运的是,Netty提供了优化的办法,可以将新接收Channel的EventLoop传到启动器的eventLoop(...)中,这样新的客户端Channel就可以使用同一个EventLoop了。因为EventLoop继承了EventLoopGroup这样就省略了那些线程上下文切换的额外工作。
除了省略了上下文切换,而且也不需要创建多余的线程来使用启动器。下图展示了同一个EventLoop作用多个Channel。
共享EventLoop也非常容易,只需要在调用启动器的eventLoop(...)方法时设置这个EventLoop即可,可以参考下面的代码。
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(new NioEventLoopGroup(), new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new SimpleChannelInboundHandler() {
ChannelFuture connectFuture;
@Override
public void channelActive(ChannelHandlerContext ctx)
throws Exception {
//创建客户端启动器
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class)
.handler(
new SimpleChannelInboundHandler() {
@Override
protected void channelRead0(
ChannelHandlerContext ctx,
ByteBuf in) throws Exception {
System.out.println("Reveived data");
in.clear();
}
});
//指定EventLoop为新的客户端EventLoopGroup
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("Bound attempt failed");
channelFuture.cause().printStackTrace();
}
}
});
这一小节我们主要学习了如何复用
EventLoop,Netty应用要尽可能的复用已有对象。如果不复用,要确保不会创建太多的实例,否则就有可能耗尽系统资源。
前面我们给出的代码例子大多都是只添加了一个ChannelHandler。但是复杂的应用往往需要添加多个ChannelHandler。比如要开发HTTP或WebSocket协议的应用,不仅要解码字节数据,还要转换成协议对象,肯定会有多个ChannelHandler。当然,Netty提供了非常易用的方式添加多个ChannelHandler。
Netty有一个优势就很方便的ChannelPipeline添加多个ChannelHandler,并且可以复用其中的大部分代码。但是如果在启动过程中只能设置一个ChannelHandler该怎么做呢?答案很简单,只使用一个,就使用一个特殊的。
为此Netty提供了一个抽象类ChannelInitializer,你可以通过继承它来初始化Channel。Channel注册EventLoop后这个ChannelInitializer就会被调用,这个时候你就可以向ChannelPipeline中添加多个ChannelHandler。ChannelInitializer方法执行完后,就会从ChannelInitializer移除。
听起来好像很复杂的样子,我们来看看代码是什么样子的。
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(new NioEventLoopGroup(), new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializerImpl());
ChannelFuture future = bootstrap.bind(new InetSocketAddress(8080));
future.sync();
final class ChannelInitializerImpl extends ChannelInitializer {
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpClientCodec());
pipeline.addLast(new HttpObjectAggregator(Integer.MAX_VALUE));
}
}
其实也就是自定义一个特殊的ChannelHandler,也就是
ChannelInitializer。在初始化Channel的时候添加自己需要的多个ChannelHandler即可。
当创建一个Channel的时候去配置它的属性是很麻烦的事情,因此Netty可以在启动器设置过程中设置ChannelOptions。这些配置会设置所有新创建的Channel上。这些属性包含很多个,甚至还有底层连接相关的属性例如keepalive或timeout。
Netty应用往往会与一个专有软件集成。这种情况下Netty的很多组件,例如Channel,会绕过在Netty中正常生命周期。这个时候,并不是所有常用属性都是有效的。因此,Netty提供了Channel属性配置。
这些属性可以让Channel和数据安全关联,这些属性对客户端和服务端都是有效的。
例如,一个Web应用客户端向服务端发出请求,为了确定这个Channel属于哪个用户,应用会将用户id作为Channel的属性存储起来。同样,任何对象或数据块都可以作为Channel属性存储起来。
使用ChannelOptions和属性会让很多事情变得简单。例如开发一个Netty的WebSocket服务端用来转发消息。通过使用Channel属性存储用户编号,就知道了什么消息该转发给谁。通过ChannelOptions配置,就可以将多久没收收到消息的Channel关闭。可以看下面的代码。
//创建AttributeKey存储属性
final AttributeKey id = new AttributeKey("ID");
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new SimpleChannelInboundHandler() {
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
//获取用户id
Integer idValue = ctx.channel().attr(id).get();
}
@Override
protected void channelRead0(
ChannelHandlerContext channelHandlerContext,
ByteBuf byteBuf) throws Exception {
System.out.println("Reveived data");
byteBuf.clear();
}
});
bootstrap.option(ChannelOption.SO_KEEPALIVE, true)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);
//设置属性
bootstrap.attr(id, 123456);
ChannelFuture future = bootstrap.connect(
new InetSocketAddress("www.manning.com", 80));
future.syncUninterruptibly();
前面的例子我们在启动器中使用
SocketChannel,这一般用于基于TCP的协议。前面还提到过启动器可以用户无连接协议例如UDP。为此Netty提供了DatagramChannel这个实现。它们唯一的不同就是DatagramChannel不需要调用connect(...)方法,只需要使用bind(...)即可。 Bootstrap bootstrap = new Bootstrap();
bootstrap.group(new OioEventLoopGroup())
.channel(OioDatagramChannel.class)
.handler(new SimpleChannelInboundHandler() {
@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("Bound attempt failed");
channelFuture.cause().printStackTrace();
}
}
});
Netty自带了很多优秀的默认配置。大部分情况下这些默认配置不需要修改,但是有些时候需要细粒度控制的应用,这种情况下Netty也提供了易用的方式帮助你完成配置。
这一章我们主要学习了如何启动基于Netty的客户端或服务端应用,也学习了如何设置指定配置,如何在Channel中使用属性。无连接的应用也启动尝试过,已经它和有连接的区别。
下一章我们会学习在实际项目中如何使用Netty,这可以帮助你如何构建自己的完整应用,学完这些知识你就有能力开发自己的Netty应用了。