Android 基于UDP协议

一、简介
与 TCP 不同,UDP 是一个面向数据包的传输层协议,进程的每一个输出操作都正好产生一个UDP数据报,并组装成一份待发送的IP数据报。格式如下:

IP数据报的最大长度为 65535 字节 ,除去首字IP 的20 字节和 UDP首部8个字节,实际上,UDP 能传输的最大字节数为 65507个字节;当我们的数据超过这个长度时,则需要考虑分包的问题。
UDP 的传输是不可靠的,它只负责把数据传输出去,并不会去考虑接收端是否能接受到。在大多不需要考虑应答的应用中,我们会优先考虑 UDP

二、DatagramSocket 和 DatagramPacket
socket 的 udp 的 api 是通过 DatagramSocket 和 DatagramPacket 来实现的。
我们知道两台计算机的通信,无论是 TCP 还是 UDP ,都需要知道 IP 和 端口。


2.1 DatagramPacket类:数据报文
(1)定义概念

此类表示数据报包。

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

(2)构造方法

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

getAddress()返回接收或发送此数据报文的机器的 IP 地址。
getData()返回接收的数据或发送出的数据。
getLength()返回发送出的或接收到的数据的长度。
getPort()返回接收或发送该数据报文的远程主机端口号。
2.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)    创建数据报套接字,将其绑定到指定的本地地址。
2.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 地址。
三、发送、接收UDP报文步骤讲解
同一端如果既要发送消息又想接收消息,一定要将发送的socket和接收的socket分开。不能用一个socket既接收信息,又发送信息,这样一定会造成阻塞。
接收线程中,一定要有 while(true){ if(标志何时结束) 代码块 } 这样的方式,不然,在接收信息时,一定会造成阻塞,就是只能接收到一次信息。
3.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();

SEND_IP即是你想要发送的地址,SEND_PORT端口号则要选用闲置端口就是向8000~9000这样的端口号。

3.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);
         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
            {

                receiveSocket = new DatagramSocket(RECEIVE_PORT);
                serverAddr = InetAddress.getByName(SEND_IP);

                while(listenStatus)
                {
                    byte[] inBuf= new byte[1024];
                    DatagramPacket inPacket=new DatagramPacket(inBuf,inBuf.length);
                    receiveSocket.receive(inPacket);

                    if(!inPacket.getAddress().equals(serverAddr)){
                        throw new IOException("未知名的报文");
                    }

                    receiveInfo = inPacket.getData();
                    receiveHandler.sendEmptyMessage(1);
                }
            } catch (Exception e)
            {
                e.printStackTrace();
            }
        }
    }
}

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

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

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

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

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

5.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();
}

你可能感兴趣的:(udp,android,tcp/ip)