ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java多协议解析器实战:JT/T 808、32960、DL/T 645、698.45编解码与避坑

Java多协议解析器实战:JT/T 808、32960、DL/T 645、698.45编解码与避坑 简介本资源为基于Java实现的多种通信协议解析器源码合集面向智能电网、自动化设备及物联网领域的开发者与学习者重点覆盖32960、808、DLT-645与DLT698.45四类协议。包内共324个文件以296个Java源码为核心辅以html页面、ttf与woff2字体、yml与xml配置、css与js脚本等压缩包约1.2MB结构清晰便于按协议模块检索。其中DLT-645解析器涉及地址字段、命令代码、数据与校验码处理DLT698.45则扩展了安全性与加密认证机制适用于AMI高级计量场景32960与808解析器则展示数据帧结构分析与编解码思路。已有1991人学习下载读者可借此理解协议解析原理、掌握Java面向对象设计在通信系统中的落地方式并获取可直接参考的帧解析与调试思路对开发与调试智能电网、自动化设备通信具有实用价值。1. 多协议解析器在Java里到底怎么落地从32960到698.45的工程视角如果你手上有一批车载终端、电表或充电桩的数据要接大概率绕不开这几个协议名JT/T 808、JT/T 32960、DL/T 645、DL/T 698.45。它们分别对应车载终端通信、新能源车远程监控、多功能电能表、以及更复杂的电能信息采集与交互。单独实现一个协议不难难的是把它们放进同一个Java工程里让解析器可复用、可扩展、可测试。我见过太多项目把协议解析写成一大坨if-else新增一个协议就要动核心链路最后没人敢改。这篇笔记就围绕“基于Java实现多协议解析器”这个方向把选型、分层、编解码、测试和踩坑讲清楚。适合正在做物联网平台、充电桩后台、车联网数据接入的Java工程师也适合想拿这类项目练手但不知道从哪下手的开发者。读完你能自己搭出一个能跑通808和645最小闭环、再逐步扩展到32960和698.45的解析框架。2. 协议解析器的分层设计为什么不能一个协议一个类2.1 先分清四类协议的报文结构差异在动手写代码之前必须把四个协议的报文结构差异摸清楚否则抽象层会设计歪。JT/T 808 是车载终端与平台之间的通信协议报文头包含消息ID、消息体属性、终端手机号、流水号消息体属性里还藏着分包标志和加密标志解析时要先拆头再按消息ID分发。JT/T 32960 是新能源车远程服务与管理平台协议报文头是起始符、命令标识、应答标志、VIN、加密方式、数据单元长度结构比808更扁平但命令标识决定了后续数据单元的解析方式。DL/T 645 是电能表通信协议有1997和2007两个版本2007版用控制码区分读、写、读后续等操作数据域用数据标识编码解析时要先判断控制码再决定数据域长度。DL/T 698.45 是面向对象的电能信息交互协议报文有链路层帧头和APDU应用层数据APDU里又分请求、响应、通知对象标识OAD、属性标识OI、方法标识OMD层层嵌套是四个协议里最复杂的。这四个协议如果各写各的解析类代码量会迅速膨胀而且日志、异常处理、连接管理这些公共逻辑会重复。常见做法是抽三层传输层负责字节流的收发和粘包处理编解码层负责把字节数组转成协议对象业务层负责把协议对象转成业务事件。传输层可以用Netty统一处理TCP连接编解码层每个协议一个Codec业务层用统一的事件模型。这样新增协议时只需要加一个Codec和对应的消息映射不动传输和业务。2.2 用接口隔离编解码用工厂管理协议类型编解码层的核心接口我一般定义成两个方法decode把ByteBuf或byte[]转成ProtocolMessageencode把ProtocolMessage转成byte[]。ProtocolMessage是一个标记接口每个协议有自己的实现类比如Jt808Message、Jt32960Message、Dlt645Message、Dlt698Message。这样做的好处是传输层和业务层都只依赖接口不依赖具体协议类。public interface ProtocolCodec { ProtocolMessage decode(ByteBuf in) throws ProtocolException; ByteBuf encode(ProtocolMessage message) throws ProtocolException; ProtocolType getProtocolType(); } public interface ProtocolMessage { ProtocolType getType(); String getDeviceId(); long getTimestamp(); }逻辑说明decode方法接收Netty的ByteBuf返回协议消息对象。encode反向操作。getProtocolType用于工厂路由。ProtocolMessage接口里我放了设备ID和时间戳因为不管哪个协议业务层最关心的就是“哪个设备在什么时候发了什么”。参数说明ByteBuf是Netty的缓冲区支持读写指针分离适合处理粘包ProtocolException是自定义异常携带错误码和原始字节方便排查。工厂类根据协议类型返回对应的Codec协议类型可以从配置、端口号或报文特征推断。比如808和32960都是TCP但端口不同645和698.45可能走串口或TCP用端口区分最省事。如果端口不能区分就在decode前先peek几个字节做特征匹配但这样会牺牲一点性能我一般优先用端口或通道属性绑定协议类型。2.3 粘包和半包Netty的LengthFieldBasedFrameDecoder怎么配四个协议里808和32960有明确的消息长度字段645和698.45的帧长度需要根据控制码或链路层长度计算。Netty自带的LengthFieldBasedFrameDecoder能解决大部分粘包问题但参数要按协议调。以808为例消息头是12字节其中第2到第3字节是消息体属性低10位是消息体长度。所以LengthFieldBasedFrameDecoder的配置是maxFrameLength设一个上限比如8192lengthFieldOffset为2lengthFieldLength为2lengthAdjustment为-2initialBytesToStrip为0。这样Netty会先读长度字段再截取完整帧但不会剥离头部因为后续Codec还需要头部信息。pipeline.addLast(new LengthFieldBasedFrameDecoder(8192, 2, 2, -2, 0)); pipeline.addLast(new Jt808Codec());逻辑说明lengthFieldOffset2表示长度字段从第3个字节开始索引2lengthFieldLength2表示长度字段占2字节lengthAdjustment-2表示长度字段的值不包含自身所以实际帧长要减去2initialBytesToStrip0表示不剥离任何字节把完整帧交给Codec。参数说明maxFrameLength根据业务最大报文设置太小会截断太大会占内存。645和698.45如果走TCP也需要类似的帧解码器但长度字段位置不同要单独配。提示如果协议既有TCP又有串口串口那边通常用SerialPortHandler粘包处理逻辑可以复用同一个FrameDecoder但要注意串口读到的可能是半包需要缓冲。2.4 协议注册与热插拔用SPI还是配置表新增协议时我不建议改工厂类的switch而是用配置表或SPI。配置表最简单在application.yml里配协议类型和Codec类的映射启动时反射实例化。SPI更灵活但Java原生的SPI会加载所有实现如果某个Codec依赖的类缺失会报错。我一般用配置表加一个ProtocolCodecRegistry注册时校验协议类型是否重复。Component public class ProtocolCodecRegistry { private final MapProtocolType, ProtocolCodec codecMap new ConcurrentHashMap(); public void register(ProtocolCodec codec) { codecMap.put(codec.getProtocolType(), codec); } public ProtocolCodec get(ProtocolType type) { ProtocolCodec codec codecMap.get(type); if (codec null) { throw new ProtocolException(未注册的协议类型: type); } return codec; } }逻辑说明register方法在Spring启动时由各Codec的PostConstruct调用把自身注册进Map。get方法按类型取Codec取不到就抛异常。参数说明ProtocolType是枚举包含JT808、JT32960、DLT645、DLT698ConcurrentHashMap保证线程安全因为Netty的IO线程可能并发调用。这样新增协议只需要加一个Codec实现类在类上加Component启动时自动注册不用改工厂。3. 808和32960的编解码实现从字节到对象的完整链路3.1 808消息头解析消息ID、手机号、流水号怎么拆808的消息头固定12字节结构是消息ID2字节、消息体属性2字节、终端手机号6字节BCD、流水号2字节。消息体属性里bit0到bit9是消息体长度bit10是加密标志bit11到bit12是分包标志bit13是保留位。解析时先把消息体属性转成二进制再按位取值。public Jt808Message decode(ByteBuf in) throws ProtocolException { if (in.readableBytes() 12) { throw new ProtocolException(808消息头长度不足); } int msgId in.readUnsignedShort(); int bodyProps in.readUnsignedShort(); int bodyLength bodyProps 0x03FF; boolean encrypted ((bodyProps 10) 0x01) 1; int subpackage (bodyProps 11) 0x03; String phone BcdUtil.decode(in, 6); int serial in.readUnsignedShort(); if (in.readableBytes() bodyLength) { throw new ProtocolException(808消息体长度不足期望 bodyLength 实际 in.readableBytes()); } byte[] body new byte[bodyLength]; in.readBytes(body); Jt808Message message new Jt808Message(); message.setMsgId(msgId); message.setPhone(phone); message.setSerial(serial); message.setEncrypted(encrypted); message.setSubpackage(subpackage); message.setBody(body); return message; }逻辑说明先读2字节消息ID再读2字节消息体属性用位运算取出长度、加密标志、分包标志。手机号是6字节BCD需要专门工具类解码。流水号2字节。然后按bodyLength读消息体。参数说明bodyProps 0x03FF取低10位作为长度最大1023字节10 0x01取加密标志11 0x03取分包标志0表示不分包1表示分包且是第一条2表示分包且是最后一条3表示分包且是中间条。如果readableBytes不够bodyLength说明半包抛异常让上层处理。3.2 32960命令标识与数据单元解析32960的报文头是起始符2字节固定0x2323、命令标识1字节、应答标志1字节、VIN17字节ASCII、加密方式1字节、数据单元长度2字节。命令标识决定数据单元的结构比如0x01是车辆登入0x02是实时信息上报0x03是补发信息上报0x04是车辆登出0x05是平台登入0x06是平台登出。实时信息上报的数据单元里又包含信息类型标志每个标志对应不同的数据项。public Jt32960Message decode(ByteBuf in) throws ProtocolException { if (in.readableBytes() 24) { throw new ProtocolException(32960报文头长度不足); } int start in.readUnsignedShort(); if (start ! 0x2323) { throw new ProtocolException(32960起始符错误: Integer.toHexString(start)); } int command in.readUnsignedByte(); int response in.readUnsignedByte(); String vin in.readCharSequence(17, StandardCharsets.US_ASCII).toString(); int encrypt in.readUnsignedByte(); int dataLength in.readUnsignedShort(); if (in.readableBytes() dataLength) { throw new ProtocolException(32960数据单元长度不足); } byte[] data new byte[dataLength]; in.readBytes(data); Jt32960Message message new Jt32960Message(); message.setCommand(command); message.setResponse(response); message.setVin(vin); message.setEncrypt(encrypt); message.setData(data); return message; }逻辑说明先读2字节起始符并校验再读命令标识、应答标志、VIN、加密方式、数据单元长度最后读数据单元。参数说明VIN是17字节ASCII用readCharSequence直接转字符串dataLength是2字节无符号短整型最大65535data保留原始字节后续按命令标识再解析。如果起始符不对说明帧同步丢了直接抛异常让上层断开重连。3.3 分包处理808和32960的分包重组策略808和32960都支持分包但机制不同。808的分包标志在消息体属性里分包消息有总包数和包序号需要按手机号加流水号做key缓存收齐后合并。32960的分包在数据单元里命令标识0x02的实时信息上报可能分多条但32960没有显式的分包标志而是靠数据单元里的信息类型标志和时间戳判断实际项目中我一般按VIN加时间戳做去重和合并。public class SubpackageCache { private final MapString, ListJt808Message cache new ConcurrentHashMap(); public Jt808Message merge(Jt808Message message) { if (message.getSubpackage() 0) { return message; } String key message.getPhone() : message.getSerial(); cache.computeIfAbsent(key, k - new ArrayList()).add(message); ListJt808Message list cache.get(key); int total list.get(0).getTotalPackets(); if (list.size() total) { list.sort(Comparator.comparingInt(Jt808Message::getPacketIndex)); ByteArrayOutputStream out new ByteArrayOutputStream(); for (Jt808Message m : list) { out.write(m.getBody(), 0, m.getBody().length); } Jt808Message merged list.get(0); merged.setBody(out.toByteArray()); merged.setSubpackage(0); cache.remove(key); return merged; } return null; } }逻辑说明用手机号加流水号做key把分包消息缓存到List。当List大小等于总包数时按包序号排序合并消息体返回合并后的消息。参数说明total从第一条分包消息的消息体里读808的分包消息体前4字节是总包数和包序号packetIndex是当前包序号。如果超时没收齐需要定时清理缓存防止内存泄漏。32960的分包重组类似但key用VIN加时间戳且要处理重复上报。3.4 编码平台下发指令怎么拼字节编码是解析的逆过程但更容易出错因为要处理转义。808的消息体里如果出现0x7E、0x7D、0x7E需要转义成0x7D 0x02、0x7D 0x01帧头帧尾用0x7E包裹。32960没有转义但起始符0x2323要保证不出现在数据单元里如果出现需要处理实际项目中32960一般不做转义靠长度字段区分。public ByteBuf encode(Jt808Message message) { ByteBuf bodyBuf Unpooled.buffer(); bodyBuf.writeBytes(message.getBody()); int bodyLength bodyBuf.readableBytes(); int bodyProps bodyLength 0x03FF; if (message.isEncrypted()) { bodyProps | 0x0400; } if (message.getSubpackage() 0) { bodyProps | (message.getSubpackage() 11); } ByteBuf frame Unpooled.buffer(); frame.writeByte(0x7E); frame.writeShort(message.getMsgId()); frame.writeShort(bodyProps); frame.writeBytes(BcdUtil.encode(message.getPhone(), 6)); frame.writeShort(message.getSerial()); frame.writeBytes(bodyBuf); frame.writeByte(0x7E); return escape(frame); }逻辑说明先拼消息体计算长度和属性再拼帧头、消息ID、属性、手机号、流水号、消息体、帧尾。最后做转义。参数说明bodyProps的低10位是长度bit10是加密bit11到12是分包。escape方法把0x7E转成0x7D 0x020x7D转成0x7D 0x01。注意转义要在帧尾加上之后做否则帧头帧尾也会被转义。4. 645和698.45的解析难点数据标识与面向对象4.1 645控制码与数据标识的映射关系DL/T 645 2007版的控制码有0x01读数据、0x02读后续数据、0x03读通信地址、0x04写数据、0x05写通信地址、0x06冻结命令、0x07更改通信速率、0x08修改密码、0x09最大需量清零、0x0A电表清零、0x0B事件清零。读数据时数据域是4字节数据标识比如0x00 0x00 0x00 0x00表示当前正向有功总电能。数据标识是压缩BCD解析时要按位拆。public Dlt645Message decode(ByteBuf in) throws ProtocolException { if (in.readableBytes() 12) { throw new ProtocolException(645帧长度不足); } int start in.readUnsignedByte(); if (start ! 0x68) { throw new ProtocolException(645起始符错误); } String address BcdUtil.decode(in, 6); int start2 in.readUnsignedByte(); if (start2 ! 0x68) { throw new ProtocolException(645第二个起始符错误); } int control in.readUnsignedByte(); int dataLength in.readUnsignedByte(); byte[] data new byte[dataLength]; in.readBytes(data); int cs in.readUnsignedByte(); int end in.readUnsignedByte(); if (end ! 0x16) { throw new ProtocolException(645结束符错误); } Dlt645Message message new Dlt645Message(); message.setAddress(address); message.setControl(control); message.setData(data); return message; }逻辑说明645的帧结构是起始符0x68、地址6字节BCD、起始符0x68、控制码、数据长度、数据域、校验和、结束符0x16。参数说明控制码决定数据域含义读数据时数据域是4字节数据标识写数据时数据域是数据标识加数据。校验和是前面所有字节的模256和解析时要校验但示例里省略了。数据标识的解析需要查表比如0x00 0x00 0x00 0x00对应正向有功总电能单位是kWh小数点位数由数据标识的高字节决定。4.2 698.45的OAD、OI、OMD对象模型698.45是面向对象的协议核心概念是OAD对象标识、OI对象标识、OMD方法标识。OAD是4字节前2字节是OI第3字节是属性标识第4字节是属性索引。比如OAD 0x2000 0x02 0x00表示正向有功总电能的当前值。APDU的结构是请求类型、请求参数、响应类型、响应参数。解析时要先读链路层帧头再读APDUAPDU里再按OAD取数据。public Dlt698Message decode(ByteBuf in) throws ProtocolException { if (in.readableBytes() 8) { throw new ProtocolException(698.45链路层长度不足); } int start in.readUnsignedByte(); if (start ! 0x68) { throw new ProtocolException(698.45起始符错误); } int length in.readUnsignedShort(); int control in.readUnsignedByte(); String address BcdUtil.decode(in, 6); int apduStart in.readUnsignedByte(); if (apduStart ! 0x68) { throw new ProtocolException(698.45 APDU起始符错误); } byte[] apdu new byte[length - 8]; in.readBytes(apdu); int cs in.readUnsignedByte(); int end in.readUnsignedByte(); if (end ! 0x16) { throw new ProtocolException(698.45结束符错误); } Dlt698Message message new Dlt698Message(); message.setAddress(address); message.setControl(control); message.setApdu(apdu); return message; }逻辑说明698.45的链路层帧是起始符0x68、长度2字节、控制码1字节、地址6字节、APDU、校验和、结束符0x16。APDU里再按请求类型解析。参数说明length是链路层长度包含APDU但不包含起始符和结束符control是链路层控制码区分请求、响应、通知apdu是应用层数据需要进一步解析。OAD的解析要按4字节读OI是前2字节属性标识是第3字节属性索引是第4字节。4.3 数据标识查表用枚举还是配置文件645和698.45的数据标识有几百个硬编码在代码里不现实。我一般用枚举加配置文件。枚举定义常用的数据标识比如正向有功总电能、反向有功总电能、A相电压、B相电压、C相电压、A相电流等。配置文件定义数据标识对应的名称、单位、小数点位数、数据类型。解析时先查枚举查不到再查配置文件都没有就返回原始字节。public enum Dlt645DataId { FORWARD_ACTIVE_TOTAL(0x00000000, 正向有功总电能, kWh, 2), REVERSE_ACTIVE_TOTAL(0x00010000, 反向有功总电能, kWh, 2), VOLTAGE_A(0x02010100, A相电压, V, 1), VOLTAGE_B(0x02010200, B相电压, V, 1), VOLTAGE_C(0x02010300, C相电压, V, 1), CURRENT_A(0x02020100, A相电流, A, 3), CURRENT_B(0x02020200, B相电流, A, 3), CURRENT_C(0x02020300, C相电流, A, 3); private final int id; private final String name; private final String unit; private final int scale; Dlt645DataId(int id, String name, String unit, int scale) { this.id id; this.name name; this.unit unit; this.scale scale; } public static Dlt645DataId fromId(int id) { for (Dlt645DataId dataId : values()) { if (dataId.id id) { return dataId; } } return null; } }逻辑说明枚举里定义数据标识的ID、名称、单位、小数点位数。fromId方法按ID查找查不到返回null。参数说明id是4字节数据标识的整数值scale是小数点位数比如2表示除以100unit是单位。解析时拿到数据标识后先fromId如果非空就按scale转换否则返回原始字节。配置文件可以用YAML或JSON启动时加载到Map和枚举合并。4.4 698.45的APDU解析请求与响应的对应关系698.45的APDU里请求和响应是成对的。请求有请求类型和请求参数响应有响应类型和响应参数。比如请求类型0x01是读取请求参数是OAD列表响应类型0x81是读取响应参数是OAD对应的数据。解析时要先读请求类型再按类型读参数。响应解析时要把OAD和数据对应起来方便业务层按OAD取值。public Dlt698Apdu parseApdu(byte[] apdu) { ByteBuf buf Unpooled.wrappedBuffer(apdu); int type buf.readUnsignedByte(); Dlt698Apdu result new Dlt698Apdu(); result.setType(type); if (type 0x01) { int oadCount buf.readUnsignedByte(); ListInteger oads new ArrayList(); for (int i 0; i oadCount; i) { oads.add(buf.readInt()); } result.setOads(oads); } else if (type 0x81) { int oadCount buf.readUnsignedByte(); MapInteger, byte[] dataMap new LinkedHashMap(); for (int i 0; i oadCount; i) { int oad buf.readInt(); int dataLength buf.readUnsignedByte(); byte[] data new byte[dataLength]; buf.readBytes(data); dataMap.put(oad, data); } result.setDataMap(dataMap); } return result; }逻辑说明先读APDU类型0x01是读取请求后面跟OAD数量、OAD列表0x81是读取响应后面跟OAD数量、每个OAD的数据长度和数据。参数说明OAD是4字节整数用readInt读dataLength是1字节最大255dataMap用LinkedHashMap保持顺序。解析后的APDU交给业务层业务层按OAD查数据标识表转换成带单位的物理量。5. 避坑与排查多协议解析器最容易翻车的五个地方5.1 字节序搞反导致数据全错现象解析出来的电压是几万伏电流是负数或者时间戳是1970年。原因Java默认大端序但有些协议或设备用小端序比如645的某些数据域、698.45的某些字段。解决用ByteBuf的order(ByteOrder.LITTLE_ENDIAN)显式设置字节序或者在读多字节整数时用readIntLE、readShortLE。我一般会在Codec的decode开头统一设置字节序但要注意协议里可能混用大小端比如808的消息头是大端消息体里的某些字段可能是小端要按协议文档逐个确认。5.2 BCD码解码时高位低位颠倒现象手机号解析出来是乱码或者电表地址是反的。原因BCD码有压缩BCD和非压缩BCD压缩BCD一个字节存两位十进制数高位在前还是低位在前要看协议。808的手机号是压缩BCD高位在前645的地址是压缩BCD但低位在前需要反转。解决写一个BcdUtil提供decode和encode方法decode时按协议指定是否反转。我一般会为每个协议单独写BCD处理不共用因为反转规则不同。5.3 粘包处理不当导致消息错位现象日志里出现大量“起始符错误”或“长度不足”但设备明明在正常上报。原因LengthFieldBasedFrameDecoder的参数配错或者协议的长度字段不包含自身导致截取的帧多一个或少一个字节。解决先用Wireshark或tcpdump抓包确认帧的实际长度和长度字段的值再调lengthAdjustment。808的lengthAdjustment是-2因为长度字段不包含自身32960的lengthAdjustment是0因为长度字段包含自身。如果还不对就在Codec里打印原始字节的十六进制对比协议文档。5.4 分包重组时内存泄漏现象运行几天后OOM堆转储里全是SubpackageCache里的List。原因分包消息没收齐缓存一直不清理。解决给SubpackageCache加过期时间比如30秒用ScheduledExecutorService定时清理。或者用Caffeine做带过期的缓存key是手机号加流水号value是分包列表设置expireAfterWrite。另外分包消息的总包数和包序号要校验如果包序号超出总包数直接丢弃防止恶意报文撑爆缓存。5.5 698.45的APDU长度计算错误现象解析698.45时APDU读多了或读少了导致后续帧全部错位。原因698.45的链路层长度字段是2字节但包含的内容和645不同645的长度字段是1字节且只包含数据域698.45的长度字段包含控制码、地址、APDU但不包含起始符和结束符。解决仔细看协议文档的长度定义用公式算链路层总长 1起始符 2长度 长度字段的值 1校验和 1结束符。如果长度字段的值是N那么从控制码到APDU结束共N字节。解析时先读长度再按长度读剩余字节最后校验结束符。6. 用JUnit和真实报文做回归让解析器敢改解析器最怕改一处崩三处所以必须有回归测试。我一般用JUnit 5加真实报文做参数化测试每个协议准备一组十六进制字符串解析后断言关键字段。比如808的车辆登入报文、32960的实时信息上报报文、645的读电压响应报文、698.45的读取响应报文。测试类里用ParameterizedTest和CsvSource把十六进制和期望值放一起。ParameterizedTest CsvSource({ 7E0200001E012345678901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 p a hrefhttps://download.csdn.net/download/tianqiquan/87791411 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p
返回列表