netty + protobuf 传输多个类

在protobuf序列化的前面,加上一个自定义的头,这个头包含序列化的长度和它的类型。在解压的时候根据包头来反序列化。

 

假设socket上要传输2个类型的数据,股票行情信息和期权行情信息:

股票的.proto定义:

复制代码
syntax = "proto3";

package test.model.protobuf;

option java_package = "test.model.protobuf";

message StockTick {
    string stockId = 1;
    int price = 2;
}
复制代码

 

期权的.proto定义:

复制代码
syntax = "proto3";

package test.model.protobuf;

option java_package = "test.model.protobuf";

message OptionTick {
    string optionId = 1;
    string securityId = 2;
    int price = 3;
}
复制代码

 

netty4官方事实上已经实现了protobuf的编解码的插件,但是只能用于传输单一类型的protobuf序列化。我这里截取一段netty代码,熟悉netty的同学马上就能理解它的作用:

复制代码
        @Override
        protected void initChannel(SocketChannel ch) throws Exception {
            ChannelPipeline pipeline = ch.pipeline();
            pipeline.addLast(new ProtobufVarint32FrameDecoder());
            pipeline.addLast(new ProtobufDecoder(StockTickOuterClass.StockTick.getDefaultInstance()));
            pipeline.addLast(new ProtobufVarint32LengthFieldPrepender());
            pipeline.addLast(new ProtobufEncoder());
            pipeline.addLast(new CustomProtoServerHandler());
        }
复制代码

 

看以上代码高亮部分,netty4官方的编解码器必须指定单一的protobuf类型才行。具体每个类的作用:

ProtobufEncoder:用于对Probuf类型序列化。
ProtobufVarint32LengthFieldPrepender:用于在序列化的字节数组前加上一个简单的包头,只包含序列化的字节长度。
ProtobufVarint32FrameDecoder:用于decode前解决半包和粘包问题(利用包头中的包含数组长度来识别半包粘包)
ProtobufDecoder:反序列化指定的Probuf字节数组为protobuf类型。

 

我们可以参考以上官方的编解码代码,将实现我们客户化的protobuf编解码插件,但是要支持多种不同类型protobuf数据在一个socket上传输:

 

编码器CustomProtobufEncoder:

复制代码
import com.google.protobuf.MessageLite;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;

/**
 * 参考ProtobufVarint32LengthFieldPrepender 和 ProtobufEncoder
 */
@Sharable
public class CustomProtobufEncoder extends MessageToByteEncoder<MessageLite> {
    
HangqingEncoder hangqingEncoder;
    
    public CustomProtobufEncoder(HangqingEncoder hangqingEncoder)
    {
        this.hangqingEncoder = hangqingEncoder;
    }
    
    @Override
    protected void encode(
            ChannelHandlerContext ctx, MessageLite msg, ByteBuf out) throws Exception {
        

        byte[] body = msg.toByteArray();
        byte[] header = encodeHeader(msg, (short)body.length);
        
        out.writeBytes(header);
        out.writeBytes(body);
        
        return;
    }
    
    private byte[] encodeHeader(MessageLite msg, short bodyLength) {
        byte messageType = 0x0f;
        
        if (msg instanceof StockTickOuterClass.StockTick) {
            messageType = 0x00;
        } else if (msg instanceof OptionTickOuterClass.OptionTick) {
            messageType = 0x01;
        }
        
        byte[] header = new byte[4];
        header[0] = (byte) (bodyLength & 0xff);
        header[1] = (byte) ((bodyLength >> 8) & 0xff);
        header[2] = 0; // 保留字段
        header[3] = messageType;

        return header;

    }
}
复制代码

 

CustomProtobufEncoder序列化传入的protobuf类型,并且为它创建了一个4个字节的包头,格式如下

 

body长度(low)body长度
(high)
保留字节类型

 

其中的encodeHeader方法具体的实现要根据你要传输哪些protobuf类型来修改代码,也可以稍加设计避免使用太多的if…else。

 

解码器CustomProtobufDecoder:

复制代码
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import java.util.List;
import com.google.protobuf.MessageLite;

/**
 * 参考ProtobufVarint32FrameDecoder 和 ProtobufDecoder
 */

public class CustomProtobufDecoder extends ByteToMessageDecoder {

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        while (in.readableBytes() > 4) { // 如果可读长度小于包头长度,退出。
            in.markReaderIndex();

            // 获取包头中的body长度
            byte low = in.readByte();
            byte high = in.readByte();
            short s0 = (short) (low & 0xff);
            short s1 = (short) (high & 0xff);
            s1 <<= 8;
            short length = (short) (s0 | s1);

            // 获取包头中的protobuf类型
            in.readByte();
            byte dataType = in.readByte();

            // 如果可读长度小于body长度,恢复读指针,退出。
            if (in.readableBytes() < length) {
                in.resetReaderIndex();
                return;
            }

            // 读取body
            ByteBuf bodyByteBuf = in.readBytes(length);

            byte[] array;
            int offset;

            int readableLen= bodyByteBuf.readableBytes();
            if (bodyByteBuf.hasArray()) {
                array = bodyByteBuf.array();
                offset = bodyByteBuf.arrayOffset() + bodyByteBuf.readerIndex();
            } else {
                array = new byte[readableLen];
                bodyByteBuf.getBytes(bodyByteBuf.readerIndex(), array, 0, readableLen);
                offset = 0;
            }
            
            //反序列化
            MessageLite result = decodeBody(dataType, array, offset, readableLen);
            out.add(result);
        }
    }

    public MessageLite decodeBody(byte dataType, byte[] array, int offset, int length) throws Exception {
        if (dataType == 0x00) {
            return StockTickOuterClass.StockTick.getDefaultInstance().
                    getParserForType().parseFrom(array, offset, length);

        } else if (dataType == 0x01) {
            return OptionTickOuterClass.OptionTick.getDefaultInstance().
                    getParserForType().parseFrom(array, offset, length);
        }

        return null; // or throw exception
    }
}
复制代码

 

CustomProtobufDecoder实现了2个功能,1)通过包头中的长度信息来解决半包和粘包。 2)把消息body反序列化为对应的protobuf类型(根据包头中的类型信息)。

其中的decodeBody方法具体的实现要根据你要传输哪些protobuf类型来修改代码,也可以稍加设计避免使用太多的if…else。

 

在Netty服务器上应用编解码器

如何把我们自定义的编解码用于netty Server:

复制代码
        @Override
        protected void initChannel(SocketChannel ch) throws Exception {
            ChannelPipeline pipeline = ch.pipeline();
            pipeline.addLast("decoder",new CustomProtobufDecoder());
            pipeline.addLast("encoder",new CustomProtobufEncoder());
            pipeline.addLast(new CustomProtoServerHandler());
        }
复制代码

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个简单的Java Netty4 + Protobuf3示例,用于传输500MB大文件。请注意,这只是一个示例,并不是最优的解决方案,您可能需要根据您的具体需求进行调整。 服务端代码: ```java public class FileServer { private final int port; private final String filePath; public FileServer(int port, String filePath) { this.port = port; this.filePath = filePath; } public void run() throws Exception { EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .option(ChannelOption.SO_BACKLOG, 100) .childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new ProtobufVarint32FrameDecoder()); ch.pipeline().addLast(new ProtobufDecoder(FileTransfer.FileRequest.getDefaultInstance())); ch.pipeline().addLast(new ProtobufVarint32LengthFieldPrepender()); ch.pipeline().addLast(new ProtobufEncoder()); ch.pipeline().addLast(new FileServerHandler(filePath)); } }); // Start the server. ChannelFuture f = b.bind(port).sync(); // Wait until the server socket is closed. f.channel().closeFuture().sync(); } finally { workerGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); } } public static void main(String[] args) throws Exception { int port = Integer.parseInt(args[0]); String filePath = args[1]; new FileServer(port, filePath).run(); } } ``` 客户端代码: ```java public class FileClient { private final String host; private final int port; private final String filePath; public FileClient(String host, int port, String filePath) { this.host = host; this.port = port; this.filePath = filePath; } public void run() throws Exception { EventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap b = new Bootstrap(); b.group(group) .channel(NioSocketChannel.class) .option(ChannelOption.TCP_NODELAY, true) .handler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new ProtobufVarint32FrameDecoder()); ch.pipeline().addLast(new ProtobufDecoder(FileTransfer.FileResponse.getDefaultInstance())); ch.pipeline().addLast(new ProtobufVarint32LengthFieldPrepender()); ch.pipeline().addLast(new ProtobufEncoder()); ch.pipeline().addLast(new FileClientHandler(filePath)); } }); // Start the client. ChannelFuture f = b.connect(host, port).sync(); // Wait until the connection is closed. f.channel().closeFuture().sync(); } finally { group.shutdownGracefully(); } } public static void main(String[] args) throws Exception { String host = args[0]; int port = Integer.parseInt(args[1]); String filePath = args[2]; new FileClient(host, port, filePath).run(); } } ``` 处理程序代码: ```java public class FileServerHandler extends SimpleChannelInboundHandler<FileTransfer.FileRequest> { private final String filePath; public FileServerHandler(String filePath) { this.filePath = filePath; } @Override public void channelRead0(ChannelHandlerContext ctx, FileTransfer.FileRequest req) throws Exception { long start = req.getStart(); long end = req.getEnd(); FileInputStream fis = new FileInputStream(filePath); fis.skip(start); byte[] bytes = new byte[(int) (end - start + 1)]; fis.read(bytes); fis.close(); FileTransfer.FileResponse.Builder builder = FileTransfer.FileResponse.newBuilder(); builder.setBytes(ByteString.copyFrom(bytes)); ctx.writeAndFlush(builder.build()); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } } public class FileClientHandler extends ChannelInboundHandlerAdapter { private final String filePath; public FileClientHandler(String filePath) { this.filePath = filePath; } @Override public void channelActive(ChannelHandlerContext ctx) { try { File file = new File(filePath); long fileSize = file.length(); long chunkSize = 1024 * 1024; // 1MB long start = 0; long end = chunkSize - 1; while (start < fileSize) { if (end >= fileSize) { end = fileSize - 1; } FileTransfer.FileRequest.Builder builder = FileTransfer.FileRequest.newBuilder(); builder.setStart(start); builder.setEnd(end); ctx.write(builder.build()); start += chunkSize; end += chunkSize; } ctx.flush(); } catch (Exception e) { e.printStackTrace(); ctx.close(); } } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { FileTransfer.FileResponse res = (FileTransfer.FileResponse) msg; FileOutputStream fos = new FileOutputStream("output"); fos.write(res.getBytes().toByteArray()); fos.close(); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } } ``` 这里的关键是将文件分成块,并将每个块作为独立的请求发送到服务器。服务器读取每个块,并将其作为字节流发送回客户端。客户端将字节流写入文件。 当然,这里还有很多需要优化的地方,例如考虑到网络波动、数据丢失、超时等异常情况。但是希望这个示例可以帮助您入门。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值