Netty
Google Protobuf
-
编码和解码
- 编写网络应用程序时,因为数据在网络中传输的都是二进制字节码数据,在发送数据时就需要编码,接收数据时就需要解码。
- codec(编解码器)的组成部分有两个:decoder(解码器)和coder(编码器)。encoder负责把业务数据转换成字节码数据;decoder负责把字节码数据转换为业务数据
-
Netty本身的编解码机制和问题分析
- netty自身提供了一些cedec(编解码器)
- netty提供了
StringEncoder
、StringDecoder
,可以对字符串数据进行编解码;ObjectEncoder
、ObjectDecoder
可以对java对象进行编解码 - Netty本身自带的
ObjectEncoder
、ObjectDecoder
可以用来实现POJO
对象或各种业务对象的编码和解码,底层使用的仍然是java序列化技术,效率不高,存在以下问题:- 无法跨语言
- 序列化后的体积太大,是二进制代码的五倍多
- 序列化性能太低
- Protobuf
- Protobuf是Google的开源项目,是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化。很适合用于做数据存储或RPC(远程过程调用)数据交换格式。——
http + json
和tcp + protobuf
Protobuf
是以message的方法是来管理数据的。使用protobuf的编译器能够自动生成代码,是将类的定义.proto
文件进行描述,然后通过proto.exe
编译器自动生成.java
文件。
- Protobuf是Google的开源项目,是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化。很适合用于做数据存储或RPC(远程过程调用)数据交换格式。——
Student.proto
syntax = "proto3"; //版本
option java_outer_classname = "StudentPOJO";//生成的外部类名,同时也是文件名
//protobuf 使用message 管理数据
message Student { //会在 StudentPOJO 外部类生成一个内部类 Student, 他是真正发送的POJO对象
int32 id = 1; // Student 类中有 一个属性 名字为 id 类型为int32(protobuf类型) 1表示属性序号,不是值
string name = 2;
}
//编译
//protoc.exe--java_out=.Student.proto
//将生成的 StudentPOJO 放入到项目使用
Netty编解码器和Handler调用机制
-
基本说明
- Netty的组件设计有:Channel、EventLoop、ChannelFuture、ChannelHandler、ChannelPipeline等
- ChannelHandler充当了入站和出站数据的应用程序逻辑的容器。例如实现
ChannelInboundHandler
接口(或者ChannelInboundHnadlerAdapter
接口),可以接收入站事件和数据。这些数据会被业务逻辑处理。当要给客户端发送响应时,可以从ChannelIndoundHandler
冲刷数据。业务逻辑通常写在一个或者多个ChannelInboundHandler
中。ChannelOutboundHandler
原理类似,只不过是用来处理出站数据的 ChannelPipeline
提供了ChannelHandler
链的容器。当事件是从Channel中进入端程序时,称事件为入站的;当事件是从端程序进入Channel中,称为出站的事件。
-
编码解码器
- 当Netty发送或者接受一个消息的时候,就将会发生一次数据转换。入站消息会被解码,从字节转换为另一种格式,如果是出站消息,会被编码成字节。
- Netty提供一系列使用的编解码器,他们都实现了
ChannelInboundHandler
或者ChannelOutboundHandler
接口。在这些类中,channelRead()
方法已经被重写了。入站时,对每个从入站Channel读取的信息,这个方法会被调用。随后,他将调用解码器提供的decode()
方法进行解码,并将已经解码的字节转发给pipeline中的下一个InboundHandler。
-
解码器 - ByteToMessageDecoder
- 由于不可能知道远程节点是否会一次性发送一个完整的信息,
tcp
有可能会出现粘包拆包的问题,这个类会对入站数据进行缓冲,直到他准备好被处理。
- 由于不可能知道远程节点是否会一次性发送一个完整的信息,
public class ToIntegerDecoder extends ByteToMessageDecoder{
@Override
protected void decode(ChannelHandlerContext ctx , ByteBuf in , List<Object> out) throws Exception{
if (in.readableBytes() >= 4){
out.add(in.readInt());
}
}
}
//在每次入站从ByteBuf中读取4字节,并将其解码为一个int,然后将其添加到一个list中。该方法会被反复调用,直到没有更多元素被添加到list集合中(即ByteBuf读取结束)时,该List的内容会被发送给下一个ChannelInboundHandler.
-
Netty的handler链的调用机制
以客户端服务器之间传送Long型数据为例,在客户端向服务器发送Long型数据时,需要在客户端channel的pipeline中添加编码器和业务处理器;在服务器端channel的pipeline中添加解码器和业务处理器。需要注意的是,解码器和编码器都需要在业务处理器之前定义到pipeline中。- 不论解码器handler还是编码器handler,接受的消息类型必须各待处理的消息类型一致,否则该handler不会被执行。
- 在编码器进行数据解码时,需要判断缓存区(ByteBuf)的数据是否足够,否则接收到的结果会和期望结果不一致。
-
解码器 - ReplayingDecoder
该解码器继承了ByteToMessageDecoder:publci abstract class ReplayingDecoder <S> extends ByteToMessageDecoder
- ReplayingDecoder扩展了ByteToMessageDecoder类,只用这个类不需要调用readableBytes()方法,参数S指定了用户状态管理的类型,其中Void表示不需要状态管理
- ReplayingDecoder使用起来较为方便,但是也具有一定的局限性
- 并不是所有的ByteBuf操作都被支持,如果调用了一个不被支持的方法,将会抛出一个
UnsupportedOperationException
- RepalyingDecoder在一些情况下可能会稍慢与ByteToMessageDecoder,如网络消息缓慢且消息格式复杂时,消息会被拆成多个碎片,速度变慢。
- 并不是所有的ByteBuf操作都被支持,如果调用了一个不被支持的方法,将会抛出一个
public class MyDecoder extends ReplayingDecoder<Void> {
@Override
protected void decode(ChannelHandlerContext context , ByteBuf byteBuf , List<Object> out) throws Exception{
System.out.println("解码器被调用...");
//ReplayingDecoder在使用时不需要判断数据是否足够读取
//if(byteBuf.readableBytes() >= 8){
// out.add(byteBuf.readLong());
//}
out.add(byteBuf.readLong());
}
}
- 其他解码器
- LineBasedFrameDecoder:这个类在 Netty 内部也有使用,它使用行尾控制字符(\n或者\r\n)作为分隔符来解析数据。
- DelimiterBasedFrameDecoder:使用自定义的特殊字符作为消息的分隔符。
- HttpObjectDecoder:一个 HTTP 数据的解码器
- LengthFieldBasedFrameDecoder:通过指定长度来标识整包消息,这样就可以自动的处理黏包和半包消息。
TCP粘包和拆包及解决方案
-
TCP粘包拆包基本介绍
- TCP是面向连接,面向流的,提供高可靠性服务。收发两端都要有一一成对的socket。因此发送端为了将多个发给接收端的包更有效的发给对方,使用了优化方法,将多次间隔较小且数据量小的数据,合并成一个大的数据块然后进行封包。这样做虽然提高了效率,但是接收端就比较难以分辨出完整的数据包了,因为面向流的通信是无消息保护边界的。
- 由于TCP无消息保护边界,需要在接收端处理消息边界问题,也就是粘包拆包问题。
- 上图的四种情况即为通信过程中可能出现粘包拆包问题的情况。
-
TCP粘包拆包问题解决方案
- 使用自定义协议+编解码器来解决
- 关键是要解决服务器端每次读取数据长度的问题,这个问题解决,就不会出现服务器多读或者少读数据的问题,从而避免TCP拆包、粘包。