用户数据报协议(UDP)位于 IP 之上,为 TCP 提供了不可靠的对应物。UDP 在网络中的两个节点之间发送单个数据包。UDP 数据包不知道其他数据包,也不能保证数据包将实际到达其预期目的地。当发送多个数据包时,无法保证到达顺序。UDP 消息只是简单地发送,然后被忘记,因为没有从收件人发送确认。
UDP 是一种无连接协议。两个节点之间没有消息交换来促进数据包传输。没有维护有关连接的状态信息。
UDP 适用于需要高效交付且无需保证交付的服务。例如用于域名系统****DNS服务、网络时间协议NTP服务、IP 语音****VOIP、P2P 网络的网络通信协调等,视频流媒体。如果视频帧丢失,如果丢失不频繁,观众可能永远不会注意到它。
有几种协议使用 UDP,包括:
- 实时流协议(RTSP):此协议用于控制媒体的流
- 路由信息协议(RIP):该协议确定用于传输数据包的路由
- 域名系统(DNS):此协议查找互联网域名并返回其 IP 地址
- 网络时间协议(NTP):该协议通过互联网同步时钟
UDP 数据包由 IP 地址和端口号组成,用于标识其目的地。UDP 数据包具有固定的大小,可以大到 65353 字节。但是,每个数据包使用至少 20 个字节作为 IP 报头,8 个字节作为 UDP 报头,将消息大小限制为 65507 个字节。如果消息大于此值,则需要发送多个数据包。
UDP 数据包也可以是多播的。这意味着数据包被发送到属于 UDP 组的每个节点。这是一种向多个节点发送信息的有效方法,而无需明确针对每个节点。相反,数据包被发送到其成员负责捕获其组数据包的组。
在本章中,我们将说明如何使用 UDP 协议:
- 支持传统的客户端/服务器模型
- 使用 NIO 通道执行 UDP 操作
- 向组成员发送多播数据包
- 将音频或视频等流媒体传送到客户端
我们将从概述 Java 对 UDP 的支持开始,并提供更多 UDP 协议的详细信息。
Java 使用DatagramSocket
类在节点之间形成套接字连接。DatagramPacket
类表示一个数据包。简单的发送和接收方法将通过网络传输数据包。
UDP 使用 IP 地址和端口号来标识节点。UDP 端口号的范围从0
到65535
。端口号分为三种类型:
- 众所周知的端口(
0
到1023
:这些是用于相对常见的服务的端口号。 - 注册端口(
1024
到49151
:这些是 IANA 分配给进程的端口号。 - 动态/专用端口(
49152
到65535
:当连接启动时,这些端口被动态分配给客户端。这些通常是临时的,不能由 IANA 分配。
下表是 UDP 特定端口分配的简短列表。它们说明了 UDP 是如何广泛用于支持多种应用程序和服务的。更完整的 TCP/UDP 端口编号列表见中的https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers :
|已知端口(0 到 1023)
|
用法
|
| --- | --- |
| 7
| 这是回送协议 |
| 9
| 这意味着在局域网上唤醒 |
| 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
| 用于 CiscoVLAN 查询协议(VQP)/VMPS |
| 2190
| 这用于 TiVoConnect 信标 |
| 2302
| 这是用于光环:战斗进化多人游戏 |
| 3000
| 这用于 BitTorrent 同步 |
| 4500
| 这用于 IPSec NAT 遍历 |
| 5353
| 用于组播 DNS(mDNS) |
| 9110
| 这用于 SSMP 消息协议 |
| 27500
至27900
| 这是用于 id 软件的地震世界 |
| 29900
至29901
| 这是用于任天堂 Wi-Fi 连接的 |
| 36963
| 这是用于虚拟软件多人游戏 |
TCP 和 UDP 之间有几个区别。这些差异包括:
- 可靠性:TCP 比 UDP 更可靠
- 排序:TCP 保证包传输的顺序将被保留
- 头大小:UDP 头比 TCP 头小
- 速度:UDP 比 TCP 快
当使用 TCP 发送数据包时,保证数据包到达。如果丢失,则会重新发送。UDP 不提供这种保证。如果数据包未到达,则不会重新发送。
TCP 保留数据包的发送顺序,而 UDP 不保留。如果 TCP 数据包到达目的地的顺序与发送顺序不同,TCP 将按照原始顺序重新组装数据包。使用 UDP 时,不会保留此顺序。
创建数据包时,附加报头信息以协助数据包的传递。对于 UDP,标头由 8 个字节组成。TCP 头的通常大小为 32 字节。
由于具有较小的报头大小和确保可靠性所需的开销,UDP 比 TCP 更高效。此外,创建连接所需的工作更少。这种效率使得流媒体成为更好的选择。
让我们从 UDP 示例开始,了解如何支持传统的客户端/服务器体系结构。
UDP 客户端/服务器应用程序的结构与 TCP 客户端/服务器应用程序的结构相似。在服务器端,创建一个 UDP 服务器套接字,等待客户端请求。客户端将创建相应的 UDP 套接字,并使用它向服务器发送消息。然后,服务器可以处理请求并发回响应。
UDP 客户端/服务器将使用DatagramSocket
类作为套接字,并使用DatagramPacket
保存消息。消息的内容类型没有限制。在我们的示例中,我们将使用文本消息。
接下来定义我们的服务器。构造器将执行服务器的工作:
public class UDPServer {
public UDPServer() {
System.out.println("UDP Server Started");
...
System.out.println("UDP Server Terminating");
}
public static void main(String[] args) {
new UDPServer();
}
}
在构造函数的 try with resources 块中,我们创建了一个DatagramSocket
类的实例。我们将使用的几种方法可能会抛出一个IOException
异常,必要时将捕获该异常:
try (DatagramSocket serverSocket =
new DatagramSocket(9003)) {
...
}
} catch (IOException ex) {
//Handle exceptions
}
创建套接字的另一种方法是使用bind
方法,如下所示。DatagramSocket
实例是使用null
作为参数创建的。然后使用bind
方法分配端口:
DatagramSocket serverSocket = new DatagramSocket(null);
serverSocket.bind(new InetSocketAddress(9003));
这两种方法都将使用端口9003
创建一个DatagramSocket
实例。
发送消息的过程包括以下内容:
- 创建字节数组
- 创建一个
DatagramPacket
实例 - 使用
DatagramSocket
实例等待消息到达
流程被封装在一个循环中,如下所示,以允许处理多个请求。接收到的消息被简单地回显到客户端程序。DatagramPacket
实例是使用字节数组及其长度创建的。它被用作DatagramSocket
类的receive
方法的参数。数据包此时不包含任何信息。此方法将阻塞,直到发出请求,然后将填充数据包:
while (true) {
byte[] receiveMessage = new byte[1024];
DatagramPacket receivePacket = new DatagramPacket(
receiveMessage, receiveMessage.length);
serverSocket.receive(receivePacket);
...
}
当方法返回时,数据包被转换成字符串。如果发送了其他数据类型,则需要进行其他转换。然后显示发送的消息:
String message = new String(receivePacket.getData());
System.out.println("Received from client: [" + message
+ "]\nFrom: " + receivePacket.getAddress());
要发送响应,需要客户端的地址和端口号。分别使用getAddress
和getPort
方法,针对拥有此信息的数据包获取这些信息。我们将在讨论客户时看到这一点。还需要表示为字节数组的消息,getBytes
方法提供:
InetAddress inetAddress = receivePacket.getAddress();
int port = receivePacket.getPort();
byte[] sendMessage;
sendMessage = message.getBytes();
使用消息、消息长度、客户端地址和端口号创建一个新的DatagramPacket
实例。send
方法向客户端发送数据包:
DatagramPacket sendPacket =
new DatagramPacket(sendMessage,
sendMessage.length, inetAddress, port);
serverSocket.send(sendPacket);
定义了服务器之后,让我们检查一下客户端。
客户端应用程序将提示用户发送消息,然后将消息发送到服务器。它将等待响应,然后显示响应。声明如下:
class UDPClient {
public UDPClient() {
System.out.println("UDP Client Started");
...
}
System.out.println("UDP Client Terminating ");
}
public static void main(String args[]) {
new UDPClient();
}
}
Scanner
类支持获取用户输入。try with resources 块创建一个DatagramSocket
实例并处理异常:
Scanner scanner = new Scanner(System.in);
try (DatagramSocket clientSocket = new DatagramSocket()) {
...
}
clientSocket.close();
} catch (IOException ex) {
// Handle exceptions
}
使用getByName
方法访问客户端的当前地址,并声明对字节数组的引用。此地址将用于创建数据包:
InetAddress inetAddress =
InetAddress.getByName("localhost");
byte[] sendMessage;
无限循环用于提示用户输入消息。当用户输入“退出”时,应用程序将终止,如下所示:
while (true) {
System.out.print("Enter a message: ");
String message = scanner.nextLine();
if ("quit".equalsIgnoreCase(message)) {
break;
}
...
}
要创建包含消息的DatagramPacket
实例,其构造函数需要一个字节数组来表示消息、消息长度以及客户端地址和端口号。在下面的代码中,服务器的端口是9003
。send
方法将数据包发送到服务器:
sendMessage = message.getBytes();
DatagramPacket sendPacket = new DatagramPacket(
sendMessage, sendMessage.length,
inetAddress, 9003);
clientSocket.send(sendPacket);
为了接收响应,将创建一个接收数据包,并以与服务器中处理该数据包相同的方式使用receive
方法。此方法将一直阻止,直到服务器响应,然后显示消息:
byte[] receiveMessage = new byte[1024];
DatagramPacket receivePacket = new DatagramPacket(
receiveMessage, receiveMessage.length);
clientSocket.receive(receivePacket);
String receivedSentence =
new String(receivePacket.getData());
System.out.println("Received from server ["
+ receivedSentence + "]\nfrom "
+ receivePacket.getSocketAddress());
现在,让我们看看这些应用程序的工作情况。
首先启动服务器。它将显示以下消息:
UDP 服务器启动
接下来,启动客户端应用程序。它将显示以下消息:
UDP 客户端启动
输入消息:
输入一条消息,例如以下消息:
输入一条消息:祝您早上愉快
服务器将显示它已收到消息,如下所示。您将看到几个空的输出行。这是用于保存消息的 1024 字节数组的内容。然后将消息回显到客户端:
**从客户处收到:**您早上好]
。。。
**From:/127.0.0.1
在客户端,将显示响应。在此示例中,用户然后输入“退出”终止应用程序:
**从服务器【您的凌晨】**收到
。。。
**从/127.0.0.1:9003
输入消息:退出
UDP 客户端终止
当我们发送和接收测试消息时,我们可以在显示消息时使用trim
方法简化消息的显示,如下所示。此代码可在服务器端和客户端上使用:
System.out.println("Received from client: ["
+ message.trim()
+ "]\nFrom: " + receivePacket.getAddress());
输出将更易于阅读,如下所示:
从客户处收到:[早上好给你]
From:/127.0.0.1
此客户端/服务器应用程序可以通过多种方式进行增强,包括使用线程,以使其能够更好地处理多个客户端。此示例演示了用 Java 开发 UDP 客户端/服务器应用程序的基础知识。在下一节中,我们将了解通道如何支持 UDP。**** ****# 对 UDP 的通道支持
DatagramChannel
类提供了对 UDP 的额外支持。它可以支持非阻塞交换。DatagramChannel
类派生自SelectableChannel
类,使多线程应用程序更容易。我们将在第 7 章、网络可扩展性中探讨其使用。
DatagramSocket
类将通道绑定到端口。该类使用后,不再直接使用。使用DatagramChannel
类的方法,我们不必直接使用数据报数据包。相反,使用ByteBuffer
类的实例传输数据。此类提供了几种方便的方法来访问其数据。
为了演示DatagramChannel
类的使用,我们将开发一个 echo 服务器和客户端应用程序。服务器将等待来自客户端的消息,然后将其发送回客户端。
UDP echo 服务器应用程序声明遵循并使用端口9000
。在main
方法中,try with resources 块打开通道并创建套接字。DatagramChannel
类没有公共构造函数。为了创建一个通道,我们使用open
方法,它返回DatagramChannel
类的一个实例。通道的socket
方法为通道创建DatagramSocket
实例:
public class UDPEchoServer {
public static void main(String[] args) {
int port = 9000;
System.out.println("UDP Echo Server Started");
try (DatagramChannel channel = DatagramChannel.open();
DatagramSocket socket = channel.socket();){
...
}
}
catch (IOException ex) {
// Handle exceptions
}
System.out.println("UDP Echo Server Terminated");
}
}
创建后,我们需要将其与端口关联。首先创建一个表示套接字地址的SocketAddress
类实例。InetSocketAddress
类派生自SocketAddress
类并实现了一个 IP 地址。其在以下代码序列中的使用将与端口9000
关联。DatagramSocket
类的bind
方法将此地址绑定到套接字:
SocketAddress address = new InetSocketAddress(port);
socket.bind(address);
ByteBuffer
类是使用数据报通道的核心。我们在第 3 章、NIO 对网络的支持中讨论了它的创建。在下一条语句中,使用allocateDirect
方法创建该类的一个实例。此方法将尝试直接在缓冲区上使用本机操作系统支持。这可能比使用数据报包方法更有效。在这里,我们创建了一个具有最大可能大小的缓冲区:
ByteBuffer buffer = ByteBuffer.allocateDirect(65507);
添加下面的无限循环,它将从客户端接收消息,显示消息,然后将其发送回:
while (true) {
// Get message
// Display message
// Return message
}
receive
方法针对通道应用,以获取客户端消息。它将一直阻止,直到收到消息为止。它的单个参数是用于保存传入数据的字节缓冲区。如果消息超过缓冲区的大小,多余的字节将被悄悄地丢弃。
flip
方法允许处理缓冲区。它将缓冲区的限制设置为缓冲区中的当前位置,然后将该位置设置为0
。后续的 get type 方法将从缓冲区的开头开始:
SocketAddress client = channel.receive(buffer);
buffer.flip();
虽然对于 echo 服务器不是必需的,但接收到的消息会显示在服务器上。这使我们能够验证消息是否已收到,并建议如何修改消息,使其不只是简单地回显消息。
为了显示消息,我们需要使用get
方法获取每个字节,然后将其转换为适当的类型。echo 服务器用于回显简单字符串。因此,在显示字节之前,需要将其转换为字符。
但是,get
方法会修改缓冲区中的当前位置。在将消息发送回客户端之前,我们需要将位置恢复到其原始状态。缓冲器的mark
和reset
方法用于此目的。
所有这些都是按照以下代码顺序执行的。mark
方法将标记设置在当前位置。StringBuilder
实例用于重新创建客户端发送的字符串。缓冲区的hasRemaining
方法控制 while 循环。显示消息,reset
方法将位置恢复到先前标记的值:
buffer.mark();
System.out.print("Received: [");
StringBuilder message = new StringBuilder();
while (buffer.hasRemaining()) {
message.append((char) buffer.get());
}
System.out.println(message + "]");
buffer.reset();
最后一步是将字节缓冲区发送回客户端。send
方法就是这样做的。显示一条指示已发送消息的消息,然后显示clear
方法。之所以使用这种方法,是因为我们已经通过了缓冲区。它会将位置设置为 0,将缓冲区的限制设置为其容量,并丢弃标记:
channel.send(buffer, client);
System.out.println("Sent: [" + message + "]");
buffer.clear();
当服务器启动时,我们将看到一条这样的消息,如下所示:
UDP 回显服务器启动
我们现在准备好看看客户端是如何实现的。
UDP echo 客户端的实现非常简单,使用以下步骤:
- 已建立到 echo 服务器的连接
- 将创建一个字节缓冲区来保存消息
- 缓冲区被发送到服务器
- 客户端将阻塞,直到消息被发回
客户端的实现细节与服务器的类似。我们从应用程序声明开始,如下所示:
public class UDPEchoClient {
public static void main(String[] args) {
System.out.println("UDP Echo Client Started");
try {
...
}
catch (IOException ex) {
// Handle exceptions
}
System.out.println("UDP Echo Client Terminated");
}
}
在服务器中,单个参数InetSocketAddress
构造函数将端口9000
与当前 IP 地址关联。在客户端中,我们需要指定服务器的 IP 地址和端口。否则,它将无法确定将消息发送到何处。这在下面的语句中使用类的双参数构造函数完成。我们使用地址127.0.0.1
,假设客户端和服务器位于同一台机器上:
SocketAddress remote =
new InetSocketAddress("127.0.0.1", 9000);
然后使用open
方法创建通道,并使用connect
方法连接到套接字地址:
DatagramChannel channel = DatagramChannel.open();
channel.connect(remote);
在下一个代码序列中,将创建消息字符串,并分配字节缓冲区。缓冲区的大小设置为字符串的长度。然后,put
方法将消息分配给缓冲区。由于put
方法需要一个字节数组,所以我们使用String
类的getBytes
方法来获取对应于消息内容的字节数组:
String message = "The message";
ByteBuffer buffer = ByteBuffer.allocate(message.length());
buffer.put(message.getBytes());
在我们将缓冲区发送到服务器之前,调用flip
方法。它将限制设置为当前位置,并将位置设置为 0。因此,当服务器接收时,可以对其进行处理:
buffer.flip();
要将消息发送到服务器,将调用通道的write
方法,如下所示。这将把底层数据包直接发送到服务器。但是,此方法仅在连接了通道的插座时有效,这是以前实现的:
channel.write(buffer);
System.out.println("Sent: [" + message + "]");
接下来,清除缓冲区,允许我们重用缓冲区。read
方法将接收缓冲区,缓冲区将使用服务器中使用的相同过程显示:
buffer.clear();
channel.read(buffer);
buffer.flip();
System.out.print("Received: [");
while(buffer.hasRemaining()) {
System.out.print((char)buffer.get());
}
System.out.println("]");
我们现在已经准备好将客户端与服务器结合使用。
需要先启动服务器。我们将看到初始的服务器消息,如下所示:
UDP 回显服务器启动
接下来,启动客户端。将显示以下输出,显示发送消息的客户端,然后显示返回的消息:
UDP 回显客户端启动
已发送:【消息】
收到:【消息】
UDP 回送客户端终止
在服务器端,我们将看到消息被接收,然后被发送回客户端:
收到:【消息】
已发送:【消息】
使用DatagramChannel
类可以加快 UDP 通信速度。
多播是同时向多个客户端发送消息的过程。每个客户端将收到相同的消息。为了参与这个过程,客户端需要加入一个多播组。发送消息时,其目标地址表示它是多播消息。多播组是动态的,客户端可以随时进出该组。
多播是旧的 IPv4 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 。它定义了支持多播的实现需求。
接下来声明服务器应用程序。此服务器是一个时间服务器,将每秒广播当前数据和时间。这对于多播消息是一个很好的用途,因为可能有多个客户端对同一信息感兴趣,可靠性不是一个问题。try 块将在异常发生时处理这些异常:
public class UDPMulticastServer {
public UDPMulticastServer() {
System.out.println("UDP Multicast Time Server Started");
try {
...
} catch (IOException | InterruptedException ex) {
// Handle exceptions
}
System.out.println(
"UDP Multicast Time Server Terminated");
}
public static void main(String args[]) {
new UDPMulticastServer();
}
}
需要一个MulticastSocket
类的实例以及一个持有多播 IP 地址的InetAddress
实例。在此示例中,地址228.5.6.7
表示多播组。使用joinGroup
方法加入该组播组,如下图:
MulticastSocket multicastSocket = new MulticastSocket();
InetAddress inetAddress = InetAddress.getByName("228.5.6.7");
multicastSocket.joinGroup(inetAddress);
为了发送消息,我们需要一个字节数组来保存消息和数据包。声明如下所示:
byte[] data;
DatagramPacket packet;
服务器应用程序将使用无限循环每秒广播一个新的日期和时间。线程暂停一秒钟,然后使用Data
类创建新的日期和时间。DatagramPacket
实例是使用此信息创建的。端口9877
已分配给此服务器,需要客户端知道。send
方法将数据包发送给感兴趣的客户端:
while (true) {
Thread.sleep(1000);
String message = (new Date()).toString();
System.out.println("Sending: [" + message + "]");
data = message.getBytes();
packet = new DatagramPacket(data, message.length(),
inetAddress, 9877);
multicastSocket.send(packet);
}
接下来将讨论客户端应用程序。
此应用程序将加入由地址228.5.6.7
定义的多播组。它将阻塞,直到收到消息,然后显示消息。应用程序定义如下:
public class UDPMulticastClient {
public UDPMulticastClient() {
System.out.println("UDP Multicast Time Client Started");
try {
...
} catch (IOException ex) {
ex.printStackTrace();
}
System.out.println(
"UDP Multicast Time Client Terminated");
}
public static void main(String[] args) {
new UDPMulticastClient();
}
}
使用端口号9877
创建MulticastSocket
类的实例。这是必需的,以便它可以连接到 UDP 多播服务器。使用228.5.6.7
的多播地址创建InetAddress
实例。然后,客户端使用joinGroup
方法加入多播组。
MulticastSocket multicastSocket = new MulticastSocket(9877);
InetAddress inetAddress = InetAddress.getByName("228.5.6.7");
multicastSocket.joinGroup(inetAddress);
需要一个DatagramPacket
实例来接收发送给客户端的消息。创建一个字节数组并用于实例化此数据包,如下所示:
byte[] data = new byte[256];
DatagramPacket packet = new DatagramPacket(data, data.length);
然后,客户端应用程序进入一个无限循环,在该循环中,它通过receive
方法阻塞,直到服务器发送消息。消息到达后,将显示消息:
while (true) {
multicastSocket.receive(packet);
String message = new String(
packet.getData(), 0, packet.getLength());
System.out.println("Message from: " + packet.getAddress()
+ " Message: [" + message + "]");
}
接下来,我们将演示客户端和服务器如何交互。
启动服务器。服务器的输出将类似于以下输出,但日期和时间将不同:
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]
发送:[2015 年 9 月 19 日星期六 13:48:45 CDT]
发送:[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 消息:[2015 年 9 月 19 日星期六 13:48:45 CDT]
消息来源:/192.168.1.7 消息:[Sat Sep 19 13:48:46 CDT 2015]
。。。
如果程序在 Mac 上执行,则可能是通过套接字异常。如果发生这种情况,请使用-Djava.net.preferIPv4Stack=true VM
选项。
如果启动后续客户端,则每个客户端都将收到相同系列的服务器消息。
我们也可以通过频道多播。我们将使用 IPv6 来演示此过程。这个过程与我们之前使用的DatagramChannel
类类似,只是需要使用多播组。为此,我们需要知道哪些网络接口可用。在讨论使用通道进行多播的细节之前,我们将演示如何获取机器的网络接口列表。
NetworkInterface
类表示一个网络接口。本课程在第 2 章、网络寻址中讨论。以下是该章中演示的方法的变体。它已被扩充以显示特定接口是否支持多播,如下所示:
try {
Enumeration<NetworkInterface> networkInterfaces;
networkInterfaces =
NetworkInterface.getNetworkInterfaces();
for (NetworkInterface networkInterface :
Collections.list(networkInterfaces)) {
displayNetworkInterfaceInformation(
networkInterface);
}
} catch (SocketException ex) {
// Handle exceptions
}
displayNetworkInterfaceInformation
方法如下所示。此方法已从改编为 https://docs.oracle.com/javase/tutorial/networking/nifs/listing.html :
static void displayNetworkInterfaceInformation(
NetworkInterface networkInterface) {
try {
System.out.printf("Display name: %s\n",
networkInterface.getDisplayName());
System.out.printf("Name: %s\n",
networkInterface.getName());
System.out.printf("Supports Multicast: %s\n",
networkInterface.supportsMulticast());
Enumeration<InetAddress> inetAddresses =
networkInterface.getInetAddresses();
for (InetAddress inetAddress :
Collections.list(inetAddresses)) {
System.out.printf("InetAddress: %s\n",
inetAddress);
}
System.out.println();
} catch (SocketException ex) {
// Handle exceptions
}
}
当执行本例时,您将得到如下类似的输出:
显示名称:软件环回接口 1
名称:lo
支持多播:true
InetAddress:/127.0.0.1
InetAddress:/0:0:0:0:0:0:0:1
显示名称:微软内核调试网络适配器
名称:eth0
支持多播:true
显示名称:Realtek PCIe FE 系列控制器
名称:eth1
支持多播:true
InetAddress:/fe80:0:0:0:91d0:8e19:31f1:cb2d%eth1
显示名称:Realtek RTL8188EE 802.11 b/g/n Wi-Fi 适配器
名称:wlan0
支持多播:true
InetAddress:/192.168.1.7
InetAddress:/2002:42be:6659:0:0:0:1001
InetAddress:/fe80:0:0:0:9cdb:371f:d3e9:4e2e%wlan0
。。。
对于我们的客户端/服务器,我们将使用eth0
接口。您需要选择一个最适合您的平台。例如,在 Mac 上,这可能是en0
或awdl0
。
UDP 通道多播服务器将:
- 设置频道和多播组
- 创建包含消息的缓冲区
- 使用无限循环发送和显示组消息
服务器的定义如下所示:
public class UDPDatagramMulticastServer {
public static void main(String[] args) {
try {
...
}
} catch (IOException | InterruptedException ex) {
// Handle exceptions
}
}
}
第一个任务使用System
类的setProperty
方法指定使用 IPv6。然后创建一个DatagramChannel
实例,并创建eth0
网络接口。setOption
方法将将信道与用于识别组的网络接口相关联。该组由使用 IPv6 节点本地作用域多播地址的InetSocketAddress
实例表示,如下所示。有关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 =
NetworkInterface.getByName("eth0");
channel.setOption(StandardSocketOptions.
IP_MULTICAST_IF,
networkInterface);
InetSocketAddress group =
new InetSocketAddress("FF01:0:0:0:0:0:0:FC",
9003);
然后根据消息字符串创建字节缓冲区。缓冲区的大小设置为字符串的长度,并使用put
和getBytes
方法的组合进行分配:
String message = "The message";
ByteBuffer buffer =
ByteBuffer.allocate(message.length());
buffer.put(message.getBytes());
在 while 循环中,缓冲区被发送给组成员。为了清楚地看到发送了什么,缓冲区的内容将使用在UDP echo server 应用程序部分中使用的相同代码显示。缓冲区被重置,以便可以再次使用。应用程序暂停一秒钟,以避免出现过多的消息。例如:
while (true) {
channel.send(buffer, group);
System.out.println("Sent the multicast message: "
+ message);
buffer.clear();
buffer.mark();
System.out.print("Sent: [");
StringBuilder msg = new StringBuilder();
while (buffer.hasRemaining()) {
msg.append((char) buffer.get());
}
System.out.println(msg + "]");
buffer.reset();
Thread.sleep(1000);
}
现在,我们已经为客户端应用程序做好了准备。
UDP 通道多播客户端将加入组,接收消息,显示消息,然后终止。正如我们将看到的,MembershipKey
类表示多播组的成员身份。
申请声明如下。首先,我们指定使用 IPv6。然后声明网络接口,该接口与服务器使用的接口相同:
public class UDPDatagramMulticastClient {
public static void main(String[] args) throws Exception {
System.setProperty("java.net.preferIPv6Stack", "true");
NetworkInterface networkInterface =
NetworkInterface.getByName("eth0");
...
}
}
接下来创建DatagramChannel
实例。该通道绑定到端口9003
并与网络接口实例关联:
DatagramChannel channel = DatagramChannel.open()
.bind(new InetSocketAddress(9003))
.setOption(StandardSocketOptions.IP_MULTICAST_IF,
networkInterface);
然后根据服务器使用的相同 IPv6 地址创建组,并使用通道的join
方法创建MembershipKey
实例,如下所示。将显示按键和等待消息,以说明客户端的工作方式:
InetAddress group =
InetAddress.getByName("FF01:0:0:0:0:0:0:FC");
MembershipKey key = channel.join(group, networkInterface);
System.out.println("Joined Multicast Group: " + key);
System.out.println("Waiting for a message...");
创建一个大小为1024
的字节缓冲区。这个大小对于本例来说足够了,然后调用receive
方法,该方法将阻塞,直到收到消息:
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.receive(buffer);
要显示缓冲区的内容,我们需要翻转它。内容的显示方式与我们之前所做的相同:
buffer.flip();
System.out.print("Received: [");
StringBuilder message = new StringBuilder();
while (buffer.hasRemaining()) {
message.append((char) buffer.get());
}
System.out.println(message + "]");
当我们使用成员身份密钥时,我们应该表明我们不再对使用drop
方法接收组消息感兴趣:
key.drop();
如果有数据包等待套接字处理,则消息仍可能到达。
首先启动服务器。此服务器将每秒显示一系列消息,如下所示:
发送多播报文:报文
已发送:【消息】
发送多播报文:报文
已发送:【消息】
发送多播报文:报文
已发送:【消息】
。。。
接下来,启动客户端应用程序。它将显示多播组,等待消息,然后显示消息,如下所示:
加入多播组:ff01:0:0:0:0:fc,eth1
正在等待消息。。。
收到:【消息】
使用通道可以提高 UDP 多播消息的性能。
使用 UDP 传输音频或视频是很常见的。它是有效的,任何数据包丢失或无序数据包都会导致最小的问题。我们将通过播放现场音频来说明这项技术。UDP 服务器将捕获麦克风的声音并将其发送到客户端。UDP 客户端将接收音频并在系统扬声器上播放。
UDP 流媒体服务器的思想是将流分解为一系列数据包,然后发送到 UDP 客户端。然后,客户端将接收这些数据包,并使用它们来重建流。
为了演示流式音频,我们需要了解一些 Java 如何处理音频流。音频由javax.sound.sampled
包中的一系列类处理。用于捕获和播放音频的主要类包括:
AudioFormat
:此类指定所用音频格式的特征。由于有几种音频格式可用,系统需要知道使用哪种格式。AudioInputStream
:此类表示正在录制或播放的音频。AudioSystem
:此类提供对系统音频设备和资源的访问。DataLine
:此接口控制对流应用的操作,如启动和停止流。SourceDataLine
:表示声音的目的地,如扬声器。TargetDataLine
:表示声源,如麦克风。
用于SourceDataLine
和TargetDataLine
接口的术语可能有点混乱。这些术语是从线条和混音器的角度来看的。
AudioUDPServer
类的声明如下。它使用TargetDataLine
实例作为音频源。它被声明为实例变量,因为它在多个方法中使用。构造函数使用setupAudio
方法初始化音频,并使用broadcastAudio
方法将此音频发送到客户端:
public class AudioUDPServer {
private final byte audioBuffer[] = new byte[10000];
private TargetDataLine targetDataLine;
public AudioUDPServer() {
setupAudio();
broadcastAudio();
}
...
public static void main(String[] args) {
new AudioUDPServer();
}
}
以下是getAudioFormat
方法,服务器和客户端都使用该方法来指定音频流特征。模拟音频信号每秒采样 1600 次。每个样本都是一个有符号的 16 位数字。channels
变量被指定为1
,这意味着音频是单声道的。示例中字节的顺序很重要,设置为 big-endian:
private AudioFormat getAudioFormat() {
float sampleRate = 16000F;
int sampleSizeInBits = 16;
int channels = 1;
boolean signed = true;
boolean bigEndian = false;
return new AudioFormat(sampleRate, sampleSizeInBits,
channels, signed, bigEndian);
}
Big-endian 和 little-endian 是指字节的顺序。Big-endian 表示一个字的最高有效字节存储在最小的内存地址,最低有效字节存储在最大的内存地址。小恩迪安颠倒了这个顺序。不同的计算机体系结构使用不同的顺序。
setupAudio
方法初始化音频。DataLine.Info
类使用音频格式信息创建表示音频的行。AudioSystem
类的getLine
方法返回一条与麦克风对应的数据线。线路打开并启动:
private void setupAudio() {
try {
AudioFormat audioFormat = getAudioFormat();
DataLine.Info dataLineInfo =
new DataLine.Info(TargetDataLine.class,
audioFormat);
targetDataLine = (TargetDataLine)
AudioSystem.getLine(dataLineInfo);
targetDataLine.open(audioFormat);
targetDataLine.start();
} catch (Exception ex) {
ex.printStackTrace();
System.exit(0);
}
}
broadcastAudio
方法创建 UDP 数据包。使用端口8000
创建套接字,并为当前机器创建InetAddress
实例:
private void broadcastAudio() {
try {
DatagramSocket socket = new DatagramSocket(8000);
InetAddress inetAddress =
InetAddress.getByName("127.0.0.1");
...
} catch (Exception ex) {
// Handle exceptions
}
}
进入无限循环,read
方法填充audioBuffer
数组并返回读取的字节数。对于大于0
的计数,使用缓冲区创建一个新数据包,并发送到监听端口9786
的客户端:
while (true) {
int count = targetDataLine.read(
audioBuffer, 0, audioBuffer.length);
if (count > 0) {
DatagramPacket packet = new DatagramPacket(
audioBuffer, audioBuffer.length, inetAddress, 9786);
socket.send(packet);
}
}
执行时,来自麦克风的声音将作为一系列数据包发送到客户端。
下面声明AudioUDPClient
应用程序。在构造函数中,调用initiateAudio
方法开始从服务器接收数据包的过程:
public class AudioUDPClient {
AudioInputStream audioInputStream;
SourceDataLine sourceDataLine;
...
public AudioUDPClient() {
initiateAudio();
}
public static void main(String[] args) {
new AudioUDPClient();
}
}
initiateAudio
方法创建绑定到端口9786
的套接字。创建字节数组以保存 UDP 数据包中包含的音频数据:
private void initiateAudio() {
try {
DatagramSocket socket = new DatagramSocket(9786);
byte[] audioBuffer = new byte[10000];
...
} catch (Exception e) {
e.printStackTrace();
}
}
无限循环将从服务器接收数据包,创建一个AudioInputStream
实例,然后调用playAudio
方法播放声音。使用以下代码创建数据包,然后阻塞,直到收到数据包:
while (true) {
DatagramPacket packet
= new DatagramPacket(audioBuffer, audioBuffer.length);
socket.receive(packet);
...
}
接下来,创建音频流。从数据包中提取字节数组。它用作ByteArrayInputStream
构造函数的参数,该构造函数与音频格式信息一起用于创建实际的音频流。这与SourceDataLine
实例关联,该实例已打开并启动。调用playAudio
方法播放声音:
try {
byte audioData[] = packet.getData();
InputStream byteInputStream =
new ByteArrayInputStream(audioData);
AudioFormat audioFormat = getAudioFormat();
audioInputStream = new AudioInputStream(
byteInputStream,
audioFormat, audioData.length /
audioFormat.getFrameSize());
DataLine.Info dataLineInfo = new DataLine.Info(
SourceDataLine.class, audioFormat);
sourceDataLine = (SourceDataLine)
AudioSystem.getLine(dataLineInfo);
sourceDataLine.open(audioFormat);
sourceDataLine.start();
playAudio();
} catch (Exception e) {
// Handle exceptions
}
所使用的getAudioFormat
方法与AudioUDPServer
申请中声明的方法相同。playAudio
方法如下。AudioInputStream
的read
方法填充一个缓冲区,该缓冲区被写入源数据行。这将有效地在系统扬声器上播放声音:
private void playAudio() {
byte[] buffer = new byte[10000];
try {
int count;
while ((count = audioInputStream.read(
buffer, 0, buffer.length)) != -1) {
if (count > 0) {
sourceDataLine.write(buffer, 0, count);
}
}
} catch (Exception e) {
// Handle exceptions
}
}
服务器运行时,启动客户端将播放服务器发出的声音。通过使用服务器和客户端中的线程来处理声音的录制和播放,可以增强播放效果。为了简化示例,省略了此细节。
在本例中,连续模拟声音被数字化并分解成数据包。然后这些数据包被发送到客户端,在那里它们被转换成声音并播放。
在其他几个框架中还发现了对 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 客户端/服务器开始,演示如何使用DatagramPacket
和DatagramSocket
类。我们看到了如何使用InetAddress
类来获取套接字和数据包使用的地址。
DatagramChannel
类支持在 UDP 环境中使用 NIO 技术,这比使用DatagramPacket
和DatagramSocket
方法更有效。该方法使用字节缓冲区来保存服务器和客户端之间发送的消息。此示例说明了在第 3 章NIO 网络支持中开发的许多技术。
接下来讨论了 UDP 多播是如何工作的。这提供了一种向组成员广播消息的简单技术。举例说明了MulticastSocket
、DatagramChannel
和MembershipKey
类的用法。后一类用于在使用DatagramChannel
类时建立一个组。
我们最后给出了一个示例,说明如何使用 UDP 支持音频流。我们详细介绍了javax.sound.sampled
包中几个类的使用,包括AudioFormat
和TargetDataLine
类用于收集和播放音频。我们使用DatagramSocket
和DatagramPacket
类来传输音频。
在下一章中,我们将研究可用于提高客户端/服务器应用程序可伸缩性的技术。****