当前位置: 首页 > 科技观察

在Android上,一个完整的UDP通信模块应该是什么样子的?

时间:2023-03-13 22:10:41 科技观察

TCP与UDP差异对比分析本文从可靠性、数据传输、适用场景等方面分析了两者的差异。本文的目的是向大家介绍如何在Android设备上通过热点将一部手机连接到另一部手机。在这种场景下,一个完整的UDP通信模块应该考虑哪些方面,应该如何优化,如何避免一些坑?Java中UDP的使用我们都知道,目前的Android应用程序大部分都是使用Java语言开发的。如何在Java语言中使用UDP协议?上一篇我们没有讲到Socket。Socket其实可以理解为在程序使用层面对TCP和UDP协议的封装,提供一些API供程序员调用和开发。这就是Socket最表面的意义。在Java中,与UDP相关的类有DatagramSocket、DatagramPacket等,这里不强调它们的使用。好了,假设大家对它们的用途有了大致的了解,就可以正式开始本文的内容了。要初始化UDPSocket,首先创建一个名为UDPSocket的类。publicUDPSocket(Contextcontext){this.mContext=context;intcpuNumbers=Runtime.getRuntime().availableProcessors();//根据CPU个数初始化线程池mThreadPool=Executors.newFixedThreadPool(cpuNumbers*POOL_SIZE);//记录对象被创建的时间lastReceiveTime=System.currentTimeMillis();}在构造方法中,我们进行了一些初始化操作。简单的说,我们创建一个线程池,记录当前时间的毫秒值。至于它们的用处,我们往下看:publicvoidstartUDPSocket(){if(client!=null)return;try{//表示这个Socket在设置的端口上监听数据。client=newDatagramSocket(CLIENT_PORT);if(receivePacket==null){//创建一个接受数据的数据包receivePacket=newDatagramPacket(receiveByte,BUFFER_LENGTH);}startSocketThread();}catch(SocketExceptione){e.printStackTrace();}}这里我们首先创建了一个DatagramSocket作为“客户端”。其实UDP本身没有client和server的概念,只有sender和receiver的概念。我们暂且把sender当成一个client。在创建DatagramSocket对象时,会传入一个端口号,这个端口号可以定义在一个范围内,表示DatagramSocket在这个端口上监听数据。然后创建一个DatagramPacket对象作为数据的接收包。***调用startSocketThread启动发送和接收数据的线程。/***启动线程发送数据*/privatevoidstartSocketThread(){clientThread=newThread(newRunnable(){@Overridepublicvoidrun(){Log.d(TAG,"clientThreadisrunning...");receiveMessage();}});isThreadRunning=true;clientThread.start();startHeartbeatTimer();}首先,clientThread线程的目的是调用DatagramSocket的receive方法,因为receive方法是阻塞的,不能放在主线程中,所以子线程自然是开了。receiveMessage是处理接收到的UDP数据报。先不看接收数据的方法。毕竟现在还没有人发消息,自然谈不上收到。心跳包保持“长连接”来到本文的第一点,我们都知道UDP本身没有连接的概念。Android端应用UDP和TCP的场景是一部手机连接另一部手机的热点,两者在同一个局域网。当两人都不知道对方的存在时,又如何找到对方?通过心跳包的方式,双方定时发送一个UDP包。如果对方收到,就可以知道对方的ip,建立通信起来。privatestaticfinallongTIME_OUT=120*1000;privatestaticfinallongHEARTBEAT_MESSAGE_DURATION=10*1000;/***开始心跳,定时器间隔十秒*/privatevoidstartHeartbeatTimer(){timer=newHeartbeatTimer();timer.setOnScheduleListener(newHeartbeatTimer.OnScheduleListenerep(){@OridbeatTimer.OnScheduleListenerep(){@OridLog.d(TAG,"timerisonSchedule...");longduration=System.currentTimeMillis()-lastReceiveTime;Log.d(TAG,"duration:"+duration);if(duration>TIME_OUT){//如果超过2分钟还没有收到我的心跳包,则认为对方不在线Log.d(TAG,"超时,对方已下线");//刷新时间,重新进入下一个心跳周期lastReceiveTime=System.currentTimeMillis();}elseif(duration>HEARTBEAT_MESSAGE_DURATION){//如果他在十秒内没有收到我的心跳包,他会再发一个。Stringstring="hello,thisisaheartbeatmessage";sendMessage(string);}}});timer.startTimer(0,1000*10);}目的这个心跳的e是每隔十秒通过sendMessage发送一条消息,看对方能不能收到。如果对方收到消息,则刷新lastReceiveTime的时间。这里我每十秒发送一个字符串给对方。privatestaticfinalStringBROADCAST_IP="192.168.43.255";/***发送心跳包**@parammessage*/publicvoidsendMessage(finalStringmessage){mThreadPool.execute(newRunnable(){@Overridepublicvoidrun(){try{InetAddresstargetAddress=InetAddress.getByName(;BROADCAST_IP)DatagramPacketpacket=newDatagramPacket(message.getBytes(),message.length(),targetAddress,CLIENT_PORT);client.send(packet);Log.d(TAG,"数据发送成功");}catch(UnknownHostExceptione){e.printStackTrace();}catch(IOExceptione){e.printStackTrace();}}});}这是发送消息的代码。刚开始填写DatagramPacket的参数时,我有一个疑问,targetAddress其实是我自己的ip地址。问题来了,我填的是自己的ip地址和对方的端口,怎么才能找到对方呢?你可能会有疑问,你自己的ip地址“192.168.43.255”是怎么来的,为什么要这样定义呢?首先,android手机开启热点时,可以理解为一个网关,有一个默认的ip地址:“192.168.43.1”这个ip地址不是我编的,在Android源码中定义的像这样:WifiStateMachineifcg=mNwService.getInterfaceConfig(intf);if(ifcg!=null){/*IP/netmask:192.168.43.1/255.255.255.0*/ifcg.setLinkAddress(newLinkAddress(NetworkUtils.numericToInetAddress("192.168.43.1"),24));ifcg.setInterfaceUp();mNwService.setInterfaceConfig(intf,ifcg);}所以我知道了所谓开放热点的ip地址,UDP在发送消息的时候还有一个特点,就是发送的消息可以被网关所有设备收到到达,所以我自己的ip地址设置为“192.168.43.255”,所以这个ip地址和“192.168.43.1”在同一个网关,你发送的消息可以已收到。至于如何判断两个ip地址是否在同一网段:判断两个IP的大小和是否在同一网段做一个阶段总结:首先,我们创建了一个senderDatagramSocket,启动了一个heartbeat程序,并每隔一段时间发送一个心跳包。因为我知道热点的ip地址是默认的“192.168.43.1”,而UDP的特点是发送的消息可以被同一网段的设备接收到。因此,发件人的ip地址设置为与热点在同一网段的“192.168.43.255”。事件和数据事件和数据两个模块与业务密切相关。先说数据吧。您可以随意定义双方发送数据的格式。当然,我觉得还是用常规的Json格式来定义比较好。可以包含一些关键的事件字段:比如广播心跳包,收到对方上线心跳包的响应包,超时下线包,以及各种业务相关的数据等等。当然发送数据的时候,就是转换为二进制数组并发送。发送汉字、图片等没有问题,但可能有些细节需要注意,随时google一下。再说说事件:有哪些与业务无关的事件?例如:DatagramSocket.send方法后面是数据发送成功的事件;DatagramSocket.receive方法后面是数据接收成功的事件;心跳包发送了一段时间,还没有收到回复,是连接超时事件;业务相关的事件就是我们上面提到的数据类型相关的,比如设备上线,心跳包响应等等。如何发送事件并通知每个页面?用Listener或者其他eventbus的三方库都没有问题,就看你的选择了。处理收到的消息/***处理收到的消息*/privatevoidreceiveMessage(){while(isThreadRunning){try{if(client!=null){client.receive(receivePacket);}lastReceiveTime=System.currentTimeMillis();Log.d(TAG,"receivepacketsuccess...");}catch(IOExceptione){Log.e(TAG,"UDP数据包接收失败!线程停止");stopUDPSocket();e.printStackTrace();return;}if(receivePacket==null||receivePacket.getLength()==0){Log.e(TAG,"无法接收UDP数据或接收到的UDP数据为空");continue;}StringstrReceive=newString(receivePacket.getData(),0,receivePacket.getLength());Log.d(TAG,strReceive+"from"+receivePacket.getAddress().getHostAddress()+":"+receivePacket.getPort());//解析接收到的json信息//每次收到UDP数据后,重新设置长度。否则,下次收到的数据包可能会被截断。if(receivePacket!=null){receivePacket.setLength(BUFFER_LENGTH);}}}在处理接收到的消息时,有几点值得注意:receive方法是阻塞的,没有接收到数据包时会一直阻塞,所以它应该放在子线程中;每条消息收到后,再次调用receivePacket.setLength;收到消息时刷新lastReceiveTime的值,暂停发送心跳包;处理接收数据的具体业务就是我们刚才讲的发送数据的问题。取决于业务。“用户”的概念在上面已经讨论了UDP的特点。如果手机开启了热点,如果连接了多部手机,则可以接收到多部手机发送的消息。如果发送者的端口和接收者的端口相同,那么即使是自己发送的消息,自己也能收到。这就很尴尬了,也就是说,我们要剔除发给自己的消息,还要区分不同手机发来的消息。这时候,就应该有一个“用户”的概念。创建一个User对象,可以使用哪些属性来查看自己的业务。本文中的示例包括ip、imei和softversion。/***创建本地用户信息*/privatevoidcreateUser(){if(localUser==null){localUser=newUsers();}if(remoteUser==null){remoteUser=newUsers();}localUser.setImei(DeviceUtil.getDeviceId(mContext));localUser.setSoftVersion(DeviceUtil.getPackageVersionCode(mContext));if(WifiUtil.getInstance(mContext).isWifiApEnabled()){//判断热点当前是否开启localUser.setIp("192.168.43.1");}else{//当前开启wifi端localUser.setIp(WifiUtil.getInstance(mContext).getLocalIPAddress());remoteUser.setIp(WifiUtil.getInstance(mContext).getServerIPAddress());}}/***

IMEI。

返回唯一的设备ID,例如,GSM的IMEI和CDMA手机的ESN。如果设备ID不可用,则返回null。*

*RequiresPermission:READ_PHONE_STATE**@paramcontext*@return*/publicsynchronizedstaticStringgetDeviceIdifcontext(Context(context==null){return"";}Stringimei="";try{TelephonyManagerrtm=(TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE);if(tm==null||TextUtils.isEmpty(tm.getDeviceId())){//双卡双待需要通过phone1和phone2获取imei,默认imeiitm=(TelephonyManager)context.getSystemService("phone1");}if(tm!=null){imei=tm.getDeviceId();}}catch(SecurityExceptione){e.printStackTrace();}returnimei;}

这里就不展开所有代码了。如果有手机的imei号码,就很容易辨别身份。可以区分不同的发件人,也可以删除自己发给自己的信息。当然,如果你需要更多的信息,你可以根据自己的业务进行区分,将这些信息作为消息通过Socket发送。写在后面:本文的大部分内容已经介绍到此为止。有些同学可能会问。您需要使用心跳来维护假的“长连接”。使用起来很麻烦,而且你可能还要忍受UDP丢包的痛苦,为什么不选择TCP呢?好问题,其实这个版本是当时做的第一个版本,然后用TCP+UDP来完成这个模块,我们看下一篇添加TCP的改进版本。