netty-04-自定义协议

TCP/IP 中消息传输基于流的方式,没有边界。如果不约定或者划定边界,就会造成消息黏包或者半包现象的发生,因此我们需要指定协议。协议的目的就是划定消息的边界,制定通信双方要共同遵守的通信规则。比如熟悉的应用层的http请求就是有http协议。

1-自定义协议

一般自定义协议包含以下几个要素:

  • 魔数:用来在第一时间判定是否是无效数据包,比如Java的class文件,开头几个字节就是表示Java魔数的。

  • 版本号:可以支持协议的升级;

  • 序列化算法:消息正文到底采用哪种序列化反序列化方式,可以由此扩展,例如:json、protobuf、hessian、jdk;

  • 指令类型:比如是登录、注册、单聊、群聊... 跟业务相关;

  • 请求序号:为了双工通信,提供异步能力;

  • 正文长度

  • 消息正文

2-自定义消息编解码协议

import com.netty.project.message.Message;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageCodec;
import lombok.extern.slf4j.Slf4j;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.List;



@Slf4j
public class MessageCodec extends ByteToMessageCodec<Message> {

    @Override
    public void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws Exception {
        // 1. 魔数-4 字节的
        out.writeBytes(new byte[]{1, 2, 3, 4});
        // 2. 版本-1 字节的
        out.writeByte(1);
        // 3. 序列化方式-1 字节的 jdk 0 , json 1
        out.writeByte(0);
        // 4. 指令类型-1 字节的
        out.writeByte(msg.getMessageType());
        // 5. 4 个字节
        out.writeInt(msg.getSequenceId());
        // 无意义,对齐填充
        out.writeByte(0xff);
        // 6. 获取内容的字节数组
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(msg);
        byte[] bytes = bos.toByteArray();
        // 7. 长度
        out.writeInt(bytes.length);
        // 8. 写入内容
        out.writeBytes(bytes);
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        int magicNum = in.readInt();
        byte version = in.readByte();
        byte serializerType = in.readByte();
        byte messageType = in.readByte();
        int sequenceId = in.readInt();
        in.readByte();
        int length = in.readInt();
        byte[] bytes = new byte[length];
        in.readBytes(bytes, 0, length);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
        Message message = (Message) ois.readObject();
        log.debug("magicNum={}, version={}, serializerType={}, messageType={}, sequenceId={}, length={}", magicNum, version, serializerType, messageType, sequenceId, length);
        log.debug("message={}", message);
        out.add(message);
    }
}

ByteToMessageCodec的子类不能添加@ChannelHandler.Sharable注解,因为在编解码过程中存在中间状态,可能造成消息编解码出错...

编解码器,何时添加@ChannelHandler.Sharable被共享?

  • 当 handler 不保存状态时,就可以安全地在多线程下被共享

  • 但要注意对于编解码器类,不能继承 ByteToMessageCodec 或 CombinedChannelDuplexHandler 父类,他们的构造方法对 @Sharable 有限制

  • 如果能确保编解码器不会保存状态,可以继承 MessageToMessageCodec 父类

下面这个MessageCodecSharable可以添加@ChannelHandler.Sharable的原因是:

在client端和server端配合了LengthFieldBasedFrameDecoder

到达这个编解码的消息已经经过了LengthFieldBasedFrameDecoder

完全转化成为我们需要的消息,不存在中间状态...

必须配合LengthFieldBasedFrameDecoder使用

 client:

NioEventLoopGroup group = new NioEventLoopGroup();
        LoggingHandler LOGGING_HANDLER = new LoggingHandler(LogLevel.DEBUG);
        MessageCodecSharable MESSAGE_CODEC = new MessageCodecSharable();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.channel(NioSocketChannel.class);
            bootstrap.group(group);
            bootstrap.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(
                        1024, 0, 4, 0,0));//in
                    ch.pipeline().addLast(LOGGING_HANDLER);//in-out
                    ch.pipeline().addLast(MESSAGE_CODEC);//in-out

server:
  NioEventLoopGroup boss = new NioEventLoopGroup();
        NioEventLoopGroup worker = new NioEventLoopGroup();
        LoggingHandler LOGGING_HANDLER = new LoggingHandler(LogLevel.DEBUG);
        MessageCodecSharable MESSAGE_CODEC = new MessageCodecSharable();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.channel(NioServerSocketChannel.class);
            serverBootstrap.group(boss, worker);
            serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(
                        1024, 0, 4, 0,0));
                    ch.pipeline().addLast(LOGGING_HANDLER);
                    ch.pipeline().addLast(MESSAGE_CODEC);

import com.netty.project.message.Message;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageCodec;
import lombok.extern.slf4j.Slf4j;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.List;

@Slf4j
@ChannelHandler.Sharable
/**
 * 必须和 LengthFieldBasedFrameDecoder 一起使用,确保接到的 ByteBuf 消息是完整的
 */
public class MessageCodecSharable extends MessageToMessageCodec<ByteBuf, Message> {
    @Override
    protected void encode(ChannelHandlerContext ctx, Message msg, List<Object> outList) throws Exception {
        ByteBuf out = ctx.alloc().buffer();
        // 1. 4 字节的魔数
        out.writeBytes(new byte[]{1, 2, 3, 4});
        // 2. 1 字节的版本,
        out.writeByte(1);
        // 3. 1 字节的序列化方式 jdk 0 , json 1
        out.writeByte(0);
        // 4. 1 字节的指令类型
        out.writeByte(msg.getMessageType());
        // 5. 4 个字节
        out.writeInt(msg.getSequenceId());
        // 无意义,对齐填充
        out.writeByte(0xff);
        // 6. 获取内容的字节数组
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(msg);
        byte[] bytes = bos.toByteArray();
        // 7. 长度
        out.writeInt(bytes.length);
        // 8. 写入内容
        out.writeBytes(bytes);
        outList.add(out);
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        int magicNum = in.readInt();
        byte version = in.readByte();
        byte serializerType = in.readByte();
        byte messageType = in.readByte();
        int sequenceId = in.readInt();
        in.readByte();
        int length = in.readInt();
        byte[] bytes = new byte[length];
        in.readBytes(bytes, 0, length);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
        Message message = (Message) ois.readObject();
        log.debug("magicNum={}, version={}, serializerType={}, messageType={}, sequenceId={}, length={}", magicNum, version, serializerType, messageType, sequenceId, length);
        log.debug("message={}", message);
        out.add(message);
    }
}

魔数4字节+版本-1 字节+序列化方式-1 字节+ 指令类型-1 字节+请求序号-4字节+消息长度-4字节=15字节;一般设计成2的整数倍。2^4=16字节,所以中间填充一个字节

测试代码:

import com.netty.project.message.LoginRequestMessage;
import com.netty.project.protocol.MessageCodec;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.logging.LoggingHandler;

public class MessageCodecTest {
    public static void main(String[] args) throws Exception{
        EmbeddedChannel channel = new EmbeddedChannel(
                new LoggingHandler(),
                new LengthFieldBasedFrameDecoder(
                        1024, 12, 4, 0, 0),
                new MessageCodec()
        );
// encode
        LoginRequestMessage message = new LoginRequestMessage("zhangsan", "123", "张三");
        channel.writeOutbound(message);
    }
}

01 02 03 04:魔数

01 :版本号

00 :序列化方式

00 :指令类型

00 00 00 00:请求序号

ff :对齐填充

00 00 00 ef:消息正文长度(e=14,f=15)14*16+15=239直接+协议上面16=255

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Netty-WebSocket-Spring-Boot-Starter是一个用于将Websocket集成到Spring Boot应用程序中的库。它使用Netty作为底层框架,提供了一种快速和可靠的方式来处理异步通信。 这个库提供了一种简单的方法来创建Websocket端点,只需要使用注释和POJO类即可。在这些端点上可以添加动态的事件处理程序,以处理连接、断开连接和消息事件等。 此外,Netty-WebSocket-Spring-Boot-Starter还包括了一些安全性的特性,如基于令牌的授权和XSS保护,可以帮助您保持您的Websocket应用程序安全。 总的来说,Netty-WebSocket-Spring-Boot-Starter提供了一种快速和易于使用的方式来构建Websocket应用程序,使得它成为应用程序开发人员的有用工具。 ### 回答2: netty-websocket-spring-boot-starter 是一个开源的 Java Web 开发工具包,主要基于 Netty 框架实现了 WebSocket 协议的支持,同时集成了 Spring Boot 框架,使得开发者可以更加方便地搭建 WebSocket 服务器。 该工具包提供了 WebSocketServer 配置类,通过在 Spring Boot 的启动配置类中调用 WebSocketServer 配置类,即可启动 WebSocket 服务器。同时,该工具包还提供了多种配置参数,如端口号、URI 路径、SSL 配置、认证配置等等,可以根据业务需求进行自定义配置。 此外,该工具包还提供了一些可扩展的接口和抽象类,如 WebSocketHandler、ChannelHandlerAdapter 等,可以通过继承和实现这些接口和抽象类来实现业务逻辑的处理和拓展。 总的来说,netty-websocket-spring-boot-starter 提供了一个高效、简单、易用的 WebSocket 服务器开发框架,可以减少开发者的开发成本和工作量。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值