pigeon底层通过Netty-3.9.2.Final实现服务端和客户端的连接通信,对应实现类为NettyServer和NettyClient。在内部,处理RPC通信的核心逻辑又分别定义在NettyServerPipelineFactory和NettyClientPipelineFactory,这两个类都实现了ChannelPipelineFactory,重写了里面的getPipeline方法,用于处理发送请求和处理请求的相关流程逻辑。
pigeon TCP协议格式
pigeon目前区分两种TCP协议方式,一种是非统一(默认)协议,为普通序列化方式如Hessian,json等方式调用,另一种是统一协议,如Thrift调用和泛化调用,其中泛化调用可以在不直到api设计的基础上,直接通过指定方法名字符串来调用相应的服务方法。基于不同协议,tcp消息包格式也是不同的,下面分开解析
粘包半包问题
在TCP传输中,一个完整的消息包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,因而数据接收方无法区分消息包的头尾,在接收方来看,可能一次接收的数据内容只是一次完整请求的部分数据,或包含多次请求的数据等情况。基于此,常见有三种解决方式:
- 消息包定长,固定每个消息包长度,不够的话用空格补,缺点是长度不能灵活控制,字节不够的情况下补空格会造成浪费,字节过多还要处理分包传输的逻辑
- 使用定长消息头和变长消息体,其中消息体的长度必须在消息头指出,在接收方每次接收数据时,先确保获取到定长的消息头,再从消息头中解析出消息体的长度,以此来界定获取以此完整请求的消息包数据。
- 在消息包尾部指定分隔符,缺点是需要遍历数据,判断是否为分隔符,影响效率。
在pigeon中,是基于第二种,使用定长消息头和变长消息体的方式实现的。
定长消息头格式
在消息头部分,统一协议和默认协议的区别较大,这里分开讲述:
默认协议消息格式
默认协议消息格式具体包括2部分:消息头、消息体,其中消息体包含变长请求体和定长请求尾两部分。
默认协议消息头固定为7个字节:
- 第1-2个字节固定为十六进制的0x39、0x3A,即十进制的57、58,可以用来区分是默认协议还是统一协议。
- 第3个字节为序列化方式,如Hessian是2,java是3,json是7等。
- 第4-7个字节:消息体长度,int,占4个字节,值为请求体长度(请求或响应对象序列化后的字节长度)+请求尾长度11。
统一协议消息格式
类似默认协议消息,统一协议消息格式也包括2部分:消息头、消息体,和默认协议不同的是,统一协议中,消息体部分为完整请求体尾部不再包含请求尾,但会在请求体头部包含一个两字节长度的请求头。这里需要区分消息头和请求头的区别。
统一协议的消息头固定为8个字节:
- 第1-2个字节固定为十六进制的0xAB、0xBA,即十进制的171、186,或8位有符号整型的-85、-70,可以用来区分是默认协议还是统一协议。
- 第3个字节为协议版本,也可以称作command字节,会用来定义编解码的相关自定义行为,如压缩方式、数据校验方式等,具体command第8位表示是否需要进行校验数据完整性,第6、7位定义了是否进行压缩及具体的压缩方式。
- 第4个字节为序列化方式,一般为1。
- 第5~8个字节为消息体长度。
消息体
消息体部分,不区分是统一协议还是默认协议,最终解析出请求和响应对象类型分别为com.dianping.dpsf.protocol.DefaultRequest或com.dianping.dpsf.protocol.DefaultResponse,而除此之外,两种协议有细微区别:
- 统一协议没有请求尾,在消息体头部会有两个定长字节,这两个字节在序列化内部赋值,是Thrift内部计算的头部长度。
- 默认协议没有请求头,在消息体尾部会有11位定长字节,前8个字节为消息sequence,long型,值请从0开始递增,每个消息的sequence都不同;后3个字节固定为:29,30,31
DefaultRequest或com的具体定义如下:
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "seq", scope = DefaultRequest.class)
public class DefaultRequest implements InvocationRequest {
/**
* 不能随意修改!
*/
private static final long serialVersionUID = 652592942114047764L;
// 必填,序列化类型,默认hessian为2
private byte serialize;
// 必填,消息sequence,long型,值请从0开始递增,每个消息的sequence都不同
@JsonProperty("seq")
private long seq;
//必填,如果调用需要返回结果,固定为1,不需要回复为2,手动回复为3
private int callType = Constants.CALLTYPE_REPLY;
// 必填,超时时间,单位毫秒
private int timeout = 0;
// 请求创建时间, 不参与序列化传输
@JsonIgnore
private transient long createMillisTime;
//必填,服务名称url,服务唯一的标识
@JsonProperty("url")
private String serviceName;
//必填,服务方法名称
private String methodName;
//必填,服务方法的参数值
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "@class")
private Object[] parameters;
//必填,消息类型,服务调用固定为2,心跳为1,服务框架异常为3,服务调用业务异常为4
private int messageType = Constants.MESSAGE_TYPE_SERVICE;
// 旧版上下文信息传递对象
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "@class")
private Object context;
// 服务版本
private String version;
// 必填,调用者所属应用名称,在META-INF/app.properties里的app.name值
private String app = ConfigManagerLoader.getConfigManager().getAppName();
// 请求体大小
@JsonIgnore
private transient int size;
}
DefaultResponse的具体定义如下:
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "seq", scope = DefaultResponse.class)
public class DefaultResponse implements InvocationResponse {
/**
* 不能随意修改!
*/
private static final long serialVersionUID = 4200559704846455821L;
private transient byte serialize;
// 返回的消息sequence,对应发送的消息sequence,long型
@JsonProperty("seq")
private long seq;
//消息类型,服务调用为2,服务调用业务异常为4,服务框架异常为3,心跳为1
private int messageType;
// 请求返回异常的相关堆栈信息
@JsonProperty("exception")
private String cause;
// 返回服务调用结果
@JsonProperty("response")
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "@class")
private Object returnVal;
// 旧版上下文传递
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "@class")
private Object context;
// 请求体大小
@JsonIgnore
private transient int size;
// 请求创建时间, 不参与序列化传输
@JsonIgnore
private transient long createMillisTime;
// 响应自定义上下文信息
private Map<String, Serializable> responseValues = null;
}
Netty3 Handler相关实现
上下游传递原理
在开始分析pigeonRPC通信实现之前,先简略总结下netty3利用ChannelHandler完成通信处理的相关逻辑。
对于netty接收到一个请求后,会调用一系列的拦截器handler来处理我们的请求,可以将请求方看成在下游,服务方的核心处理逻辑在上游,服务方接收到请求后,会将请求不断往上游传递,交由一个个ChannelUpstreamHandler#handleUpstream方法处理,在到达最上游并由核心逻辑处理完后,又交由一个个ChannelDownStreamHandler#handleDownstream处理,到最下游通过网络通信将结果回传给客户端。
基于此,我们可以通过实现ChannelUpstreamHandler和ChannelDownStreamHandler,在请求的上下游传递中,拓展我们的逻辑。
相关实现
ChannelHandlerContext
请求在上下游处理过程中,处理的上下文数据是通过ChannelHandlerContext实现的。比如我们在特定的handler中通过ChannelHandlerContext的sendUpstream和sendDownstream方法将请求传递到下一个handler中处理。对于传递处理的上下文数据,可以通过getAttachment和setAttachment进行读写。
OneToOneDecoder & OneToOneEncoder
请求数据传输在进入服务端之后和返回客户端之前,分别需要进行解码和编码操作,这由OneToOneDecoder和OneToOneEncoder两个抽象类分别实现,两者分别实现了ChannelUpstreamHandler和ChannelDownstreamHandler接口,一般通过继承这两个抽象类,并实现内部的encode或decode方向模版方法来实现具体的编解码操作。
SimpleChannelHandler
SimpleChannelHandler同时实现了ChannelUpstreamHandler和ChannelDownstreamHandler接口,是一个双向handler,内部简单地实现了handleUpstream和handleDownstream,会根据传入ChannelEvent地类型,进行必要地向下转型,得到更加有意义的子类型,而后调用相关的处理方法。默认实现本Handler一般是作为最上层地handler,如果在本Handler之后还需要向更上游传递,需要确保在handleUpstream地最后,手动调用了super.handleUpstream方法。
ChannelEvent
netty有多种事件可以在Channel中传递,交由用户定义的handler处理,这些事件都以ChennelEvent的形式定义,常用有以下事件:
- MessageEvent:正常消息请求事件
- ChannelStateEvent:channel状态变更事件,包括以下几种:
- OPEN: channel开启
- BOUND: channel绑定到特定的本地地址
- CONNECTED: channel连接到特定的远程地址
- INTEREST_OPS: channel对特定感兴趣的操作会进行暂停
pigeon RPC通信的核心实现原理
下面分成服务端和客户端两部分,从这两个类展开分析pigeon
服务端实现
下面先看NettyServerPipelineFactory的实现:
public class NettyServerPipelineFactory implements ChannelPipelineFactory {
// 服务端连接实例引用
private NettyServer server;
// 通过编解码工厂,获取单例编解码配置
private static CodecConfig codecConfig = CodecConfigFactory.createClientConfig();
public NettyServerPipelineFactory(NettyServer server) {
this.server = server;
}
// 初始化pipeline
public ChannelPipeline getPipeline() {
ChannelPipeline pipeline = pipeline();
pipeline.addLast("framePrepender", new FramePrepender());
pipeline.addLast("frameDecoder", new FrameDecoder());
pipeline.addLast("crc32Handler", new Crc32Handler(codecConfig));
pipeline.addLast("compressHandler", new CompressHandler(codecConfig));
pipeline.addLast("providerDecoder", new ProviderDecoder());
pipeline.addLast("providerEncoder", new ProviderEncoder());
pipeline.addLast("serverHandler", new NettyServerHandler(server));
return pipeline;
}
}
对于上面所有的Handler,可以分为3类:
- UpStreamHandler
- FrameDecoder
- ProviderDecoder
- NettyServerHandler
- DownStreamHandler
- FramePrepender
- ProviderEncoder
- 双向Handler
- Crc32Handler
- compressHandler
结合代码分析,最终的调用时序如下所示:
下面根据每个handler的处理顺序,依次分析每个handler的处理逻辑
FrameDecoder
顾名思义,FrameDecoder用来解析出通信管道中一次请求的数据,解决tcp通信中粘包和半包的问题。
pigeon的FrameDecoder继承自netty的org.jboss.netty.handler.codec.frame.FrameDecoder,而org.jboss.netty.handler.codec.frame.FrameDecoder又继承自SimpleChannelUpstreamHandler,在netty实现的FrameDecoder,核心实现方法是messageReceived
,大致实现原理是不断读取接收到的字节流,并累加到cumulation变量,通过调用callDecode
来尝试对当前累加的字节Buffer cumulation进行解码,直到解析出一个完整请求的feame对象,最后会调用Channels#fireMessageReceived触发Handler的pipeline调用来完成一次完整请求。
在解码过程callDecode
中调用了一个抽象模版方法decode
来完成具体的解码逻辑,decode
方法尝试解析cumulation变量,如果不能按照自定义解析规则解析出一个完整请求的数据包,就返回null,否则返回一个完整的数据包,这里读取成功的同时,需要更新cumulation的字节起始点到当前完整数据包字节的尾部。
分析完Netty的解码流程,具体看看pigeon如何基于自己设计的协议格式来进行数据包解析。
在pigeon中,decode
方法在自定义的FrameDecoder中实现,代码尝试先解析出请求头,再通过头部的请求体长度,解析出一次完整请求的包体,在代码中请求尾的长度包含在请求体的长度内部,具体实现如下所示:
public class FrameDecoder extends org.jboss.netty.handler.codec.frame.FrameDecoder {
@Override
protected Object decode(ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer)
throws Exception {
Object message = null;
// 如果当前累积的小于两个字节,直接返回null
if (buffer.readableBytes() <= 2) {
return message;
}
byte[] headMsgs = new byte[2];
// 复制buff两个字节到headMsgs
buffer.getBytes(buffer.readerIndex(), headMsgs);
if ((0x39 == headMsgs[0] && 0x3A == headMsgs[1])) {
// 0x39=57,0x3A=58
//old protocol
message = doDecode(buffer);
} else if ((byte) 0xAB == headMsgs[0] && (byte) 0xBA == headMsgs[1]) {
//0xAB=171,0xBA=186
//new protocol
message = _doDecode(buffer);
} else {
throw new IllegalArgumentException("Decode invalid message head:" +
headMsgs[0] + " " + headMsgs[1] + ", " + "message:" + buffer);
}
return message;
}
protected Object doDecode(ChannelBuffer buffer) throws Exception {
CodecEvent codecEvent = null;
// FRONT_LENGTH = 7,即如果buffer小于消息头7位长度,直接返回null
if (buffer.readableBytes() <= CodecConstants.FRONT_LENGTH) {
return codecEvent;
}
// 从消息的第3位开始,读取4位字节位一个无符号整数,实际位请求体大小
int totalLength = (int) buffer.getUnsignedInt(
buffer.readerIndex() +
CodecConstants.HEAD_LENGTH);
// 最后包体大小是请求体大小+请求头大小
int frameLength = totalLength + CodecConstants.FRONT_LENGTH;
// 当前累积的buffer是否已经包含一个完整的数据包
if (buffer.readableBytes() >= frameLength) {
// 获取具体数据包字节内容
ChannelBuffer frame = buffer.slice(buffer.readerIndex(), frameLength);
// 更新累积缓存的读起点,方便读取处理下一个数据包
buffer.readerIndex(buffer.readerIndex() + frameLength);
// 用CodecEvent包装frame,并标记位非统一协议
codecEvent = new CodecEvent(frame, false);
// 设置接收时间
codecEvent.setReceiveTime(System.currentTimeMillis());
}
return codecEvent;
}
protected Object _doDecode(ChannelBuffer buffer)
throws Exception {
CodecEvent codecEvent = null;
// _FRONT_LENGTH = 10,即如果buffer小于消息头10位长度,直接返回null
if (buffer.readableBytes() <= CodecConstants._FRONT_LENGTH) {
return codecEvent;
}
// 从消息的第4位开始,读取4位字节位一个无符号整数,实际位请求体大小
int totalLength = (int) (buffer.getUnsignedInt(
buffer.readerIndex() +
CodecConstants._HEAD_LENGTH));
// 最后包体大小是请求体大小+请求头大小
int frameLength = totalLength + CodecConstants._FRONT_LENGTH_;
// 当前累积的buffer是否已经包含一个完整的数据包
if (buffer.readableBytes() >= frameLength) {
// 获取具体数据包字节内容
ChannelBuffer frame = buffer.slice(buffer.readerIndex(), frameLength);
// 更新累积缓存的读起点,方便读取处理下一个数据包
buffer.readerIndex(buffer.readerIndex() + frameLength);
// 用CodecEvent包装frame,并标记位统一协议
codecEvent = new CodecEvent(frame, true);
// 设置接收时间
codecEvent.setReceiveTime(System.currentTimeMillis());
}
return codecEvent;
}
}
Crc32Handler
Crc32Handler主要用于校验统一协议请求的数据完整性,在解析出完整消息包长度数据之后,在解码为DefaultRequest之前,会先获取实际的数据包数据,计算crc32校验和,再和消息尾部传入的校验和进行比对,如果一致,说明校验通过,否则校验失败。而在请求结束发送相应时,又会对数据计算校验和,放在消息包尾中,以便客户端获取校验。
看看代码的具体实现:
public class Crc32Handler extends SimpleChannelHandler {
private static final Logger logger = LoggerLoader.getLogger(Crc32Handler.class);
private static ThreadLocal<Adler32> adler32s = new ThreadLocal<Adler32>();
private CodecConfig codecConfig;
public Crc32Handler(CodecConfig codecConfig) {
this.codecConfig = codecConfig;
}
// 上游接收数据处理
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
if (e.getMessage() == null || !(e.getMessage() instanceof CodecEvent))