温馨提示:这篇文章已超过424天没有更新,请注意相关的内容是否还可用!
摘要:Java支持串口通信,包括RS232和RS485协议。通过Java的串口通信API,开发者可以实现与设备的串行数据传输。这种通信方式广泛应用于数据传输、设备控制等领域。Java串口通信具有灵活性高、可靠性强的特点,可以方便地实现与各种设备的连接和数据交换。
Java 串口通信(RS232/485)
- 一.串口通信页面
- 二.串口服务实现
- 1.Java 串口通信配置
- 1.扩展包和依赖库
- 2.Pom配置
- 2.启动类
- 3.工具包类
- 1.Common
- 2.Crc16Modbus
- 3.SerialUtil
- 4.WebSocket 配置
- 1.启动配置
- 2.监听配置
- 5.UI交互类
- 1.串口配置对象
- 2.串口信息获取接口
- 3.RS232接口
- 4.RS485接口
- 6.串口配置类
- 1.串口配置
- 2.RS232串口配置
- 3.RS232串口监听
- 4.RS485串口配置
- 5.RS485串口监听
- 三.UI代码
- 四.测试效果
- 1.串口通信
- 2.CRC16通信
一.串口通信页面
Java 实现串口通信,同时通过 WebSocket 与 UI 实时交互传递通信数据
准备工作:
虚拟串口工具:Launch Virtual Serial Port Driver
串口调试助手:SSCOM
RS485
根据 Modbus 协议,常规485通讯的信息发送形式如下: 地址 功能码 数据信息 校验码 1byte 1byte nbyte 2byte
在线 CRC检验码计算:CRC 测试链接
二.串口服务实现
1.Java 串口通信配置
1.扩展包和依赖库
RXTXcomm.jar 放入 {JAVA_HOME}/jre/lib/ext rxtxserial.dll 放入 {JAVA_HOME}/jre/bin
以上两个包可以直接网上下载,注意和JDK版本搭配即可
2.Pom配置
串口通信包:rxtx
4.0.0 org.example SerialPort 1.0-SNAPSHOT 8 8 UTF-8 org.springframework.boot spring-boot-starter-web 2.7.4 org.projectlombok lombok 1.18.24 org.springframework spring-websocket 5.3.27 org.rxtx rxtx 2.1.7 nexus-aliyun nexus-aliyun http://maven.aliyun.com/nexus/content/groups/public/ true false public aliyun nexus http://maven.aliyun.com/nexus/content/groups/public/ true false
2.启动类
package com.serial.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * @author * @date 2023-07-01 12:41 * @since 1.8 */ @SpringBootApplication public class SerialApplication { public static void main(String[] args) { SpringApplication.run(SerialApplication.class,args); } }
3.工具包类
1.Common
package com.serial.demo.util; /** * @author * @date 2023-07-03 22:17 * @since 1.8 */ public class Common { public static String HEX_STRING = "0123456789ABCDEF"; public static final String NONE = "无"; public static final String ODD = "奇"; public static final String EVEN = "偶"; public static final String FORMAT_HEX="HEX"; }
2.Crc16Modbus
CRC16 Modbus Java 实现:计算数据的校验码
package com.serial.demo.util; /** * @author * @date 2023-07-04 20:37 * @since 1.8 */ public class Crc16Modbus { /** * CRC 循环冗余校验 即通过生成多项式对原始数据进行计算,将计算结果拼接到数据上一起发送 * 接收方计算接收到的数据校验接收结果是否准确 * CRC 即对生成多项式的模二运算 * * 1.预置1个16位的寄存器为十六进制 FFFF(即全为1),称此寄存器为CRC寄存器 * 2.把第1个8位二进制数据(帧头字节)与 CRC 寄存器的低8位相异或并写回寄存器 高8位数据不变 * 3.把 CRC 循环右移 高位补 0 取得移出位 * 4.如果移出位为 0 继续右移 如果移出位为 1 则 CRC 寄存器与多项式 A001(1010 0000 0000 0001)进行异或运算 * 5.重复步骤 3 和 4 直到右移 8 次 * 6.重复步骤 2 到 5 进行数据帧下一个字节的处理 直到将数据帧所有字节按上述步骤计算 * 7.根据需要将寄存器的高、低字节进行交换 得到最终 CRC码 * */ /** * 初始值 CRC-16 寄存器 */ private static final int INITIAL_VALUE = 0xFFFF; private static final boolean IS_OUT_PUT_OVER_TURN = true; /** * 原始数据 + CRC码 * * @param hexes 16 进制字符串 * @return */ public static byte[] getData(String... hexes) { byte[] data = new byte[hexes.length]; int i = 0; for (String hex:hexes){ //先转为数字在转为 byte data[i++] = (byte) Integer.parseInt(hex, 16); } return merge(data); } /** * 原始数据 + CRC码 * * @param data * @return */ public static byte[] merge(byte[] data) { byte[] crc = getCrc16(data); int dLen = data.length; int cLen = crc.length; byte[] result = new byte[dLen + cLen]; System.arraycopy(data,0,result,0,dLen); System.arraycopy(crc,0,result,dLen,cLen); return result; } /** * 基于 CRC16 Modbus 计算校验码 * CRC 16 Modbus 默认多项式为 x16+x15+x2+1 => 8005 反转即 A001 * * @param data * @return */ private static byte[] getCrc16(byte[] data) { int len = data.length; int crc = INITIAL_VALUE; int i, j; for (i = 0; i 0) { // 如果移出位为 1, CRC寄存器与多项式A001进行异或 crc = crc >> 1; crc = crc ^ 0xA001; } else { // 如果移出位为 0,再次右移一位 crc = crc >> 1; } } } return intToBytes(crc); } /** * 将 int 转换成 byte 数组 低位在前 高位在后 */ private static byte[] intToBytes(int value) { byte[] src = new byte[2]; byte hig = (byte) ((value>>8) & 0xFF); byte low = (byte) (value & 0xFF); if (IS_OUT_PUT_OVER_TURN){ src[0] = low; src[1] = hig; } else { src[0] = hig; src[1] = low; } return src; } /** * 将字节数组转换成十六进制字符串 */ public static String byteTo16String(byte[] data) { StringBuffer buffer = new StringBuffer(); for (byte b : data) { byteToHex(buffer,b); } return buffer.toString().toUpperCase(); } /** * 将字节转换成十六进制字符串 * * int 转 byte 对照表 * [128,255],0,[1,128) * [-128,-1],0,[1,128) */ public static void byteToHex(StringBuffer buffer ,byte b) { if (b 0 && b buffer.append("0" + Integer.toString(b, 16)); } else if (b 15) { buffer.append(Integer.toString(b, 16)); } buffer.append(" "); } }
3.SerialUtil
package com.serial.demo.util; import gnu.io.SerialPort; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; /** * @author * @date 2023-07-03 21:52 * @since 1.8 */ public class SerialUtil { /** * 转为 HEX * @param str * @return */ public static String toHex(String str){ StringBuffer sbf = new StringBuffer(); byte[] b = str.getBytes(StandardCharsets.UTF_8); for (int i = 0; i
4.WebSocket 配置
1.启动配置
package com.serial.demo.socket; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter; /** * @author * @date 2023-07-02 21:05 * @since 1.8 */ @Configuration public class WebSocketConfig { /** * 开启 websocket 配置 * @return */ @Bean public ServerEndpointExporter serverEndpointExporter(){ return new ServerEndpointExporter(); } }
2.监听配置
package com.serial.demo.socket; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import javax.websocket.OnClose; import javax.websocket.OnMessage; import javax.websocket.OnOpen; import javax.websocket.Session; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.io.IOException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * @author * @date 2023-07-02 21:07 * @since 1.8 */ @Slf4j @Component @ServerEndpoint("/websocket/{sid}") public class SerialWebSocket { /** * 缓存通信实例 */ private static Map webSocketMap = new ConcurrentHashMap(16); /** * 会话 */ private Session session; /** * 标识 */ private String sid; /** * 建立连接 * @param sid * @param session */ @OnOpen public void onOpen(@PathParam("sid") String sid,Session session){ this.session = session; this.sid = sid; webSocketMap.put(sid,this); //sendMessage(sid,"Hello:"); } /** * 关闭连接 * @param sid */ @OnClose public void onClose(@PathParam("sid") String sid){ try { SerialWebSocket socket = webSocketMap.remove(sid); if (socket != null){ socket.session.close(); socket = null; } } catch (IOException e) { log.error("Close {} exception:",sid,e); } } /** * 接收消息 * @param message */ @OnMessage public void onMessage(String message){ log.info("sid {} msg {}",this.sid,message); } /** * 发送消息 * @param message * @param sid */ public static void sendMessage(String sid,String message){ SerialWebSocket socket = webSocketMap.get(sid); if (socket != null){ try { socket.session.getBasicRemote().sendText(message); } catch (IOException e) { log.error("Send {} message {} exception:",sid,message,e); } } } /** * 广播消息 * @param message */ public static void broadcast(String message){ for (String sid:webSocketMap.keySet()){ sendMessage(sid,message); } } }
5.UI交互类
1.串口配置对象
package com.serial.demo.entity; import lombok.Data; /** * @author * @date 2023-07-02 22:58 * @since 1.8 */ @Data public class SerialEntity { private String portId; private int bitRate; private int dataBit; private int stopBit; private String checkBit; private String format; }
2.串口信息获取接口
package com.serial.demo.controller; import com.serial.demo.config.SerialPortConfig; import com.serial.demo.util.SerialUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; /** * @author * @date 2023-07-01 16:37 * @since 1.8 */ @CrossOrigin @RestController @RequestMapping("/serial") public class SerialController { @Autowired SerialPortConfig serial; /** * 获取端口列表 * @return */ @GetMapping("/getSerialPortList") public List getSerialPortList(){ return serial.getSerialPortList(); } /** * 字符串 转 HEX * @return */ @GetMapping("/toHex") public String toHex(String str){ return SerialUtil.toHex(str); } /** * HEX 转 字符串 * @return */ @GetMapping("/toStr") public String toStr(String hex){ return SerialUtil.toStr(hex); } }
3.RS232接口
package com.serial.demo.controller; import com.serial.demo.config.Rs232Config; import com.serial.demo.entity.SerialEntity; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; /** * @author * @date 2023-07-03 1:03 * @since 1.8 */ @CrossOrigin @RestController @RequestMapping("/serial/232") public class Rs232Controller { @Autowired Rs232Config rs232Config; /** * 监听端口 * @param serial */ @PostMapping("/open") public boolean open(@RequestBody SerialEntity serial){ return rs232Config.openPort(serial); } /** * 获取端口列表 * @return */ @GetMapping("/close/{portId}") public void close(@PathVariable("portId") String portId){ rs232Config.closePort(portId); } /** * 获取端口列表 * @return */ @GetMapping("/send/{portId}/{format}/{msg}") public void close(@PathVariable("portId") String portId,@PathVariable("format") String format,@PathVariable("msg") String msg){ rs232Config.sendData(portId,format,msg); } }
4.RS485接口
package com.serial.demo.controller; import com.serial.demo.config.Rs485Config; import com.serial.demo.entity.SerialEntity; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; /** * @author * @date 2023-07-03 23:33 * @since 1.8 */ @CrossOrigin @RestController @RequestMapping("/serial/485") public class Rs485Controller { @Autowired Rs485Config rs485Config; /** * 监听端口 * @param serial */ @PostMapping("/open") public boolean open(@RequestBody SerialEntity serial){ return rs485Config.openPort(serial); } /** * 获取端口列表 * @return */ @GetMapping("/close/{portId}") public void close(@PathVariable("portId") String portId){ rs485Config.closePort(portId); } /** * 获取端口列表 * @return */ @GetMapping("/send/{portId}/{format}/{msg}") public void close(@PathVariable("portId") String portId,@PathVariable("format") String format,@PathVariable("msg") String msg){ rs485Config.sendData(portId,format,msg); } }
6.串口配置类
1.串口配置
package com.serial.demo.config; import gnu.io.CommPortIdentifier; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import java.util.Collections; import java.util.Enumeration; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; /** * @author * @date 2023-07-03 1:01 * @since 1.8 */ @Slf4j @Component public class SerialPortConfig { /** * 缓存端口信息 */ private static Map serialMap; @PostConstruct private void init(){ refreshCom(); } /** * 刷新端口 */ public void refreshCom(){ Enumeration portList = CommPortIdentifier.getPortIdentifiers(); CommPortIdentifier serial; Map temp = new ConcurrentHashMap(16); while (portList.hasMoreElements()){ serial = portList.nextElement(); if (serial.getPortType() == CommPortIdentifier.PORT_SERIAL){ temp.put(serial.getName(),serial); } } serialMap = Collections.unmodifiableMap(temp); } /** * 获取端口列表 * @return */ public List getSerialPortList(){ return serialMap.keySet().stream().sorted().collect(Collectors.toList()); } /** * 获取串口 * @return */ public Map getSerialMap(){ return serialMap; } }
2.RS232串口配置
package com.serial.demo.config; import com.serial.demo.entity.SerialEntity; import com.serial.demo.util.Common; import com.serial.demo.util.SerialUtil; import gnu.io.CommPortIdentifier; import gnu.io.PortInUseException; import gnu.io.SerialPort; import gnu.io.UnsupportedCommOperationException; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.TooManyListenersException; import java.util.concurrent.ConcurrentHashMap; /** * @author * @date 2023-07-01 16:22 * @since 1.8 */ @Slf4j @Component public class Rs232Config { private static final int DELAY_TIME = 1000; @Autowired SerialPortConfig config; /** * 缓存端口实例 */ private Map serialPortMap = new ConcurrentHashMap(16); /** * 监听端口 * @param serial */ public boolean openPort(SerialEntity serial) { String portId = serial.getPortId(); CommPortIdentifier commPortIdentifier = config.getSerialMap().get(portId); if (null != commPortIdentifier){ SerialPort serialPort = null; int bitRate = 0,dataBit = 0,stopBit = 0,parity = 0; try { serialPort = (SerialPort) commPortIdentifier.open(portId,DELAY_TIME); // 设置监听器生效 当有数据时通知 serialPort.notifyOnDataAvailable(true); // 比特率、数据位、停止位、奇偶校验位 bitRate = serial.getBitRate(); dataBit = serial.getDataBit(); stopBit = serial.getStopBit(); parity = SerialUtil.getParity(serial.getCheckBit()); serialPort.setSerialPortParams(bitRate, dataBit, stopBit,parity); } catch (PortInUseException e) { log.error("Open CommPortIdentifier {} Exception:",serial.getPortId(),e ); return false; } catch (UnsupportedCommOperationException e) { log.error("Set SerialPortParams BitRate {} DataBit {} StopBit {} Parity {} Exception:",bitRate,dataBit,stopBit,parity,e); return false; } // 设置当前串口的输入输出流 InputStream input; OutputStream output; try { input = serialPort.getInputStream(); output = serialPort.getOutputStream(); } catch (IOException e) { log.error("Get serialPort data stream exception:",e); return false; } // 给当前串口添加一个监听器 try { serialPort.addEventListener(new Serial232Listener(input,output,serial.getFormat())); } catch (TooManyListenersException e) { log.error("Get serialPort data stream exception:",e); return false; } serialPortMap.put(portId,serialPort); return true; } return false; } /** * 关闭端口 * @param portId */ public void closePort(String portId){ SerialPort serialPort = serialPortMap.remove(portId); if (null != serialPort){ serialPort.close(); } } /** * 发送数据 * @param portId * @param format * @param message */ public void sendData(String portId,String format,String message){ SerialPort serialPort = serialPortMap.get(portId); if (null == serialPort){ return; } OutputStream output = null; try { byte[] bytes; if (Common.FORMAT_HEX.equals(format)){ bytes = SerialUtil.hexToByte(message); } else { bytes = message.getBytes(StandardCharsets.UTF_8); } output = serialPort.getOutputStream(); output.write(bytes); output.flush(); } catch (IOException e) { throw new RuntimeException(e); } finally { if (null != output){ try { output.close(); } catch (IOException e) { throw new RuntimeException(e); } } } } }
3.RS232串口监听
package com.serial.demo.config; import com.serial.demo.socket.SerialWebSocket; import com.serial.demo.util.Crc16Modbus; import com.serial.demo.util.SerialUtil; import gnu.io.SerialPortEvent; import gnu.io.SerialPortEventListener; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; /** * @author * @date 2023-07-01 17:06 * @since 1.8 */ public class Serial232Listener implements SerialPortEventListener { InputStream inputStream; OutputStream outputStream; String format; public Serial232Listener(InputStream input, OutputStream output, String format){ inputStream = input; outputStream = output; this.format = format; } @Override public void serialEvent(SerialPortEvent event) { switch (event.getEventType()) { case SerialPortEvent.BI: case SerialPortEvent.OE: case SerialPortEvent.FE: case SerialPortEvent.PE: case SerialPortEvent.CD: case SerialPortEvent.CTS: case SerialPortEvent.DSR: case SerialPortEvent.RI: case SerialPortEvent.OUTPUT_BUFFER_EMPTY: break; case SerialPortEvent.DATA_AVAILABLE: // 当有可用数据时读取数据 byte[] readBuffer = null; int availableBytes = 0; try { availableBytes = inputStream.available(); while (availableBytes > 0) { readBuffer = SerialUtil.readFromPort(inputStream); String needData = Crc16Modbus.byteTo16String(readBuffer); SerialWebSocket.broadcast(needData); availableBytes = inputStream.available(); } } catch (IOException e) { } default: break; } } }
4.RS485串口配置
package com.serial.demo.config; import com.serial.demo.entity.SerialEntity; import com.serial.demo.util.Common; import com.serial.demo.util.Crc16Modbus; import com.serial.demo.util.SerialUtil; import gnu.io.CommPortIdentifier; import gnu.io.PortInUseException; import gnu.io.SerialPort; import gnu.io.UnsupportedCommOperationException; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.TooManyListenersException; import java.util.concurrent.ConcurrentHashMap; /** * @author * @date 2023-07-03 1:00 * @since 1.8 */ @Slf4j @Component public class Rs485Config { private static final int DELAY_TIME = 1000; @Autowired SerialPortConfig config; /** * 缓存端口实例 */ private Map serialPortMap = new ConcurrentHashMap(16); /** * 监听端口 * @param serial */ public boolean openPort(SerialEntity serial) { String portId = serial.getPortId(); CommPortIdentifier commPortIdentifier = config.getSerialMap().get(portId); if (null != commPortIdentifier){ SerialPort serialPort; int bitRate = 0,dataBit = 0,stopBit = 0,parity = 0; try { serialPort = (SerialPort) commPortIdentifier.open(portId,DELAY_TIME); // 设置监听器生效 当有数据时通知 serialPort.notifyOnDataAvailable(true); serialPort.setDTR(true); serialPort.setRTS(true); // 比特率、数据位、停止位、奇偶校验位 bitRate = serial.getBitRate(); dataBit = serial.getDataBit(); stopBit = serial.getStopBit(); parity = SerialUtil.getParity(serial.getCheckBit()); serialPort.setSerialPortParams(bitRate, dataBit, stopBit,parity); } catch (PortInUseException e) { log.error("Open CommPortIdentifier {} Exception:",serial.getPortId(),e ); return false; } catch (UnsupportedCommOperationException e) { log.error("Set SerialPortParams BitRate {} DataBit {} StopBit {} Parity {} Exception:",bitRate,dataBit,stopBit,parity,e); return false; } // 设置当前串口的输入输出流 InputStream input; OutputStream output; try { input = serialPort.getInputStream(); output = serialPort.getOutputStream(); } catch (IOException e) { log.error("Get serialPort data stream exception:",e); return false; } // 给当前串口添加一个监听器 try { serialPort.addEventListener(new Serial485Listener(input,output,serial.getFormat())); } catch (TooManyListenersException e) { log.error("Get serialPort data stream exception:",e); return false; } serialPortMap.put(portId,serialPort); return true; } return false; } /** * 关闭端口 * @param portId */ public void closePort(String portId){ SerialPort serialPort = serialPortMap.remove(portId); if (null != serialPort){ serialPort.close(); } } /** * 发送数据 * @param portId * @param format * @param message */ public void sendData(String portId,String format,String message){ SerialPort serialPort = serialPortMap.get(portId); if (null == serialPort){ return; } OutputStream output = null; try { byte[] bytes = new byte[0]; if (Common.FORMAT_HEX.equals(format)){ bytes = SerialUtil.hexToByte(message); bytes = Crc16Modbus.merge(bytes); } output = serialPort.getOutputStream(); output.write(bytes); output.flush(); } catch (IOException e) { throw new RuntimeException(e); } finally { if (null != output){ try { output.close(); } catch (IOException e) { throw new RuntimeException(e); } } } } }
5.RS485串口监听
package com.serial.demo.config; import com.serial.demo.socket.SerialWebSocket; import com.serial.demo.util.Crc16Modbus; import com.serial.demo.util.SerialUtil; import gnu.io.SerialPortEvent; import gnu.io.SerialPortEventListener; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; /** * @author * @date 2023-07-03 23:21 * @since 1.8 */ public class Serial485Listener implements SerialPortEventListener { InputStream inputStream; OutputStream outputStream; String format; public Serial485Listener(InputStream input, OutputStream output, String format){ inputStream = input; outputStream = output; this.format = format; } @Override public void serialEvent(SerialPortEvent event) { switch (event.getEventType()) { case SerialPortEvent.BI: case SerialPortEvent.OE: case SerialPortEvent.FE: case SerialPortEvent.PE: case SerialPortEvent.CD: case SerialPortEvent.CTS: case SerialPortEvent.DSR: case SerialPortEvent.RI: case SerialPortEvent.OUTPUT_BUFFER_EMPTY: break; case SerialPortEvent.DATA_AVAILABLE: // 当有可用数据时读取数据 byte[] readBuffer = null; int availableBytes = 0; try { availableBytes = inputStream.available(); while (availableBytes > 0) { readBuffer = SerialUtil.readFromPort(inputStream); String needData = printHexString(readBuffer); SerialWebSocket.broadcast(needData); availableBytes = inputStream.available(); } } catch (IOException e) { } default: break; } } /** * 转为 16 进制字符串 * @param b * @return */ public static String printHexString(byte[] b) { return Crc16Modbus.byteTo16String(b); } }
三.UI代码
Serial Communication .btn-group{ display: inline-block; } .left { width: 300px; height: 500px; float: left; } .right { width: calc(100% - 330px); height: 500px; margin-left: 300px; } .configSelect{ width: 100%; } .bk{ border-color: #D5DBDB; background-color: #1E1E1E; margin: 1px; } .bkw{ border-color: #D5DBDB; background-color: #1E1E1E; margin: 1px; border-left: none; border-right: none; border-bottom: none; border-top: none; } select{ background-color: #3C3C3C; color: white; } button{ background-color: #3C3C3C; color: white; cursor: pointer; }
WebSocket连接 断开 清空发送区设置自动发送 数据格式 类型 发送 发送数据 类型转换字符串转HEX HEX转字符串 STR HEX
文章版权声明:除非注明,否则均为VPS857原创文章,转载或复制请以超链接形式并注明出处。
还没有评论,来说两句吧...