基于mina框架的GPS设备与服务器之间的交互
偶然得了一个谷米的车载GPS设备(gt02d),做为程序员的我,开始躁动了:想着做一个服务器程序,记录GPS设备上传的坐标,然后在地图上绘制每天的轨迹。。。想想还是挺有意思的(其实前两年还有一个失败的经历,自己还弄了一个"TA在哪儿"的Android版本的程序,就是登录后,每1分钟通过Http上传坐标,这样你的好友就可以看到你在哪儿,还专门让老婆坐公交,我骑车测试,结果反应太慢了。后来,没有了,再后来,好些软件就有位置共享的功能了,哈哈。。。),只不过,轨迹大部分时间还是三点一线(宿舍,公司,球场)。再加上,前段时间我们的订餐系统使用了superwebsocket框架做为服务器和APP通信的媒介,但是不是特别稳定(也有可能是我们的程序有问题),经常出现无法链接的问题,可能主要的原因还是superwebsocket以IIS为宿主,而应用程序池会回收,并且这个回收很多时间不可控,这时服务器和APP通信被中断了,回收后,程序池启动时,会重新建立“通信通道”,但从我们的实践中,会有经常出现无法建立的情况,并且不知道什么时间会出现建立失败的情况,这个很可怕。于是,三天两头接到电话说APP登录不了,后来我只能索性让客户自己重起服务器,有时每天要重起好几次,所以,他们也烦了!于是,我的日子就不好过了!也正好借些机会了解下一些通信的内容。
前几年,我们为杭州一个外卖网,开发订单调度系统时,当时,他们就是给每个配送员的电瓶车,安装了一个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,定位包,登录,心跳包正常后,设备开始发送位置信息包。
数据流程图如下,流程还是比较清晰的。
(数据流程图)
打开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 }
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 }
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 }
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 }
正确回复登录包后,设备会发送一个心跳包,操作基本同登录包,回复的包只要修改下协议号即可。正确响应心跳包后,开始上传定位数据,这个数据包就包含了,经度、纬度、速度、航向等信息。还好开发文档中提供了解析的代码,虽没有什么难度,就是比较繁琐。解析出信息后,再通过一个Tttp请求加到数据库,这个流程就基本完成了。其实在漫长的摸索过程中,一直在想,完成时我会有多兴奋。但是,当真正看到控制台输出坐标信息时,不是兴奋,而是心中顿觉踏实了,想着晚上终于可以安心的睡一个好觉了。当时,就是看到的就是下图,第一个正常的定位信息,当时还特意截图,主要是为了在媳妇面前邀功,哈哈。
当时,只是一时兴起,然后变成欲罢不能。也许这就是程序员吧。也许这个东西对好些人来说,不值一提,但对我还是有点意义,还是有好些东西值得思考,我还是要感谢我家媳妇的鼓励。下一步,可能就是把这个应用到我们订餐系统与App通信上面了。鄙人对java了解不多,可能好些地方说的不对,或者不好的地方,也请大家指出,也希望能对某些需要的人提供一些帮助,共同进步!
成为一名优秀的程序员!