Android收发UDP报文详解 及 优雅解决接收不到问题

Android 学习笔记 专栏收录该内容
54 篇文章 1 订阅

前段时间项目组接了一个研究所项目,移动端这边需要做一个UDP接收报文的程序APP,其中还涉及到多页面之间收发报文、动态修改地址、端口号等等。原本编写这个收发程序并不难,步骤也比较固定,在网上找了相关例子进行二次开发,可是发现UDP报文接收不到,这其中还是隐藏着某些坑,仅以此篇文章来总结其奥妙精髓。

一. DatagramSocket收发报文相关知识点

此节将讲解数据报通信的原理,即不需要面向连线的通信方式,数据报通信方式采用的是UDP(User Datagram Protocol)协议,这里同TCP作比较再次介绍。(对相关知识不熟悉者,可先看此篇博客稍作了解: http://blog.csdn.net/itermeng/article/details/72970099

1. UDP与TCP学习

  • UDP(User Datagram Protocol)
    非面向连接的提供不可靠的数据包式的数据传输协议。类似于从邮局发送信件的过程,发送信件是通过邮局系统一站一站进行传递,中间也有可能丢失。Java中有些类是基于UDP协议来进行网络通讯的,有DatagramPacket、DatagramSocket、MulticastSocket等类。

  • TCP(Transport Control Protocol)
    面向连接的能够提供可靠的流式数据传输的协议。类似于打电话的过程,在拨完电话后两者之间会先建立连接,为了更好的通话,确保通话连接后两者开始互相传输信息。相对应的类有URL、URLConnection Socket、ServerSocket等

    UDP 与 TCP的区别

  • TCP有建立时间,UDP无

  • UDP传输有大小限制,每一个数据报需在64K以内
  • TCP常见应用:Telnet远程登录、Ftp文件传输
  • UDP常见应用:ping指令,用来检测网络上某台服务器是否还在提供服务。

2. DatagramSocket学习

(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)创建数据报套接字,将其绑定到指定的本地地址。

3. InetAddress学习

(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 地址。

4. DatagramPacket学习

(1)定义概念

此类表示数据报包。

数据报包用来实现无连接包投递服务。每条报文仅根据该包中包含的信息从一台机器路由到另一台机器。从一台机器发送到另一台机器的多个包可能选择不同的路由,也可能按不同的顺序到达。不对包投递做出保证。

(2)构造方法

构造函数名称含义
DatagramPacket(byte[] buf, int length)用来接收长度为 length 的数据包
DatagramPacket(byte[] buf, int length, InetAddress address, int port)构造数据报包,用来将长度为 length 的包发送到指定主机上的指定端口号



二. 发送、接收UDP报文步骤讲解

在此程序中,将UDP报文的发送、接收操作分别写入到两个线程中,根据具体需求进行调用。

1. 数据报发送 解析

以下步骤都在发送线程内,需要注意的是发送UDP报文逻辑单一,顺序执行完毕线程即可结束,不涉及到后台等待的需求,所以执行完后即可关闭套接字连接

发送步骤

  • 构造DatagramSocket对象
  • 根据发送IP 来创建InetAddress对象
  • 根据InetAddress对象、发送端口号、发送数据 来创建发送的DatagramPacket数据包对象
  • 调用DatagramSocket对象的send(datagramPacket) 方法,发送UDP报文
  • 调用DatagramSocket对象的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();

2. 数据报接收 解析

原理
以下步骤都在接收线程内,同发送线程大致相同,但需要注意这两者本质的区别:发送线程里的逻辑执行一遍即可结束,但是接收线程需要在后台待定等待接收UDP报文,不可执行一遍就结束!相当于在一个界面中,可多次创建发送线程用来发送报文,但是接收线程只需在界面初始化时创建,从而一直监听报文接收(若重新进入界面,逻辑如上)。

所以,在接收线程内部需要用到循环,在循环内部调用套接字对象的receive() 方法来接收UDP报文。注意套接字对象的连接关闭,发送线程中单一的逻辑,执行完发送过程即可关闭连接,但是在接收线程中使用了循环,所以需要用一个全局标识量来控制循环,若界面退出或销毁则将标示值置为false,这样接收线程即可结束,再关闭套接字连接。

接收步骤

  • 需要根据接收端口号 构造DatagramSocket对象
  • 创建发送的DatagramPacket数据包对象
  • 调用DatagramSocket对象的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);
     }



三. 界面收发UDP报文完整代码

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报文了,至少我自测是没有问题的,是修改好的版本。一开始我实在网上找的版本进行二次修改的,只是那代码有些小缺陷,导致有时候接收UDP报文有问题,前2个原因是根据此次工作发现并解决了此问题,如果前两点未解决,第三点是另一种尝试。

1. 发送与接收DatagramSocket需不同

此点很重要,之前在网上找的例子就将接收、发送线程中的DatagramSocket共用同一个全局变量,这会直接导致应用程序无法接收到UDP报文!

首先接收、发送UDP的逻辑分别在两个不同的线程中的run() 方法里,根据线程的启动去调用它们。注意两个线程存在的生命周期

  • 接收线程 在一个界面中(不退出界面的情况下)只会被创建并启动一次,即线程一直存在于后台等待接收UDP报文,此时它的DatagramSocket对象也是要一直存在的;

  • 发送线程 在需要的情况下会多次被重复创建并启动,它的每次启动都会去创建DatagramSocket对象,发送完报文后会立即关闭掉DatagramSocket的连接。

若两个线程共用一个DatagramSocket对象,接收线程开启后,DatagramSocket对象存在于后台,此时发送一次报文后,DatagramSocket对象会被关闭连接,这样应用程序就无法再接收到UDP报文了。所以,这两个线程的DatagramSocket对象需独立不相同!


2. 退出界面需关闭接收Socket连接

在理解了上一点后,在不退出界面的情况下应用程序可以很好的收发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();
    }

3. Rom关闭了手机接收UDP报功能

我的程序在修改完以上后基本没有出什么问题了,但是这点原因在网上出现的概率很大,遂一起写进来,为读者拓展思维。有的手机不能直接接收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报文,着实是边学边摸索,总算从坑里爬了出来,可能我所记录的并不详细或者有误,虚心请教~

希望对你们有帮助:)

  • 9
    点赞
  • 9
    评论
  • 32
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 书香水墨 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值