Netty学习之Bootstrapping
前言
在前面的内容中,我们基本把Netty的核心组件都学习完了,各个组件的作用及组件之间的关系也基本理清楚了,一个完整的Netty应用基本上也能写出来了,当然,还差最后一步,启动应用,本小节我们来学习如何启动一个Netty应用。
Bootstrap Class
Bootstrap类包含两个子类,Bootstrap
及ServerBootstrap
,分别对应于客户端应用及服务端应用,他们的区别在于,服务端需要两个Channel,父Channel用于建立连接,子Channel用于管理已经建立的连接。
Bootstrap常用API
-
group()
,指定所要使用的EventLoopGroup -
channel()
,选择所要使用的channel -
localAddress()
,指定所要绑定的地址 -
option()
,设置ChannelOption -
handler()
,设置ChannelHandler -
remoteAddress()
,设置远程地址 -
connect()
,连接到远程服务,并且返回一个ChannelFuture,用于通知结果 -
bind()
,绑定Channel,并且返回一个ChannelFuture,用于通知绑定结果
启动客户端
启动流程基本如下
public void start() {
// 指定EventLoopGroup
EventLoopGroup group = new NioEventLoopGroup();
// 创建Bootstrap
Bootstrap bootstrap = new Bootstrap();
// 绑定所要使用的EventLoopGroup
bootstrap.group(group)
.remoteAddress(new InetSocketAddress(HOST, PORT))
// 指定所要使用的Channel
// 要注意,Channel的类型必须跟EventLoop的类型相匹配
.channel(NioSocketChannel.class)
// 指定对应的处理器
// 如果只有一个handler,也可以直接添加即可
// 使用ChannelInitializer主要是用于添加多个handler
.handler(new ChannelInitializer() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new EchoClientHandler());
}
});
try {
ChannelFuture future = bootstrap.connect().sync();
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
try {
group.shutdownGracefully().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
需要注意的是,启动客户端,也就是调用bind()
或者connect()
之前,必须要配置group()
、channel()/channelFactory()
、handler()
,不然会报IllegalStateException
,其中hanlder()
是非常重要的,用于配置处理的逻辑操作。
在配置的时候,要注意指定的group()
与channel()
必须匹配,如NioEventLoopGroup
必须与NioSocketChannel
或者NioServerSocketChannel
,不能混用Nio
与Oio
,不然也会报IllegalStateException
可供使用的EventLoopGroup及Channel如下
channel
|--nio
| NioEventLoopGroup
|--oio
| OioEventLoopGroup
|--socket
|--nio
| NioDatagramChannel
| NioServerSocketChannel
| NioSocketChannel
|--oio
| OioDatagramChannel
| OioServerSocketChannel
| OioSocketChannel
启动服务端
private void start() {
final EchoServerHandler serverHandler = new EchoServerHandler();
// 创建一个EventLoopGroup--boss
EventLoopGroup boss = new NioEventLoopGroup();
// 创建一个EventLoopGroup--worker
EventLoopGroup worker = new NioEventLoopGroup();
// ServerBootstrap
ServerBootstrap bootstrap = new ServerBootstrap();
int port = 8888;
// 如果只配置一个group,则表示同一个group同于两个用途:父Channel、子Channel
// 如果配置两个,则分别使用啦
bootstrap.group(boss, worker)
// 设置地址
.localAddress(new InetSocketAddress(port))
// 指定使用NioServerSocketChannel
.channel(NioServerSocketChannel.class)
// 添加子处理器,用于处理建立之后的连接
// 这里需要注意,handler()方法是用于配置ServerChannel本身
// childHandler()才是用于配置建立的连接
.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(serverHandler);
}
});
try {
// .sync()表示等待绑定完成,当前线程会阻塞
ChannelFuture future = bootstrap.bind().sync();
// 等待关闭
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
try {
worker.shutdownGracefully().sync();
boss.shutdownGracefully().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
如果需要添加多个Handler,可以通过添加一个ChannelInitialize
的实现类对象,然后在该对象的protected vodi initChannel(Channel ch)
中通过channel.pipeline()
获取对应的pipeline,然后通过pipeline注册多个handler。
从Channel中启动一个客户端
有时候我们的服务端也需要充当客户端去连接其他的服务端,比如请求oauth授权、或者代理等。
可以直接在建立的channel中再起一个boostrap用于去连接第三方服务,但是这种操作不是很合理,这种方式需要重新起一个EventLoop(新建一个Channel,则会重新绑定了新的EventLoop),所以当在两个不同的channel交换数据时会带来额外的线程开销和上下文切换 。
更好地方式是通过调用group()
方法,共享已经建立连接的EventLoop,这样子对应的子channel也是在同一个线程上下文中,所以避免了上下文切换的消耗。
public class Test {
public static void main(String[] args) {
ServerBootstrap bootstrap = new ServerBootstrap();
EventLoopGroup boss = new NioEventLoopGroup();
EventLoopGroup worker = new NioEventLoopGroup();
bootstrap.group(boss, worker)
.channel(NioServerSocketChannel.class)
.childHandler(new SimpleChannelInboundHandler() {
ChannelFuture connectFuture;
/**
* 通道连接建立后,建立与第三方的连接
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 启动bootstrap充当客户端去连接新的服务
Bootstrap client = new Bootstrap();
client.channel(NioSocketChannel.class)
.handler(new SimpleChannelInboundHandler() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
System.out.println("Received data: " + msg.toString(CharsetUtil.UTF_8));
}
});
// 重点是这里,复用了父channel的eventLoop
client.group(ctx.channel().eventLoop());
connectFuture = client.connect(new InetSocketAddress("www.baidu.com", 80));
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
// 连接建立完成
if (connectFuture.isDone()) {
// 其他的操作,比如发送请求等
}
}
});
try {
ChannelFuture future = bootstrap.bind(8080).sync();
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
worker.shutdownGracefully();
boss.shutdownGracefully();
}
}
}
使用ChannelOptions或者attributes
如果手动为每个建立的Channel都进行配置,是一件非常痛苦的事情,所以Netty提供了option()
方法用于传入一个ChannelOptions
对象,每个配置会自动应用到所有建立的channel中。
此外,Netty还提供了AttributeMap
及其子类AttributeKey
用于在Channel中传递额外的属性信息,然后通过channel#attr("key")
可以将attributeKey
获取出来,然后通过其get
方法就能将对应的属性值获取出来。
public class Test {
public static void main(String[] args) {;
Bootstrap bootstrap = new Bootstrap();
EventLoopGroup group = new NioEventLoopGroup();
// 新建一个id属性键
final AttributeKey id = AttributeKey.newInstance("ID");
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new SimpleChannelInboundHandler() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
// ops
}
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
Integer integer = ctx.channel().attr(id).get();
}
});
bootstrap.option(ChannelOption.SO_KEEPALIVE, true)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);
// 设置属性键及其值
bootstrap.attr(id, 123456);
ChannelFuture future = bootstrap.connect("host", 8080);
future.syncUninterruptibly();
}
}
UDP
在之前的例子中,我们使用的都是基于TCP的连接方式,Netty3之后,同样支持UDP连接方式,只需要使用*DatagramChannel*
类,同时不使用connect()
或者bind()
即可。
public static void main(String[] args) {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(new OioEventLoopGroup())
.channel(OioDatagramChannel.class)
.handler(new SimpleChannelInboundHandler() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket msg) throws Exception {
}
});
}
关闭
关闭Netty应用的时候,需要关闭EventLoopGroup,EventLoopGroup绑定了很多线程嘛,通过调用eventLoopGroup.shutdownGracefully()
即可,由于该操作同样是异步操作,所以要么阻塞,要么注册一个监听器,该方法会释放所有资源并且关闭所有在使用的Channel
,也可以显示调用Channel.close()
,然后再关闭EventLoopGroup
在关闭的时候,我们需要根据情况,看是关闭当前的channel还是关闭整个服务,如果是关闭整个服务,则应该关闭当前channel对应的父channel(对于服务端来说),客户端只需要关闭当前channel即可。
关闭之后,Netty会发送“关闭事件”给服务端,并触发对应的事件,即一开始我们所编写的
// 绑定地址并且获取对应的channel,此时的channel是父channel
ChannelFuture future = bootstrap.bind(PORT).sync();
// 阻塞直至连接关闭
// 父channel关闭时,该操作会收到通知,进而关闭应用
future.channel().closeFuture().sync();
总结
本小节我们主要学习了Netty的Boostrap,这个组件是Netty必备组件的最后一个组件了,通过该组件将前面所有涉及到的组件串联起来,并且绑定地址,启动服务或者客户端,在Netty中,如果能复用EventLoop就应该尽量复用EventLoop,从而可以减少线程上下文的切换,比如在服务端需要重新启动另一个客户端的时候,这时就可以直接复用当前channel的EventLoop即可。