如何创建 Java UDP Socket

1. 前言

UDP 的英文全称是:User Datagram Protocol,翻译成中文叫用户数据报协议,它是 TCP/IP 协议族中一个非常重要的传输层协议。UDP 是一个无连接的、不可靠的传输层协议,没有丢包重传机制、没有流控机制、没有拥塞控制机制。UDP 不保证数据包的顺序,UDP 传输经常出现乱序,UDP 不对重复包进行过滤。

既然 UDP 这么多缺点,我们还有学习的必要吗?其实不然,正是因为 UDP 没有提供复杂的各种保障机制,才使得它具有实时、高效的传输特性。那么 UDP 到底有哪些优势呢?

  • 第一,UDP 是面向消息的传输协议,保证数据包的边界,不需要进行消息解析,处理逻辑非常简单。
  • 第二,UDP 具有实时、高效的传输特性。
  • 第三,协议栈没有对 UDP 进行过多的干预,这给应用层带来了很多便利,应用程序可以根据自己的需要对传输进行控制。比如,自己实现优先级控制、流量控制、可靠性机制等。当然还有其他一些优势,我就不再一一列举。

UDP 在音视频会议、VOIP、音视频实时通信等行业有着广泛的应用。为此,我们是非常有必要学好 UDP 的。由于 UDP 相对简单,学习起来也会轻松很多。

2. 传统 UDP 客户端和服务器建立过程

同样,我们展示了通过 C 语言 Socket API 编写 UDP 客户端和服务器程序的步骤,如下:
图片描述

图中的矩形方框都是 C 函数。对比 TCP 客户端、服务器的建立过程,我们发现 UDP 客户端可以调用 connect 函数,但是并不会去连接服务器,只是和本地接口做绑定;UDP 服务器是没有 listen 和 accept 调用的。对于 UDP 客户端来说,connect 函数的调用是可选的。接下来,我们就探讨一下如何用 Java 语言编写 UDP 客户端和服务器程序。

3. Java DatagramSocket 分析

Java 语言抽象了 java.net.DatagramSocket 类,表示一个 UDP Socket,既可以用在客户端,又可以用在服务器端。java.net.DatagramSocket 是一个包装类,对外抽象了一组方法,具体实现是在 java.net.DatagramSocketImpl 类中完成的,它允许用户自定义具体实现。java.net.DatagramSocket 类包含的主要功能如下:

  • 创建 UDP Socket,具体就是创建一个 java.net.DatagramSocket 类的对象。
  • 将 Socket 绑定到本地接口 IP 地址或者端口,可以调用 java.net.DatagramSocket 类的构造方法bind 方法完成。
  • 将客户端 UDP Socket 和远端 Socket 做绑定,可以通过 java.net.DatagramSocket 类的 connect 方法完成。

提示:
UDP 客户端调用 connect 方法,仅仅是将本地 Socket 和远端 Socket 做绑定,并不会有类似 TCP 三次握手的过程。

  • 关闭连接,可以调用 java.net.DatagramSocket 类的 close 方法完成。

  • 接收数据,可以通过 java.net.DatagramSocket 类的 receive 方法实现数据接收。

  • 发送数据,可以通过 java.net.DatagramSocket 类的 send 方法实现数据发送。

java.net.Socket 类提供了一组重载的构造方法,方便程序员选择,大体分为四类:

  • 无参
public DatagramSocket() throws SocketException

绑定到任意可用的端口和通配符 IP 地址,比如 IPv4 的 0.0.0.0。一般用作 UDP 客户端 Socket 的创建。

  • 传入 port 参数
public DatagramSocket(int port) throws SocketException

绑定到由 port 指定的端口和通配符 IP 地址,比如 IPv4 的 0.0.0.0。一般用作 UDP 服务端 Socket 的创建。

  • 传入指定的 IP 和 Port 参数
public DatagramSocket(SocketAddress bindaddr) throws SocketException
public DatagramSocket(int port, InetAddress laddr) throws SocketException

绑定到指定的端口和指定的网络接口。如果你的主机有多个网卡,并且你指向在某个指定的网卡上收发数据,可以用此构造方法。既可以用作 UDP 客户端 Socket,也可以用作 UDP 服务端 Socket。

4. Java UDP 客户端

我们创建一个简单的 UDP 客户端程序,代码如下:

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketAddress;

public class UDPClient {
    private static final int PORT = 9002;
    private static final String DST_HOST = "127.0.0.1";
    private static final int RECV_BUFF_LEN = 1500;
    private static byte[] inBuff = new byte[RECV_BUFF_LEN];

    public static void main(String[] args) {
        // 创建 UDP 客户端 Socket,选择无参构造方法,由系统分配本地端口号和网络接口
        try (DatagramSocket udpClient = new DatagramSocket()){
            // 构造发送的目标地址,指定目标 IP 和目标端口号
            SocketAddress to = new InetSocketAddress(DST_HOST, PORT);
            while (true){
                String req = "Hello Server!";
                // 构造发送数据包,需要传入消息内容和目标地址结构 SocketAddress
                DatagramPacket message = new DatagramPacket(req.getBytes(), req.length(), to);
                // 发送消息
                udpClient.send(message);
                System.out.println("Send UDP message:"
                        + req + " to server:" + message.getSocketAddress().toString());

                // 构造接收消息的数据包,需要传入 byte 数组
                DatagramPacket inMessage = new DatagramPacket(inBuff, inBuff.length);
                // 接收消息
                udpClient.receive(inMessage);

                System.out.println("Recv UDP message:"
                        + new String(inMessage.getData(), 0, inMessage.getLength())
                        + " from server:" + inMessage.getSocketAddress().toString());

                // 每隔 2 秒发送一次消息
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

5. Java UDP 服务端

我们创建一个简单的 UDP 服务端程序,代码如下:


import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;

public class UDPServer {
    private static final int BIND_PORT = 9002;
    private static final String BIND_HOST = "127.0.0.1";
    private static final int RECV_BUFF_LEN = 1500;
    private static byte[] inBuff = new byte[RECV_BUFF_LEN];

    public static void main(String[] args) {
        // 构造服务器 Socket,绑定到一个固定的端口,监听的 IP 是 0.0.0.0
        try (DatagramSocket udpServer = new DatagramSocket(BIND_PORT)) {
            // 构造接收消息的数据包,需要传入 byte 数组。
            // 我们将这条语句放在循环外,不需要每次消息收发都构造此结构
            DatagramPacket inMessage = new DatagramPacket(inBuff, inBuff.length);
            while (true){
                // 接收客户端消息
                udpServer.receive(inMessage);
                System.out.println("Recv UDP message:"
                        + new String(inMessage.getData(), 0, inMessage.getLength())
                        + " from Client:"
                        + inMessage.getSocketAddress().toString());

                String rsp = "Hello Client!";
                // 构造发送的消息结构
                // 注意!!!对于服务器来说,发送的目标地址一定是接收消息时的源地址,所以从 inMessage 结构获取
                DatagramPacket message = new DatagramPacket(rsp.getBytes(), rsp.length(),
                        inMessage.getSocketAddress());
                // 发送消息
                udpServer.send(message);
                System.out.println("Send UDP message:"
                        + rsp + " to Client:" + message.getSocketAddress().toString());
                // 重置接收数据包消息长度,准备接收下一个消息
                inMessage.setLength(inBuff.length);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

6. 小结

用 Java 语言编写 UDP 客户端和服务器程序非常简单,你只需要创建一个 java.net.DatagramSocket 实例。如果你构造的是服务器 Socket,需要传入监听的端口号,监听的接口 IP 是可选的,默认是监听通配符 IP。如果你创建的是客户端 Socket,你可以传入绑定的本地 Port 和接口地址,也可以不传入任何参数。对于客户端 UDP Socket 来说,你也可以调用 connect 方法,只是和远端 Socket 绑定,没有类似 TCP 的三次握手过程。

示例代码我们采用的是 try-with-resources 写法,对于 Java 7 以后的版本,可以不显式调用 close 方法。如果是非此写法,需要显式调用 close 方法。