上一篇主要介绍了Netty Server端的开发,并且根据源码分析了Server的启动过程。这篇我们主要介绍一下如何编写客户端,并对之前文章中没有提到ChannelPipeline和ChannelHandler,ChannelHandlerContext进行解释。本文中的完整源码可以看这篇文章。亲测可以运行。
PS:本文只是针对初学Netty的同学看,不会特别深入,如果看完本文,你觉得还是不过瘾,强烈推荐闪电侠同学,这位同学对于Netty源代码分析的非常透彻,我看了之后受益匪浅。
客户端代码
客户端代码编写基本和服务端的差不多。只是它的Channel需要设置为NioSocketChannel。下面是一个简单的例子。
public class MyClient {
public static void main(String[] args) throws Exception {
// 配置客户端NIO线程组
//首先创建客户端处理I/O读写的NioEventLoop Group线程组
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
try {
//继续创建客户端辅助启动类Bootstrap,随后需要对其进行配置。
//与服务端不同的是,它的Channel需要设置为NioSocketChannel
//然后为其添加handler。
// 在初始化它的时候将它的ChannelHandler设置到ChannelPipeline中,用于处理网络I/O事件。
Bootstrap b = new Bootstrap();
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(eventLoopGroup).channel(NioSocketChannel.class)
.handler(new MyClientInitializer());
// 发起异步连接操作
//客户端启动辅助类设置完成之后,调用connect方法发起异步连接,
//然后调用同步方法等待连接成功。
ChannelFuture channelFuture = bootstrap.connect("localhost", 8899).sync();
channelFuture.channel().closeFuture().sync();
}
finally {
// 优雅退出,释放NIO线程组
//在退出之前,释放NIO线程组的资源。
eventLoopGroup.shutdownGracefully();
}
}
}
下面我们重点看一下MyClientInitializer
这个类。
public class MyClientInitializer extends ChannelInitializer{
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//(1) 加入拆包器
pipeline.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE,0,4,0,4));
//(2) 加入粘包器
pipeline.addLast(new LengthFieldPrepender(4));
//字符串解码 (3)
pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
//字符串编码 (4)
pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
//(5)
pipeline.addLast(new MyClientHandler());
}
}
可以看到了之前没有遇到过的ChannelPipeline
, 下面通过对ChannelPipeline
的源码分析看一下他到底是干什么的。
ChannelPipeline简介
通过源代码可以看到,AbstractChannel中包含了一个pipeline,DefaultChannelPipeline
继承了ChannelPipeline
。他们的关系如下:
看一下DefaultChannelPipeline类的部分源代码:
.....
final AbstractChannelHandlerContext head; //保存handler上下文信息
final AbstractChannelHandlerContext tail;
....
protected DefaultChannelPipeline(Channel channel) {
....
tail = new TailContext(this); //实现ChannelOutboundHandler接口
head = new HeadContext(this); //实现ChannelInboundHandler接口
head.next = tail;
tail.prev = head;
}
在ChannelPipeline中最为重要的是ChannelHandlerContext
,ChannelInboundHandler
以及ChannelOutboundHandler
三个类。ChannelHandlerContext
负责控制事件的流动,将事件从一个ChannelHandler传播到下一个ChannelHandler中处理。
传播顺序(按照MyClientInitializer的例子):
入站:1---->3---->5
出站:5---->4---->2
那么如何控制事件的流转呢?
在ChannelHandlerContext
代码中可以发现很多以"fire"开头的函数,这些函数的作用就是将事件传播到下一个Handler。
入站事件一般由I/O线程触发,可以归纳为下面几种
ChannelRegistered() // Channel注册到EventLoop
ChannelActive() // Channel激活
ChannelRead(Object) // Channel读取到数据
ChannelReadComplete() // Channel读取数据完毕
ExceptionCaught(Throwable) // 捕获到异常
UserEventTriggered(Object) // 用户自定义事件
ChannelWritabilityChanged() // Channnel可写性改变,由写高低水位控制
ChannelInactive() // Channel不再激活
ChannelUnregistered() // Channel从EventLoop中注销
出站事件一般由用户触发,可以归纳为下面几种
bind(SocketAddress, ChannelPromise) // 绑定到本地地址
connect(SocketAddress, SocketAddress, ChannelPromise) // 连接一个远端机器
write(Object, ChannelPromise) // 写数据,实际只加到Netty出站缓冲区
flush() // flush数据,实际执行底层写
read() // 读数据,实际设置关心OP_READ事件,当数据到来时触发ChannelRead入站事件
disconnect(ChannelPromise) // 断开连接,NIO Server和Client不支持,实际调用close
close(ChannelPromise) // 关闭Channel
deregister(ChannelPromise) // 从EventLoop注销Channel
总结一下:ChannelPipeline 就是一个容器,或者理解成一个双向链表更加形象,用来存放入站和出站事件的Handler。出站入站事件可以通过ChannelHandlerContext传递到下一个Handler中。具体ChannelPipeline如何做到初始化、添加节点、删除节点的操作,大家可以参考这篇文章。
ChannelPipeline、ChannelHandler、ChannelHandlerContext、Channel他们之间的关系可以用下面的图表示:
ChannelHandler简介
我们看到ChannelHandler接口一共才包含三个方法(有一个被废弃)。它还定义了 Sharable 注解,使得Handler在ChannelPipeline中的实例只有一个。
根据代码中的注释我们可以推断出 handlerAdded在handler添加之后被调用。handlerRemoved 在handler被移出的时候被调用。
/**
* Gets called after the {@link ChannelHandler} was added to the actual context and it's ready to handle events.
*/
void handlerAdded(ChannelHandlerContext ctx) throws Exception;
/**
* Gets called after the {@link ChannelHandler} was removed from the actual context and it doesn't handle events
* anymore.
*/
void handlerRemoved(ChannelHandlerContext ctx) throws Exception;
下面我们逐个看一下ChannelHandler的各个子类。
ChannelHanderAdapter
ChannelHanderAdapter 只是个框架,里面的方法并没有具体内容。
ChannelOutboundHandler
ChannelOutboundHandler用于拦截出站事件。
/**
* Intercepts {@link ChannelHandlerContext#read()}.
*/
void read(ChannelHandlerContext ctx) throws Exception;
ChannelInboundHandler
ChannelInboundHandler用于拦截入站事件,函数名称大都以channelXXX命名,这里看名字也可以推断,当channel注册时,channel active的时候调用。
其他Handler
回到最开始的客户端代码,我们的最后一个handler 是 MyClientHandler,下面是该类的代买,他继承自SimpleChannelInboundHandler
public class MyClientHandler extends SimpleChannelInboundHandler{
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
//服务端的远程地址
System.out.println(ctx.channel().remoteAddress());
System.out.println("client output: "+msg);
ctx.writeAndFlush("from client: "+ LocalDateTime.now());
}
/**
* 当服务器端与客户端进行建立连接的时候会触发,如果没有触发读写操作,则客户端和客户端之间不会进行数据通信,也就是channelRead0不会执行,
* 当通道连接的时候,触发channelActive方法向服务端发送数据,触发服务器端的handler的channelRead0回调,然后
* 服务端向客户端发送数据触发客户端的channelRead0,依次触发。
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush("来自与客户端的问题!");
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
SimpleChannelInboundHandler
SimpleChannelInboundHandler的继承关系图如下
SimpleChannelInboundHandler继承自 ChannelInboundHandlerAdapter
。其中channelRead0可以用来处理特定类型的消息。比如在这个例子中的消息类型是String类型。消息的处理我们留到下一篇再讲,这里不再赘述。
protected abstract void channelRead0(ChannelHandlerContext ctx, I msg) throws Exception;
ChannelHandlerContext简介
ChannelHandlerContext 保存的是ChannelHandler 与ChannelPipeline的关系。比如你可以通过channel()方法拿到与ChannelPipeline 绑定的Channel,handler()方法拿到与ChannelHandlerContext绑定的Handler。
/**
* The {@link ChannelHandler} that is bound this {@link ChannelHandlerContext}.
*/
ChannelHandler handler();
ChannelHandlerContext的其他函数都fireXXX命名,上文有介绍过这样方法作用是将事件传递到下一个Handler中。ChannelHandlerContext的继承关系如下图,我们进入AbstractChannelHandlerContext看一下context是如何传递事件的。
我们以注册事件为例。
首先看一下DefaultChannelPipeline 中的fireChannelRegistered 方法。
DefaultChannelPipeline
@Override
public final ChannelPipeline fireChannelRegistered() {
AbstractChannelHandlerContext.invokeChannelRegistered(head);
return this;
}
该方法会调用AbstractChannelHandlerContext类中的静态方法invokeChannelRegistered,同时把head作为参数传入。
static void invokeChannelRegistered(final AbstractChannelHandlerContext next) {
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {
next.invokeChannelRegistered();
} else {
executor.execute(new Runnable() {
@Override
public void run() {
next.invokeChannelRegistered();
}
});
}
}
为了保证线程安全,Netty首先会确认该操作是在Reactor线程中执行的。invokeChannelRegistered 会直接调用fireChannelRegistered
private void invokeChannelRegistered() {
if (invokeHandler()) {
try {
((ChannelInboundHandler) handler()).channelRegistered(this);
} catch (Throwable t) {
notifyHandlerException(t);
}
} else {
fireChannelRegistered();
}
}
fireChannelRegistered 的源码如下,发现他会去调用findContextInbound方法。
@Override
public ChannelHandlerContext fireChannelRegistered() {
invokeChannelRegistered(findContextInbound());
return this;
}
findContextInbound 源码如下,他会线性搜索channelHandlerContext是否是inbound。那么什么是inbound呢?
private AbstractChannelHandlerContext findContextInbound() {
AbstractChannelHandlerContext ctx = this;
do {
ctx = ctx.next;
} while (!ctx.inbound);
return ctx;
}
DefaultChannelHandlerContext
在DefaultChannelHandlerContext类中找到了判断的方法。通过instanceof 方法判断是inboundHandler 还是outboundhandler。
private static boolean isInbound(ChannelHandler handler) {
return handler instanceof ChannelInboundHandler;
}
private static boolean isOutbound(ChannelHandler handler) {
return handler instanceof ChannelOutboundHandler;
}
回到上面的findContextInbound方法,该方法会线性查找到下一个inboundHandler。
至此 客户端这边的 ChannelPipeline和ChannelHandler,ChannelHandlerContext类分析完了,但是MyClientInitializer 类中的LengthFieldBasedFrameDecoder 类我们还没有分析,这里会涉及到Netty的拆包和解码,会在下一篇文章中介绍。