Netty(5)-自定义通信协议包的编码解码

如何利用 Netty 实现自定义协议通信? 通信协议设计 1. 魔数 2. 协议版本号 3. 序列化算法 4. 报文类型 5. 长度域字段 6. 请求数据 7. 状态 8. 保留字段 Netty 如何实现自定义通信协议 Netty 常用编码器类型 Netty 常用解码器类型 MessageToByteEncoder MessageToMessageEncoder 抽象解码类 抽象解码类 ByteToMessageDecoder。 抽象解码类 MessageToMessageDecoder。 通信协议实战 总结 阅读详情

一.何为通信协议

这里指的通信协议并不是狭义的TCP、UDP这类【标准通信协议】,而是指的Netty用于客户端与服务端之间数据交互的【自定义通信协议】。无论是使用 Netty 还是原始的 Socket 编程,基于 TCP 通信的数据包格式均为二进制,协议指的就是客户端与服务端事先商量好的,每一个二进制数据包中每一段字节分别代表什么含义的规则。如下图所示为一个简单的登陆所用的协议:
在这里插入图片描述

在这个数据包中,第一个字节为 1 表示这是一个登录指令,接下来是用户名和密码,这两个值以 \0 分割,客户端发送这段二进制数据包到服务端,服务端就能根据这个协议来取出用户名密码,进行登录逻辑。
在这里插入图片描述
如何设计一个通信协议包?

  1. 首先,第一个字段是魔数,通常情况下为固定的几个字节(我们这边规定为4个字节)。 为什么需要这个字段,而且还是一个固定的数?假设我们在服务器上开了一个端口,比如 80 端口,如果没有这个魔数,任何数据包传递到服务器,服务器都会根据自定义协议来进行处理,包括不符合自定义协议规范的数据包。例如,我们直接通过 http://服务器ip 来访问服务器(默认为 80 端口), 服务端收到的是一个标准的 HTTP 协议数据包,但是它仍然会按照事先约定好的协议来处理 HTTP 协议,显然,这是会解析出错的。而有了这个魔数之后,服务端首先取出前面四个字节进行比对,能够在第一时间识别出这个数据包并非是遵循自定义协议的,也就是无效数据包,为了安全考虑可以直接关闭连接以节省资源。在 Java 的字节码的二进制文件中,开头的 4 个字节为0xcafebabe 用来标识这是个字节码文件,亦是异曲同工之妙。

  2. 接下来一个字节为版本号,通常情况下是预留字段,用于协议升级的时候用到,有点类似 TCP 协议中的一个字段标识是 IPV4 协议还是 IPV6 协议,大多数情况下,这个字段是用不到的,不过为了协议能够支持升级,我们还是先留着。

  3. 第三部分,序列化算法表示如何把 Java 对象转换二进制数据以及二进制数据如何转换回 Java 对象,比如 Java 自带的序列化,json,hessian 等序列化方式。

  4. 第四部分的字段表示指令,关于指令相关的介绍,我们在前面已经讨论过,服务端或者客户端每收到一种指令都会有相应的处理逻辑,这里,我们用一个字节来表示,最高支持256种指令,对于我们这个 IM 系统来说已经完全足够了。

  5. 接下来的字段为数据部分的长度,占四个字节。

  6. 最后一个部分为数据内容,每一种指令对应的数据是不一样的,比如登录的时候需要用户名密码,收消息的时候需要用户标识和具体消息内容等等。

在这里插入图片描述

二.代码实现

  • 项目目录

在这里插入图片描述

  • utils/command
/*
 * @Author ARong
 * @Description 定义指令集
 * @Date 2020/2/4 8:58 下午
 **/
public interface Command {
    byte LOGIN = 1;
}
  • utils/MySerializer

/*
 * @Author ARong
 * @Description 定义序列化器
 * @Date 2020/2/4 9:06 下午
 **/
public interface MySerializer {

    /*
     * @Author ARong
     * @Description 将Java对象转化为二进制字节流
     * @Date 2020/2/4 9:07 下午
     * @Param object
     * @return byte[]
     **/
    byte[] serialize(Object object);


    /*
     * @Author ARong
     * @Description 二进制转换成 java 对象
     * @Date 2020/2/4 9:08 下午
     * @Param [clazz, bytes]
     * @return T
     **/
    <T> T deserialize(Class<T> clazz, byte[] bytes);
}
  • utils/Serialize
/*
 * @Author ARong
 * @Description 定义序列化方式
 * @Date 2020/2/4 9:05 下午
 **/
public interface Serialize {
    byte JSON = 1;
}
  • utils/SerializerFactory
/*
 * @Author ARong
 * @Description 序列化器工厂
 * @Date 2020/2/4 8:56 下午
 **/
public class SerializerFactory {
    /*
     * @Author ARong
     * @Description 通过序列化名称获取相应的序列化器
     * @Date 2020/2/4 9:17 下午
     * @Param [serName]
     * @return io_learn.netty.part4_protocol.utils.MySerializer
     **/
    public static MySerializer getSerializer(byte serName) {
        if (serName == Serialize.JSON) {
            return new MyJsonSerializer();
        }
        return null;
    }
}

  • utils/MyJsonSerializer
/**
 * @Auther: ARong
 * @Date: 2020/2/4 9:09 下午
 * @Description:
 */
public class MyJsonSerializer implements MySerializer {
    /*
     * @Author ARong
     * @Description 将Java对象转化为二进制字节流
     * @Date 2020/2/4 9:07 下午
     * @Param object
     * @return byte[]
     **/
    @Override
    public byte[] serialize(Object object) {
        return JSON.toJSONBytes(object);
    }

    /*
     * @Author ARong
     * @Description 二进制转换成 java 对象
     * @Date 2020/2/4 9:08 下午
     * @Param [clazz, bytes]
     * @return T
     **/
    @Override
    public <T> T deserialize(Class<T> clazz, byte[] bytes) {
        return JSON.parseObject(bytes, clazz);
    }
}
  • utils/PacketUtil
/**
 * @Auther: ARong
 * @Date: 2020/2/4 9:14 下午
 * @Description: 用于包编码和解码
 */
public class PacketUtil {
    /*
     * @Author ARong
     * @Description 将Packet按照约定序列化方式编码成ByteBuf
     * @Date 2020/2/4 9:15 下午
     * @Param [packet]
     * @return io.netty.buffer.ByteBuf
     **/
    public static ByteBuf encode(Packet packet) {
        // 创建ByteBuf对象
        ByteBuf byteBuf = ByteBufAllocator.DEFAULT.ioBuffer();
        // 获取序列化器序列化对象
        MySerializer serializer = SerializerFactory.getSerializer(packet.getSerMethod());
        byte[] data = serializer.serialize(packet);
        // 按照通信协议填充ByteBUf
        byteBuf.writeInt(packet.getMagic());// 魔数
        byteBuf.writeByte(packet.getVersion()); // 版本号
        byteBuf.writeByte(packet.getSerMethod()); // 序列化方式
        byteBuf.writeByte(packet.getCommand()); // 指令
        byteBuf.writeInt(data.length);// 数据长度
        byteBuf.writeBytes(data); // 数据
        return byteBuf;
    }

    /*
     * @Author ARong
     * @Description 将ByteBuf按照约定序列化方式解码成Packet
     * @Date 2020/2/4 9:21 下午
     * @Param [byteBuf]
     * @return io_learn.netty.part4_protocol.packet.Packet
     **/
    public static Packet decode(ByteBuf byteBuf) {
        // 暂不判断魔数,跳过
        byteBuf.skipBytes(4);
        // 暂不判断魔数版本号,跳过
        byteBuf.skipBytes(1);
        // 获取序列化方式
        byte serMethod = byteBuf.readByte();
        // 获取指令
        byte command = byteBuf.readByte();
        // 获取数据包长度
        int length = byteBuf.readInt();
        // 获取存储数据的字节数组
        byte[] data = new byte[length];
        byteBuf.readBytes(data);
        // 反序列化数据,获取Packet
        Class<? extends Packet> packetType = getPacketType(command);
        Packet res = SerializerFactory.getSerializer(serMethod).deserialize(packetType, data);
        return res;
    }


    /*
     * @Author ARong
     * @Description 通过指令获取相应的Packet
     * @Date 2020/2/4 9:31 下午
     * @Param [commond]
     * @return io_learn.netty.part4_protocol.packet.Packet
     **/
    public static Class<? extends Packet> getPacketType(byte commond) {
        if (commond == Command.LOGIN) {
            return LoginPacket.class;
        }
        return null;
    }
}

  • packet/Packet
/**
 * @Auther: ARong
 * @Date: 2020/2/4 8:44 下午
 * @Description: 自定义通信协议的抽象包
 */
@Data
public abstract class Packet {
    private int magic; // 魔数
    private byte version; // 版本号
    private byte serMethod; // 的序列化/反序列化方式
    private byte command; // 指令
}
  • packet/LoginPacket
/**
 * @Auther: ARong
 * @Date: 2020/2/4 8:52 下午
 * @Description: 用以登陆的登陆协议包
 */
@Data
public class LoginPacket extends Packet {
    private int magic = 20202020; // 魔数
    private byte version = 1; // 版本号
    private byte serMethod = Serialize.JSON; // 序列化反序列化方式为Json
    private byte command = Command.LOGIN; // 登陆指令
    // ---------以下为登陆数据----------
    private long userId;
    private String name;
    private String password;


    @Override
    public String toString() {
        return "LoginPacket{" +
                "magic=" + magic +
                ", version=" + version +
                ", serMethod=" + serMethod +
                ", command=" + command +
                ", userId=" + userId +
                ", name='" + name + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}
  • server/NettyServer
/**
 * @Auther: ARong
 * @Date: 2020/2/4 9:13 下午
 * @Description: Netty Server
 */
public class NettyServer {
    public static void main(String[] args) {
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        NioEventLoopGroup bossGroup = new NioEventLoopGroup();
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();
        serverBootstrap.group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .childHandler(
                        new ChannelInitializer<SocketChannel>() {
                            @Override
                            // 初始化channel
                            protected void initChannel(SocketChannel ch) throws Exception {
                                ch.pipeline().addLast(new MyServerHandler());
                            }
                        }
                ).bind(8000);
    }
}

  • server/NettyClient
/**
 * @Auther: ARong
 * @Date: 2020/2/5 1:28 下午
 * @Description: NettyClient
 */
public class NettyClient {
    public static void main(String[] args) throws InterruptedException {
        Bootstrap bootstrap = new Bootstrap();
        NioEventLoopGroup group = new NioEventLoopGroup();

        bootstrap.group(group).channel(NioSocketChannel.class).handler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
                // 责任链模式,添加第一次连接的客户端处理逻辑
//                ch.pipeline().addLast(new FirstClientHandler());
            }
        });

        Channel channel = bootstrap.connect("127.0.0.1", 8000).channel();
        LoginPacket packet = new LoginPacket();
        packet.setUserId(123l);
        packet.setName("ARong");
        packet.setPassword("123");
        ByteBuf byteBuf = PacketUtil.encode(packet);
        System.out.println("客户端正在发送包");
        channel.writeAndFlush(byteBuf);
    }
}

  • handler/MyServerHandler
/**
 * @Auther: ARong
 * @Date: 2020/2/5 1:17 下午
 * @Description:
 */
public class MyServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("服务端接收到消息,正在解码");
        Packet packet = PacketUtil.decode((ByteBuf) msg);
        if (packet.getCommand() == Command.LOGIN) {
            packet = (LoginPacket)packet;
        }
        System.out.println(packet);
    }
}

在这里插入图片描述

Netty_03_ByteBuf和网络中拆包粘包问题及其解决 Netty 阅读详情

相关推荐

《科研伦理与学术规范》期末考试答案2023

26单选(2 分)某学术杂志的评稿人同时是某高校相关专业的硕士生导师,在评判一个稿件的时候,他发现该作者研究的题目很有价值, 他便一边压下这个稿子,一边将稿子的纲要传给自己门下的硕士生, 指导他们使用这个题目独立写出一篇论文,并帮助他们发表在其他期 刊上。D.要善待实验动物,是指在饲养管理和使用实验动物过程中,要采取有效措施,使实验动物免遭不必要的伤害、饥渴、不适、惊恐、折磨、疾病和疼痛,保证动物能够实现自然行为,受到良好的管理与照料,为其提供清洁、舒适的生活环境。张某在压力之下不得不屈从。

Glenseeds的博客 4万+

Netty使用kryo序列化传输对象

Netty使用kryo序列化传输对象 横渡 Netty使用kryo序列化传输对象 - 简书参考文章:https://blog.csdn.net/eguid_1/article/details/79316403https://blog.csdn.net/top_co...https://www.jianshu.com/p/e075647275e7 参考文章:https://blog.csdn.net/eguid_1/article/details/79316403https://blog.csdn.ne

优秀技术文章收录 693

QGIS v3.28 高效加载OSM在线地图的三种实用方法

本文详细介绍了在QGIS v3.28中高效加载OSM在线地图的三种实用方法:通过XYZ Tiles直接连接、使用QuickMapServices插件以及配置WMS/WTMS服务。针对每种方法提供了具体操作步骤、常见问题解决方案和性能优化建议,帮助用户根据需求选择最适合的方案,提升GIS工作效率。

weixin_29233333的博客 435

Netty进阶之自定义协议

Netty进阶之自定义协议

weixin_38598961的博客 841

从零配置华为S5720交换机VLAN:Web界面与命令行双模式详解(附拓扑图)

本文详细解析华为S5720交换机VLAN配置的全过程,涵盖Web界面与命令行(CLI)双模式操作指南。从基础环境准备到多交换机环境配置同步,特别针对S5720-52X系列交换机的VLANif接口、端口分配策略及交换机堆叠技术进行深入讲解,并附实用拓扑图与配置示例,助力企业网络管理员高效完成VLAN部署。

happy2的博客 1374

用STM32定时器PWM高级功能驱动步进电机:F103C8T6+TB6600实战教程

本文详细介绍了如何利用STM32F103C8T6微控制器的定时器PWM高级功能驱动TB6600步进电机驱动器,实现高效、灵活的运动控制方案。通过硬件配置、PWM参数计算、运动控制算法及TB6600高级功能应用的全面解析,为工业自动化和精密控制领域提供了实用的技术参考。

game4的博客 186

Netty 编解码与自定义编解码

实现一个解码器,首先要继承ByteToMessageDecoder抽象类,然后实现其基类的decode()抽象方法。继承ByteToMessageDecoder抽象类。实现基类的decode()抽象方法,将ByteBuf到目标POJO的解码逻辑写入此方法,以将ByteBuf中的二进制数据解码成一个一个的JavaPOJO对象。解码完成后,需要将解码后的JavaPOJO对象放入decode()方法的List实参中,此实参是父类所传入的解码结果收集容器。......

CodingAnHour 1938

知识导航神器:Obsidian Breadcrumbs 插件全攻略

Parent: 插件Child: Breadcrumbs 用法这样,它就能识别出你和其他笔记的关系。

m0_73735578的博客 1630

netty自定义通讯协议校验魔数

1、参考之前的文章netty之服务端热插拔登录校验Handler_非ban必选的博客-优快云博客服务端和客户端编解码Handler修改如下 PacketCodecHandler.java ,主要修改解码decode方法 修改服务端和和客户端自定义编解码类 PacketCodec 的解码decode方法,注释 int magic = byteBuf.readInt();该行代码即可 我们把客户端的魔数改成和服务端不一样,然后发消息给服务端,可以看到控制台输入如下 然后我们在把客户端的魔数改成和服

zsj777的专栏 795

PostgreSQL 核心原理:大字段(大对象)是如何被压缩和存储的(TOAST存储机制)

PostgreSQL通过TOAST机制高效处理大字段存储问题,解决单行超过8KB页大小限制的挑战。TOAST采用透明分片、按需加载和自动压缩策略,支持TEXT、JSONB等变长类型,无需用户干预。其核心包括四种存储策略(plain/external/main/extended),自动将大字段分片存入专用TOAST表,主表仅保留指针。读取时按需解压拼接,支持部分加载优化。TOAST表结构包含chunk_id、序列号和分片数据,配合内置PGLZ压缩算法,平衡存储效率与访问性能。该机制使PostgreSQL能稳定

数据知道的博客 2630

LangGraph教程8:LangGraph流式处理

流式处理可显著提升基于LLM应用的响应性。LangGraph支持两种流式模式:values模式返回每一步的完整结果,适合需要完整上下文处理的场景;updates模式仅返回增量变化,适用于实时更新需求。示例代码展示了如何用create_react_agent实现这两种模式,其中values模式完整记录处理流程,updates模式逐步输出结果。通过异步迭代输出流,开发者可以灵活选择适合应用场景的流式处理方式。

Cachel Wood的博客 623

Netty学习七:编解码之自定义通信协议

Netty学习系列

weixin_41402069的博客 4396

西门子TP900精智系列触摸屏和三菱Q系列PLC以太网通信的组态步骤

西门子TP900精智系列触摸屏和三菱Q系列PLC以太网通信的组态步骤

Robot_PLC_HMI_Automation 6100

Netty 学习(六)实现自定义协议通信

本文记录如何设计一个高效、可扩展、易维护的自定义通信协议,以及如何使用 Netty 实现自定义的通信协议。

大鱼的博客 2589

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

蓝牙模块HM-10手册

蓝牙模块HM-10手册,该手册对蓝牙模块HM-10进行了详细的介绍和使用方法以及一些AT指令。

Netty从0到1系列之拆包、粘包【5】

本文介绍了Netty自定义通信协议的设计与实现。首先阐述了协议设计原则,包括消息边界、元数据、扩展性等要点。接着详细设计了一个包含魔数、版本号、序列化算法、消息类型等字段的协议结构,并提供了Mermaid格式的示意图。最后给出了Java实现示例,包括消息实体类定义,展示了如何封装协议字段(如魔数、版本号、序列化算法等)。该设计方案可有效解决粘包拆包问题,支持协议升级,适用于高性能网络通信场景。

编程技术圈 1058

IDEA的使用指南

IDEA基于java的使用手册,可以最快速的上手,在使用中不断的丰富自己的使用技巧.

21.Netty源码之编码器

highlight: arduino-light Netty如何实现自定义通信协议 在学习完如何设计协议之后,我们又该如何在 Netty 中实现自定义的通信协议呢?其实 Netty 作为一个非常优秀的网络通信框架,已经为我们提供了非常丰富的编解码抽象基类,帮助我们更方便地基于这些抽象基类扩展实现自定义协议。 首先我们看下 Netty 中常用的编解码器有哪些。 一次编解码器和二次编解码器...

qq_30635523的博客 1260

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

Netty 进阶知识 编解码器、Protobuf、TCP粘包、出站入站

首先,在.proto文件中定义您的消息格式。通过这些步骤,可以在 Netty 应用程序中集成 Protobuf,利用其高效的二进制数据序列化/反序列化能力。这对于需要处理复杂数据结构或要求高性能的网络通信应用尤其有用。Protobuf 提供了一种高效、灵活的方式来定义和处理结构化数据。在分布式系统、大数据应用和微服务架构中,Protobuf 是数据交换的理想选择,特别是在对性能和带宽使用有严格要求的场景中。TCP流式传输TCP是面向连接的、面向流的协议,它不保留消息边界。

日常记录 1937

22.Netty源码之解码器

highlight: arduino-light 抽象解码类 https://mp.weixin.qq.com/s/526p5f9fgtZu7yYq5j7LiQ 解码器 Netty 常用解码器类型: ByteToMessageDecoder/ReplayingDecoder 将字节流解码为消息对象; MessageToMessageDecoder 将一种消息类型解码为另外一种消息类...

qq_30635523的博客 1283

Netty编解码相关

1.TCP最基本的拆包/粘包问题 为什么有拆包/粘包? 网络通信过程中,每次可发送的数据包大小是受限制的,如 MTU:传输单元大小,链路层一次最大传输数据大小,一般为1500byte MSS:最大分段大小,TCP最大报文段长度,是传输层一次发送最大数据大小 滑动窗口:数据接收方设置的窗口大小 TCP报文如何保证数据包按次序到达并不丢失? 所有数据帧都是有编号的,TCP对多个报文段回复一次ACK 如果一次传输的网络数据大小超过传输单元大小,数据会拆分为多个数据包发送 如果每.

zhuyong7的博客 445

Netty自定义协议开发,实现私有通信协议

3. **采用协议缓冲**:如Protobuf可以减少序列化开销。2. **更小的数据体积**:针对特定业务设计紧凑的数据结构。3. **更好的扩展性**:可以根据业务需求灵活调整协议格式。1. **更高的性能**:去掉不必要的报文头和冗余字段。1. **魔术字**:通常放在消息开头用于识别协议类型。4. **消息类型**:区分不同类型的业务消息。1. **使用对象池**:避免频繁创建消息对象。2. **消息长度**:指示整个消息的长度。3. **版本号**:用于协议升级兼容。

2509_93945572的博客 409

一种基于Netty的服务端通用通信协议转换方法

各种类型的通信协议根据上层协议定义的格式,经过半包、粘包处理后,形成完整包,后续进行包头解析,将解析的结果全部转换为统一的ServletRequest,ServletResponse对象,然后将2个参数传入到SpringMVC的DispatcherServlet的service方法中。将Netty放在Spring容器中作为一个Bean进行管理,Netty的服务继承ApplicationRunner的接口,覆盖重写run的接口,在Spring容器启动的时候,同时启动Netty的服务。

weixin_41800760的博客 1031

Netty从0到1系列之拆包、粘包【4】

本文介绍了Netty自定义通信协议的设计要点。一个完整的通信协议需要包含魔数(安全验证)、协议版本号(兼容性)、报文类型(区分业务场景)、长度域字段(确定报文边界)、序列化算法(数据转换方法)、请求数据(业务内容)、状态字段(请求状态)和保留字段(未来扩展)等关键要素。通过合理设计这些要素,可以构建出高性能、可扩展且安全的私有协议,避免使用通用协议可能带来的性能损耗和安全隐患。文中给出了一个典型协议结构示例,展示了各字段的排列方式。

编程技术圈 834

Netty进阶之粘包与拆包

TCP粘包和拆包问题,可以说是网络编程的常见问题了~

yehongzhi1994的博客 1031

netty学习笔记13 - Protobuf 和 Netty的编码与解码

编码与解码 基本介绍 编写网络应用数据时,因为数据在网络中传输的都是二进制字节码数据,在发送数据时就需要编码,接收数据时就需要解码。 codec(编解码器)的组成部分有两个:decoder(解码器),encoder(编码器)。encoder负责把业务数据转成字节码数据,decoder负责把字节码数据转成业务数据。 netty本身编码与解码的机制和问题分析 Netty 自身提供了一些 cod...

方亚军的博客 638

快速搞定netty自定义通信协议,就是这么简单!!

1、首先需要定义一个协议;2、其次定制一个编码器,便于发送端把自定义的协议给解码成数据流发送到网络上;3、最后定制一个解码器,便于接收端把收到的数据流再解码成自定协议的数据结构;下面我们分别纤细介绍这三个数据结构。

xunaiu34的博客 1071

Netty编解码框架分析

1. 背景 1.1. 编解码技术 通常我们也习惯将编码(Encode)称为序列化(serialization),它将对象序列化为字节数组,用于网络传输、数据持久化或者其它用途。 反之,解码(Decode)/ 反序列化(deserialization)把从网络、磁盘等读取的字节数组还原成原始对象(通常是原始对象的拷贝),以方便后续的业务逻辑操作。 进行远程跨进程服务调用时(例如 RPC 调用),需要...

Vincent2014Linux 947
上一篇: Netty(4)-二进制载体ByteBuf
下一篇: Netty(6)-客户端登陆并与服务端收发信息
BoringRong
BoringRong 领域专家: 后端开发技术领域 领域专家: 后端开发技术领域
博客等级 码龄9年 2万+粉丝 386原创
评论 5
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BoringRong

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值