本文使用场景:Java作为客户端和C++系统进行数据交换,以得到satellite设备上的数据(temperature,current,voltage)及指令(upstream,downstream)。 为了实现以上目标,我们从以下几方面考虑:
- 通信协议制订
- 使用大端还是小端字节序
经讨论最终结果为:使用小端字节序的自定义通信协议,其中包体部分使用protobuf进行编码。 自定义协议如下: MagicNumber,Length(数据包的总长度,包括包头和包体),version(版本号,为后续协议升级做准备),kind(数据包类型),serialize(序列化类型),guid(数据包标识,用于包追踪),包体(业务实体序列化数据)
以上问题解决以后就是编码实现了.
源代码见此处:github.com/gisonwin/ne…
- 小端实现
//读包体长度 LE是小端 little endian
int length = in.readIntLE();
log.info("package length =" + length);
//读取主版本号
short majorVersion = in.readShortLE();
//读取次版本号
short minorVersion = in.readShortLE();
//读包类型
byte kind = in.readByte();
//读取序列化类型,默认为protobuf
byte serialize = in.readByte();
byte就不区分大小端了.
- 编解码
public class MessageDecoder extends ReplayingDecoder<Void> {
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf in, List<Object> out) throws Exception {
//......
int BASE_LENGTH = 4 + 4; //HEADER+length 4+4
int readableBytes = in.readableBytes();
if (readableBytes > BASE_LENGTH) { //HEADER+length 4+4 ,可读字节要大于header和包体长度才会解码
//读取Header
int beginReader;
//一直读数据,直到讲到的数据是header以后,再向后读取数据
int header;
while (true) {
beginReader = in.readerIndex();
in.markReaderIndex();//标记包头开始的位置
int readInt = in.readIntLE();
if ((header = readInt) == MAGIC_NUMBER) {
//读到了header, 开始读后面的数据
break;
}
in.resetReaderIndex();//重置reader index
in.readByte();
// 未读到包头,略过一个字节
// 每次略过,一个字节,去读取,包头信息的开始标记
// 当略过,一个字节之后,
// 数据包的长度,又变得不满足
// 此时,应该结束。等待后面的数据到达
if (in.readableBytes() < BASE_LENGTH) {
return;
}
}
......
int dataLength = length - BASE_LENGTH;//后续数据的长度
// 判断请求数据包数据是否到齐
int readableBytes1 = in.readableBytes();
if (readableBytes1 < dataLength) {
// 还原读指针
in.readerIndex(beginReader);
log.error("数据包不完整,{},{}", readableBytes1, dataLength);
return;
}
}}
#编码器
public class MessageEncoder extends MessageToByteEncoder<NettyMessage> {
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, NettyMessage msg, ByteBuf out) throws Exception {
out.writeIntLE(msg.getMagicNumber());//同步字
out.writeIntLE(msg.getLength());//长度
out.writeShortLE(msg.getMajorVersion());//主版本号
out.writeShortLE(msg.getMinorVersion());//次版本号
out.writeByte(msg.getKind());//包类型
out.writeByte(msg.getSerialize());//序列化方式
//将guid byte array 修改为小端 写出去
byte[] data = EndianUtils.BigEndian16BytesToLittleEndianBytes(msg.getGuid());
out.writeBytes(data);//guid little endian
out.writeBytes(msg.getByteData());//写的proto buff的字节流
}
}
- 客户端
public class NettyClientBootstrap {
//......
private ChannelFuture start() throws Exception {
Bootstrap bootstrap = new Bootstrap();
InetSocketAddress address = new InetSocketAddress(host, port);
bootstrap.group(eventLoopGroup).channel(NioSocketChannel.class)
.option(ChannelOption.SO_KEEPALIVE, true)
.handler(new LoggingHandler(LogLevel.DEBUG))
.handler(new ClientInitializeHandler());
//.......
主要注意LoggingHandler里的日志级别为DEBUG.该级别会在通信时打印出READ WRITE的详细信息
- 服务端
@Component
public class NettyTcpServerBootstrap {
@Value("${tcp.server.port}")
int port;
//......
@PostConstruct
//@PostConstruct是随着springboot启动时 调用该标识标注的方法.
public void start() {
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
//设置线程队列得到连接个数
.option(ChannelOption.SO_BACKLOG, 128)
.handler(new LoggingHandler(LogLevel.DEBUG))
//设置保持活动连接状态
.childOption(ChannelOption.SO_KEEPALIVE, true)
//设置NoDelay禁用Nagel,消息会立即发送出去,不用等到一定数量才发送出去
.childOption(ChannelOption.TCP_NODELAY, true)
//这里的log是boss group的日志级别
.handler(new LoggingHandler(LogLevel.DEBUG))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline p = ch.pipeline();
//这里log handler是跟随worker group的日志级别
p.addLast(new LoggingHandler(LogLevel.DEBUG));
//解码器
p.addLast(new MessageEncoder());
//编码器
p.addLast(new MessageDecoder());
//心跳包
// p.addLast(new IdleStateHandler(0, 0, 6, TimeUnit.SECONDS));
//处理器
p.addLast(nettyQueryHandler);
}
});
//......
- 启动 本文在open jdk 11 && jetbrains IDEA 社区版 测试通过.如果大家有疑问或建议,请留言.