基于mina框架的GPS设备与服务器之间的交互

      偶然得了一个谷米的车载GPS设备(gt02d),做为程序员的我,开始躁动了:想着做一个服务器程序,记录GPS设备上传的坐标,然后在地图上绘制每天的轨迹。。。想想还是挺有意思的(其实前两年还有一个失败的经历,自己还弄了一个"TA在哪儿"的Android版本的程序,就是登录后,每1分钟通过Http上传坐标,这样你的好友就可以看到你在哪儿,还专门让老婆坐公交,我骑车测试,结果反应太慢了。后来,没有了,再后来,好些软件就有位置共享的功能了,哈哈。。。),只不过,轨迹大部分时间还是三点一线(宿舍,公司,球场)。再加上,前段时间我们的订餐系统使用了superwebsocket框架做为服务器和APP通信的媒介,但是不是特别稳定(也有可能是我们的程序有问题),经常出现无法链接的问题,可能主要的原因还是superwebsocket以IIS为宿主,而应用程序池会回收,并且这个回收很多时间不可控,这时服务器和APP通信被中断了,回收后,程序池启动时,会重新建立“通信通道”,但从我们的实践中,会有经常出现无法建立的情况,并且不知道什么时间会出现建立失败的情况,这个很可怕。于是,三天两头接到电话说APP登录不了,后来我只能索性让客户自己重起服务器,有时每天要重起好几次,所以,他们也烦了!于是,我的日子就不好过了!也正好借些机会了解下一些通信的内容。

 

只有.jar?jd-gui来开路

   前几年,我们为杭州一个外卖网,开发订单调度系统时,当时,他们就是给每个配送员的电瓶车,安装了一个GPS设备,系统中配送员对应一个GPS设备的imei,并提供了一个服务器程序(不过只有发布后的gar文件,直接通过命令运行),这样就可以根据配送员的位置,调度订单给他们了(如图1)。

      

                                                   (图1)

  前几年,智能手机在配送员中还不是特别普及,这确实是一个不错的方案,虽然现在多数都用智能手机了,我们的客户后来基本也都是直接用手机上传坐标了,但是手机用电就消耗得快很多了。原本,以为把当年的程序拿过来部署下就可以了,结果呢?一直不上传坐标,发送短信指定,设备也是正常回复,定位也成功了,端口也是被“占用”,直接:netstat -ano|findstr "8889",显示如图2,说明正常。最后,来回问了他们好几个客服和技术,才了解到,他们的设备只支持TCP,不支持UDP,几年前测试时,就是用的UDP协议上传坐标的,当时还因为他们提供的文档说,UDP暂不支持,结果当时只能用UDP,所以印象十分深刻,现在怎么突然不支持了。没办法,只能看能不能反编译,修改下代码,鄙人在学校是学了2年java,但是上班后,只是偶尔客串下Android的开发,心里还是十分没底。

     

                              (图2)

  乘着这股躁动,说干就干,先是下载了jd-gui,打开gar文件,一看代码,代码不多,也是很文明的,心中也踏实了许多!然后,Save All Sources,随后,用eclipse新建一个项目,导入源码,只有几个地方有点小错误,修改下,直接编译通过了,这个又是朝着“胜利之门”前进了一大步。唯一不好的地方就是,反编译的代码,每行前面总有一些注释,虽不影响生成,但是看着还是纠心,主要鄙人对代码的格式很在意,一一删除了几行,才想起可以用正则表达式替换,正则表达式真是个好东西,谁用谁知道!下面是替换前后的对比,两个正则表达式为:  /\* [\s]* \*/  (替换中间为空的行),/\* [0-9]* \*/(替换中间是数字的)。

                

                            (替换前)                                                                                                     (替换后)

 

程序未动,协议文档先行

   向设备技术要了协议文档(他们客服很拽,一听说要自己开发平台,就基本不理人了,说是他们的利益都来自至说平台,你自己做平台了,影响他们利益了,还好硬件技术还是很好说话,要不怎么说程序员都是好人呢,时间都用在技术上了,哪还有时间使坏心眼哦),细读了文档,基本就是3个交互,其他包我没用上,也就没写了:

  1,登录,设备与服务器建立链接后,发送登录包,服务器必须根据协议回复相应数据即可,登录包中含设备编号,8个字节,这样后面的定位包中就不需要传设备编号了,原来的设备是每个定位包中都含设备编号,也许是太耗流量了,才修改成现在的模式了;

  2,心跳,设备登录后,会间隔几分钟,发送心跳包,确认链接正常,服务器必须正确响应数据(登录后,第一个包会是心跳包);

  3,定位包,登录,心跳包正常后,设备开始发送位置信息包。

     数据流程图如下,流程还是比较清晰的。

      

                                              (数据流程图)

  

分析代码,学习mina

   打开main.java,看到引用 mina-core-2.0.4.jar.XXX。再百度mina:

   Apache MINA是一个网络应用程序框架,用来帮助用户简单地开发高性能和高可靠性的网络应用程序。它提供了一个通过Java NIO在不同的传输例如TCP/IP和UDP/IP上抽象的事件驱动的异步API。

      原来是基于此框架建立的通信,再看代码中 NioDatagramAcceptor acceptor = new NioDatagramAcceptor(); 才知道这个表示建立了UDP协议的通信。于是,再查了些资料,把代码修改成建立TCP协议的通信,运行后,再查看端口占用情况,已经是TCP类型了,如下图,再发短信指定设置设备,用IPAnalyse抓包,看到已经能正常上传数据包了,于是,再一次的前进了一大步。

  

  调试程序,第一步肯定是把日志功能调通,在学校时,写java程序时,日志也是用的log4j,当时,只是一句代码 PropertyConfigurator.configure("log4j.properties") 就ok了,还好一下子找到了在学校时写的代码(6,7年了,真不容易呀),把log4j.properties放到特定目录测试,一下子就ok了。当然,这个东西网上一搜,肯定是一箩筐,但是现在好些都是:天下文章一大抄,粘贴复制加剪刀。抄没有问题,但是至少得验证是否正确吧。

  走到这里时,躁动开始让人异常兴奋,失去了程序员应有的冷静。于是,胡乱的调试,胡乱的输入日志,陪上一个周末,10天的“晚自习”,依然是毫无近展,在几近放弃的时候,当然,这时躁动也基本变成了平静,才知道要冷静,回头再仔细分析下MINA的消息流程,原来MINA使用的是异步机制,而程序中也是用了一个线程来处理消息,所以之前通过单步跟踪,或者输入日志的方式自己确认的消息流程错误的,当时以为是:设备->encoder->decoder->设备,所以一直调试不通,后来查了相关资料才了解正确的消息流程,如下图,其实最重要的部分,也是之前一直没有重视的部分就是在 messageReceived 方法中,把 request 转成 respose,当然,request 有很多,如登录、心跳、定位包,所以不能强制转化。

  

  

  理清流程后,真有点“拨开云雾见月明”的感觉,再按开发文档 Decoder 登录包,转成回复包,Encoder 回复包 ,一切都变得顺理成章了。登录包,及回复协议如下图      

     

  下面,附上基本流程的代码,没有什么技术含量的,代码可能也比较丑(在对一个数组赋值时,没注意,全是下标为0的元素赋值,结果错误校验位一直不对,浪费好多时间呀),请务见怪哈。

  

 1 public class Decoder extends CumulativeProtocolDecoder
 2  {
 3      private static final Logger log = Logger.getLogger(Decoder.class);
 4     public boolean doDecode(IoSession session, IoBuffer buffer,
 5             ProtocolDecoderOutput output) {
 6         try {
 7             
 8             log.info("Decoder:" + buffer.toString());
 9             
10             byte[] head = new byte[2];
11             buffer.get(head, 0, 2);
12             if (head.length < 2 || head[0] != 120) {
13                 return false;
14             }
15                 
16             //長度和協議號
17             byte[] lenbyte = new byte[2]; 
18             buffer.get(lenbyte,0,2);
19                     
20             byte proto =  lenbyte[1];
21             
22             log.info("Decoder.body:" + buffer.toString());
23                 
24             System.out.println("proto:" +proto);
25             
26             Message msg;
27             switch (proto) {
28             case 1://登录包
29                 {
30                     byte[] body = new byte[18]; 
31                     buffer.get(body);
32                     
33                     msg =new Login();
34                     byte[] crc = new byte[4];        
35                     byte[] loginbody = new byte[8];
36                     loginbody[0] = crc[0] = 0x05;
37                     loginbody[1] = crc[1]= 0x01;
38                     loginbody[2] = crc[2]= body[12];
39                     loginbody[3] = crc[3]= body[13];
40                     //协议体中从“包长度”到“信息序列号”(包括“包长度”、“信息序列号”)这部分数据的 CRC-ITU 值。
41                     CRC16Util crc16 = new CRC16Util();
42                     
43                     crc16.reset();
44                     crc16.update(crc);
45                     
46                     byte[] crcresult = Byte2Hex.short2bytes((short)crc16.getCrcValue());        
47                     loginbody[4] = crcresult[0];
48                     loginbody[5] = crcresult[1];
49                     //停止位(2位) 0x0D 0x0A
50                     loginbody[6] = 0x0D;
51                     loginbody[7] = 0x0A;
52                     
53                     msg.setHeadBuf(head);
54                     msg.setBodyBuf(loginbody);
55                     
56                     msg.fromHead(head);
57                     msg.fromBody(loginbody);
58                     //获取终端编号
59                     String termid = Byte2Hex.Bytes2HexString(body);
60                     termid = termid.substring(1,16);
61                     
62                     System.out.println("termid:" +termid);            
63                     session.setAttribute("termid", termid);
64                     
65                     msg.setSession(session);
66                     output.write(msg);
67                     return true;
68                 }
69             default:
70                 return false;
71             }
72             return true;
73             
74         } catch (Exception e) {
75             System.out.println("decode error:" + e.toString());
76         }
77         return false;
78     }
79  }
Decoder

 

 1     public Main() throws IOException {
 2         Config.init();
 3 
 4         IoAcceptor acceptor = new NioSocketAcceptor();
 5         acceptor.getFilterChain().addLast("logger", new LoggingFilter());
 6         acceptor.getFilterChain().addLast("protocol", new ProtocolCodecFilter(new CodecFactory()));
 7         acceptor.setHandler(this);
 8         acceptor.getSessionConfig().setReadBufferSize(2048);
 9         acceptor.getSessionConfig().setIdleTime(IdleStatus.BOTH_IDLE, 10);
10         acceptor.bind(new InetSocketAddress(Config.PORT));
11         
12         PropertyConfigurator.configure("D:/javaworkspace/gt02/libs/log4j.properties");
13         //PropertyConfigurator.configure("log4j.properties");
14 
15         this.worker = new Worker();
16         this.worker.start();
17 
18     }
19 
20     public void messageReceived(IoSession session, Object message)
21             throws Exception {
22         if ((message instanceof Message))
23         {
24             this.worker.addRecvMsg((Message) message);
25         }
26         
27     }
Main
 1 public void run() {
 2         super.run();
 3         while (true)
 4             try {
 5                 Message msg = waitForProcessRecvMsg();
 6                 Message ret = msg.process();
 7                  
 8                 IoSession session = msg.getSession();
 9                  if (ret != null)
10                  {
11                      ret.head = msg.head; //设置要回复的内容,写到respost中
12                      ret.body = msg.body;
13                      session.write(ret);
14                 }
15 
16             } catch (Exception e) {
17                 System.out.println("Worker Exception:" + e.getMessage());
18             }
19     }
worker
 1 public class Encoder extends ProtocolEncoderAdapter {
 2      private static final Logger log = Logger.getLogger(Encoder.class);
 3 
 4     public void encode(IoSession session, Object message,
 5             ProtocolEncoderOutput output) {
 6         try {
 7 
 8             Message msg = (Message) message;
 9             msg.toHead();
10             msg.toBody();
11 
12             IoBuffer buffer = IoBuffer.allocate(msg.getLength(),false).setAutoExpand(true);
13                 
14             buffer.put(msg.getHeadBuf());
15             if (msg.getBodyBuf() != null) {
16                 buffer.put(msg.getBodyBuf());
17             }
18             
19             buffer.flip();                
20             log.info("Encoder.buffer:" + buffer.toString());
21 
22             output.write(buffer);
23         } catch (Exception e) {
24             System.out.println("encode error:" + e.toString());
25         }
26     }
27 }
Encoder

 

  

大功告成

   正确回复登录包后,设备会发送一个心跳包,操作基本同登录包,回复的包只要修改下协议号即可。正确响应心跳包后,开始上传定位数据,这个数据包就包含了,经度、纬度、速度、航向等信息。还好开发文档中提供了解析的代码,虽没有什么难度,就是比较繁琐。解析出信息后,再通过一个Tttp请求加到数据库,这个流程就基本完成了。其实在漫长的摸索过程中,一直在想,完成时我会有多兴奋。但是,当真正看到控制台输出坐标信息时,不是兴奋,而是心中顿觉踏实了,想着晚上终于可以安心的睡一个好觉了。当时,就是看到的就是下图,第一个正常的定位信息,当时还特意截图,主要是为了在媳妇面前邀功,哈哈。

   

 

结语

   当时,只是一时兴起,然后变成欲罢不能。也许这就是程序员吧。也许这个东西对好些人来说,不值一提,但对我还是有点意义,还是有好些东西值得思考,我还是要感谢我家媳妇的鼓励。下一步,可能就是把这个应用到我们订餐系统与App通信上面了。鄙人对java了解不多,可能好些地方说的不对,或者不好的地方,也请大家指出,也希望能对某些需要的人提供一些帮助,共同进步!

  

   成为一名优秀的程序员!

 

posted @ 2014-08-28 10:53  2J  阅读(2735)  评论(3编辑  收藏  举报