前段时间项目组接了一个研究所项目,移动端这边需要做一个UDP接收报文的程序APP,其中还涉及到多页面之间收发报文、动态修改地址、端口号等等。原本编写这个收发程序并不难,步骤也比较固定,在网上找了相关例子进行二次开发,可是发现UDP报文接收不到,这其中还是隐藏着某些坑,仅以此篇文章来总结其奥妙精髓。
此节将讲解数据报通信的原理,即不需要面向连线的通信方式,数据报通信方式采用的是UDP(User Datagram Protocol)协议,这里同TCP作比较再次介绍。(对相关知识不熟悉者,可先看此篇博客稍作了解: http://blog.csdn.net/itermeng/article/details/72970099)
UDP(User Datagram Protocol)
非面向连接的提供不可靠的数据包式的数据传输协议。类似于从邮局发送信件的过程,发送信件是通过邮局系统一站一站进行传递,中间也有可能丢失。Java中有些类是基于UDP协议来进行网络通讯的,有DatagramPacket、DatagramSocket、MulticastSocket等类。
TCP(Transport Control Protocol)
面向连接的能够提供可靠的流式数据传输的协议。类似于打电话的过程,在拨完电话后两者之间会先建立连接,为了更好的通话,确保通话连接后两者开始互相传输信息。相对应的类有URL、URLConnection Socket、ServerSocket等
UDP 与 TCP的区别
TCP有建立时间,UDP无
(1)定义概念
此类表示用来发送和接收数据报包的套接字。
数据报套接字是包投递服务的发送或接收点。每个在数据报套接字上发送或接收的包都是单独编址和路由的。从一台机器发送到另一台机器的多个包可能选择不同的路由,也可能按不同的顺序到达。
在 DatagramSocket 上总是启用 UDP 广播发送。为了接收广播包,应该将 DatagramSocket 绑定到通配符地址。在某些实现中,将 DatagramSocket 绑定到一个更加具体的地址时广播包也可以被接收。
(2)构造函数总结
构造函数名称 | 含义 |
---|---|
DatagramSocket() | 构造数据报套接字并将其绑定到本地主机上任何可用的端口。 |
DatagramSocket(int port) | 创建数据报套接字并将其绑定到本地主机上的指定端口。 |
DatagramSocket(int port, InetAddress laddr) | 创建数据报套接字,将其绑定到指定的本地地址。 |
(3)重要方法摘要
方法名称 | 含义 |
---|---|
void close() | 关闭此数据报套接字。 |
void connect(InetAddress address, int port) | 将套接字连接到此套接字的远程地址。 |
boolean isClosed() | 返回是否关闭了套接字。 |
void receive(DatagramPacket p) | 从此套接字接收数据报包。 |
void send(DatagramPacket p) | 从此套接字发送数据报包。 |
DatagramSocket(int port, InetAddress laddr) | 创建数据报套接字,将其绑定到指定的本地地址。 |
(1)定义概念
此类表示互联网协议 (IP) 地址。
IP 地址是 IP 使用的 32 位或 128 位无符号数字,它是一种低级协议,UDP 和 TCP 协议都是在它的基础上构建的。InetAddress 的实例包含 IP 地址,还可能包含相应的主机名(取决于它是否用主机名构造或者是否已执行反向主机名解析)。
(2)创建方法
注意,创建此类事通过类方法而获取,并非构造方法。
方法名称 | 含义 |
---|---|
static InetAddress getByAddress(byte[] addr) | 在给定原始 IP 地址的情况下,返回 InetAddress 对象。 |
static InetAddress getByAddress(String host, byte[] addr) | 根据提供的主机名和 IP 地址创建 InetAddress。 |
static InetAddress getByName(String host) | 在给定主机名的情况下确定主机的 IP 地址。 |
(1)定义概念
此类表示数据报包。
数据报包用来实现无连接包投递服务。每条报文仅根据该包中包含的信息从一台机器路由到另一台机器。从一台机器发送到另一台机器的多个包可能选择不同的路由,也可能按不同的顺序到达。不对包投递做出保证。
(2)构造方法
构造函数名称 | 含义 |
---|---|
DatagramPacket(byte[] buf, int length) | 用来接收长度为 length 的数据包 |
DatagramPacket(byte[] buf, int length, InetAddress address, int port) | 构造数据报包,用来将长度为 length 的包发送到指定主机上的指定端口号 |
在此程序中,将UDP报文的发送、接收操作分别写入到两个线程中,根据具体需求进行调用。
以下步骤都在发送线程内,需要注意的是发送UDP报文逻辑单一,顺序执行完毕线程即可结束,不涉及到后台等待的需求,所以执行完后即可关闭套接字连接。
发送步骤
send(datagramPacket)
方法,发送UDP报文close()
关闭套接字连接对应以上步骤,代码展示(仅为部分重要代码):
Byte[] buf="hello android! ".getBytes();
DatagramSocket sendSocket = new DatagramSocket();
InetAddress serverAddr = InetAddress.getByName(SEND_IP);
DatagramPacket outPacket = new DatagramPacket(buf, buf.length,serverAddr, SEND_PORT);
sendSocket.send(outPacket);
sendSocket.close();
原理
以下步骤都在接收线程内,同发送线程大致相同,但需要注意这两者本质的区别:发送线程里的逻辑执行一遍即可结束,但是接收线程需要在后台待定等待接收UDP报文,不可执行一遍就结束!相当于在一个界面中,可多次创建发送线程用来发送报文,但是接收线程只需在界面初始化时创建,从而一直监听报文接收(若重新进入界面,逻辑如上)。
所以,在接收线程内部需要用到循环,在循环内部调用套接字对象的receive()
方法来接收UDP报文。注意套接字对象的连接关闭,发送线程中单一的逻辑,执行完发送过程即可关闭连接,但是在接收线程中使用了循环,所以需要用一个全局标识量来控制循环,若界面退出或销毁则将标示值置为false,这样接收线程即可结束,再关闭套接字连接。
接收步骤
receive(datagramPacket)
方法,接收UDP报文对应以上步骤,代码展示(仅为部分重要代码):
DatagramSocket receiveSocket = new DatagramSocket(RECEIVE_PORT);
while(listenStatus){
byte[] inBuf= new byte[1024];
DatagramPacket inPacket=new DatagramPacket(inBuf,inBuf.length);
receiveSocket.receive(inPacket); i if(!inPacket.getAddress().equals(serverAddr)){
throw new IOException("未知名的报文");
}
receiveInfo = inPacket.getData();
receiveHandler.sendEmptyMessage(1);
}
public class TextActivity extends AppCompatActivity{
/*
* Data
* */
private final static String SEND_IP = "27.18.140.100"; //发送IP
private final static int SEND_PORT = 8989; //发送端口号
private final static int RECEIVE_PORT = 8080; //接收端口号
private boolean listenStatus = true; //接收线程的循环标识
private byte[] receiveInfo; //接收报文信息
private byte[] buf;
private DatagramSocket receiveSocket;
private DatagramSocket sendSocket;
private InetAddress serverAddr;
private SendHandler sendHandler = new SendHandler();
private ReceiveHandler receiveHandler = new ReceiveHandler();
/*
* UI
* */
private TextView tvMessage;
private Button btnSendUDP;
class ReceiveHandler extends Handler{
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
tvMessage.setText("接收到数据了" + receiveInfo.toString());
Toast.makeText(TextActivity.this, "接收到数据了", Toast.LENGTH_SHORT).show();
}
}
class SendHandler extends Handler{
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
tvMessage.setText("UDP报文发送成功");
Toast.makeText(TextActivity.this, "成功发送", Toast.LENGTH_SHORT).show();
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_text);
btnSendUDP = (Button) findViewById(R.id.btn_send);
tvMessage = (TextView) findViewById(R.id.tv_show);
btnSendUDP.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//点击按钮则发送UDP报文
new UdpSendThread().start();
}
});
//进入Activity时开启接收报文线程
new UdpReceiveThread().start();
}
@Override
protected void onDestroy() {
super.onDestroy();
//停止接收线程,关闭套接字连接
listenStatus = false;
receiveSocket.close();
}
/*
* UDP数据发送线程
* */
public class UdpSendThread extends Thread
{
@Override
public void run()
{
try
{
buf="i am an android developer, hello android! ".getBytes();
// 创建DatagramSocket对象,使用随机端口
sendSocket = new DatagramSocket();
serverAddr = InetAddress.getByName(SEND_IP);
DatagramPacket outPacket = new DatagramPacket(buf, buf.length,serverAddr, SEND_PORT);
sendSocket.send(outPacket);
sendSocket.close();
sendHandler.sendEmptyMessage(1);
} catch (Exception e)
{
e.printStackTrace();
}
}
}
/*
* UDP数据接收线程
* */
public class UdpReceiveThread extends Thread
{
@Override
public void run()
{
try
{
sendSocket = new DatagramSocket(RECEIVE_PORT);
serverAddr = InetAddress.getByName(SEND_IP);
while(listenStatus)
{
byte[] inBuf= new byte[1024];
DatagramPacket inPacket=new DatagramPacket(inBuf,inBuf.length);
sendSocket.receive(inPacket);
if(!inPacket.getAddress().equals(serverAddr)){
throw new IOException("未知名的报文");
}
receiveInfo = inPacket.getData();
receiveHandler.sendEmptyMessage(1);
}
} catch (Exception e)
{
e.printStackTrace();
}
}
}
}
以上代码已经可以实现发送、接收UDP报文了,至少我自测是没有问题的,是修改好的版本。一开始我实在网上找的版本进行二次修改的,只是那代码有些小缺陷,导致有时候接收UDP报文有问题,前2个原因是根据此次工作发现并解决了此问题,如果前两点未解决,第三点是另一种尝试。
此点很重要,之前在网上找的例子就将接收、发送线程中的DatagramSocket共用同一个全局变量,这会直接导致应用程序无法接收到UDP报文!
首先接收、发送UDP的逻辑分别在两个不同的线程中的run()
方法里,根据线程的启动去调用它们。注意两个线程存在的生命周期:
接收线程 在一个界面中(不退出界面的情况下)只会被创建并启动一次,即线程一直存在于后台等待接收UDP报文,此时它的DatagramSocket对象也是要一直存在的;
发送线程 在需要的情况下会多次被重复创建并启动,它的每次启动都会去创建DatagramSocket对象,发送完报文后会立即关闭掉DatagramSocket的连接。
若两个线程共用一个DatagramSocket对象,接收线程开启后,DatagramSocket对象存在于后台,此时发送一次报文后,DatagramSocket对象会被关闭连接,这样应用程序就无法再接收到UDP报文了。所以,这两个线程的DatagramSocket对象需独立不相同!
在理解了上一点后,在不退出界面的情况下应用程序可以很好的收发UDP报文,再思考全面一点。考虑用户在使用中退出界面又重新回到界面,重点还是放在DatagramSocket对象上,一般出错大多是出在这个上面。
发送线程执行完逻辑后会关闭DatagramSocket连接,线程结束。
可是接收线程是一直运行在后台,除非循环标识量置为False,接收线程才会结束。
如果不对接收线程中的DatagramSocket对象进行处理,在退出界面又重新回到界面时,接收线程会被重新创建,之前创建的DatagramSocket对象连接未关闭,此时再重新创建,便会出现异常,导致应用程序无法接收到UDP报文。异常如下:
java.net.BindException: bind failed: EADDRINUSE (Address already in use)
很明显,显示地址被占用。所以界面退出时应对接收线程的回收及Socket对象的连接关闭,修改代码如下:
@Override
protected void onDestroy() {
super.onDestroy();
//停止接收线程,关闭套接字连接
listenStatus = false;
receiveSocket.close();
}
我的程序在修改完以上后基本没有出什么问题了,但是这点原因在网上出现的概率很大,遂一起写进来,为读者拓展思维。有的手机不能直接接收UDP包,可能是手机厂商在定制Rom的时候把这个功能给关掉了,解决办法如下:
(1)可先在oncreate()方法里面实例化一个WifiManager.MulticastLock 对象lock;具体如下:
WifiManager manager = (WifiManager) this
.getSystemService(Context.WIFI_SERVICE);
WifiManager.MulticastLock lock= manager.createMulticastLock("test wifi");
(2)在调用广播发送、接收报文之前先调用lock.acquire()方法;
(3)用完之后及时调用lock.release()释放资源,否决多次调用lock.acquire()方法,程序可能会崩,详情请见
Caused by: java.lang.UnsupportedOperationException: Exceeded maximum number of wifi locks
注:记得在配置文件里面添加如下权限:
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
经过这样处理后,多数手机都能正常发送接收到广播报文。
关于这个测试环境,我是借助手机和电脑来完成的,可能大多数人用的是 Wireshark 软件来测试UDP报文收发,但是对于新手而言需要学习一会,这里推荐一个上手难度为0的测试软件:网络调试助手NetAsssist,资源在最后。
用法
将手机和电脑连接在同一局域网下,我这里是将两者全部连接校园Wifi,然后打开电脑软件,上面自动显示电脑分配的IP,查看手机连接Wifi高级设置获取手机分配IP。选择好网络调试助手软件的协议类型和端口号,手机可发送报文到电脑上,在电脑网络调试助手软件上填写好“手机IP:接收端口号”,点击发送按钮,手机即可接收到电脑发送的UDP报文。
软件使用截图:
程序应用测试截图:
资源链接:
http://download.csdn.net/detail/itermeng/9880334
最后说明一下,关于所做的项目需求略微复杂,但是应用程序的骨干精华就是第三小节的那些代码,而一些坑及需要注意的问题在第四点提及,这次接触UDP报文,着实是边学边摸索,总算从坑里爬了出来,可能我所记录的并不详细或者有误,虚心请教~
希望对你们有帮助:)