第 6 章 UDP 和多播

用户数据报协议( UDP ) 位于 IP 之上,并提供与 TCP 不可靠的对应物。UDP 在网络中的两个节点之间发送单独的数据包。UDP 数据包不知道其他数据包,并且不能保证数据包将实际到达其预定目的地。当发送多个数据包时,无法保证到达顺序。UDP 消息只是简单地发送,然后被遗忘,因为接收方没有发送确认。

UDP 是一种无连接协议。两个节点之间没有消息交换以促进数据包传输。不维护有关连接的状态信息。

UDP 适用于需要高效交付且不需要保证交付的服务。例如,它用于域名系统DNS ) 服务、网络时间协议NTP ) 服务、IP 语音VOIP )P2P 网络的网络通信协调以及视频流。如果视频帧丢失,如果丢失不经常发生,观众可能永远不会注意到它。

有几种使用 UDP 的协议,包括:

  • 实时流协议 (RTSP):该协议用于控制媒体流
  • 路由信息协议 (RIP):该协议确定用于传输数据包的路由
  • 域名系统 (DNS):此协议查找 Internet 域名并返回其 IP 地址
  • 网络时间协议 (NTP):此协议在 Internet 上同步时钟

UDP 数据包由 IP 地址和端口号组成,用于标识其目的地。UDP 数据包具有固定大小,最大可达 65,353 字节。但是,每个数据包至少使用 20 个字节的 IP 标头和 8 个字节的 UDP 标头,将消息的大小限制为 65,507 个字节。如果消息大于该值,则需要发送多个数据包。

UDP 数据包也可以是多播的。这意味着数据包被发送到属于 UDP 组的每个节点。这是将信息发送到多个节点的有效方式,而无需明确针对每个节点。相反,数据包被发送到一个组,该组的成员负责捕获其组的数据包。

在本章中,我们将说明如何使用 UDP 协议:

  • 支持传统的客户端/服务器模式
  • 使用 NIO Channels 执行 UDP 操作
  • 组播数据包到组成员
  • 将音频或视频等媒体流式传输到客户端

我们将从 Java UDP 支持的概述开始,并提供更多 UDP 协议详细信息。

Java UDP 的支持

Java 使用DatagramSocket该类在节点之间DatagramPacket形成套接字连接。所述类表示数据的分组。简单的发送和接收方法将通过网络传输数据包。

UDP 使用 IP 地址和端口号来标识节点。UDP 端口号范围从065535。端口号分为三种类型:

  • 知名端口 ( 0to 1023):这些是用于相对常见服务的端口号。
  • 注册端口 ( 1024to 49151):这些是 IANA 分配给进程的端口号。
  • 动态/专用端口(49152to 65535):这些是在启动连接时动态分配给客户端的。这些通常是临时的,不能由 IANA 分配。

下表是 UDP 特定端口分配的简短列表。它们说明了 UDP 如何被广泛用于支持许多不同的应用程序和服务。更完整的 TCP/UDP 端口号列表可在https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers找到:

知名端口(0 1023

用法

7

这是回声协议

9

这意味着LAN 唤醒

161

这是简单 网络管理协议SNMP )

319

这些是精确时间协议PTP ) 事件消息

320

这些是 PTP 一般消息

513

这表明用户是谁

514

这是系统日志——用于系统日志记录

520

这是路由信息协议RIP )

750

这是kerberos-iv,Kerberos 版本 IV

944

这是网络文件系统服务

973

这是基于 IPv6 服务的网络文件系统

下表列出了已注册的端口及其用途:

注册端口(1024 49151

用法

1534

这用于Eclipse目标通信框架TCF )

1581

这用于MIL STD 2045-47001 VMF

1589

这用于Cisco VLAN 查询协议VQP )/VMPS

2190

这用于 TiVoConnect 信标

2302

这用于 Halo: Combat Evolved 多人游戏

3000

这用于 BitTorrent 同步

4500

这用于 IPSec NAT 穿越

5353

这用于多播 DNS ( mDNS )

9110

这用于 SSMP 消息协议

27500 到 27900

这用于 id Software 的 QuakeWorld

29900 到 29901

这用于任天堂 Wi-Fi 连接

36963

这用于Unreal Software 多人游戏

TCP UDP

TCP UDP 之间有几个区别。这些差异包括:

  • 可靠性:TCP比UDP更可靠
  • Ordering : TCP 保证数据包传输的顺序将被保留
  • 标头大小:UDP 标头小于 TCP 标头
  • 速度:UDP 比TCP快

当使用 TCP 发送数据包时,保证数据包到达。如果丢失,则重新发送。UDP 不提供此保证。如果数据包没有到达,则不会重新发送。

TCP 保留发送数据包的顺序,而 UDP 则不保留。如果 TCP 数据包到达目的地的顺序与其发送顺序不同,则 TCP 将按原始顺序重新组装数据包。使用 UDP 时,不会保留此顺序。

创建数据包时,会附加标头信息以帮助传输数据包。对于 UDP,报头由 8 个字节组成。TCP 报头的通常大小为 32 字节。

UDP 的报头大小更小,并且没有确保可靠性的开销,因此比 TCP 更有效。此外,创建连接所需的工作量更少。这种效率使其成为流媒体的更好选择。

让我们从如何支持传统的客户端/服务器架构开始我们的 UDP 示例。

UDP客户端/服务器

UDP 客户端/服务器应用程序在结构上类似于用于 TCP 客户端/服务器应用程序的结构。在服务器端,创建了一个 UDP 服务器套接字,它等待客户端请求。客户端将创建相应的 UDP 套接字并使用它向服务器发送消息。然后服务器可以处理请求并发回响应。

UDP 客户端/服务器将使用DatagramSocket套接字类和 aDatagramPacket来保存消息。消息的内容类型没有限制。在我们的示例中,我们将使用文本消息。

UDP 服务器应用程序

接下来定义我们的服务器。该构造函数将执行服务器的工作:

公共类 UDPServer {
    公共 UDPServer() {
        System.out.println("UDP 服务器启动");
        ...
        System.out.println("UDP 服务器终止");
    }
 
    公共静态无效主(字符串 [] args){
        新的 UDPServer();
    }
}

在构造函数的 try-with-resources 块中,我们创建了一个DatagramSocket类的实例。我们将使用的几个方法可能会抛出IOException异常,必要时将被捕获:

        试试 (DatagramSocket serverSocket = 
                新数据报套接字(9003)){
            ...
            }
        } catch (IOException ex) {
            //处理异常
        }

创建套接字的另一种方法是使用该bind方法,如下所示。该DatagramSocket实例是使用创建null作为参数。然后使用以下bind方法分配端口:

        DatagramSocket serverSocket = new DatagramSocket(null); 
        serverSocket.bind(new InetSocketAddress(9003)); 

这两种方法都将DatagramSocket使用 port创建一个实例9003

发送消息的过程包括以下内容:

  • 创建字节数组
  • 创建DatagramPacket实例
  • 使用DatagramSocket实例等待消息到达

该过程包含在一个循环中,如下所示,以允许处理多个请求。收到的消息只是简单地回显给客户端程序。该DatagramPacket实例是使用字节数组及其长度创建的。它用作DatagramSocketreceive方法的参数。数据包此时不保存任何信息。此方法将阻塞,直到发出请求,然后将填充数据包:

        而(真){
            字节[]接收消息=新字节[1024];
            DatagramPacket receivePacket = new DatagramPacket(
                接收消息,接收消息。长度);
            serverSocket.receive(receivePacket);
            ...
        }

当方法返回时,数据包被转换为字符串。如果发送了某种其他数据类型,则需要进行某种其他转换。然后显示发送的消息:

        String message = new String(receivePacket.getData());
        System.out.println("从客户端收到:[" + message
               + "]\n发件人:" + receivePacket.getAddress());

要发送响应,需要客户端的地址和端口号。这些是分别使用getAddressgetPort方法针对拥有此信息的数据包获得的。当我们讨论客户时,我们会看到这一点。还需要表示为字节数组的消息,该getBytes方法提供:

        InetAddress inetAddress = receivePacket.getAddress();
        int port = receivePacket.getPort();
        字节[] 发送消息;
        sendMessage = message.getBytes();

DatagramPacket使用消息、其长度以及客户端的地址和端口号创建一个新实例。该send方法将数据包发送到客户端:

        DatagramPacket sendPacket = 
            新数据报包(发送消息,
                sendMessage.length, inetAddress, port);
        serverSocket.send(sendPacket);

定义服务器后,让我们检查客户端。

UDP 客户端应用程序

客户端应用程序将提示用户发送消息,然后将消息发送到服务器。它将等待响应,然后显示响应。它在这里声明:

类 UDPClient {
    公共 UDPClient() {
        System.out.println("UDP 客户端启动");
        ...
        }
        System.out.println("UDP 客户端终止");
    }
 
    公共静态无效主(字符串参数[]){
        新的 UDPClient();
    }
}

Scanner类支持获取用户输入。try-with-resources 块创建一个DatagramSocket实例并处理异常:

        扫描仪scanner = new Scanner(System.in);
        尝试 (DatagramSocket clientSocket = new DatagramSocket()) {
            ...
            }
            clientSocket.close();
        } catch (IOException ex) {
            // 处理异常
        }

使用该getByName方法访问客户端的当前地址,并声明对字节数组的引用。此地址将用于创建数据包:

        InetAddress inetAddress = 
            InetAddress.getByName("localhost");
        字节[] 发送消息;

无限循环用于提示用户输入消息。当用户输入“quit”时,应用程序将终止,如下所示:

        而(真){
            System.out.print("请输入信息:");
            字符串消息=scanner.nextLine();
            如果(“退出”.equalsIgnoreCase(消息)){
                 休息;
            }
        ...
        }

要创建一个DatagramPacket保存消息的实例,它的构造函数需要一个字节数组来表示消息、它的长度以及客户端的地址和端口号。在下面的代码中,服务器的端口是9003。该send方法会将数据包发送到服务器:

            sendMessage = message.getBytes();
            DatagramPacket sendPacket = new DatagramPacket(
                sendMessage,sendMessage.length, 
                内网地址,9003);
            clientSocket.send(sendPacket);

要接收响应,会创建一个接收数据包,并以与receive在服务器中处理它的方式相同的方式使用该方法。此方法将阻塞,直到服务器响应,然后显示消息:

            字节[]接收消息=新字节[1024];
            DatagramPacket receivePacket = new DatagramPacket(
                    接收消息,接收消息。长度);
            clientSocket.receive(receivePacket);
            String receivedSentence = 
                新字符串(receivePacket.getData());
            System.out.println("从服务器接收到[" 
                + 收到的句子 + "]\n来自 "
                + receivePacket.getSocketAddress());

现在,让我们看看这些应用程序是如何工作的。

UDP 客户端/服务器正在运行

服务器首先启动。它将显示以下消息:

UDP 服务器启动

接下来,启动客户端应用程序。它将显示以下消息:

UDP 客户端启动

输入消息:

输入一条消息,例如以下内容:

输入留言:给你的早晨

服务器将显示它已收到消息,如下所示。您将看到几行空输出。这是用于保存消息的 1024 字节数组的内容。然后将消息回显给客户端:

从客户收到:[早上给你的顶部

...

]

来自:/127.0.0.1

在客户端,显示响应。在这个例子中,用户然后输入“quit”来终止应用程序:

从服务器收到[早上给你的头顶

...

]

来自 /127.0.0.1:9003

输入消息:退出

UDP 客户端终止

当我们发送和接收测试消息时,我们可以使用trim消息显示时的方法来简化消息的显示,如下所示。此代码可用于服务器端和客户端:

        System.out.println("从客户端收到:[" 
                + message.trim()
                + "]\n发件人:" + receivePacket.getAddress());

输出将更容易阅读,如下所示:

从客户那里收到:[早上的头条给你]

来自:/127.0.0.1

此客户端/服务器应用程序可以通过多种方式进行增强,包括使用线程,以使其能够更好地与多个客户端一起工作。此示例说明了用 Java 开发 UDP 客户端/服务器应用程序的基础知识。在下一节中,我们将看到通道如何支持 UDP

UDP 通道支持

DatagramChannel类提供了UDP额外的支撑。它可以支持非阻塞交换。本类是从派生,使多线程应用程序更容易类。我们将在第 7 章网络可扩展性中检查它的使用。 DatagramChannelSelectableChannel

所述DatagramSocket类结合的信道到一个端口。使用DatagramChannel这个类后,就不再直接使用了。使用类意味着,我们不必直接使用数据报包。相反,数据是使用类的实例传输的。此类提供了几种方便的方法来访问其数据。 ByteBuffer

为了演示DatagramChannel该类的使用,我们将开发一个回显服务器和客户端应用程序。服务器将等待来自客户端的消息,然后将其发送回客户端。

UDP 回显服务器应用程序

UDP 回显服务器应用程序声明遵循并使用端口9000。在该main方法中,try-with-resources 块打开通道并创建套接字。本DatagramChannel类不具有公共构造函数。要创建通道,我们使用open方法,该方法返回DatagramChannel类的实例。通道的socket方法DatagramSocket为通道创建一个实例:

公共类 UDPEchoServer {
 
    公共静态无效主(字符串 [] args){
        int 端口 = 9000;
        System.out.println("UDP Echo Server 启动");
        试试 (DatagramChannel channel = DatagramChannel.open();
            DatagramSocket socket = channel.socket();){
                ...
            }
        }
        捕获(IOException ex){
            // 处理异常
        }
        System.out.println("UDP Echo Server 终止");
    }
}

创建后,我们需要将其与端口相关联。这首先通过创建SocketAddress类的实例来完成,该实例表示套接字地址。本InetSocketAddress类是从派生SocketAddress类并实现一个IP地址。它在以下代码序列中的使用会将其与 port 相关联9000。本DatagramSocket类的bind方法绑这个地址套接字:

            SocketAddress 地址 = 新的 InetSocketAddress(port);
            套接字绑定(地址);

ByteBuffer类是中央的使用数据报通道。我们在讨论其创作第3章NIO支持网络。在下一条语句中,将使用该allocateDirect方法创建此类的一个实例。此方法将尝试直接在缓冲区上使用本机操作系统支持。这比使用数据报包方法更有效。在这里,我们创建了一个尽可能大的缓冲区:

            ByteBuffer 缓冲区 = ByteBuffer.allocateDirect(65507);

添加后面的无限循环,它将接收来自客户端的消息,显示该消息,然后将其发送回:

            而(真){
                // 获取消息
                // 显示消息
                // 返回消息
            }

receive方法应用于通道以获取客户端的消息。它将阻塞,直到收到消息。它的唯一参数是用于保存传入数据的字节缓冲区。如果消息超过缓冲区的大小,则额外的字节将被悄悄丢弃。

flip方法使缓冲区能够被处理。它将缓冲区的限制设置为缓冲区中的当前位置,然后将位置设置为0。后续的 get 类型方法将在缓冲区的开头开始:

        SocketAddress client = channel.receive(buffer);
        缓冲。翻转();

虽然对于回显服务器不是必需的,但接收到的消息会显示在服务器上。这使我们能够验证消息是否已收到,并建议如何修改消息以做更多的事情,而不仅仅是简单地回显消息。

为了显示消息,我们需要使用get方法来获取每个字节,然后将其转换为适当的类型。回显服务器旨在回显简单的字符串。因此,字节需要在显示之前转换为字符。

但是,该get方法会修改缓冲区中的当前位置。在将消息发送回客户端之前,我们需要将位置恢复到其原始状态。缓冲区markreset方法用于此目的。

所有这些都在以下代码序列中执行。该mark方法在当前位置设置标记。一个StringBuilder实例用于重建,是由客户端发送的字符串。缓冲区的hasRemaining方法控制 while 循环。显示消息并且该reset方法将位置恢复到先前标记的值:

        缓冲区。标记();
        System.out.print("收到:[");
        StringBuilder message = new StringBuilder();
        而 (buffer.hasRemaining()) {
            message.append((char) buffer.get());
        }
        System.out.println(message + "]");
        缓冲。重置();

最后一步是将字节缓冲区发送回客户端。该send方法执行此操作。将显示一条消息,指示消息已发送,然后是clear方法。使用此方法是因为我们已经完成了缓冲区。它将位置设置为 0,将缓冲区的限制设置为其容量,并丢弃标记:

        通道发送(缓冲区,客户端);
        System.out.println("发送:[" + message + "]");
        缓冲区清除();

当服务器启动时,我们会看到一条这样的消息,如下所示:

UDP Echo 服务器启动

我们现在准备看看客户端是如何实现的。

UDP 回显客户端应用程序

在执行UDP回应客户端简单,使用下列步骤操作:

  • 与回显服务器的连接已建立
  • 创建一个字节缓冲区来保存消息
  • 缓冲区被发送到服务器
  • 客户端阻塞,直到消息被发回

客户端的实现细节与服务器的类似。我们从应用程序的声明开始,如下所示:

公共类 UDPEchoClient {
    
    公共静态无效主(字符串 [] args){
        System.out.println("UDP Echo 客户端启动");
        尝试 {
            ...
        }
        捕获(IOException ex){
            // 处理异常
        }
        System.out.println("UDP Echo 客户端终止");
    }
}

在服务器中,单参数InetSocketAddress构造函数将端口9000与当前 IP 地址相关联。在客户端中,我们需要指定服务器的 IP 地址以及端口。否则,它将无法确定将消息发送到何处。这是使用类的双参数构造函数在以下语句中127.0.0.1完成的。我们使用地址 ,假设客户端和服务器在同一台机器上:

        SocketAddress 远程 = 
            新的 InetSocketAddress("127.0.0.1", 9000);

然后使用该open方法创建通道并使用该方法连接到套接字地址connect

        DatagramChannel channel = DatagramChannel.open();
        通道连接(远程);

在下一个代码序列中,创建消息字符串,并分配字节缓冲区。缓冲区的大小设置为字符串的长度。put然后该方法将消息分配给缓冲区。由于该put方法需要一个字节数组,因此我们使用String该类的getBytes方法来获取与消息内容对应的字节数组:

        String message = "消息";
        ByteBuffer 缓冲区 = ByteBuffer.allocate(message.length());
        buffer.put(message.getBytes());

在我们将缓冲区发送到服务器之前,flip调用该方法。它会将限制设置为当前位置并将位置设置为 0。因此,当服务器接收到它时,可以对其进行处理:

        缓冲。翻转();

要将消息发送到服务器,将write调用通道的方法,如下所示。这会将底层数据包直接发送到服务器。但是,此方法仅在通道的套接字已连接时才有效,这是之前实现的:

        通道写(缓冲区);
        System.out.println("发送:[" + message + "]");

接下来,缓冲区被清除,允许我们重用缓冲区。该read方法将接收缓冲区,并将使用与服务器中使用的相同的进程显示缓冲区:

        缓冲区清除();
        通道读取(缓冲区);
        缓冲。翻转();
        System.out.print("收到:[");
        while(buffer.hasRemaining()) {
            System.out.print((char)buffer.get());
        }
        System.out.println("]");

我们现在准备好将客户端与服务器结合使用。

UDP 回显客户端/服务器在运行

需要先启动服务器。我们将看到初始服务器消息,如下所示:

UDP Echo 服务器启动

接下来,启动客户端。将显示以下输出,显示客户端发送消息,然后显示返回的消息:

UDP Echo 客户端启动

发送:【消息】

收到:【消息】

UDP Echo 客户端终止

在服务器端,我们将看到消息被接收然后被发送回客户端:

收到:【消息】

发送:【消息】

使用DatagramChannel该类可以使 UDP 通信更快。

UDP多播

多播是同时向多个客户端发送消息的过程。每个客户端都会收到相同的消息。为了参与这个过程,客户端需要加入一个多播组。当一个消息被发送时,它的目的地址表明它是一个多播消息。组播组是动态的,客户端随时进入和离开组。

多播是旧的 IPv4 CLASS D 空间,224.0.0.0通过239.255.255.255. IPv4 多播地址空间注册表列出了多播地址分配,可在http://www.iana.org/assignments/multicast-addresses/multicast-addresses.xml 中找到。IP 多播主机扩展文档位于http://tools.ietf.org/html/rfc1112。它定义了支持多播的实现要求。

UDP 多播服务器

接下来声明服务器应用程序。这个服务器是一个时间服务器,它会每秒广播当前的数据和时间。这是多播消息的一个很好的用途,因为可能有多个客户端对相同的信息感兴趣,并且可靠性不是问题。try 块将在发生异常时进行处理:

公共类 UDPMulticastServer {
 
    公共 UDPMulticastServer() {
        System.out.println("UDP 多播时间服务器启动");
        尝试 {
            ...
        } catch (IOException | InterruptedException ex) {
            // 处理异常
        }
        System.out.println(
            "UDP 多播时间服务器终止");
    }
    
    公共静态无效主(字符串参数[]){
        新的 UDPMulticastServer();
    }
}

该实例MulticastSocket中有需要的类一起InetAddress实例保存的组播IP地址。在此示例中,地址228.5.6.7代表多播组。该joinGroup方法用于加入该组播组,如下所示:

    MulticastSocket multicastSocket = new MulticastSocket();
    InetAddress inetAddress = InetAddress.getByName("228.5.6.7");
    多播Socket.joinGroup(inetAddress);

为了发送消息,我们需要一个字节数组来保存消息和一个数据包。这些声明如下所示:

    字节[]数据;
    DatagramPacket 数据包;

服务器应用程序将使用无限循环每秒广播一个新的日期和时间。线程暂停一秒钟,然后使用Data该类创建新的日期和时间。该DatagramPacket实例使用该信息来创建。端口9877是为此服务器分配的,客户端需要知道该端口。该send方法将数据包发送给感兴趣的客户端:

    而(真){
        线程睡眠(1000);
        String message = (new Date()).toString();
        System.out.println("发送:[" + message + "]");
        数据 = message.getBytes();
        packet = new DatagramPacket(data, message.length(), 
                内网地址,9877);
        多播Socket.send(数据包);
    }

接下来讨论客户端应用程序。

UDP 多播客户端

此应用程序将加入由地址定义的多播组228.5.6.7。它将阻塞,直到收到一条消息,然后才会显示该消息。该应用程序定义如下:

公共类 UDPMulticastClient {
 
    公共 UDPMulticastClient() {
        System.out.println("UDP 多播时间客户端启动");
        尝试 {
            ...
        } catch (IOException ex) {
            ex.printStackTrace();
        }
        
        System.out.println(
            "UDP 多播时间客户端终止");
    }
    
    公共静态无效主(字符串 [] args){
        新的 UDPMulticastClient();
    }
}

MulticastSocket使用端口号创建类的实例9877。这是必需的,以便它可以连接到 UDP 多播服务器。InetAddress使用 的多播地址创建一个实例228.5.6.7。然后客户端使用该joinGroup 方法加入多播组。

    MulticastSocket multicastSocket = new MulticastSocket(9877);
    InetAddress inetAddress = InetAddress.getByName("228.5.6.7");
    多播Socket.joinGroup(inetAddress);

DatagramPacket需要一个实例来接收发送到客户端的消息。一个字节数组被创建并用于实例化这个数据包,如下所示:

    字节[]数据=新字节[256];
    DatagramPacket packet = new DatagramPacket(data, data.length);

客户端应用程序然后进入一个无限循环,在那里它阻塞在receive方法上,直到服务器发送消息。消息到达后,将显示消息:

    而(真){
        多播Socket.receive(数据包);
        字符串消息 = 新字符串(
            packet.getData(), 0, packet.getLength());
        System.out.println("消息来自:" + packet.getAddress() 
            + " 消息:[" + 消息 + "]");
    }

接下来,我们将演示客户端和服务器如何交互。

运行中的 UDP 多播客户端/服务器

启动服务器。服务器的输出将与以下类似,但日期和时间会有所不同:

UDP 多播时间服务器启动

发送: [Sat Sep 19 13:48:42 CDT 2015]

发送: [Sat Sep 19 13:48:43 CDT 2015]

发送: [Sat Sep 19 13:48:44 CDT 2015]

发送: [Sat Sep 19 13:48:45 CDT 2015]

发送:[Sat Sep 19 13:48:46 CDT 2015]

发送: [Sat Sep 19 13:48:47 CDT 2015]

...

接下来,启动客户端应用程序。它将开始接收类似于以下内容的消息:

UDP 多播时间客户端启动

消息来自:/192.168.1.7 消息:[Sat Sep 19 13:48:44 CDT 2015]

消息来自:/192.168.1.7 消息:[Sat Sep 19 13:48:45 CDT 2015]

消息来自:/192.168.1.7 消息:[Sat Sep 19 13:48:46 CDT 2015]

...

笔记

如果程序在 Mac 上执行,则可能是通过套接字异常。如果发生这种情况,请使用该-Djava.net.preferIPv4Stack=true VM选项。

如果启动后续客户端,每个客户端都会收到相同系列的服务器消息。

带通道的 UDP 多播

我们还可以使用频道进行DatagramChannel多播。我们将使用 IPv6 来演示这个过程。该过程与我们之前使用该类的过程类似,只是我们需要使用一个多播组。为此,我们需要知道哪些网络接口可用。在我们讨论使用通道进行多播的细节之前,我们将演示如何获取机器的网络接口列表。

所述NetworkInterface类表示一个网络接口。这个类是在讨论第2章网络寻址。以下是该章中演示的方法的变体。它已被增强以显示特定接口是否支持多播,如下所示:

        尝试 {
            枚举<NetworkInterface> networkInterfaces;
            网络接口 = 
                网络接口.getNetworkInterfaces();
            对于(网络接口网络接口: 
                    Collections.list(networkInterfaces)) {
                显示网络接口信息(
                    网络接口);
            }
        } catch (SocketException ex) {
            // 处理异常
        }

displayNetworkInterfaceInformation方法如下所示。这种方法改编自https://docs.oracle.com/javase/tutorial/networking/nifs/listing.html

    static void displayNetworkInterfaceInformation(
            网络接口网络接口){
        尝试 {
            System.out.printf("显示名称:%s\n", 
                networkInterface.getDisplayName());
            System.out.printf("名称:%s\n", 
                networkInterface.getName());
            System.out.printf("支持多播:%s\n", 
                networkInterface.supportsMulticast());
            枚举<InetAddress> inetAddresses = 
                networkInterface.getInetAddresses();
            对于 (InetAddress inetAddress : 
                    Collections.list(inetAddresses)) {
                System.out.printf("Inet 地址:%s\n", 
                    inet地址);
            }
            System.out.println();
        } catch (SocketException ex) {
            // 处理异常
        }
    }

执行此示例时,您将获得类似于以下内容的输出:

显示名称:软件环回接口 1

姓名:罗

支持组播:true

Inet地址:/127.0.0.1

Inet地址:/0:0:0:0:0:0:0:1

显示名称:Microsoft 内核调试网络适配器

名称:eth0

支持组播:true

显示名称:Realtek PCIe FE 系列控制器

名称:eth1

支持组播:true

Inet地址:/fe80:0:0:0:91d0:8e19:31f1:cb2d%eth1

显示名称:Realtek RTL8188EE 802.11 b/g/n Wi-Fi Adapter

名称: wlan0

支持组播:true

Inet 地址:/192.168.1.7

Inet 地址:/2002:42be:6659:0:0:0:0:1001

Inet地址:/fe80:0:0:0:9cdb:371f:d3e9:4e2e%wlan0

...

对于我们的客户端/服务器,我们将使用该eth0接口。您需要选择一个最适合您的平台。例如,在 Mac 上,这可能是en0awdl0

UDP 通道组播服务器

UDP 通道多播服务器将:

  • 设置频道和组播组
  • 创建一个包含消息的缓冲区
  • 使用无限循环发送和显示群消息

服务器的定义如下:

公共类 UDPDatagramMulticastServer {
 
    公共静态无效主(字符串 [] args){
        尝试 {
            ...
            }
        } catch (IOException | InterruptedException ex) {
            // 处理异常
        }
    }
 
}

第一个任务使用System类的setProperty方法来指定使用 IPv6DatagramChannel然后创建一个实例,并创建eth0网络接口。该setOption方法将通道与用于标识组的网络接口相关联。该组由InetSocketAddress使用 IPv6 节点本地范围多播地址的实例表示,如下所示。有关IPv6 多播地址空间注册文档的更多详细信息,请访问 http://www.iana.org/assignments/ipv6-multicast-addresses/ipv6-multicast-addresses.xhtml

            System.setProperty(
                "java.net.preferIPv6Stack", "true");
            DatagramChannel channel = DatagramChannel.open();
            网络接口 networkInterface = 
                NetworkInterface.getByName("eth0");
            channel.setOption(StandardSocketOptions.
                IP_MULTICAST_IF, 
                网络接口);
            InetSocketAddress 组 = 
                new InetSocketAddress("FF01:0:0:0:0:0:0:FC", 
                        9003);

然后根据消息字符串创建字节缓冲区。缓冲区的大小设置为字符串的长度,并使用putgetBytes方法的组合进行分配:

            String message = "消息";
            ByteBuffer 缓冲区 = 
                ByteBuffer.allocate(message.length());
            buffer.put(message.getBytes());

while 循环中,缓冲区被发送给组成员。为了清楚地看到发送的内容,缓冲区的内容使用在UDP 回显服务器应用程序部分中使用的相同代码显示。缓冲区被重置,以便它可以再次使用。应用程序暂停一秒钟以避免此示例中的过多消息:

            而(真){
                通道发送(缓冲区,组);
                System.out.println("发送多播消息:" 
                    + 消息);
                缓冲区清除();
 
                缓冲区。标记();
                System.out.print("发送:[");
                StringBuilder msg = new StringBuilder();
                而 (buffer.hasRemaining()) {
                    msg.append((char) buffer.get());
                }
                System.out.println(msg + "]");
                缓冲。重置();
 
                线程睡眠(1000);
        }

我们现在已经为客户端应用程序做好了准备。

UDP 通道组播客户端

UDP 通道组播客户端将加入组,接收消息,显示它,然后终止。正如我们将看到的,MembershipKey该类表示多播组的成员资格。

该应用程序声明如下。首先,我们指定要使用 IPv6。然后声明网络接口,这与服务器使用的接口相同:

公共类 UDPDatagramMulticastClient {
    public static void main(String[] args) 抛出异常 {
        System.setProperty("java.net.preferIPv6Stack", "true");
        网络接口 networkInterface = 
            NetworkInterface.getByName("eth0");
        ...
    }
}

DatagramChannel接下来被创建的实例。通道绑定到端口9003并与网络接口实例相关联:

        DatagramChannel 通道 = DatagramChannel.open()
                .bind(new InetSocketAddress(9003))
                .setOption(StandardSocketOptions.IP_MULTICAST_IF, 
                    网络接口);

然后根据服务器使用的相同 IPv6 地址创建组,并MembershipKey使用通道的join方法创建一个实例,如下所示。显示密钥和等待消息以说明客户端的工作方式:

        InetAddress 组 = 
            InetAddress.getByName("FF01:0:0:0:0:0:0:FC");
        MembershipKey key = channel.join(group, networkInterface);
        System.out.println("加入的组播组:" + key);
        System.out.println("等待消息...");

创建大小为 的字节缓冲区1024。这个大小对于这个例子就足够了,receive然后调用该方法,该方法将阻塞,直到收到消息:

        ByteBuffer 缓冲区 = ByteBuffer.allocate(1024);
        通道接收(缓冲区);

要显示缓冲区的内容,我们需要翻转它。内容像我们之前一样显示:

        缓冲。翻转();
        System.out.print("收到:[");
        StringBuilder message = new StringBuilder();
        而 (buffer.hasRemaining()) {
            message.append((char) buffer.get());
        }
        System.out.println(message + "]");

当我们使用完成员键后,我们应该表明我们不再对使用以下drop方法接收组消息感兴趣:

        键.drop();

如果有数据包等待套接字处理,消息可能仍会到达。

UDP 通道多播客户端/服务器在运行

首先启动服务器。该服务器将每秒显示一系列消息,如下所示:

发送多播消息:消息

发送:【消息】

发送多播消息:消息

发送:【消息】

发送多播消息:消息

发送:【消息】

...

接下来,启动客户端应用程序。它将显示多播组,等待消息,然后显示消息,如下所示:

加入的组播组:<ff01:0:0:0:0:0:0:fc,eth1>

等待消息...

收到:【消息】

使用通道可以提高UDP 多播消息的性能。

UDP

使用 UDP 流式传输音频或视频很常见。它是高效的,任何数据包丢失或无序数据包都会导致最小的问题。我们将通过直播音频来说明这种技术。UDP 服务器将捕获麦克风的声音并将其发送给客户端。UDP 客户端将接收音频并在系统的扬声器上播放。

UDP 流服务器的想法是将流分解为一系列发送到 UDP 客户端的数据包。客户端然后将接收这些数据包并使用它们来重构流。

为了说明流式音频,我们需要了解一些关于 Java 如何处理音频流的知识。音频由包中的一系列类处理javax.sound.sampled。用于捕获和播放音频的主要类包括:

  • AudioFormat:此类指定所使用的音频格式的特征。由于有多种音频格式可用,系统需要知道正在使用的是哪一种。
  • AudioInputStream:这个类代表正在录制或播放的音频。
  • AudioSystem:此类提供对系统音频设备和资源的访问。
  • DataLine:此接口控制对流应用的操作,例如启动和停止流。
  • SourceDataLine:这代表声音的目的地,例如扬声器。
  • TargetDataLine:这代表声音的来源,例如麦克风。

用于SourceDataLineTargetDataLine接口的术语可能有点混乱。这些术语是从一条线和一个混合器的角度来看的。

UDP 音频服务器实现

类的声明如下。它使用一个实例作为音频源。它被声明为一个实例变量,因为它在多个方法中使用。构造函数使用一个方法来初始化音频和一个方法来将此音频发送到客户端: AudioUDPServerTargetDataLinesetupAudiobroadcastAudio

公共类 AudioUDPServer {
    私有最终字节 audioBuffer[] = 新字节 [10000];
    私有目标数据线目标数据线;
 
    公共音频UDP服务器(){
        设置音频();
        广播音频();
    }
    ...
    公共静态无效主(字符串 [] args){
        新的音频UDP服务器();
    }
}

下面是方法,在服务器端和客户端都使用它来指定音频流特性。模拟音频信号每秒采样 1,600 次。每个样本都是一个有符号的 16 位数字。该变量分配,这意味着音频是单声道。样本中的字节顺序很重要,并设置为大端: getAudioFormatchannels1

    私人音频格式 getAudioFormat() {
        浮动采样率 = 16000F;
        int sampleSizeInBits = 16;
        整数通道 = 1;
        布尔符号 = 真;
        boolean bigEndian = false;
        返回新的 AudioFormat(sampleRate, sampleSizeInBits, 
            频道,签名,bigEndian);
    }

Big endian little endian 是指字节的顺序。大端意味着一个字的最高有效字节存储在最小的内存地址,最低有效字节存储在最大的内存地址。小端颠倒这个顺序。不同的计算机体系结构使用不同的顺序。

setupAudio方法初始化音频。本DataLine.Info类使用音频格式信息来创建表示音频的线路。本AudioSystem类的getLine方法返回一个数据线对应于麦克风。线路打开并启动:

    私有无效 setupAudio() {
        尝试 {
            AudioFormat audioFormat = getAudioFormat();
            DataLine.Info dataLineInfo = 
                new DataLine.Info(TargetDataLine.class, 
                        音频格式);
            目标数据线 = (目标数据线) 
                AudioSystem.getLine(dataLineInfo);
            targetDataLine.open(audioFormat);
            targetDataLine.start();
        } 捕捉(异常前){
            ex.printStackTrace();
            System.exit(0);
        }
    }

broadcastAudio方法创建 UDP 数据包。使用端口创建套接字8000InetAddress为当前机器创建实例:

    私有无效广播音频(){
        尝试 {
            DatagramSocket socket = new DatagramSocket(8000);
            InetAddress inetAddress = 
                InetAddress.getByName("127.0.0.1");
            ...
        } 捕捉(异常前){
            // 处理异常
        }
    }

进入无限循环,其中read方法填充audioBuffer数组并返回读取的字节数。对于大于 的计数0,将使用缓冲区创建一个新数据包并将其发送到侦听端口的客户端9786

    而(真){
        int count = targetDataLine.read(
            audioBuffer, 0, audioBuffer.length);
        如果(计数> 0){
            DatagramPacket 数据包 = 新的 DatagramPacket(
            音频缓冲区,audioBuffer.length,inetAddress,9786);
            套接字。发送(数据包);
        }
    }

执行时,来自麦克风的声音以一系列数据包的形式发送到客户端。

UDP 音频客户端实现

AudioUDPClient接下来声明该应用程序。在构造函数中,initiateAudio调用了一个方法来启动从服务器接收数据包的过程:

公共类 AudioUDPClient {
    音频输入流音频输入流;
    SourceDataLine sourceDataLine;
    ...
    公共音频UDP客户端(){
        启动音频();
    }
 
    公共静态无效主(字符串 [] args){
        新的 AudioUDPClient();
    }
}

initiateAudio方法创建一个绑定到 port 的套接字9786。创建一个字节数组来保存 UDP 数据包中包含的音频数据:

    私有无效启动音频(){
        尝试 {
            DatagramSocket socket = new DatagramSocket(9786);
            字节 [] 音频缓冲区 = 新字节 [10000];
            ...
        } 捕获(异常 e){
            e.printStackTrace();
        }
    }

一个无限循环将接收来自服务器的数据包,创建一个AudioInputStream实例,然后调用该playAudio方法播放声音。数据包在以下代码中创建,然后阻塞直到收到数据包:

    而(真){
        DatagramPacket 数据包
            = new DatagramPacket(audioBuffer, audioBuffer.length);
        socket.receive(数据包);
        ...
    }

接下来,创建音频流。从数据包中提取字节数组。它用作ByteArrayInputStream构造函数的参数,与音频格式信息一起用于创建实际的音频流。这与SourceDataLine打开和启动的实例相关联。playAudio调用该方法播放声音:

        尝试 {
            字节音频数据[] = packet.getData();
            InputStream byteInputStream = 
                新的 ByteArrayInputStream(audioData);
            AudioFormat audioFormat = getAudioFormat();
            音频输入流 = 新音频输入流(
                字节输入流, 
                audioFormat, audioData.length / 
                audioFormat.getFrameSize());
            DataLine.Info dataLineInfo = new DataLine.Info(
                SourceDataLine.class, audioFormat);
            sourceDataLine = (SourceDataLine) 
                AudioSystem.getLine(dataLineInfo);
            sourceDataLine.open(audioFormat);
            sourceDataLine.start();
            播放音频();
        } 捕获(异常 e){
            // 处理异常
        }

使用的getAudioFormat方法与AudioUDPServer应用程序中声明的方法相同。该playAudio方法如下。填充readAudioInputStream缓冲区的方法,该缓冲区被写入源数据行。这有效地在系统的扬声器上播放声音:

    私人无效播放音频(){
        字节[]缓冲区=新字节[10000];
        尝试 {
            整数计数;
            while ((count = audioInputStream.read(
                   缓冲区, 0, 缓冲区.长度)) != -1) {
                如果(计数> 0){
                    sourceDataLine.write(buffer, 0, count);
                }
            }
        } 捕获(异常 e){
            // 处理异常
        }
    }

在服务器运行时,启动客户端将播放来自服务器的声音。可以通过在服务器和客户端中使用线程来处理声音的录制和播放来增强播放效果。为了简化示例,已省略此细节。

在这个例子中,连续的模拟声音被数字化并分解成数据包。这些数据包然后被发送到客户端,在那里它们被转换回声音并播放。

在其他几个框架中还提供了对 UDP 流的额外支持。在Java媒体框架JMF)(http://www.oracle.com/technetwork/articles/javase/index-jsp-140239.html)支持音频和视频媒体的处理。该实时传输协议RTP)(https://en.wikipedia.org/wiki/Real-time_Transport_Protocol)用于在网络上发送音频和视频数据。

概括

在本章中,我们研究了 UDP 协议的性质以及 Java 如何支持它。我们比较了 TCP UDP,以提供一些指导,以确定哪种协议最适合给定的问题。

我们从一个简单的 UDP 客户端/服务器开始来演示如何使用DatagramPacketDatagramSocket类。我们看到了如何使用InetAddress该类来获取套接字和数据包使用的地址。

DatagramChannel类支持使用NIO技术在UDP的环境中,其可以是比使用更高效的DatagramPacketDatagramSocket的方法。该方法使用字节缓冲区来保存在服务器和客户端之间发送的消息。这个例子说明许多在被开发的技术第3章NIO支持网络

随后讨论了 UDP 多播的工作原理。这提供了一种向组成员广播消息的简单技术。说明了MulticastSocketDatagramChannelMembershipKey类的使用。后一个类用于在使用DatagramChannel类时建立一个组。

我们以一个如何使用 UDP 来支持音频流的例子结束。我们详细介绍了javax.sound.sampled包中几个类的使用,包括用于收集和播放音频的AudioFormatTargetDataLine类。我们使用DatagramSocketDatagramPacket类来传输音频。

在下一章中,我们将研究可用于提高客户端/服务器应用程序可伸缩性的技术。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
QT以太网通信UDP是一种基于用户数据报协议(UDP)的通信方式。在QT中,可以使用QUdpSocket类来实现UDP通信。通过使用QUdpSocket类的相关方法和信号槽机制,可以实现在不同主机之间进行数据的传输和通信。 在QT中,可以通过引入network模块并在.pro文件中加入相应的代码来启用UDP通信功能。使用QUdpSocket类可以创建一个UDP套接字,其中目标端口使用QSpinBox组件,目标IP使用QLineEdit组件,接收框使用PlainTextEdit组件。通过绑定相应的槽函数,可以实现当套接字准备好读取数据时触发相应的事件。可以使用on_pushButton_clicked()槽函数来实现点击按钮时发送数据的功能。通过getLocalIP()函数可以获取本机的IP地址。 对于UDP通信的具体实现代码,请参考文末的代码链接。 需要注意的是,为了保证UDP通信的成功,需要确保IP地址和端口号的正确设置,并避免与其他程序或设备占用相同的端口号。在单机测试时,可以开启两个QT应用,一个用于发送数据,一个用于接收数据。在多机使用时,需要保证发送端和接收端处于同一网段,并使用接收端的IP地址进行设置。 总结起来,QT以太网通信UDP是一种基于UDP协议的通信方式,通过QUdpSocket类和相关的Qt组件可以实现在不同主机之间进行数据的传输和通信。具体的实现代码请参考文末提供的链接。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [QT下的UDP通信](https://blog.csdn.net/weixin_42454651/article/details/127137758)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值