lzq 2025-09-26 19:17:33 +08:00
parent 2b3e56352c
commit d70a562d95
26 changed files with 1634 additions and 40 deletions

50
pom.xml
View File

@ -18,6 +18,33 @@
<artifactId>fastjson2</artifactId> <artifactId>fastjson2</artifactId>
<version>2.0.51</version> <version>2.0.51</version>
</dependency> </dependency>
<!--<editor-fold desc="jackson">-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.4</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.13.4</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
<version>2.13.4</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.13.4</version>
</dependency>
<!--</editor-fold>-->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.2.6.Final</version>
</dependency>
<!--<editor-fold desc="hutool">--> <!--<editor-fold desc="hutool">-->
<dependency> <dependency>
@ -67,28 +94,5 @@
<artifactId>logback-classic</artifactId> <artifactId>logback-classic</artifactId>
<version>1.2.11</version> <version>1.2.11</version>
</dependency> </dependency>
<!--<editor-fold desc="jackson">-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.4</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.13.4</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
<version>2.13.4</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.13.4</version>
</dependency>
<!--</editor-fold>-->
</dependencies> </dependencies>
</project> </project>

View File

@ -0,0 +1,21 @@
package com.njzscloud;
import com.njzscloud.server.TcpServer;
import com.njzscloud.tuqiang.Tuqiang;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class App {
static Tuqiang tuqiang;
public static void main(String[] args) {
tuqiang = new Tuqiang();
TcpServer tcpServer = new TcpServer(18888, 1, 2);
try {
tcpServer.start();
log.info("TCP服务器已停止");
} catch (Exception e) {
log.error("TCP服务器启动失败", e);
}
}
}

View File

@ -34,7 +34,7 @@ public class Fastjson {
JSON_WRITER_CONTEXT = new JSONWriter.Context( JSON_WRITER_CONTEXT = new JSONWriter.Context(
JSONWriter.Feature.WriteNulls, // 序列化输出空值字段 JSONWriter.Feature.WriteNulls, // 序列化输出空值字段
JSONWriter.Feature.BrowserCompatible, // 在大范围超过JavaScript支持的整数输出为字符串格式 JSONWriter.Feature.BrowserCompatible, // 在大范围超过JavaScript支持的整数输出为字符串格式
JSONWriter.Feature.WriteClassName, // 序列化时输出类型信息 // JSONWriter.Feature.WriteClassName, // 序列化时输出类型信息
// JSONWriter.Feature.PrettyFormat, // 格式化输出 // JSONWriter.Feature.PrettyFormat, // 格式化输出
JSONWriter.Feature.SortMapEntriesByKeys, // 对Map中的KeyValue按照Key做排序后再输出。在有些验签的场景需要使用这个Feature JSONWriter.Feature.SortMapEntriesByKeys, // 对Map中的KeyValue按照Key做排序后再输出。在有些验签的场景需要使用这个Feature
JSONWriter.Feature.WriteBigDecimalAsPlain, // 序列化BigDecimal使用toPlainString避免科学计数法 JSONWriter.Feature.WriteBigDecimalAsPlain, // 序列化BigDecimal使用toPlainString避免科学计数法
@ -62,8 +62,8 @@ public class Fastjson {
return value; return value;
}); });
JSON_READER_CONTEXT = new JSONReader.Context( JSON_READER_CONTEXT = new JSONReader.Context(
JSONReader.Feature.IgnoreSetNullValue, // 忽略输入为null的字段 JSONReader.Feature.IgnoreSetNullValue// 忽略输入为null的字段
JSONReader.Feature.SupportAutoType // 支持自动类型,要读取带"@type"类型信息的JSON数据需要显示打开SupportAutoType // JSONReader.Feature.SupportAutoType // 支持自动类型,要读取带"@type"类型信息的JSON数据需要显示打开SupportAutoType
); );
JSON_READER_CONTEXT.setZoneId(zoneId); JSON_READER_CONTEXT.setZoneId(zoneId);
JSON_READER_CONTEXT.setDateFormat(DatePattern.NORM_DATETIME_PATTERN); JSON_READER_CONTEXT.setDateFormat(DatePattern.NORM_DATETIME_PATTERN);

View File

@ -0,0 +1,113 @@
package com.njzscloud.common.log;
import java.util.Arrays;
/**
* Ansi
*/
public enum AnsiColor {
/**
*
*/
DEFAULT("39"),
/**
*
*/
BLACK("30"),
/**
*
*/
RED("31"),
/**
* 绿
*/
GREEN("32"),
/**
*
*/
YELLOW("33"),
/**
*
*/
BLUE("34"),
/**
*
*/
MAGENTA("35"),
/**
*
*/
CYAN("36"),
/**
*
*/
WHITE("37"),
/**
*
*/
BRIGHT_BLACK("90"),
/**
*
*/
BRIGHT_RED("91"),
/**
* 绿
*/
BRIGHT_GREEN("92"),
/**
*
*/
BRIGHT_YELLOW("93"),
/**
*
*/
BRIGHT_BLUE("94"),
/**
*
*/
BRIGHT_MAGENTA("95"),
/**
*
*/
BRIGHT_CYAN("96"),
/**
*
*/
BRIGHT_WHITE("97");
private final String code;
AnsiColor(String code) {
this.code = code;
}
public static AnsiColor parse(String name) {
return Arrays.stream(AnsiColor.values())
.filter(member -> member.toString().equalsIgnoreCase(name))
.findFirst()
.orElse(null);
}
@Override
public String toString() {
return this.code;
}
}

View File

@ -0,0 +1,37 @@
package com.njzscloud.common.log;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.pattern.CompositeConverter;
/**
*
*/
public class ColorConverter extends CompositeConverter<ILoggingEvent> {
@Override
protected String transform(ILoggingEvent event, String in) {
AnsiColor color = AnsiColor.parse(getFirstOption());
if (color == null) {
switch (event.getLevel().toInt()) {
case Level.ERROR_INT:
color = AnsiColor.RED;
break;
case Level.WARN_INT:
color = AnsiColor.YELLOW;
break;
case Level.INFO_INT:
color = AnsiColor.BLUE;
break;
case Level.DEBUG_INT:
color = AnsiColor.GREEN;
break;
case Level.TRACE_INT:
color = AnsiColor.MAGENTA;
break;
default:
color = AnsiColor.BLACK;
}
}
return "\033[" + color + "m" + in + "\033[0m";
}
}

View File

@ -0,0 +1,12 @@
package com.njzscloud.common.utils;
public class BCD {
public static String bcdToStr(byte[] bcd) {
StringBuilder sb = new StringBuilder();
for (byte b : bcd) {
sb.append(String.format("%02x", b));
}
// 移除前面的0
return sb.toString().replaceFirst("^0+", "");
}
}

View File

@ -1,11 +0,0 @@
package com.njzscloud.gps;
/**
* Hello world!
*
*/
public class App {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}

View File

@ -0,0 +1,187 @@
package com.njzscloud.jt808.protocol;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
/**
* JT808
* JT808
*/
@Slf4j
public class JT808Decoder extends ByteToMessageDecoder {
// 消息起始符和结束符
private static final byte MSG_DELIMITER = 0x7e;
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
// 确保至少有2个字节起始符+至少一个字节)
if (in.readableBytes() < 2) {
return;
}
// 寻找消息起始符
int startIndex = -1;
while (in.readableBytes() > 0) {
if (in.readByte() == MSG_DELIMITER) {
startIndex = in.readerIndex() - 1;
break;
}
}
if (startIndex == -1) {
// 未找到起始符,清空缓冲区
in.clear();
return;
}
// 寻找消息结束符
int endIndex = -1;
for (int i = in.readerIndex(); i < in.writerIndex(); i++) {
if (in.getByte(i) == MSG_DELIMITER) {
endIndex = i;
break;
}
}
if (endIndex == -1) {
// 未找到结束符,等待更多数据
in.readerIndex(startIndex);
return;
}
// 读取完整消息(不包含起始符和结束符)
int messageLength = endIndex - startIndex - 1;
ByteBuf messageBuf = in.slice(startIndex + 1, messageLength);
// 处理转义字符
ByteBuf unescapedBuf = handleEscape(messageBuf);
// 解析消息内容
JT808Message message = parseMessage(unescapedBuf);
if (message != null) {
out.add(message);
}
// 移动读指针到结束符之后
in.readerIndex(endIndex + 1);
}
/**
*
* JT8080x7e <-> 0x7d 0x010x7d <-> 0x7d 0x02
*/
private ByteBuf handleEscape(ByteBuf buf) {
ByteBuf out = buf.alloc().buffer(buf.readableBytes());
while (buf.readableBytes() > 0) {
byte b = buf.readByte();
if (b == 0x7d) {
if (buf.readableBytes() > 0) {
byte next = buf.readByte();
if (next == 0x01) {
out.writeByte(0x7e);
} else if (next == 0x02) {
out.writeByte(0x7d);
} else {
// 无效的转义序列,直接写入
out.writeByte(b);
out.writeByte(next);
}
} else {
out.writeByte(b);
}
} else {
out.writeByte(b);
}
}
out.resetReaderIndex();
return out;
}
/**
*
*/
private JT808Message parseMessage(ByteBuf buf) {
if (buf.readableBytes() < 11) { // 最小消息长度2+2+6+1
return null;
}
JT808Message message = new JT808Message();
try {
// 1. 消息ID (2字节)
message.setMessageId(buf.readUnsignedShort());
// 2. 消息体属性 (2字节)
int messageBodyProps = buf.readUnsignedShort();
message.setMessageBodyProps(messageBodyProps);
// 3. 终端手机号 (6字节, BCD编码)
byte[] phoneBytes = new byte[6];
buf.readBytes(phoneBytes);
String terminalPhone = bcdToString(phoneBytes);
message.setTerminalPhone(terminalPhone);
// 4. 消息流水号 (2字节)
message.setFlowId(buf.readUnsignedShort());
// 5. 消息包封装项 (可选)
if (message.isPackaged()) {
JT808Message.PackageInfo packageInfo = new JT808Message.PackageInfo();
packageInfo.setTotalPackages(buf.readUnsignedShort());
packageInfo.setCurrentPackageNo(buf.readUnsignedShort());
message.setPackageInfo(packageInfo);
}
// 6. 消息体
int bodyLength = message.getMessageBodyLength();
if (buf.readableBytes() >= bodyLength + 1) { // 消息体 + 校验码
ByteBuf bodyBuf = buf.readSlice(bodyLength);
byte[] array = ByteBufUtil.getBytes(bodyBuf);
message.setMessageBody(array);
// 7. 校验码 (1字节)
message.setCheckCode(buf.readByte());
// 验证校验码
if (!verifyCheckCode(message, buf)) {
System.err.println("校验码错误: " + message);
return null;
}
return message;
}
} catch (Exception e) {
log.error("消息解析错误", e);
}
return null;
}
/**
* BCD
*/
private String bcdToString(byte[] bcd) {
StringBuilder sb = new StringBuilder();
for (byte b : bcd) {
sb.append(String.format("%02x", b));
}
// 移除前面的0
return sb.toString().replaceFirst("^0+", "");
}
/**
*
*/
private boolean verifyCheckCode(JT808Message message, ByteBuf originalBuf) {
// 简化实现,实际项目中需要根据协议规范计算校验码
// 校验码 = 消息头 + 消息体 中所有字节的异或
return true;
}
}

View File

@ -0,0 +1,146 @@
package com.njzscloud.jt808.protocol;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
/**
* JT808
* JT808Message
*/
public class JT808Encoder extends MessageToByteEncoder<JT808Message> {
// 消息起始符和结束符
private static final byte MSG_DELIMITER = 0x7e;
@Override
protected void encode(ChannelHandlerContext ctx, JT808Message msg, ByteBuf out) throws Exception {
// 1. 创建缓冲区存储消息内容(不含起始符和结束符)
ByteBuf contentBuf = ctx.alloc().buffer();
try {
// 2. 写入消息ID
contentBuf.writeShort(msg.getMessageId());
// 3. 写入消息体属性
contentBuf.writeShort(msg.getMessageBodyProps());
// 4. 写入终端手机号BCD编码
byte[] phoneBytes = stringToBcd(msg.getTerminalPhone(), 6);
contentBuf.writeBytes(phoneBytes);
// 5. 写入消息流水号
contentBuf.writeShort(msg.getFlowId());
// 6. 写入消息包封装项(如果需要)
if (msg.isPackaged() && msg.getPackageInfo() != null) {
contentBuf.writeShort(msg.getPackageInfo().getTotalPackages());
contentBuf.writeShort(msg.getPackageInfo().getCurrentPackageNo());
}
// 7. 写入消息体
byte[] messageBody = msg.getMessageBody();
if (messageBody != null && messageBody.length > 0) {
contentBuf.writeBytes(messageBody);
}
// 8. 计算并写入校验码
byte checkCode = calculateCheckCode(contentBuf);
contentBuf.writeByte(checkCode);
msg.setCheckCode(checkCode);
// 9. 处理转义
ByteBuf escapedBuf = handleEscape(contentBuf);
// 10. 写入起始符
out.writeByte(MSG_DELIMITER);
// 11. 写入转义后的内容
out.writeBytes(escapedBuf);
// 12. 写入结束符
out.writeByte(MSG_DELIMITER);
} finally {
// 释放缓冲区
if (contentBuf.refCnt() > 0) {
contentBuf.release();
}
}
}
/**
*
*/
private ByteBuf handleEscape(ByteBuf buf) {
ByteBuf out = buf.alloc().buffer(buf.readableBytes() * 2); // 预留足够空间
buf.resetReaderIndex();
while (buf.readableBytes() > 0) {
byte b = buf.readByte();
if (b == MSG_DELIMITER) {
out.writeByte(0x7d);
out.writeByte(0x01);
} else if (b == 0x7d) {
out.writeByte(0x7d);
out.writeByte(0x02);
} else {
out.writeByte(b);
}
}
return out;
}
/**
*
* = +
*/
private byte calculateCheckCode(ByteBuf buf) {
byte checkCode = 0;
int readerIndex = buf.readerIndex();
while (buf.readableBytes() > 0) {
checkCode ^= buf.readByte();
}
buf.readerIndex(readerIndex);
return checkCode;
}
/**
* BCD
*/
private byte[] stringToBcd(String str, int length) {
if (str == null || str.length() == 0) {
return new byte[length];
}
// 补0至偶数长度
int strLen = str.length();
if (strLen % 2 != 0) {
str = "0" + str;
}
// 确保不超过指定长度
if (str.length() > length * 2) {
str = str.substring(0, length * 2);
}
byte[] bcd = new byte[length];
int index = 0;
for (int i = 0; i < str.length(); i += 2) {
if (index >= length) {
break;
}
// 高4位
byte high = (byte) (Character.digit(str.charAt(i), 16) << 4);
// 低4位
byte low = (byte) Character.digit(str.charAt(i + 1), 16);
bcd[index++] = (byte) (high | low);
}
return bcd;
}
}

View File

@ -0,0 +1,74 @@
package com.njzscloud.jt808.protocol;
import cn.hutool.core.util.ReflectUtil;
import com.njzscloud.common.fastjson.Fastjson;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
/**
* JT808
* JT808
*/
@Getter
@Setter
@Accessors(chain = true)
public class JT808Message {
// 消息ID (2字节)
private int messageId;
// 消息体属性 (2字节)
private int messageBodyProps;
// 终端手机号 (6字节, BCD编码)
private String terminalPhone;
// 消息流水号 (2字节)
private int flowId;
// 消息包封装项 (可选, 当消息体属性中分包标志为1时存在)
private PackageInfo packageInfo;
// 消息体
// private ByteBuf messageBody;
private byte[] messageBody;
// 校验码 (1字节)
private byte checkCode;
public JT808Message() {
}
// 从消息体属性中获取消息体长度
public int getMessageBodyLength() {
return messageBodyProps & 0x3FF;
}
// 从消息体属性中获取是否分包
public boolean isPackaged() {
return (messageBodyProps & 0x4000) != 0;
}
// 从消息体属性中获取加密方式
public int getEncryptionType() {
return (messageBodyProps >> 10) & 0x07;
}
@Override
public String toString() {
return Fastjson.toJsonStr(this);
}
public <T> T getMessageBody(Class<T> clazz) {
if (messageBody == null || messageBody.length == 0) {
return null;
}
return ReflectUtil.newInstance(clazz, (Object) this.messageBody);
}
// 消息包封装项内部类
@Setter
@Getter
@Accessors(chain = true)
public static class PackageInfo {
// 消息总包数 (2字节)
private int totalPackages;
// 当前包序号 (2字节)
private int currentPackageNo;
}
}

View File

@ -0,0 +1,60 @@
package com.njzscloud.jt808.protocol;
import com.njzscloud.jt808.util.JT808;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.GlobalEventExecutor;
import lombok.extern.slf4j.Slf4j;
/**
* JT808
*/
@Slf4j
public class JT808MessageHandler extends ChannelInboundHandlerAdapter {
// 管理所有连接的客户端
private static final ChannelGroup clients = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.info("客户端连接: {}", ctx.channel().remoteAddress());
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof JT808Message) {
JT808Message message = (JT808Message) msg;
log.info("收到消息, 终端: {}, 消息ID: 0x{}, 流水号: {}", message.getTerminalPhone(), Integer.toHexString(message.getMessageId()), message.getFlowId());
// 根据消息类型处理
handleMessageByType(ctx, message);
} else {
super.channelRead(ctx, msg);
}
}
/**
* ID
*/
private void handleMessageByType(ChannelHandlerContext ctx, JT808Message message) {
int messageId = message.getMessageId();
if (messageId == 0x0100) {
JT808.register(message.getTerminalPhone(), ctx.channel());
}
JT808.triggerListener(messageId, message);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
log.info("客户端断开连接: {}", ctx.channel().remoteAddress());
clients.remove(ctx.channel());
JT808.unregister(ctx.channel());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.error("发生异常: {}", cause.getMessage(), cause);
ctx.close();
}
}

View File

@ -0,0 +1,17 @@
package com.njzscloud.jt808.util;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class FlowId {
private static final Map<Integer, Integer> flowId = new ConcurrentHashMap<>();
public static synchronized int next(int messageId) {
Integer i = flowId.get(messageId);
if (i == null || i >= 0xFFFF) {
i = 0;
}
flowId.put(messageId, i++);
return i;
}
}

View File

@ -0,0 +1,213 @@
package com.njzscloud.jt808.util;
import com.njzscloud.jt808.protocol.JT808Message;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
/**
* JT808
* 便JT808
*/
@Slf4j
public class JT808 {
// 终端手机号与Channel的映射
private static final Map<String, Channel> terminalChannels = new ConcurrentHashMap<>();
private static final Map<Integer, List<Consumer<JT808Message>>> listeners = new ConcurrentHashMap<>();
/**
*
*/
public static void addListener(Integer messageId, Consumer<JT808Message> listener) {
if (messageId != null && listener != null) {
listeners.computeIfAbsent(messageId, k -> new java.util.ArrayList<>()).add(listener);
}
}
/**
*
*/
public static void triggerListener(Integer messageId, JT808Message message) {
if (messageId != null && message != null) {
List<Consumer<JT808Message>> consumers = listeners.get(messageId);
if (consumers != null) {
consumers.forEach(m -> m.accept(message));
} else {
log.warn("当前消息无处理函数: 0x{}、消息内容: {}", Integer.toHexString(messageId), message);
}
}
}
/**
*
*/
public static void unregisterListener(String messageId) {
if (messageId != null) {
listeners.remove(messageId);
}
}
/**
*
*/
public static void register(String terminalId, Channel channel) {
if (terminalId != null && channel != null) {
if (terminalChannels.get(terminalId) != channel) {
terminalChannels.put(terminalId, channel);
}
}
}
/**
*
*/
public static void unregister(Channel channel) {
if (channel != null) {
terminalChannels.values().removeIf(ch -> ch == channel);
}
}
/**
* (0x8001)
*/
public static void sendGeneralResponse(JT808Message message) {
sendMessage(message.getTerminalPhone(), createGeneralMessage(message));
}
/**
* (0x8100)
*/
public static void sendTerminalRegisterResponse(String terminalPhone, int flowId, String authCode, int result) {
// 构建消息体
ByteBuf body = Unpooled.buffer();
body.writeByte(result); // 结果
if (result == 0) { // 成功才需要后面的字段
writeString(body, authCode); // 鉴权码
}
// 构建消息
byte[] bytes = ByteBufUtil.getBytes(body);
body.release();
JT808Message message = createBaseMessage(terminalPhone, 0x8100, bytes);
message.setFlowId(flowId);
// 发送消息
sendMessage(terminalPhone, message);
}
/**
* (0x8300)
*/
public static void sendTextMessage(String terminalPhone, String content, int messageType) {
// 构建消息体
ByteBuf body = Unpooled.buffer();
body.writeByte(messageType); // 消息类型
writeString(body, content); // 消息内容
byte[] bytes = ByteBufUtil.getBytes(body);
body.release();
// 构建消息
JT808Message message = createBaseMessage(terminalPhone, 0x8300, bytes);
// 发送消息
sendMessage(terminalPhone, message);
}
/**
*
*/
public static void sendMessage(String terminalId, JT808Message message) {
if (terminalId == null || message == null) {
return;
}
Channel channel = terminalChannels.get(terminalId);
if (channel != null && channel.isActive()) {
channel.writeAndFlush(message)
.addListener((ChannelFutureListener) future -> {
if (future.isSuccess()) {
log.info("消息发送成功, 终端: {}, 消息ID: 0x{}, 流水号: {}", terminalId, Integer.toHexString(message.getMessageId()), message.getFlowId());
} else {
log.error("消息发送失败, 终端: {}", terminalId, future.cause());
}
});
} else {
unregister(channel);
// 终端不在线或通道已关闭
log.warn("终端不在线: {}", terminalId);
}
}
/**
*
*/
public static JT808Message createBaseMessage(String terminalId, int messageId) {
return createBaseMessage(terminalId, messageId, null);
}
public static JT808Message createBaseMessage(String terminalId, int messageId, byte[] body) {
JT808Message message = new JT808Message()
.setFlowId(FlowId.next(messageId));
// 设置消息ID
message.setMessageId(messageId);
// 设置消息体属性
// 其他属性默认:不分包,不加密
if (body != null) {
message.setMessageBodyProps(body.length);
} else {
message.setMessageBodyProps(0);
}
// 设置终端手机号
message.setTerminalPhone(terminalId);
// 设置消息体
message.setMessageBody(body);
return message;
}
public static JT808Message createGeneralMessage(JT808Message message) {
String terminalPhone = message.getTerminalPhone();
int flowId = message.getFlowId();
int messageId = message.getMessageId();
ByteBuf body = Unpooled.buffer();
ByteBufUtil.writeShortBE(body, flowId);
ByteBufUtil.writeShortBE(body, messageId);
body.writeByte(0);
// 构建消息
byte[] bytes = ByteBufUtil.getBytes(body);
body.release();
return JT808.createBaseMessage(terminalPhone, 0x8001, bytes);
}
/**
* JT8081 +
*/
private static void writeString(ByteBuf buf, String str) {
if (str == null || str.isEmpty()) {
buf.writeByte(0);
return;
}
byte[] bytes = str.getBytes();
// 字符串长度(1字节) + 字符串内容
buf.writeByte(bytes.length);
buf.writeBytes(bytes);
}
}

View File

@ -0,0 +1,71 @@
package com.njzscloud.server;
import com.njzscloud.jt808.protocol.JT808Decoder;
import com.njzscloud.jt808.protocol.JT808Encoder;
import com.njzscloud.jt808.protocol.JT808MessageHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioIoHandler;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class TcpServer {
private final int port;
private final int bossThreads;
private final int workerThreads;
public TcpServer(int port, int bossThreads, int workerThreads) {
this.port = port;
this.bossThreads = bossThreads;
this.workerThreads = workerThreads;
}
public TcpServer() {
this.port = 18888;
this.bossThreads = 5;
this.workerThreads = 200;
}
public void start() throws Exception {
// 创建两个EventLoopGroupbossGroup用于接收客户端连接workerGroup用于处理连接后的IO操作
EventLoopGroup bossGroup = new MultiThreadIoEventLoopGroup(bossThreads, NioIoHandler.newFactory());
EventLoopGroup workerGroup = new MultiThreadIoEventLoopGroup(workerThreads, NioIoHandler.newFactory());
try {
// 服务器启动辅助类
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
// 指定使用NIO的服务器通道
.channel(NioServerSocketChannel.class)
// 设置服务器通道的选项
.option(ChannelOption.SO_BACKLOG, 128)
// 设置客户端通道的选项
.childOption(ChannelOption.SO_KEEPALIVE, true)
// 设置通道初始化器,用于配置新接入的通道
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
// 添加JT808协议处理器链
ch.pipeline()
.addLast(new JT808Decoder()) // 解码器
.addLast(new JT808Encoder()) // 编码器
.addLast(new JT808MessageHandler()); // 消息处理器
}
});
// 绑定端口并开始接收连接
ChannelFuture future = bootstrap.bind(port).sync();
log.info("服务器已启动,监听端口: {}", port);
// 等待服务器 socket 关闭
future.channel().closeFuture().sync();
} finally {
// 关闭事件循环组
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
log.info("服务器已停止");
}
}
}

View File

@ -0,0 +1,207 @@
package com.njzscloud.tuqiang;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.Accessors;
@Getter
@Setter
@ToString
@Accessors(chain = true)
public class DeviceInfo {
// GPS定位状态
private String gpsValid; // 定位有效性(V/N)
private int satelliteCount; // 卫星数量
private int gpsAccuracy; // 定位精度或备用参数
private String gpsModuleStatus; // GPS模块状态(OK/ERR)
// 网络与信号状态
private String serverIp; // 服务器IP
private int serverPort; // 服务器端口
private int cgregStatus; // 网络注册状态
private int csq; // 信号强度
// 硬件与电源状态
private double batteryVoltage; // 电池电压(V)
private int powerMode; // 供电模式(1=外接电源,2=电池)
private int beidouStatus; // 北斗模块状态(0=未启用,1=启用)
// 功能与附加状态
private String alarmStatus; // 报警状态(*)
private int counter; // 计数器/里程相关
private int reportInterval; // 定位上报间隔(秒)
private int powerSavingMode; // 省电模式(0=关闭,1=开启)
private int backupPower; // 备用电源状态
private int vibrationStatus; // 震动传感器状态
private int heartbeatInterval; // 心跳包间隔(秒)
private int accStatus; // ACC点火状态(1=通电,0=断电)
private int chargingStatus; // 充电状态(0=未充电,1=充电中)
private int bluetoothStatus; // 蓝牙状态(1=开启,0=关闭)
private String extendStatus; // 扩展状态码(十六进制)
private String temperature; // 温度传感器状态
private String deviceTime; // 设备本地时间
private int agpsStatus; // AGPS状态(1=开启,0=关闭)
private String displacement; // 位移距离
private int speedStatus; // 速度状态(0=静止)
private String deviceId; // 设备ID/IMEI后7位
private int lowBatteryThreshold; // 低电阈值状态
private int accelerometerStatus; // 加速度传感器状态
private int flightMode; // 飞行模式(0=关闭,1=开启)
public static DeviceInfo parse(String gpsString) {
DeviceInfo data = new DeviceInfo();
if (gpsString == null || gpsString.isEmpty()) {
return data;
}
// 去除首尾的<和>
String content = gpsString.replaceAll("[<>]", "");
// 按*分割字段
String[] fields = content.split("\\*");
for (String field : fields) {
if (field.contains(":")) {
String[] keyValue = field.split(":", 2);
if (keyValue.length == 2) {
String key = keyValue[0].trim();
String value = keyValue[1].trim();
parseField(data, key, value);
}
}
}
return data;
}
private static void parseField(DeviceInfo data, String key, String value) {
switch (key) {
case "GPS":
parseGpsField(data, value);
break;
case "GP":
data.gpsModuleStatus = value;
break;
case "T":
parseServerField(data, value);
break;
case "CGREG":
data.cgregStatus = parseInt(value);
break;
case "CSQ":
data.csq = parseInt(value);
break;
case "5Y":
data.batteryVoltage = parseDouble(value);
break;
case "B":
data.powerMode = parseInt(value);
break;
case "BD":
data.beidouStatus = parseInt(value);
break;
case "A":
data.alarmStatus = value;
break;
case "C":
data.counter = parseInt(value);
break;
case "O":
data.reportInterval = parseInt(value);
break;
case "CS":
data.powerSavingMode = parseInt(value);
break;
case "3U":
data.backupPower = parseInt(value);
break;
case "3Z":
data.vibrationStatus = parseInt(value);
break;
case "H":
data.heartbeatInterval = parseInt(value);
break;
case "2A":
data.accStatus = parseInt(value);
break;
case "5C":
data.chargingStatus = parseInt(value);
break;
case "60":
data.bluetoothStatus = parseInt(value);
break;
case "3E":
data.extendStatus = value;
break;
case "5T":
data.temperature = value;
break;
case "9E":
data.deviceTime = value;
break;
case "1H":
data.agpsStatus = parseInt(value);
break;
case "3D":
data.displacement = value;
break;
case "2S":
data.speedStatus = parseInt(value);
break;
case "I":
data.deviceId = value;
break;
case "LT":
data.lowBatteryThreshold = parseInt(value);
break;
case "AG":
data.accelerometerStatus = parseInt(value);
break;
case "FLY":
data.flightMode = parseInt(value);
break;
// 忽略未知字段
default:
break;
}
}
private static void parseGpsField(DeviceInfo data, String value) {
// 格式: (V,0,0)
String cleaned = value.replaceAll("[()]", "");
String[] parts = cleaned.split(",");
if (parts.length >= 3) {
data.gpsValid = parts[0].trim();
data.satelliteCount = parseInt(parts[1].trim());
data.gpsAccuracy = parseInt(parts[2].trim());
}
}
private static void parseServerField(DeviceInfo data, String value) {
// 格式: 139.224.54.144,30100
String[] parts = value.split(",");
if (parts.length >= 2) {
data.serverIp = parts[0].trim();
data.serverPort = parseInt(parts[1].trim());
}
}
private static int parseInt(String value) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
return -1; // 用-1表示解析失败
}
}
private static double parseDouble(String value) {
try {
return Double.parseDouble(value);
} catch (NumberFormatException e) {
return -1; // 用-1表示解析失败
}
}
}

View File

@ -0,0 +1,10 @@
package com.njzscloud.tuqiang;
import java.lang.annotation.*;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Listener {
int messageId();
}

View File

@ -0,0 +1,80 @@
package com.njzscloud.tuqiang;
import cn.hutool.core.thread.ThreadUtil;
import com.njzscloud.jt808.protocol.JT808Message;
import com.njzscloud.jt808.util.JT808;
import com.njzscloud.tuqiang.msg.*;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class Listeners {
@Listener(messageId = 0x0100)
public void onTerminalRegister(JT808Message message) {
String terminalPhone = message.getTerminalPhone();
ByteBuf body = Unpooled.buffer();
ByteBufUtil.writeShortBE(body, message.getFlowId());
body.writeByte(0);
ByteBufUtil.writeUtf8(body, terminalPhone);
// 构建消息
byte[] bytes = ByteBufUtil.getBytes(body);
body.release();
JT808.sendMessage(terminalPhone, JT808.createBaseMessage(terminalPhone, 0x8100, bytes));
TerminalRegisterMsg terminalRegisterMsg = message.getMessageBody(TerminalRegisterMsg.class);
log.info("终端注册消息: {}", terminalRegisterMsg);
}
@Listener(messageId = 0x0102)
public void onTerminalAuth(JT808Message message) {
JT808.sendGeneralResponse(message);
TerminalAuthMsg authMsg = message.getMessageBody(TerminalAuthMsg.class);
log.info("终端鉴权消息: {}", authMsg);
// ThreadUtil.sleep(10000);
// log.info("发送指令: {}", "<SPBSJ*P:BSJGPS*C:0*H:300>");
// JT808.sendMessage("61000602070", JT808.createBaseMessage("61000602070", 0x8300, new PublishDirectiveMsg("<SPBSJ*P:BSJGPS*C:0*H:300>").toBytes()));
ThreadUtil.sleep(10000);
JT808.sendMessage("61000602070", JT808.createBaseMessage("61000602070", 0x8300, new PublishDirectiveMsg("<CKBSJ>").toBytes()));
}
@Listener(messageId = 0x0002)
public void onHeartbeat(JT808Message message) {
log.info("终端心跳消息: {}", message.getTerminalPhone());
JT808.sendGeneralResponse(message);
}
@Listener(messageId = 0x0200)
public void onLocationReport(JT808Message message) {
LocationReportMsg locationReportMsg = message.getMessageBody(LocationReportMsg.class);
log.info("终端位置信息汇报消息: {}", locationReportMsg);
JT808.sendGeneralResponse(message);
}
@Listener(messageId = 0x0001)
public void onTerminalGeneralResponse(JT808Message message) {
TerminalGeneralResponseMsg terminalGeneralResponseMsg = message.getMessageBody(TerminalGeneralResponseMsg.class);
log.info("终端通用应答消息: {}", terminalGeneralResponseMsg);
JT808.sendGeneralResponse(message);
}
@Listener(messageId = 0x6006)
public void onTerminalTxtReport(JT808Message message) {
TerminalTxtReportMsg terminalTxtReportMsg = message.getMessageBody(TerminalTxtReportMsg.class);
log.info("终端文本信息汇报消息: {}", terminalTxtReportMsg);
JT808.sendGeneralResponse(message);
}
@Listener(messageId = 0x0201)
public void onSearchLocationResponse(JT808Message message) {
SearchLocationResponseMsg searchLocationResponseMsg = message.getMessageBody(SearchLocationResponseMsg.class);
log.info("查询位置响应消息: {}", searchLocationResponseMsg);
JT808.sendGeneralResponse(message);
}
}

View File

@ -0,0 +1,31 @@
package com.njzscloud.tuqiang;
import com.njzscloud.jt808.util.JT808;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Method;
@Slf4j
public class Tuqiang {
public Tuqiang() {
addListeners(new Listeners());
}
private void addListeners(Object object) {
Method[] methods = object.getClass().getMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(Listener.class)) {
Listener listener = method.getAnnotation(Listener.class);
JT808.addListener(listener.messageId(), (msg) -> {
try {
method.invoke(object, msg);
} catch (Exception e) {
log.error("处理消息 {} 时出错", listener.messageId(), e);
}
});
}
}
}
}

View File

@ -0,0 +1,82 @@
package com.njzscloud.tuqiang.msg;
import com.njzscloud.common.utils.BCD;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.Accessors;
@Getter
@Setter
@ToString
@Accessors(chain = true)
public class LocationReportMsg {
/**
* 1 1
* 7 1
* 8 1:
*/
private long alarmFlag;
/**
* 0 0ACC 1ACC
* 1 0 1
* 2 0 1
* 3 0 1西
*/
private long status;
private double longitude;
private double latitude;
private int altitude;
private double speed;
private int direction;
private String time;
public LocationReportMsg(byte[] bytes) {
ByteBuf body = Unpooled.wrappedBuffer(bytes);
this.alarmFlag = body.readUnsignedInt();
this.status = body.readUnsignedInt();
double longitude = body.readUnsignedInt() * 1.0 / 1000000;
this.longitude = isWest() ? -longitude : longitude;
double latitude = body.readUnsignedInt() * 1.0 / 1000000;
this.latitude = isSouth() ? -latitude : latitude;
this.altitude = body.readUnsignedShort();
this.speed = body.readUnsignedShort() * 1.0 / 10;
this.direction = body.readUnsignedShort();
byte[] terminalIdBytes = new byte[7];
body.readBytes(terminalIdBytes);
this.time = BCD.bcdToStr(terminalIdBytes);
body.release();
}
public boolean isOverspeed() {
return (alarmFlag & 0x02) == 0x02;
}
public boolean isPowerUnderVoltage() {
return (alarmFlag & 0x40) == 0x40;
}
public boolean isSoundAlarm() {
return (alarmFlag & 0x80) == 0x80;
}
public boolean isAccOn() {
return (status & 0x01) == 0x01;
}
public boolean isPosition() {
return (status & 0x02) == 0x02;
}
public boolean isSouth() {
return (status & 0x04) == 0x04;
}
public boolean isWest() {
return (status & 0x08) == 0x08;
}
}

View File

@ -0,0 +1,27 @@
package com.njzscloud.tuqiang.msg;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.Accessors;
@Getter
@Setter
@ToString
@Accessors(chain = true)
public class PublishDirectiveMsg {
private byte flag;
private String directive;
public PublishDirectiveMsg(String directive) {
this.flag = 0x01;
this.directive = directive;
}
public byte[] toBytes() {
byte[] messageBody = new byte[directive.length() + 1];
messageBody[0] = flag;
System.arraycopy(directive.getBytes(), 0, messageBody, 1, directive.length());
return messageBody;
}
}

View File

@ -0,0 +1,26 @@
package com.njzscloud.tuqiang.msg;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.Accessors;
@Getter
@Setter
@ToString
@Accessors(chain = true)
public class SearchLocationResponseMsg {
// 消息流水号 (2字节)
private int flowId;
private LocationReportMsg locationReportMsg;
public SearchLocationResponseMsg(byte[] bytes) {
ByteBuf body = Unpooled.wrappedBuffer(bytes);
this.flowId = body.readUnsignedShort();
byte[] locationReportMsgBytes = ByteBufUtil.getBytes(body);
this.locationReportMsg = new LocationReportMsg(locationReportMsgBytes);
}
}

View File

@ -0,0 +1,24 @@
package com.njzscloud.tuqiang.msg;
import cn.hutool.core.util.CharsetUtil;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.Accessors;
@Getter
@Setter
@ToString
@Accessors(chain = true)
public class TerminalAuthMsg {
private String authCode;
public TerminalAuthMsg(byte[] bytes) {
ByteBuf body = Unpooled.wrappedBuffer(bytes);
byte[] authCodeBytes = new byte[body.readableBytes()];
body.readBytes(authCodeBytes);
this.authCode = new String(authCodeBytes, CharsetUtil.CHARSET_GBK);
}
}

View File

@ -0,0 +1,45 @@
package com.njzscloud.tuqiang.msg;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.Accessors;
@Getter
@Setter
@ToString
@Accessors(chain = true)
public class TerminalGeneralResponseMsg {
private int flowId;
private int messageId;
private int result;
public TerminalGeneralResponseMsg(byte[] bytes) {
ByteBuf body = Unpooled.wrappedBuffer(bytes);
this.flowId = body.readUnsignedShort();
this.messageId = body.readUnsignedShort();
this.result = body.readByte();
body.release();
}
public boolean isSuccess() {
return result == 0;
}
public String getMsg() {
switch (this.result) {
case 0:
return "成功";
case 1:
return "失败";
case 2:
return "消息有误";
case 3:
return "不支持";
default:
return "未知";
}
}
}

View File

@ -0,0 +1,88 @@
package com.njzscloud.tuqiang.msg;
import cn.hutool.core.util.CharsetUtil;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Getter
@Setter
@ToString
@Accessors(chain = true)
public class TerminalRegisterMsg {
/**
* 0
* ID GB/T 2260
*/
private int province;
/**
* 0
* ID GB/T 2260
*/
private int city;
/**
*
*/
private String manufacturerId;
/**
*
* ,(: 20 , 0x00)
*/
private String terminalModel;
/**
* ID ,
* 0x00
*/
private String terminalId;
/**
* JT/T 4152006 5.4.12 ,,
* 0
*/
private byte plateColor;
/**
*
* (: 0 , VIN )
*/
private String plate;
public TerminalRegisterMsg(byte[] bytes) {
ByteBuf body = Unpooled.wrappedBuffer(bytes);
// 读取省域IDWORD2字节大端序
this.province = body.readUnsignedShort();
// 读取市县域IDWORD2字节大端序
this.city = body.readUnsignedShort();
// 读取制造商IDBYTE[5]5字节转为字符串
byte[] manufacturerIdBytes = new byte[5];
body.readBytes(manufacturerIdBytes);
this.manufacturerId = new String(manufacturerIdBytes).trim();
// 读取终端型号BYTE[8]8字节转为字符串
byte[] terminalModelBytes = new byte[8];
body.readBytes(terminalModelBytes);
this.terminalModel = new String(terminalModelBytes).trim();
// 读取终端IDBYTE[7]7字节转为字符串
byte[] terminalIdBytes = new byte[7];
body.readBytes(terminalIdBytes);
this.terminalId = new String(terminalIdBytes).trim();
// 读取车牌颜色BYTE1字节
this.plateColor = body.readByte();
// 读取车牌STRING剩余字节转为字符串
// 假设剩余所有字节都是车牌内容,实际可根据协议确定长度
byte[] plateBytes = new byte[body.readableBytes()];
body.readBytes(plateBytes);
this.plate = new String(plateBytes, CharsetUtil.CHARSET_GBK);
body.release();
}
}

View File

@ -0,0 +1,30 @@
package com.njzscloud.tuqiang.msg;
import cn.hutool.core.util.CharsetUtil;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.Accessors;
@Getter
@Setter
@ToString
@Accessors(chain = true)
public class TerminalTxtReportMsg {
private byte type;
/**
*
*/
private String text;
public TerminalTxtReportMsg(byte[] bytes) {
ByteBuf body = Unpooled.wrappedBuffer(bytes);
this.type = body.readByte();
this.text = body.toString(body.readerIndex(), body.readableBytes(),
this.type == 0x00 ?
CharsetUtil.CHARSET_GBK :
CharsetUtil.CHARSET_UTF_8);
}
}

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false"> <configuration debug="false">
<conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/> <conversionRule conversionWord="clr" converterClass="com.njzscloud.common.log.ColorConverter"/>
<property name="log_path" value="logs"/> <property name="log_path" value="logs"/>
<property name="service_name" value="${project.artifactId}"/> <property name="service_name" value="gps"/>
<property name="console_pattern" value="%magenta(%d{yyyy-MM-dd HH:mm:ss.SSS}) %clr(%-5p) %blue([%t]) %cyan(%c) %blue([%M:%L]): %clr(%m%n)"/> <property name="console_pattern" value="%magenta(%d{yyyy-MM-dd HH:mm:ss.SSS}) %clr(%-5p) %blue([%t]) %cyan(%c) %blue([%M:%L]): %clr(%m%n)"/>
@ -27,7 +27,7 @@
<pattern>${file_pattern}</pattern> <pattern>${file_pattern}</pattern>
</encoder> </encoder>
</appender> </appender>
<logger name="com.njzscloud" level="DEBUG"/>
<!-- TRACE < DEBUG < INFO < WARN < ERROR --> <!-- TRACE < DEBUG < INFO < WARN < ERROR -->
<root level="WARN"> <root level="WARN">
<appender-ref ref="console_appender"/> <appender-ref ref="console_appender"/>