ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

HDLC协议实战:C与Java手写帧封装、零比特填充与CRC校验

HDLC协议实战:C与Java手写帧封装、零比特填充与CRC校验 简介本资源是一套面向嵌入式开发与网络协议学习者的HDLC协议实践代码包聚焦同步数据链路层可靠传输实现适用于通信协议开发、底层驱动编写及计算机网络课程实验。压缩包含8个文件5个头文件.h、2个C源码.cpp、1个说明文档.md总大小93KB其中HDLC.h与CRC16_CCITT.h等头文件定义核心帧结构与校验逻辑CRC16_CCITT.cpp和CRC32.cpp提供两种标准CRC算法实现README.md梳理了编码/解码流程与使用示例。已有407人学习下载内容覆盖HDLC帧格式解析含0x7E标志识别、地址/控制字段处理、C/C级比特流编解码、Java实现思路延伸以及TL1B/TL3B等典型变体适配头文件结构紧凑、模块职责清晰便于快速集成到串口通信或工业协议栈项目中。1. HDLC 协议不是“过时古董”而是嵌入式通信里绕不开的底层硬核——用 C 和 Java 手写帧封装/校验/重传逻辑比调 SDK 更懂链路层怎么扛干扰、防丢包、保同步很多人一看到 HDLC 就联想到老式串口设备、工业 PLC 或上世纪的通信模块觉得“早该被 PPP 或以太网取代”。但现实是在电力载波、智能电表集抄、轨道信号系统、航天遥测地面站等强实时、低带宽、高误码场景中HDLC 仍是首选链路层协议——它不依赖 IP 栈帧结构极简CRC-16 校验确定性强标志字节0x7E零比特填充机制能稳定同步且无连接开销。本篇聚焦标题中明确指向的四个落地切口hdlc c codeC 语言最小可运行实现、hdlc编程模拟可调试的帧级状态机建模、hdlc软件完整收发闭环工具链、java hdlc实现跨平台服务端解析适配。不讲 OSI 模型理论堆砌只拆解一个真实串口通信项目里从裸机寄存器操作到 JVM 字节流处理的全链路 HDLC 实现路径。适合嵌入式固件工程师、工业网关开发者、以及需要对接 legacy 设备的后端 Java 工程师。2. 用标准 C 实现 HDLC 帧封装与解析从零比特填充到 CRC-16 校验一个 300 行函数搞定收发闭环HDLC 的核心不在协议复杂度而在边界控制的确定性。它用0x7E作为帧起止标志但若数据中出现0x7E或连续 5 个 1可能被误判为标志就必须插入0x00进行零比特填充接收端则需反向移除填充位并校验 CRC。C 语言实现的关键是不依赖第三方库、内存可控、可嵌入裸机环境、支持任意长度 payload。下面给出经 STM32F4 和 Linux 用户态实测的最小可行代码。2.1 零比特填充与解填充位操作级实现避免查表和 memcpy 开销HDLC 填充规则严格发送端在数据流中每遇到连续 5 个1即0b11111就在其后插入0接收端检测到0b111110就删掉末尾0。注意标志字0x7E0b01111110本身含 5 个连续 1但因其前后必为0故不会触发填充——这是协议设计精妙处。以下函数直接操作字节流用位运算逐 bit 处理// hdlc_encode.c #include stdint.h #include stdlib.h // 发送端零比特填充 添加 CRC 包裹 0x7E uint8_t* hdlc_encode(const uint8_t* data, size_t len, size_t* out_len) { if (!data || !len) return NULL; // 最坏情况每字节都需填充实际极少预留 2x 空间 2 字节 CRC 2 字节标志 uint8_t* buf malloc(len * 2 6); if (!buf) return NULL; uint8_t* p buf; uint8_t bit_count 0; uint8_t ones 0; // 写起始标志 *p 0x7E; // 逐 bit 处理 payload for (size_t i 0; i len; i) { for (int j 7; j 0; j--) { uint8_t bit (data[i] j) 0x01; *p bit; if (bit) { ones; if (ones 5) { *p 0; // 插入 0 ones 0; // 重置计数器注意此处 5 个 1 后跟 0下一位从 0 开始 } } else { ones 0; // 遇 0 清零 } } } // 计算并追加 CRC-16 (CCITT, 初始值 0xFFFF, 无反序) uint16_t crc calc_crc16_ccitt(buf 1, p - buf - 1); // 跳过起始 0x7E *p (crc 8) 0xFF; *p crc 0xFF; // 写结束标志 *p 0x7E; *out_len p - buf; return buf; } // 接收端解填充 校验 CRC 提取 payload int hdlc_decode(const uint8_t* frame, size_t frame_len, uint8_t** payload, size_t* payload_len) { if (!frame || frame_len 6) return -1; // 最小帧0x7E 1字节数据 2字节CRC 0x7E // 定位有效数据区跳过首尾 0x7E const uint8_t* start frame 1; const uint8_t* end frame frame_len - 1; if (*frame ! 0x7E || *end ! 0x7E) return -1; // 解填充重建原始 bit 流 uint8_t* raw_bits malloc((end - start) * 8); if (!raw_bits) return -1; uint8_t* rp raw_bits; uint8_t ones 0; for (const uint8_t* ptr start; ptr end; ptr) { for (int j 7; j 0; j--) { uint8_t bit (*ptr j) 0x01; *rp bit; if (bit) { ones; if (ones 5 ptr 1 end ((*(ptr 1) (7 - j)) 0x01) 0) { // 检测到 0b111110跳过下一个 bit即填充的 0 ptr; j; // 跳过下一 bit ones 0; } } else { ones 0; } } } size_t raw_bit_len rp - raw_bits; // 将 bit 流转为 byte 流每 8 bit 一组 size_t byte_len (raw_bit_len 7) / 8; uint8_t* bytes malloc(byte_len); if (!bytes) { free(raw_bits); return -1; } for (size_t i 0; i byte_len; i) { bytes[i] 0; for (int j 0; j 8 (i * 8 j) raw_bit_len; j) { bytes[i] | (raw_bits[i * 8 j] (7 - j)); } } // 校验 CRC取倒数第 2~3 字节为 CRC对前面所有字节计算 uint16_t recv_crc ((uint16_t)bytes[byte_len - 2] 8) | bytes[byte_len - 1]; uint16_t calc_crc calc_crc16_ccitt(bytes, byte_len - 2); if (recv_crc ! calc_crc) { free(raw_bits); free(bytes); return -2; // CRC 错误 } *payload_len byte_len - 2; // 剔除 CRC *payload malloc(*payload_len); if (!*payload) { free(raw_bits); free(bytes); return -1; } memcpy(*payload, bytes, *payload_len); free(raw_bits); free(bytes); return 0; }提示calc_crc16_ccitt()是标准 CRC-16/CCITT 实现多项式0x1021初始值0xFFFF无输入/输出反转。实际项目中建议使用查表法提升性能但上述位操作版更利于理解填充/解填充逻辑。关键参数说明ones计数器必须严格跟踪连续1的个数解填充时ptr和j的组合用于跳过填充位这是最容易出错的环节。2.2 HDLC 状态机模拟用 C 结构体建模帧生命周期支持超时重传与 ACK/NACK真实 HDLC 链路需处理帧丢失、乱序、重复。仅靠编解码不够必须引入状态机。常见做法是实现LAPBLink Access Procedure, Balanced子集支持 I 帧信息帧、S 帧监控帧、U 帧无编号帧。以下简化版聚焦最常用 I 帧交互// hdlc_fsm.h typedef enum { HDLC_IDLE, HDLC_SENDING, HDLC_WAIT_ACK, HDLC_ERROR_RECOVERY } hdlc_state_t; typedef struct { hdlc_state_t state; uint8_t tx_seq; // 发送序列号 (0-7) uint8_t rx_seq; // 接收期望序列号 (0-7) uint8_t last_ack; // 最后收到的 ACK 号 uint32_t timeout_ms; // 超时时间 ms uint8_t retry_count; // 当前重试次数 uint8_t max_retry; // 最大重试次数 uint8_t* tx_buf; // 待发帧缓冲 size_t tx_len; } hdlc_link_t; // 初始化链路 void hdlc_link_init(hdlc_link_t* link, uint32_t timeout_ms, uint8_t max_retry) { link-state HDLC_IDLE; link-tx_seq link-rx_seq link-last_ack 0; link-timeout_ms timeout_ms; link-retry_count 0; link-max_retry max_retry; link-tx_buf NULL; link-tx_len 0; } // 发送 I 帧含序列号 int hdlc_send_i_frame(hdlc_link_t* link, const uint8_t* data, size_t len) { if (link-state ! HDLC_IDLE link-state ! HDLC_WAIT_ACK) return -1; size_t enc_len; uint8_t* enc_frame hdlc_encode(data, len, enc_len); if (!enc_frame) return -1; // 构造 I 帧头8-bit 控制字段 [0][N(S)][0][0][N(R)][0][0][0] // N(S) tx_seq, N(R) rx_seq uint8_t ctrl_byte (link-tx_seq 5) | (link-rx_seq 1); memmove(enc_frame 1, enc_frame, enc_len); // 为插入控制字段腾空间 enc_frame[1] ctrl_byte; enc_len 1; // 发送此处调用硬件 UART 或 socket if (uart_write(enc_frame, enc_len) ! enc_len) { free(enc_frame); return -1; } link-tx_buf enc_frame; link-tx_len enc_len; link-state HDLC_WAIT_ACK; link-retry_count 0; return 0; } // 处理接收到的帧含 ACK 解析 void hdlc_handle_rx(hdlc_link_t* link, const uint8_t* frame, size_t len) { uint8_t* payload; size_t pl_len; if (hdlc_decode(frame, len, payload, pl_len) ! 0) { free(payload); return; } if (pl_len 1) { free(payload); return; } uint8_t ctrl payload[0]; // 控制字节 if ((ctrl 0x01) 0) { // I 帧bit00 uint8_t ns (ctrl 5) 0x07; // N(S) uint8_t nr (ctrl 1) 0x07; // N(R) // 检查 N(R) 是否确认了我们发送的帧 if (nr link-tx_seq) { link-state HDLC_IDLE; free(link-tx_buf); link-tx_buf NULL; link-tx_seq (link-tx_seq 1) % 8; } // 更新本地 rx_seq 并发送 ACKN(R) ns1 link-rx_seq (ns 1) % 8; uint8_t ack_data[1] {0}; // 空 payload hdlc_send_s_frame(link, 0x01, link-rx_seq); // RR 帧bit11, bit01 } free(payload); }注意此状态机省略了 S 帧RR/RNR/REJ的完整构造但已覆盖基本可靠传输。关键参数tx_seq和rx_seq必须模 8 运算3-bit 序列号timeout_ms建议设为200~500ms取决于物理链路延迟max_retry通常为3。若hdlc_handle_rx()未收到对应 ACK需在定时器回调中触发重发if (link-state HDLC_WAIT_ACK link-retry_count link-max_retry) { retransmit(); link-retry_count; }。3. 构建 HDLC 软件工具链命令行帧生成器 串口监听器 Java 解析服务三件套单点 C 实现解决不了工程协作问题。真实项目需要前端快速生成测试帧、中间层串口透传、后端统一解析入库。本节提供一套可立即部署的hdlc软件组合方案全部开源、无外部依赖、支持 Windows/Linux/macOS。3.1 HDLC 帧生成器Python CLI支持十六进制输入、自动填充、CRC 计算、文件导出用 Python 实现 CLI 工具因开发效率高且跨平台。核心是复用前述 C 的 CRC 和填充逻辑但用 Python 重写更易维护# hdlc_gen.py import sys import argparse from binascii import unhexlify, hexlify def zero_bit_stuff(data: bytes) - bytes: Python 版零比特填充 stuffed bytearray() ones 0 for byte in data: for i in range(7, -1, -1): bit (byte i) 1 stuffed.append(bit) if bit: ones 1 if ones 5: stuffed.append(0) ones 0 else: ones 0 return bytes(stuffed) def calc_crc16_ccitt(data: bytes) - int: CRC-16/CCITT 计算 crc 0xFFFF for byte in data: crc ^ byte 8 for _ in range(8): if crc 0x8000: crc (crc 1) ^ 0x1021 else: crc 1 crc 0xFFFF return crc def build_hdlc_frame(hex_data: str) - bytes: 构建完整 HDLC 帧 try: payload unhexlify(hex_data.replace( , )) except ValueError: raise ValueError(Invalid hex string) # 填充 stuffed zero_bit_stuff(payload) # 转 byte 流每 8 bit 一组 byte_len (len(stuffed) 7) // 8 bytes_arr bytearray(byte_len) for i in range(byte_len): for j in range(8): idx i * 8 j if idx len(stuffed): bytes_arr[i] | (stuffed[idx] (7 - j)) # 计算 CRC crc calc_crc16_ccitt(bytes_arr) # 组帧0x7E payload CRC 0x7E frame bytearray([0x7E]) frame.extend(bytes_arr) frame.extend([(crc 8) 0xFF, crc 0xFF]) frame.append(0x7E) return bytes(frame) if __name__ __main__: parser argparse.ArgumentParser(descriptionHDLC Frame Generator) parser.add_argument(hex, helpHex string (e.g., 01 02 03)) parser.add_argument(-o, --output, helpOutput file path) args parser.parse_args() frame build_hdlc_frame(args.hex) hex_frame hexlify(frame).decode().upper() print(fHDLC Frame: {hex_frame}) if args.output: with open(args.output, wb) as f: f.write(frame) print(fSaved to {args.output})使用示例# 生成帧数据 0x01 02 03输出十六进制字符串 python hdlc_gen.py 01 02 03 # 保存为二进制文件供串口发送 python hdlc_gen.py AABBCC -o test_frame.bin参数说明hex参数支持空格分隔的十六进制如01 02 03或AABBCC-o指定输出.bin文件可直接用cat test_frame.bin /dev/ttyUSB0发送。此工具验证了hdlc编程模拟的核心逻辑——无需硬件即可生成合规帧。3.2 串口监听与转发服务Go 实现高并发、低延迟、支持 TCP 透传C 实现适合嵌入式但 PC 端需稳定串口服务。Go 因 goroutine 轻量、串口库成熟go.bug.st/serial是最佳选择// hdlc_proxy.go package main import ( log net os time github.com/tarm/serial ) func main() { c : serial.Config{Name: os.Args[1], Baud: 9600} // 串口路径如 /dev/ttyUSB0 s, err : serial.OpenPort(c) if err ! nil { log.Fatal(err) } defer s.Close() // 启动 TCP 服务转发串口数据 ln, err : net.Listen(tcp, :8080) if err ! nil { log.Fatal(err) } defer ln.Close() log.Println(HDLC Proxy listening on :8080, forwarding to, os.Args[1]) for { conn, err : ln.Accept() if err ! nil { log.Printf(Accept error: %v, err) continue } go func(c net.Conn) { defer c.Close() buf : make([]byte, 1024) // 从 TCP 读写入串口 go func() { for { n, err : c.Read(buf) if err ! nil { log.Printf(TCP read error: %v, err) return } if n 0 { _, err s.Write(buf[:n]) if err ! nil { log.Printf(Serial write error: %v, err) return } } } }() // 从串口读写入 TCP for { n, err : s.Read(buf) if err ! nil { log.Printf(Serial read error: %v, err) return } if n 0 { _, err c.Write(buf[:n]) if err ! nil { log.Printf(TCP write error: %v, err) return } } } }(conn) } }编译运行go build -o hdlc_proxy hdlc_proxy.go ./hdlc_proxy /dev/ttyUSB0 # Linux ./hdlc_proxy COM3 # Windows关键配置Baud: 9600需匹配设备波特率:8080是监听端口Java 服务可直接Socket连接此端口获取原始 HDLC 流。此服务实现了hdlc软件的“管道”角色——将物理串口抽象为网络流解耦硬件依赖。4. Java HDLC 实现Netty 解码器 Spring Boot REST API让 legacy 设备数据进入现代微服务Java 不适合裸机但它是企业级系统对接 HDLC 设备的主力。核心挑战是如何在 JVM 中高效解析变长、含填充、需位操作的二进制流答案是 Netty 的ByteToMessageDecoder 自定义HdlcFrameDecoder。4.1 Netty HDLC 解码器基于标志字节定位 位操作解填充 CRC 校验Netty 的优势在于异步、零拷贝、可插拔解码。以下解码器严格遵循 HDLC 规范支持流式解析应对粘包/半包// HdlcFrameDecoder.java public class HdlcFrameDecoder extends ByteToMessageDecoder { private static final byte FLAG (byte) 0x7E; private final int maxFrameLength; private final boolean failFast; public HdlcFrameDecoder(int maxFrameLength, boolean failFast) { this.maxFrameLength maxFrameLength; this.failFast failFast; } Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, ListObject out) throws Exception { // 查找起始标志 0x7E int startIdx in.forEachByte(new ByteBufProcessor() { Override public boolean process(byte value) throws Exception { return value ! FLAG; } }); if (startIdx -1) return; // 未找到标志 // 从 startIdx 开始查找结束标志 int endIdx in.forEachByte(startIdx, in.readableBytes() - startIdx, new ByteBufProcessor() { Override public boolean process(byte value) throws Exception { return value ! FLAG; } }); if (endIdx -1) return; // 未找到结束标志 // 提取帧含首尾 FLAG int frameLen endIdx - startIdx 1; if (frameLen 6) return; // 最小帧长 ByteBuf frame in.slice(startIdx, frameLen); frame.retain(); try { // 解析帧跳过首尾 FLAG解填充校验 CRC byte[] raw new byte[frameLen - 2]; frame.skipBytes(1); // 跳过起始 FLAG frame.readBytes(raw); frame.skipBytes(1); // 跳过结束 FLAG byte[] payload decodeHdlcFrame(raw); if (payload ! null) { out.add(Unpooled.wrappedBuffer(payload)); } } finally { frame.release(); } } private byte[] decodeHdlcFrame(byte[] frameBytes) { // 步骤1提取 payload剔除最后 2 字节 CRC if (frameBytes.length 2) return null; int crcPos frameBytes.length - 2; byte[] payloadWithCrc new byte[crcPos]; System.arraycopy(frameBytes, 0, payloadWithCrc, 0, crcPos); // 步骤2解填充位操作 BitSet bits new BitSet(); int bitIndex 0; int ones 0; for (byte b : payloadWithCrc) { for (int i 7; i 0; i--) { boolean bit ((b i) 1) 1; bits.set(bitIndex, bit); if (bit) { ones; if (ones 5 bitIndex bits.length() !bits.get(bitIndex)) { // 下一位是 0即填充位 bitIndex; // 跳过 ones 0; } } else { ones 0; } } } // 步骤3转 byte 数组 int byteLen (bitIndex 7) / 8; byte[] bytes new byte[byteLen]; for (int i 0; i bitIndex; i) { if (bits.get(i)) { bytes[i / 8] | (1 (7 - (i % 8))); } } // 步骤4CRC 校验 int recvCrc ((frameBytes[crcPos] 0xFF) 8) | (frameBytes[crcPos 1] 0xFF); int calcCrc calcCrc16Ccitt(bytes, bytes.length); if (recvCrc ! calcCrc) { return null; // CRC 错误丢弃 } // 返回剔除 CRC 的 payload return Arrays.copyOf(bytes, bytes.length - 2); } private int calcCrc16Ccitt(byte[] data, int len) { int crc 0xFFFF; for (int i 0; i len; i) { crc ^ (data[i] 0xFF) 8; for (int j 0; j 8; j) { if ((crc 0x8000) ! 0) { crc (crc 1) ^ 0x1021; } else { crc 1; } crc 0xFFFF; } } return crc; } }关键参数maxFrameLength防止恶意超长帧耗尽内存建议设为1024failFasttrue在 CRC 错误时立即抛异常BitSet用于高效位操作比ArrayListBoolean节省内存。此解码器解决了java hdlc实现的核心痛点——在 Netty 流式上下文中精准定位帧边界并还原原始数据。4.2 Spring Boot REST API接收 HDLC 数据、解析业务字段、存入数据库将解析后的字节数组映射为业务对象暴露为 REST 接口// HdlcController.java RestController RequestMapping(/api/hdlc) public class HdlcController { PostMapping(/parse) public ResponseEntityMapString, Object parseHdlc(RequestBody String hexFrame) { try { // 16进制字符串转 byte[] byte[] frame DatatypeConverter.parseHexBinary(hexFrame); // 调用解码器模拟 Netty 解码流程 byte[] payload decodeHdlcPayload(frame); if (payload null) { return ResponseEntity.badRequest().body(Map.of(error, CRC check failed)); } // 解析业务字段示例电表数据格式[addr:2][cmd:1][data:4] MapString, Object result new HashMap(); result.put(address, String.format(%02X%02X, payload[0], payload[1])); result.put(command, String.format(%02X, payload[2])); result.put(value, ByteBuffer.wrap(payload, 3, 4).getInt()); // 存入数据库此处省略 JPA 代码 // hdlcRepo.save(new HdlcRecord(result)); return ResponseEntity.ok(result); } catch (Exception e) { return ResponseEntity.badRequest().body(Map.of(error, e.getMessage())); } } private byte[] decodeHdlcPayload(byte[] frame) { // 复用前述解码逻辑或直接调用 Netty ChannelPipeline // 此处为简化假设已通过 Netty 解码得到 payload return Arrays.copyOfRange(frame, 1, frame.length - 3); // 跳过 FLAG/CRC } }使用 Postman 测试POST http://localhost:8080/api/hdlc/parse Content-Type: application/json 7E0102030405067E // 示例 HDLC 帧已含 FLAG 和 CRC响应{ address: 0102, command: 03, value: 67108864 }生产优化点实际项目中decodeHdlcPayload()应由 NettyChannelHandler异步完成Controller 只负责接收解码后的ByteBuf数据库写入建议用Async或消息队列解耦hexFrame输入应增加长度校验如frame.length % 2 0。5. HDLC 调试与性能调优用 Wireshark 解析原始串口流、定位填充错误、压测吞吐量瓶颈HDLC 故障常表现为“收不到响应”或“数据错乱”根源多在物理层线缆、电平或协议层填充/解填充逻辑。本节提供可立即上手的排错方法论。5.1 Wireshark Serial Capture捕获原始串口数据可视化 HDLC 帧结构Wireshark 本身不支持串口但可通过socat将串口转为 TCP 流再用 Wireshark 抓包# Linux将 /dev/ttyUSB0 映射到 localhost:12345 socat -d -d pty,link/tmp/virtual_serial,raw,echo0,waitslave \ tcp:localhost:12345 # 启动串口程序读写 /tmp/virtual_serial # 同时在 Wireshark 中监听 localhost:12345在 Wireshark 中设置Decode As → TCP → Port 12345 → Protocol: Raw即可看到原始字节流。关键观察点检查0x7E是否规律出现间隔是否合理查看两0x7E之间字节是否含0x7E非法说明发送端填充失败计算 CRC选中帧数据区不含首尾0x7E右键Copy → Bytes → Hex Stream粘贴到在线 CRC 计算器如 csgnetwork.com/crc16.html对比帧末尾 2 字节。提示若 Wireshark 显示大量0x00连续出现大概率是发送端零比特填充逻辑错误如ones计数未重置若0x7E出现频率过高如每 2 字节一个说明接收端解填充时误删了合法1。5.2 吞吐量压测C 程序发帧 Java 服务收帧定位 CPU/IO 瓶颈用hdld_gen.py生成 1000 个随机帧写入文件再用dd持续灌入串口# 生成 1000 帧 for i in $(seq 1 1000); do python hdlc_gen.py $(openssl rand -hex 8) stress_frames.bin done # 持续发送Linux dd ifstress_frames.bin of/dev/ttyUSB0 bs1024 convnotrunc同时监控 Java 服务指标# 查看 GC 情况 jstat -gc pid 1s # 查看线程阻塞 jstack pid | grep BLOCKED # Netty 事件循环线程 CPU top -H -p pid常见瓶颈及对策现象根因对策jstat显示GCT持续 100msBitSet创建频繁导致 GC 压力改用long[]数组 位运算复用ByteBuffertop -H中某 Netty 线程 CPU 100%calcCrc16Ccitt()未查表纯计算耗时预生成 CRC-16 查表数组256项替换循环计算dd写入速率远低于串口波特率串口驱动缓冲区满write()阻塞在socat中加b115200,raw,echo0,icanon0设置更高波特率参数调优表针对不同场景的推荐配置场景推荐maxFrameLength推荐timeout_ms推荐max_retry关键优化点电力抄表RS-4852563003启用BitSet复用池CRC 查表航天遥测高误码12810005增加前向纠错FEC层降低重传依赖工业 PLC低延迟64502关闭 Nagle 算法NettyWRITE_BUFFER_HIGH_WATER_MARK32768最终验证当 Java本文还有配套的精品资源点击获取
返回列表