diff --git a/README.md b/README.md index b25fa36..4fab7fe 100644 --- a/README.md +++ b/README.md @@ -1,97 +1,35 @@ -部标808协议快速开发包 +基于Netty实现Mvc开发模式的框架 ==================== -# 项目介绍 -* 基于Netty,实现JT/T 808部标协议的消息分发,与编码解码; -* 与Spring解耦合,协议编码解码和Netty服务均可独立运行(Android客户端同样适用); -* SpringBoot 仅负责将协议暴露至Web接口,目的是方便测试,且为二次开发提供样例; -* 最简洁、清爽、易用的部标开发框架。 -问题交流群:[906230542] +## 特性 +* 基于Netty,实现传统的MVC开发方式 -# 主要特性 -* 代码足够精简,便于二次开发; -* 致敬Spring、Hibernate设计理念,熟悉Web开发的同学上手极快; -* 使用注解描述协议,告别繁琐的封包、解包; -* 实时兼容2011、2013、2019部标协议版本,支持分包请求; -* 支持JT/T1078音视频协议,T/JSATL12苏标主动安防协议; -* 支持异步批量处理,显著提升MySQL入库性能; -* 提供报文解释器(解析过程分析工具),编码解码不再抓瞎; -* 全覆盖的测试用例,稳定发版。 +## 场景 +* TCP协议服务端开发 -# 代码仓库 - * Gitee仓库地址:[https://gitee.com/yezhihao/jt808-server/tree/master](https://gitee.com/yezhihao/jt808-server/tree/master) - * Github仓库地址:[https://github.com/yezhihao/jt808-server/tree/master](https://github.com/yezhihao/jt808-server/tree/master) +## 代码仓库 + * Gitee仓库地址:[https://gitee.com/yezhihao/netmc/tree/master](https://gitee.com/yezhihao/netmc/tree/master) + * Github仓库地址:[https://github.com/yezhihao/netmc/tree/master](https://github.com/yezhihao/netmc/tree/master) -# 下载方式 - * Gitee下载命令:`git clone https://gitee.com/yezhihao/jt808-server -b master` - * Github下载命令:`git clone https://github.com/yezhihao/jt808-server -b master` +## 下载方式 + * Gitee下载命令:`git clone https://gitee.com/yezhihao/netmc -b master` + * Github下载命令:`git clone https://github.com/yezhihao/netmc -b master` -# 使用说明 - -## 项目分为四部分: - -## 1.framework,核心模块,不推荐修改,有BUG或扩展的需求,建议提交issues或联系作者 +## 项目结构 ```sh └── framework ├── codec 编码解码 - ├── mvc 消息分发、处理 - ├── netty 网络通信 - ├── orm 序列化相关 + ├── core 消息分发、处理 └── session 消息发送和会话管理 ``` -注解: + +## 使用说明 * @Endpoint,服务接入点,等价SpringMVC的 @Controller; * @Mapping,定义消息ID,等价SpringMVC中 @RequestMapping; * @AsyncBatch, 异步批量消息,对于并发较高的消息,如0x0200(位置信息汇报),使用该注解,显著提升Netty和MySQL入库性能。 - -* @Message,协议类型,等价Hibernate的 @Table; -* @Field,属性定义,等价Hibernate的 @Column; -* @Fs,多版本协议支持 - -## 2.protocol 部标协议定义,不推荐做大量修改 -```sh -└── protocol - ├── basics 部标协议通用消息头,以及公共的消息定义 - ├── codec 部标编码解码工具 - ├── commons 部标协议ID,工具类等 - ├── jsatl12 T/JSATL12 苏标协议(已完成) - ├── t808 JT/T808 部标协议(已完成) - └── t1078 JT/T1078 音视频协议(已完成) - ``` - 消息定义样例: - ```java -@Message(JT808.定位数据批量上传) -public class T0704 extends AbstractMessage
{ - - private Integer total; - private Integer type; - private List items; - - @Field(index = 0, type = DataType.WORD, desc = "数据项个数") - public Integer getTotal() { return total; } - public void setTotal(Integer total) { this.total = total; } - - @Field(index = 2, type = DataType.BYTE, desc = "位置数据类型 0:正常位置批量汇报,1:盲区补报") - public Integer getType() { return type; } - public void setType(Integer type) { this.type = type; } - - @Field(index = 3, type = DataType.LIST, desc = "位置汇报数据项") - public List getItems() { return items; } - public void setItems(List items) { this.items = items; this.total = items.size(); } -} -``` - -## 3.web 开箱即用的Demo,业务需求在这个包下开发,可随意修改 -```sh -└── web - ├── config spring 相关配置 - ├── component.mybatis 附赠极简的mybatis分页插件:D - ├── endpoint 808消息入口,所有netty进入的请求都会根据@Mapping转发到此 - └── controller service mapper ... 不再赘述 - ``` -##### 消息接入: +## 消息接入: ```java @Endpoint public class JT808Endpoint { @@ -132,105 +70,9 @@ public class JT808Endpoint { } ``` -##### 消息下发: -```java -@Controller -@RestController("terminal") -public class TerminalController { +详细的例子请参考Test目录 - private MessageManager messageManager = MessageManager.getInstance(); - - @ApiOperation("设置终端参数") - @PostMapping("{terminalId}/parameters") - public T0001 updateParameters(@PathVariable("terminalId") String terminalId, @RequestBody List parameters) { - T8103 request = new T8103(terminalId); - request.setItems(parameters); - T0001 response = messageManager.request(request, T0001.class); - return response; - } -} -``` -##### 已集成Swagger文档,启动后可访问如下地址 - -* Swagger UI:[http://127.0.0.1:8000/swagger-ui.html](http://127.0.0.1:8000/swagger-ui.html) -* Bootstrap UI:[http://127.0.0.1:8000/doc.html](http://127.0.0.1:8000/doc.html) -![Bootstrap UI](https://images.gitee.com/uploads/images/2020/0731/135035_43dfca8e_670717.png "doc2.png") - -## 4.test 808协议全覆盖的测试用例,以及报文解释器 - -* QuickStart 不依赖Spring的启动,可用于Android客户端 -* Beans 测试数据 -* TestBeans 消息对象的封包解包 -* TestHex 原始报文测试 - -* Elucidator 报文解释器 - 解码 -* DarkRepulsor 报文解释器 - 编码 - -分析报文内每个属性所处的位置以及转换后的值,以便查询报文解析出错的原因 - -Elucidator 运行效果如下: -``` -0 [0200] 消息ID: 512 -2 [4061] 消息体属性: 16481 -4 [01] 协议版本号: 1 -5 [00000000017299841738] 终端手机号: 17299841738 -15 [ffff] 流水号: 65535 -0 [00000400] 报警标志: 1024 -4 [00000800] 状态: 2048 -8 [06eeb6ad] 纬度: 116307629 -12 [02633df7] 经度: 40058359 -16 [0138] 海拔: 312 -18 [0003] 速度: 3 -20 [0063] 方向: 99 -22 [200707192359] 时间: 2020-07-07T19:23:59 -0 [01] 附加信息ID: 1 -1 [04] 参数值长度: 4 -2 [0000000b] 参数值: {0,0,0,11} -0 [02] 附加信息ID: 2 -1 [02] 参数值长度: 2 -2 [0016] 参数值: {0,22} -0 [03] 附加信息ID: 3 -1 [02] 参数值长度: 2 -2 [0021] 参数值: {0,33} -0 [04] 附加信息ID: 4 -1 [02] 参数值长度: 2 -2 [002c] 参数值: {0,44} -0 [05] 附加信息ID: 5 -1 [03] 参数值长度: 3 -2 [373737] 参数值: {55,55,55} -0 [11] 附加信息ID: 17 -1 [05] 参数值长度: 5 -2 [4200000042] 参数值: {66,0,0,0,66} -0 [12] 附加信息ID: 18 -1 [06] 参数值长度: 6 -2 [4d0000004d4d] 参数值: {77,0,0,0,77,77} -0 [13] 附加信息ID: 19 -1 [07] 参数值长度: 7 -2 [00000058005858] 参数值: {0,0,0,88,0,88,88} -0 [25] 附加信息ID: 37 -1 [04] 参数值长度: 4 -2 [00000063] 参数值: {0,0,0,99} -0 [2a] 附加信息ID: 42 -1 [02] 参数值长度: 2 -2 [000a] 参数值: {0,10} -0 [2b] 附加信息ID: 43 -1 [04] 参数值长度: 4 -2 [00000014] 参数值: {0,0,0,20} -0 [30] 附加信息ID: 48 -1 [01] 参数值长度: 1 -2 [1e] 参数值: {30} -0 [31] 附加信息ID: 49 -1 [01] 参数值长度: 1 -2 [28] 参数值: {40} -28 [01040000000b02020016030200210402002c05033737371105420000004212064d0000004d4d1307000000580058582504000000632a02000a2b040000001430011e310128] 位置附加信息: [BytesAttribute[id=1,value={0,0,0,11}], BytesAttribute[id=2,value={0,22}], BytesAttribute[id=3,value={0,33}], BytesAttribute[id=4,value={0,44}], BytesAttribute[id=5,value={55,55,55}], BytesAttribute[id=17,value={66,0,0,0,66}], BytesAttribute[id=18,value={77,0,0,0,77,77}], BytesAttribute[id=19,value={0,0,0,88,0,88,88}], BytesAttribute[id=37,value={0,0,0,99}], BytesAttribute[id=42,value={0,10}], BytesAttribute[id=43,value={0,0,0,20}], BytesAttribute[id=48,value={30}], BytesAttribute[id=49,value={40}]] -020040610100000000017299841738ffff000004000000080006eeb6ad02633df701380003006320070719235901040000000b02020016030200210402002c05033737371105420000004212064d0000004d4d1307000000580058582504000000632a02000a2b040000001430011e31012863 -``` - -使用发包工具模拟请求 -``` -7e020040610100000000017299841738ffff000004000000080006eeb6ad02633df701380003006320070719235901040000000b02020016030200210402002c05033737371105420000004212064d0000004d4d1307000000580058582504000000632a02000a2b040000001430011e310128637e -``` -![使用发包工具模拟请求](https://images.gitee.com/uploads/images/2019/0705/162745_9becaf08_670717.png) +使用该组件的项目:[https://gitee.com/yezhihao/jt808-server/tree/master](https://gitee.com/yezhihao/jt808-server/tree/master) 项目会不定期进行更新,建议star和watch一份,您的支持是我最大的动力。 diff --git a/src/test/java/io/github/yezhihao/netmc/QuickStart.java b/src/test/java/io/github/yezhihao/netmc/QuickStart.java new file mode 100644 index 0000000..eab96bb --- /dev/null +++ b/src/test/java/io/github/yezhihao/netmc/QuickStart.java @@ -0,0 +1,29 @@ +package io.github.yezhihao.netmc; + +import io.github.yezhihao.netmc.core.DefaultHandlerMapping; +import io.github.yezhihao.netmc.endpoint.MyHandlerInterceptor; +import io.github.yezhihao.netmc.codec.MyMessageDecoder; +import io.github.yezhihao.netmc.codec.MyMessageEncoder; +import io.github.yezhihao.netmc.session.SessionManager; + +import java.nio.charset.StandardCharsets; + +public class QuickStart { + + public static void main(String[] args) { + NettyConfig jtConfig = new NettyConfig.Builder() + .setPort(8080) + .setMaxFrameLength(1024) + .setDelimiters(new byte[][]{"|".getBytes(StandardCharsets.UTF_8)}) + .setDecoder(new MyMessageDecoder()) + .setEncoder(new MyMessageEncoder()) + .setHandlerMapping(new DefaultHandlerMapping("io.github.yezhihao.netmc.endpoint")) +// .setHandlerMapping(new SpringHandlerMapping("org.yzh.web.endpoint")) + .setHandlerInterceptor(new MyHandlerInterceptor()) + .setSessionManager(new SessionManager()) + .build(); + + TCPServer tcpServer = new TCPServer("Test服务", jtConfig); + tcpServer.start(); + } +} \ No newline at end of file diff --git a/src/test/java/io/github/yezhihao/netmc/Test.java b/src/test/java/io/github/yezhihao/netmc/Test.java index dbeba5f..e1cf787 100644 --- a/src/test/java/io/github/yezhihao/netmc/Test.java +++ b/src/test/java/io/github/yezhihao/netmc/Test.java @@ -1,7 +1,13 @@ package io.github.yezhihao.netmc; +import io.netty.buffer.ByteBufUtil; + +import java.nio.charset.StandardCharsets; + public class Test { public static void main(String[] args) { - System.out.println(); + //测试代码的消息结构“|客户端ID,消息类型,消息流水号;消息体|” + byte[] bytes = "|123,1,123;test|".getBytes(StandardCharsets.UTF_8); + System.out.println(ByteBufUtil.hexDump(bytes)); } } diff --git a/src/test/java/io/github/yezhihao/netmc/codec/MyMessageDecoder.java b/src/test/java/io/github/yezhihao/netmc/codec/MyMessageDecoder.java new file mode 100644 index 0000000..a7bd6c3 --- /dev/null +++ b/src/test/java/io/github/yezhihao/netmc/codec/MyMessageDecoder.java @@ -0,0 +1,34 @@ +package io.github.yezhihao.netmc.codec; + +import io.github.yezhihao.netmc.model.MyHeader; +import io.github.yezhihao.netmc.model.MyMessage; +import io.github.yezhihao.netmc.session.Session; +import io.netty.buffer.ByteBuf; + +import java.nio.charset.StandardCharsets; + +public class MyMessageDecoder implements MessageDecoder { + + @Override + public Object decode(ByteBuf buf) { + return decode(buf); + } + + @Override + public Object decode(ByteBuf buf, Session session) { + String msgStr = buf.readCharSequence(buf.readableBytes(), StandardCharsets.UTF_8).toString(); + String[] allStr = msgStr.split(";"); + String[] headStr = allStr[0].split(","); + String bodyStr = allStr[1]; + + MyHeader header = new MyHeader(); + header.setClientId(headStr[0]); + header.setType(Integer.valueOf(headStr[1])); + header.setSerialNo(Integer.valueOf(headStr[2])); + + MyMessage message = new MyMessage(); + message.setHeader(header); + message.setBody(bodyStr); + return message; + } +} \ No newline at end of file diff --git a/src/test/java/io/github/yezhihao/netmc/codec/MyMessageEncoder.java b/src/test/java/io/github/yezhihao/netmc/codec/MyMessageEncoder.java new file mode 100644 index 0000000..7293064 --- /dev/null +++ b/src/test/java/io/github/yezhihao/netmc/codec/MyMessageEncoder.java @@ -0,0 +1,25 @@ +package io.github.yezhihao.netmc.codec; + +import io.github.yezhihao.netmc.model.MyHeader; +import io.github.yezhihao.netmc.model.MyMessage; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; + +import java.nio.charset.StandardCharsets; + +public class MyMessageEncoder implements MessageEncoder { + + @Override + public ByteBuf encode(MyMessage message) { + MyHeader header = message.getHeader(); + + StringBuilder msg = new StringBuilder(); + msg.append(header.getClientId()).append(','); + msg.append(header.getType()).append(','); + msg.append(header.getSerialNo()).append(';'); + msg.append(message.getBody()); + + byte[] bytes = msg.toString().getBytes(StandardCharsets.UTF_8); + return Unpooled.wrappedBuffer(bytes); + } +} \ No newline at end of file diff --git a/src/test/java/io/github/yezhihao/netmc/endpoint/MyEndpoint.java b/src/test/java/io/github/yezhihao/netmc/endpoint/MyEndpoint.java new file mode 100644 index 0000000..b0986bb --- /dev/null +++ b/src/test/java/io/github/yezhihao/netmc/endpoint/MyEndpoint.java @@ -0,0 +1,15 @@ +package io.github.yezhihao.netmc.endpoint; + +import io.github.yezhihao.netmc.core.annotation.Endpoint; +import io.github.yezhihao.netmc.core.annotation.Mapping; +import io.github.yezhihao.netmc.model.MyMessage; +import io.github.yezhihao.netmc.session.Session; + +@Endpoint +public class MyEndpoint { + + @Mapping(types = 1, desc = "注册") + public void register(MyMessage request, Session session) { + System.out.println(request); + } +} diff --git a/src/test/java/io/github/yezhihao/netmc/endpoint/MyHandlerInterceptor.java b/src/test/java/io/github/yezhihao/netmc/endpoint/MyHandlerInterceptor.java new file mode 100644 index 0000000..810c5dc --- /dev/null +++ b/src/test/java/io/github/yezhihao/netmc/endpoint/MyHandlerInterceptor.java @@ -0,0 +1,70 @@ +package io.github.yezhihao.netmc.endpoint; + +import io.github.yezhihao.netmc.core.HandlerInterceptor; +import io.github.yezhihao.netmc.model.MyHeader; +import io.github.yezhihao.netmc.model.MyMessage; +import io.github.yezhihao.netmc.session.Session; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MyHandlerInterceptor implements HandlerInterceptor { + + private static final Logger log = LoggerFactory.getLogger(MyHandlerInterceptor.class.getSimpleName()); + + /** 未找到对应的Handle */ + @Override + public MyMessage notSupported(MyMessage request, Session session) { + log.warn(">>>>>>>>>>未识别的消息{},{}", session, request); + + MyHeader header = request.getHeader(); + MyMessage response = new MyMessage(); + response.setHeader(new MyHeader(400, header.getClientId(), session.nextSerialNo())); + response.setBody("success"); + + log.info("<<<<<<<<<<未识别的消息{},{}", session, response); + return response; + } + + + /** 调用之后,返回值为void的 */ + @Override + public MyMessage successful(MyMessage request, Session session) { + log.info(">>>>>>>>>>消息请求成功{},{}", session, request); + + MyHeader header = request.getHeader(); + MyMessage response = new MyMessage(); + response.setHeader(new MyHeader(200, header.getClientId(), session.nextSerialNo())); + response.setBody("success"); + + log.info("<<<<<<<<<<通用应答消息{},{}", session, response); + return response; + } + + /** 调用之后抛出异常的 */ + @Override + public MyMessage exceptional(MyMessage request, Session session, Exception ex) { + log.warn(">>>>>>>>>>消息处理异常{},{}", session, request); + + MyHeader header = request.getHeader(); + MyMessage response = new MyMessage(); + response.setHeader(new MyHeader(500, header.getClientId(), session.nextSerialNo())); + response.setBody("error"); + + log.info("<<<<<<<<<<异常处理应答{},{}", session, response); + return response; + } + + /** 调用之前 */ + @Override + public boolean beforeHandle(MyMessage request, Session session) { + request.setSession(session); + return true; + } + + /** 调用之后 */ + @Override + public void afterHandle(MyMessage request, MyMessage response, Session session) { + log.info(">>>>>>>>>>消息请求成功{},{}", session, request); + log.info("<<<<<<<<<<应答消息{},{}", session, response); + } +} \ No newline at end of file diff --git a/src/test/java/io/github/yezhihao/netmc/model/MyHeader.java b/src/test/java/io/github/yezhihao/netmc/model/MyHeader.java new file mode 100644 index 0000000..2dac7f8 --- /dev/null +++ b/src/test/java/io/github/yezhihao/netmc/model/MyHeader.java @@ -0,0 +1,50 @@ +package io.github.yezhihao.netmc.model; + +import io.github.yezhihao.netmc.core.model.Header; + +public class MyHeader implements Header { + + /** 客户端ID */ + private String clientId; + /** 消息类型 */ + private int type; + /** 消息流水号 */ + private int serialNo; + + public MyHeader() { + } + + public MyHeader(int type, String clientId, int serialNo) { + this.type = type; + this.clientId = clientId; + this.serialNo = serialNo; + } + + @Override + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + @Override + public Integer getType() { + return type; + } + + public void setType(Integer type) { + this.type = type; + } + + @Override + public int getSerialNo() { + return serialNo; + } + + @Override + public void setSerialNo(int serialNo) { + this.serialNo = serialNo; + } +} \ No newline at end of file diff --git a/src/test/java/io/github/yezhihao/netmc/model/MyMessage.java b/src/test/java/io/github/yezhihao/netmc/model/MyMessage.java new file mode 100644 index 0000000..37d37ff --- /dev/null +++ b/src/test/java/io/github/yezhihao/netmc/model/MyMessage.java @@ -0,0 +1,53 @@ +package io.github.yezhihao.netmc.model; + +import io.github.yezhihao.netmc.core.model.Message; +import io.github.yezhihao.netmc.session.Session; + +public class MyMessage implements Message { + + private Session session; + + private MyHeader header; + + private String body; + + public Session getSession() { + return session; + } + + public void setSession(Session session) { + this.session = session; + } + + @Override + public MyHeader getHeader() { + return header; + } + + public void setHeader(MyHeader header) { + this.header = header; + } + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } + + @Override + public Object getMessageType() { + return header.getType(); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("MyMessage{"); + sb.append("session=").append(session); + sb.append(", header=").append(header); + sb.append(", body='").append(body).append('\''); + sb.append('}'); + return sb.toString(); + } +}