Netty学习摘记 —— 再谈引导
本文参考
本篇文章是对《Netty In Action》一书第八章"引导"的学习摘记,主要内容为引导客户端和服务端、从channel内引导客户端、添加ChannelHandler和使用ChanneOption
引导类层次结构
服务端ServerBootstrap和客户端Bootstrap都继承和实现了抽象类AbstractBootstrap
抽象类AbstractBootstrap实现了Cloneable接口,当需要创建多个具有类似配置或者完全相同配置的Channel时,不需要为每个Channel都创建并配置一个新的引导类实例,在一个已经配置完成的引导类实例上调用clone()方法将返回另一个可以立即使用的引导类实例
注意,这种方式只会创建引导类实例的EventLoopGroup的一个浅拷贝,所以,被浅拷贝的EventLoopGroup将在所有克隆的Channel实例之间共享。因为通常这些克隆的Channel的生命周期都很短暂,一个典型的场景是创建一个Channel以进行一次HTTP请求
public abstract B clone();
Returns a deep clone of this bootstrap which has the identical configuration. This method is useful when making multiple Channels with similar settings. Please note that this method does not clone the EventLoopGroup deeply but shallowly, making the group a shared resource
Bootstrap类引导客户端
A Bootstrap that makes it easy to bootstrap a Channel to use for clients.
The bind() methods are useful in combination with connectionless transports such as datagram (UDP). For regular TCP connections, please use the provided connect() methods.
Bootstrap类负责为客户端和使用无连接协议的应用程序创建Channel,原书中的此图我认为有错误,调用bind()方法创建的是UDP连接,并不会再调用connect()方法
下面的代码引导了一个使用NIO TCP传输的客户端
//设置 EventLoopGroup,提供用于处理 Channel 事件的 EventLoop
EventLoopGroup group = new NioEventLoopGroup();
//创建一个Bootstrap类的实例以创建和连接新的客户端Channel
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
//指定要使用的Channel 实现
.channel(NioSocketChannel.class)
//设置用于 Channel 事件和数据的ChannelInboundHandler
.handler(new SimpleChannelInboundHandler<ByteBuf>() {
@Override
protected void channelRead0(
ChannelHandlerContext channelHandlerContext,ByteBuf byteBuf) throws Exception {
System.out.println("Received data");
}
});
//连接到远程主机
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();
}
}
});
注意EventLoopGroup和Channel传输类型的兼容,OIO和NIO不可混用,以及在调用bind()或者connect()方法之前,必须调用group()、channel()或者channelFactory()、handler()方法添加组件,否则将会导致IllegalStateException异常
ServerBootstrap类引导服务器
Bootstrap sub-class which allows easy bootstrap of ServerChannel
在前面"深入了解Netty核心组件"的文章中,我们已经了解到Server端需要配置两个EventLoopGroup,第一组将只包含一个 ServerChannel,代表服务器自身的已绑定到某个本地端口的正在监听的套接字,而第二组将包含所有已创建的用来处理传入客户端连接的 Channel
此处我们可以看到服务端和客户端的不同之处
服务器致力于使用一个父Channel来接受来自客户端的连接,并创建子Channel以用于它们之间的通信;而客户端将最可能只需要一个单独的、没有父Channel的Channel来用于所有的网络交互
因此在ServerBootstrap类中存在Bootstrap类所没有的方法:childHandler()、 childAttr()和childOption()
拿handler()和childHandler()两个方法来说,handler()设置被添加到ServerChannel的ChannelPipeline中的ChannelHandler,而childHandler()设置将被添加到已被接受的子Channel的ChannelPipeline中的Channel- Handler。前者所添加的 ChannelHandler 由接受子 Channel 的 ServerChannel 处理,而 childHandler()方法所添加的ChannelHandler将由已被接受的子Channel 处理,其代表一个绑定到远程节点的套接字
下面的代码引导了一个使用NIO TCP传输的服务端
NioEventLoopGroup group = new NioEventLoopGroup();
//创建 Server Bootstrap
ServerBootstrap bootstrap = new ServerBootstrap();
//设置 EventLoopGroup,其提供了用于处理 Channel 事件的EventLoop
bootstrap.group(group)
//指定要使用的 Channel 实现
.channel(NioServerSocketChannel.class)
//设置用于处理已被接受的子 Channel 的I/O及数据的 ChannelInboundHandler
.childHandler(new SimpleChannelInboundHandler<ByteBuf>() {
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf)
throws Exception {
System.out.println("Received data");
}
});
//通过配置好的 ServerBootstrap 的实例绑定该 Channel
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("Bind attempt failed");
channelFuture.cause().printStackTrace();
}
}
});
从Channel引导客户端
若有一个第三方的客户端接入现有的服务端 / 客户端连接,我们可以通过将已被接受的子Channel的EventLoop传递给Bootstrap 的group()方法来共享该EventLoop,以此避免创建一个新的Bootstrap和额外的线程开销,这种共享的解决方案如图所示
//创建 ServerBootstrap 以创建 ServerSocketChannel,并绑定它
ServerBootstrap bootstrap = new ServerBootstrap();
//设置 EventLoopGroup,其将提供用以处理 Channel 事件的 EventLoop
bootstrap.group(new NioEventLoopGroup(), new NioEventLoopGroup())
//指定要使用的 Channel 实现
.channel(NioServerSocketChannel.class)
//设置用于处理已被接受的子 Channel 的 I/O 和数据的 ChannelInboundHandler
.childHandler(new SimpleChannelInboundHandler<ByteBuf>() {
ChannelFuture connectFuture;
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//创建一个 Bootstrap 类的实例以连接到远程主机
Bootstrap bootstrap = new Bootstrap();
//指定 Channel 的实现
bootstrap.channel(NioSocketChannel.class).handler(
//为入站 I/O 设置 ChannelInboundHandler
new SimpleChannelInboundHandler<ByteBuf>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
System.out.println("Received data");
}
});
//使用与分配给已被接受的子Channel相同的EventLoop
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()) {
//当连接完成时,执行一些数据操作(如代理)
// do something with the data
}
}
});
//通过配置好的 ServerBootstrap 绑定该 ServerSocketChannel
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("Bind attempt failed");
channelFuture.cause().printStackTrace();
}
}
});
在引导过程中添加多个ChannelHandler
我们在"Netty客户端 / 服务端概览"一文中已经接触了这中添加ChannelHandler的方式,Netty 提供了一个特殊的ChannelInboundHandlerAdapter子类ChannelInitializer
A special ChannelInboundHandler which offers an easy way to initialize a Channel once it was registered to its EventLoop. Implementations are most often used in the context of Bootstrap.handler(ChannelHandler) , ServerBootstrap.handler(ChannelHandler) and ServerBootstrap.childHandler(ChannelHandler) to setup the ChannelPipeline of a Channel.
它的initChannel方法法提供了一种将多个 ChannelHandler 添加到一个 ChannelPipeline 中的简便方法。我们只需要简单地向 Bootstrap 或 ServerBootstrap 的实例提供你的 ChannelInitializer实现即可,并且一旦Channel被注册到了它的EventLoop之后,就会调用 initChannel(),在该方法返回之后,ChannelInitializer的实例将会从 ChannelPipeline中移除它自己
下面的代码定义了ChannelInitializerImpl类,并通过ServerBootstrap的 childHandler()方法注册它
public void bootstrap() throws InterruptedException {
//创建 ServerBootstrap 以创建和绑定新的 Channel
ServerBootstrap bootstrap = new ServerBootstrap();
//设置 EventLoopGroup,其将提供用以处理 Channel 事件的 EventLoop
bootstrap.group(new NioEventLoopGroup(), new NioEventLoopGroup())
//指定 Channel 的实现
.channel(NioServerSocketChannel.class)
//注册一个 ChannelInitializerImpl 的实例来设置 ChannelPipeline
.childHandler(new ChannelInitializerImpl());
//绑定到地址
ChannelFuture future = bootstrap.bind(new InetSocketAddress(8080));
future.sync();
}
//用以设置 ChannelPipeline 的自定义 ChannelInitializerImpl 实现
//在大部分的场景下,如果不需要使用只存在于SocketChannel上的方法使用ChannelInitializer<Channel>即可
//否则你可以使用ChannelInitializer<SocketChannel>,其中 SocketChannel 扩展了Channel
final class ChannelInitializerImpl extends ChannelInitializer<Channel> {
@Override
//将所需的 ChannelHandler 添加到 ChannelPipeline
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpClientCodec());
pipeline.addLast(new HttpObjectAggregator(Integer.MAX_VALUE));
}
}
ChannelOption和attr属性
A ChannelOption allows to configure a ChannelConfig in a type-safe way. Which ChannelOption is supported depends on the actual implementation of ChannelConfig and may depend on the nature of the transport it belongs to.
option()-> 指定要应用到新创建的 ServerChannel 的 ChannelConfig 的 ChannelOption。这些选项将会通过bind()方法设置到Channel。在 bind()方法 被调用之后,设置或者改变 ChannelOption 都不会有任何的效果,另外还有针对子Channel的childOption()方法
attr() -> 指定ServerChannel上的属性,属性将会通过bind()方法设置给Channel。 在调用bind()方法之后改变它们将不会有任何的效果,另外还有针对子Channel的childAttr()方法
我们可以使用 option()方法来将 ChannelOption 应用到引导。我们提供的值将会被自动应用到引导所创建的所有Channel。可用的ChannelOption包括了底层连接的详细信息,如 keep-alive或者超时属性以及缓冲区设置
Netty 提供了 AttributeMap 抽象(一个由 Channel 和引导类提供的集合)以及 AttributeKey<T>(一 个用于插入和获取属性值的泛型类)。使用这些工具,便可以安全地将任何类型的数据项与客户 端和服务器Channel(包含ServerChannel的子Channel)相关联
下面的代码展示了可以如何使用ChannelOption 来配置Channel,以及如果使用属性来存储整型值
//创建一个 AttributeKey 以标识该属性
final AttributeKey<Integer> id = AttributeKey.newInstance("ID");
//创建一个 Bootstrap 类的实例以创建客户端 Channel 并连接它们
Bootstrap bootstrap = new Bootstrap();
//设置 EventLoopGroup,其提供了用以处理 Channel 事件的 EventLoop
bootstrap.group(new NioEventLoopGroup())
//指定 Channel 的实现
.channel(NioSocketChannel.class)
.handler(
//设置用以处理 Channel 的 I/O 以及数据的 ChannelInboundHandler
new SimpleChannelInboundHandler<ByteBuf>() {
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
//使用 AttributeKey 检索属性以及它的值
Integer idValue = ctx.channel().attr(id).get();
// do something with the idValue
}
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext,ByteBuf byteBuf)
throws Exception {
System.out.println("Received data");
}
}
);
//设置 ChannelOption,其将在 connect()或者bind()方法被调用时被设置到已经创建的 Channel 上
bootstrap.option(ChannelOption.SO_KEEPALIVE, true)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);
//存储该 id 属性
bootstrap.attr(id, 123456);
//使用配置好的 Bootstrap 实例连接到远程主机
ChannelFuture future = bootstrap.connect(new InetSocketAddress("www.manning.com", 80));
future.syncUninterruptibly();
引导DatagramChannel
前面的引导代码示例使用的都是基于 TCP 协议的 SocketChannel,但是 Bootstrap 类也可以被用于无连接的协议。为此,Netty 提供了各种DatagramChannel的实现。唯一区别就是,不再调用connect()方法,而是只调用bind()方法,这也印证了前面Bootstrap引导过程图中的错误之处
//创建一个 Bootstrap 的实例以创建和绑定新的数据报 Channel
Bootstrap bootstrap = new Bootstrap();
//设置 EventLoopGroup,其提供了用以处理 Channel 事件的 EventLoop
bootstrap.group(new OioEventLoopGroup()).channel(
//指定 Channel 的实现
OioDatagramChannel.class).handler(
//设置用以处理 Channel 的 I/O 以及数据的 ChannelInboundHandler
new SimpleChannelInboundHandler<DatagramPacket>() {
@Override
public void channelRead0(ChannelHandlerContext ctx, DatagramPacket msg) throws Exception {
// Do something with the packet
}
}
);
//调用 bind() 方法,因为该协议是无连接的
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("Bind attempt failed");
channelFuture.cause().printStackTrace();
}
}
});
关闭
最重要的是,我们需要关闭EventLoopGroup,即EventLoopGroup.shutdownGracefully()方法,它将处理任何挂起的事件和任务,并且随后释放所有活动的线程。这个方法调用将会返回一个Future,这个Future将在关闭完成时接收到通知
注意,shutdownGracefully()方法也是一个异步的操作,所以你需要阻塞等待直到它完成,或者向所返回的Future注册一个监听器以在关闭完成时获得通知
//创建处理 I/O 的EventLoopGroup
EventLoopGroup group = new NioEventLoopGroup();
//创建一个 Bootstrap 类的实例并配置它
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
//...
.handler(new SimpleChannelInboundHandler<ByteBuf>() {
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf)
throws Exception {
System.out.println("Received data");
}
}
);
bootstrap.connect(new InetSocketAddress("www.manning.com", 80)).syncUninterruptibly();
//...
//shutdownGracefully()方法将释放所有的资源,并且关闭所有的当前正在使用中的 Channel
Future<?> future = group.shutdownGracefully();
// block until the group has shutdown
future.syncUninterruptibly();
在最后我们调用了syncUninterruptibly()方法,它和sync()方法的区别如下:
/**
* Waits for this future until it is done, and rethrows the cause of the failure if this future
* failed.
*/
Future<V> sync() throws InterruptedException;
/**
* Waits for this future until it is done, and rethrows the cause of the failure if this future
* failed.
*/
Future<V> syncUninterruptibly();