美团霸王餐接口长连接推送:Netty自定义协议与Protobuf编解码细节
为清晰呈现美团霸王餐接口长连接推送的核心技术,本文将围绕Netty自定义协议设计、Protobuf编解码实现展开,结合Java代码深入技术细节。
一、技术背景与核心需求
美团霸王餐接口需通过长连接用户中奖通知等消息,要求低延迟、高可靠性和数据紧凑性。选用Netty作为长连接通信框架,自定义协议解决TCP粘包/拆包问题,Protobuf作为序列化方案优化数据传输效率,三者协同满足业务需求。

二、Netty自定义协议设计
2.1 协议格式定义
自定义协议需包含魔数、版本号、消息长度、消息类型、校验码和消息体,结构如下:
| 字段 | 字节数 | 说明 |
|---|---|---|
| 魔数 | 4 | 0xCAFEBABE,用于校验合法性 |
| 版本号 | 1 | 协议版本,支持兼容升级 |
| 消息长度 | 4 | 消息体字节数(大端序) |
| 消息类型 | 2 | 1-活动推送,2-中奖通知等 |
| 校验码 | 2 | CRC16校验消息体 |
| 消息体 | N | Protobuf序列化后的字节流 |
2.2 协议实体类(Java)
package juwatech.cn.meituan.protocol;
import lombok.Data;
@Data
public class CustomProtocol {
private int magicNumber = 0xCAFEBABE; // 魔数
private byte version = 1; // 版本号
private int contentLength; // 消息体长度
private short messageType; // 消息类型
private short checkCode; // 校验码
private byte[] content; // 消息体(Protobuf序列化后)
// CRC16校验计算
public void calculateCheckCode() {
this.checkCode = Crc16Util.calculate(content);
}
}
2.3 CRC16校验工具类
package juwatech.cn.meituan.util;
public class Crc16Util {
public static short calculate(byte[] data) {
int crc = 0xFFFF;
for (byte b : data) {
crc ^= (int) b & 0xFF;
for (int i = 0; i < 8; i++) {
if ((crc & 0x0001) != 0) {
crc = (crc >> 1) ^ 0xA001;
} else {
crc >>= 1;
}
}
}
return (short) crc;
}
}
三、Protobuf数据结构定义
3.1 .proto文件编写
创建coupon_message.proto定义消息结构,包含活动推送和中奖通知两种类型:
syntax = "proto3";
package juwatech.cn.meituan.protobuf;
// 活动推送消息
message ActivityPush {
string activityId = 1; // 活动ID
string title = 2; // 活动标题
string description = 3; // 活动描述
int64 startTime = 4; // 开始时间(时间戳)
int64 endTime = 5; // 结束时间(时间戳)
}
// 中奖通知消息
message WinNotice {
string noticeId = 1; // 通知ID
string userId = 2; // 用户ID
string couponId = 3; // 优惠券ID
string couponName = 4; // 优惠券名称
int64 receiveTime = 5; // 领取时间(时间戳)
}
// 统一消息体
message CouponMessage {
oneof messageBody {
ActivityPush activityPush = 1;
WinNotice winNotice = 2;
}
}
3.2 生成Java代码
通过Protobuf编译器生成Java类,命令如下:
protoc --java_out=src/main/java coupon_message.proto
生成的Java类位于juwatech.cn.meituan.protobuf包下,包含序列化和反序列化方法。
四、Netty编解码器实现
4.1 编码器(CustomProtocolEncoder)
负责将CustomProtocol对象编码为字节流,写入Channel:
package juwatech.cn.meituan.codec;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import juwatech.cn.meituan.protocol.CustomProtocol;
public class CustomProtocolEncoder extends MessageToByteEncoder<CustomProtocol> {
@Override
protected void encode(ChannelHandlerContext ctx, CustomProtocol msg, ByteBuf out) throws Exception {
// 写入魔数
out.writeInt(msg.getMagicNumber());
// 写入版本号
out.writeByte(msg.getVersion());
// 写入消息体长度
out.writeInt(msg.getContentLength());
// 写入消息类型
out.writeShort(msg.getMessageType());
// 写入校验码
out.writeShort(msg.getCheckCode());
// 写入消息体
out.writeBytes(msg.getContent());
}
}
4.2 解码器(CustomProtocolDecoder)
基于Netty的LengthFieldBasedFrameDecoder解决粘包/拆包,解码字节流为CustomProtocol对象:
package juwatech.cn.meituan.codec;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import juwatech.cn.meituan.protocol.CustomProtocol;
import juwatech.cn.meituan.util.Crc16Util;
public class CustomProtocolDecoder extends LengthFieldBasedFrameDecoder {
// 长度字段偏移量:魔数(4) + 版本(1) = 5
private static final int LENGTH_FIELD_OFFSET = 5;
// 长度字段长度:4字节
private static final int LENGTH_FIELD_LENGTH = 4;
public CustomProtocolDecoder() {
super(Integer.MAX_VALUE, LENGTH_FIELD_OFFSET, LENGTH_FIELD_LENGTH);
}
@Override
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
ByteBuf frame = (ByteBuf) super.decode(ctx, in);
if (frame == null) return null;
CustomProtocol protocol = new CustomProtocol();
// 读取魔数
protocol.setMagicNumber(frame.readInt());
// 读取版本号
protocol.setVersion(frame.readByte());
// 读取消息体长度
int contentLength = frame.readInt();
protocol.setContentLength(contentLength);
// 读取消息类型
protocol.setMessageType(frame.readShort());
// 读取校验码
protocol.setCheckCode(frame.readShort());
// 读取消息体
byte[] content = new byte[contentLength];
frame.readBytes(content);
protocol.setContent(content);
// 校验码验证
short calculateCode = Crc16Util.calculate(content);
if (protocol.getCheckCode() != calculateCode) {
throw new IllegalArgumentException("校验码错误,消息可能被篡改");
}
return protocol;
}
}
五、Protobuf编解码集成
5.1 消息序列化(发送端)
将业务对象序列化为Protobuf字节流,封装到CustomProtocol中:
package juwatech.cn.meituan.service;
import juwatech.cn.meituan.protocol.CustomProtocol;
import juwatech.cn.meituan.protobuf.ActivityPush;
import juwatech.cn.meituan.protobuf.CouponMessage;
public class MessageSender {
public CustomProtocol buildActivityPushProtocol(ActivityPush activityPush) {
// Protobuf序列化
byte[] content = CouponMessage.newBuilder()
.setActivityPush(activityPush)
.build()
.toByteArray();
CustomProtocol protocol = new CustomProtocol();
protocol.setMessageType((short) 1); // 活动推送类型
protocol.setContentLength(content.length);
protocol.setContent(content);
protocol.calculateCheckCode(); // 计算校验码
return protocol;
}
}
5.2 消息反序列化(接收端)
从CustomProtocol中提取消息体,反序列化为Protobuf对象:
package juwatech.cn.meituan.handler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import juwatech.cn.meituan.protocol.CustomProtocol;
import juwatech.cn.meituan.protobuf.CouponMessage;
public class MessageHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
CustomProtocol protocol = (CustomProtocol) msg;
byte[] content = protocol.getContent();
// Protobuf反序列化
CouponMessage couponMessage = CouponMessage.parseFrom(content);
// 根据消息类型处理
switch (protocol.getMessageType()) {
case 1:
handleActivityPush(couponMessage.getActivityPush());
break;
case 2:
handleWinNotice(couponMessage.getWinNotice());
break;
default:
System.err.println("未知消息类型:" + protocol.getMessageType());
}
}
private void handleActivityPush(ActivityPush activityPush) {
System.out.println("收到活动推送:" + activityPush.getTitle());
// 业务逻辑处理...
}
private void handleWinNotice(CouponMessage.WinNotice winNotice) {
System.out.println("用户" + winNotice.getUserId() + "中奖:" + winNotice.getCouponName());
// 业务逻辑处理...
}
}
六、Netty服务端与客户端配置
6.1 服务端启动类
package juwatech.cn.meituan.server;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import juwatech.cn.meituan.codec.CustomProtocolDecoder;
import juwatech.cn.meituan.codec.CustomProtocolEncoder;
import juwatech.cn.meituan.handler.MessageHandler;
public class NettyServer {
public void start(int port) throws InterruptedException {
NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline()
.addLast(new CustomProtocolDecoder()) // 解码器
.addLast(new CustomProtocolEncoder()) // 编码器
.addLast(new MessageHandler()); // 消息处理器
}
});
bootstrap.bind(port).sync().channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws InterruptedException {
new NettyServer().start(8888);
}
}
本文著作权归吃喝不愁app开发者团队,转载请注明出处!

被折叠的 条评论
为什么被折叠?



