UDP协议是一种不可靠的网络协议,之所以说这种协议不可靠,是因为它在通信实例的两端各建立一个Socket,但这两个Socket之间并没有虚拟链路。这两个Socket只是发送、接收数据报的对象。Java 提供了DatagramSocket对象作为基于UDP协议的Socket,而使用DatagramPacket代表DatagramSocket发送、接收的数据报。本小节将详细讲解基于UDP协议的网络编程技术。
UDP是User Datagram Protocol的缩写,意为“用户数据报协议”。UDP协议主要用来支持那些需要在计算机之间传输数据的网络连接,虽然UDP协议目前应用不如TCP协议广泛,但UDP协议依然是一个非常实用和可行的网络传输层协议,尤其是在一些实时性很强的应用场景中,比如网络游戏、视频会议等。
UDP协议是一种面向非连接的协议,面向非连接指的是在正式通信前不必与对方先建立连接,不管对方状态就直接发送。至于对方是否可以接收到这些数据内容,UDP协议无法控制,因此说UDP协议是一种不可靠的协议。UDP协议适用于一次只传送少量数据、对可靠性要求不高的应用环境。
与前面介绍的TCP协议一样,UDP协议直接建立在IP协议之上。实际上,UDP协议和TCP协议都属于传输层协议。正因为UDP协议是面向非连接的协议,没有建立连接的过程,因此它的通信效率很高,但也正因为如此,它的可靠性不如TCP协议。
UDP协议的主要作用是完成网络数据流和数据报之间的转换:在信息的发送端,UDP协议将网络数据流封装成数据报,然后将数据报发送出去,而在信息的接收端,UDP协议将数据报转换成实际数据内容。
读者可以把UDP协议下的DatagramSocket想象成手机,把DatagramPacket想像成短信,也就是说使用DatagramSocket发送DatagramPacket,而无论对发是否做好接收准备,我们都可以用手机发短信。把UDP协议和TCP协议做个简单的对比,可以发现:TCP协议可靠,传输大小无限制,但是需要连接建立时间,差错控制开销大。而UDP协议不可靠,差错控制开销较小,传输大小限制在64KB以下,不需要建立连接。
之前讲过:DatagramSocket就如同是手机,而DatagramPacket如同是短信。DatagramSocket不产生IO流,它唯一的作用就是收发DatagramPacket。DatagramSocket的构造方法如表15-7所示。
表15-7 DatagramSocket的构造方法
方法 | 功能 |
DatagramSocket() | 创建一个DatagramSocket 实例,并将该对象绑定到本机默认IP地址、本机所有可用端口中随机选择的某个端口 |
DatagramSocket(int prot) | 创建一个DatagramSocket实例,并将该对象绑定到本机默认IP地址和指定端口 |
DatagramSocket(int port, InetAddress laddr) | 创建一个DatagramSocket实例,并将该对象绑定到指定的IP地址和端口号 |
从表15-7可以看出:DatagramSocket只是定义了自己在哪个IP地址的计算机以及自己接收数据报的端口号,并没有定义数据报要发到哪个IP地址以及发给哪个端口号。实际上,数据报到底要发送到哪里是由数据报,也就是DatagramPacket自身所决定的。DatagramPacket的构造方法如表15-8所示。
表15-8 DatagramPacket的构造方法
方法 | 功能 |
DatagramPacket(byte[] buf,int length) | 以一个空数组来创建DatagramPacket对象,该对象的作用是接收DatagramSocket中的数据。 |
DatagramPacket(byte[] buf, int length, InetAddress addr, int port) | 以一个包含数据的数组来创建DatagramPacket对象,创建该DatagramPacket对象时指定了IP地址和端口,IP地址和端口决定了该数据报的目的地 |
DatagramPacket(byte[] buf, int offset, int length) | 以一个空数组来创建DatagramPacket对象,并 指定接收到的数据放入buf数组中时从下标offset开始,最多放length个字节 |
DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port) | 创建一个用于发送的DatagramPacket对象,指定发送buf数组中从offset开始,总共length个字节 |
DatagramSocket发送和接收数据报的方法分别是send()和received(),它们都以DatagramPacket类型的对象作为参数。需要强调:使用UDP协议时,实际上并没有明显的服务器端和客户端,因为双方都需要先建立一个DatagramSocket 对象,用来接收或发送数据报,然后使用DatagramPacket对象作为传输数据的载体。但通常固定IP 地址、固定端口的DatagramSocket对象所在的程序被称为服务器,这是因为该DatagramSocket有固定的IP地址,其他DatagramSocket可以准确的找到它。
发送数据时,应调用表15-8中第二个或第四个构造方法创建DatagramPacket 对象,此时的字节数组里存放了想发送的数据。除此之外,还要给出完整的目的地址,包括IP 地址和端口号。发送数据是通过DatagramSocket的send()方法实现的,send0方法根据数据报的目的地址来寻径以传送数据报。
接收数据时,应该调用表15-8中第一个或第三个构造方法创建DatagramPacket 对象,并给出接收数据的字节数组及其长度。然后调用DatagramSocket的receive()方法等待数据报的到来,receive()方法将一直等待,直到收到一个数据报为止。
可以看到,创建DatagramPacket对象时,必须传入一个字节数组,而这个数组的长度决定了该DatagramPacket能放多少数据。DatagramPacket提供了一个getData()方法,这个方法能够返回Datagram Packet对象中封装的字节数组。此外,程序员通过DatagramPacket类的getLength()方法可以获得字节数组中有效字节的长度。
当DatagramSocket收到一个DatagramPacket对象后,可以向该数据报的发送者回复信息,但由于UDP协议是面向非连接的,所以接收者并不知道每个数据报由谁发送过来的,此时可以使用表15-9所示方法来获取发送者的IP地址和端口。
表15-9 获取DatagramPacket的IP地址和端口号的方法
方法 | 功能 |
InetAddress getAddress() | 当程序准备发送此数据报时,该方法返回此数据报的目标机器的IP地址,当程序刚接收到一个数据报时,该方法返回该数据报的发送主机的IP地址 |
int getPort() | 当程序准备发送此数据报时,该方法返回此数据报的目标机器的端口号,当程序刚接收到一个数据报时,该方法返回该数据报的发送主机的端口号 |
SocketAddress getSocketAddress() | 当程序准备发送此数据报时,该方法返回此数据报的目标SocketAddress,当程序刚接收到一个数据报时,该方法返回该数据报的发送主机的SocketAddress |
表15-9中第三个方法的返回值是一个SocketAddress对象,该对象中封装了一个InetAddress对象和一个代表端口的整数,所以使用SocketAddress 对象可以同时代表IP地址和端口。下面的【例15_06】展示了如何使用DatagramSocket收发消息。
【例15_06 使用DatagramSocket收发数据】
Exam15_06.java
- import java.net.*;
- //接收消息的线程
- class ReceiveThread extends Thread {
- public void run() {
- try {
- DatagramSocket inSocket = new DatagramSocket(4567);
- for (int i=1;i<=3;i++) {
- byte[] in = new byte[1024];
- DatagramPacket inPack = new DatagramPacket(in,in.length);
- inSocket.receive(inPack);
- String message = new String(in,0,inPack.getLength());
- System.out.println(message);
- }
- inSocket.close();
- }catch (Exception e){
- e.printStackTrace();
- }
- }
- }
- //发送消息的线程
- class SendThread extends Thread{
- public void run() {
- try {
- DatagramSocket outSocket = new DatagramSocket();
- String message1 = "网络编程";
- String message2 = "是Java语言中很重要的模块";
- String message3 = "虽然有难度,但我会努力";
- byte[] out1 = message1.getBytes();
- byte[] out2 = message2.getBytes();
- byte[] out3 = message3.getBytes();
- //创建三个数据报,它们代表要被发送的消息,数据包指定了消息要发送到哪个IP地址和端口号
- DatagramPacket outPack1 = new DatagramPacket(out1,out1.length,InetAddress.getLocalHost(),4567);
- DatagramPacket outPack2 = new DatagramPacket(out2,out2.length,InetAddress.getLocalHost(),4567);
- DatagramPacket outPack3 = new DatagramPacket(out3,out3.length,InetAddress.getLocalHost(),4567);
- //发送消息
- outSocket.send(outPack1);
- outSocket.send(outPack2);
- outSocket.send(outPack3);
- outSocket.close();
- }catch (Exception e){
- e.printStackTrace();
- }
- }
- }
- public class Exam15_06 {
- public static void main(String[] args) {
- //创建并启动线程
- new ReceiveThread().start();
- new SendThread().start();
- }
- }
【例15_06】中创建了两个线程,分别是接收消息的ReceiveThread和发送消息的SendThread。ReceiveThread的run()方法中创建了用于接收消息的inSocket,并规定接收消息的端口号是4567。当接收到消息后,调用DatagramPacket类的getLength()方法可以获得字节数组中有效字节的长度,并以数组中有效字节部分创建消息字符串。
在SendThread线程的run()方法中创建了用于发送消息的outSocket,并且使用它连续发送了三条长短不一的消息。【例15_06】的运行结果如图15-7所示。
图15-7【例15_06】运行结果
DatagramSocket只允许数据报发送给指定的目标地址,而MulticastSocket可以将数据报以广播方式发送到多个地址的计算机。如果想要使用多点广播,则需要让一个数据报指定一组目标主机地址,当数据报发出后,整个组的所有主机都能收到该数据报。IP多点广播实现了将单一信息发送到多个接收者的广播,实现多点广播的原理是:设置一组特殊网络地址作为多点广播地址,每一个多点广播地址都被看做一个组,当客户端需要发送、接收广播信息时,加入到该组即可。IP协议为多点广播提供了这批特殊的IP地址,这些IP地址的范围是224.0.0.0~239.255.255.255。多点广播原理如图15-8 所示。
图15-8 多点广播原理图
从图17-8 中可以看出:MulticastSocket 类是实现多点广播的关键,当MulticastSocket 把一个数据报发送到多点广播IP地址时,该数据报将被自动广播到加入该地址的所有MulticastSocket。
MulticastSocket既可以将数据报发送到多点广播地址,也可以接收其他主机的广播信息。MulticastSocket有点像DatagramSocket,事实上MulticastSocket是DatagramSocket的一个子类,也就是说,MulticastSocket 是特殊的DatagramSocket。当要发送一个数据报时,可以使用随机端口创建MulticastSocket,也可以在指定端口创建MulticastSocket。MulticastSocket 提供了三个构造方法,如表15-10所示。
表15-10 MulticastSocket的构造方法
方法 | 功能 |
MulticastSocket() | 使用本机默认地址、随机端口来创建对象 |
MulticastSocket(int portNumber) | 使用本机默认地址、指定端口来创建对象 |
MulticastSocket(SocketAddress bindaddr) | 使用本机指定IP地址来创建对象 |
如果创建仅用于发送数据报的MulticastSocket 对象,则使用默认地址、随机端口即可。但如果创建接收数据报的MulticastSocket对象,则该MulticastSocket对象必须具有指定端口,否则发送方无法确定发送数据报的目标端口。创建MulticastSocket 对象后,还需要将该MulticastSocket 加入到指定的多点广播地址,程序员需要调用MulticastSocket类的joinGroup()方法把对象加入指定组,如果要让对象脱离一个组,则使用leaveGroup()方法实现。
在某些系统中,可能有多个网络接口。这可能会给多点广播带来问题,这时候程序需要在一个指定的网络接口上监听,通过调用setInterface()方法可以强制MulticastSocket使用指定的网络接口,也可以使用getInterface()方法查询MulticastSocket监听的网络接口。
MulticastSocket用于发送、接收数据报的方法与DatagramSocket完全一样。但MulticastSocket比DatagramSocket多了一个setTimeToLive(int t)方法,该ttl参数用于设置数据报最多可以跨过多少个网络,当ttl的值为0时,指定数据报应停留在本地主机,当ttl 的值为1时,指定数据报发送到本地局域网,当t的值为32时,意味着只能发送到本站点的网络上,当ttl的值为64时,意味着数据报应保留在本地区;当ttl的值为128时,意味着数据报应保留在本大洲,当ttl的值为255时,意味着数据报可发送到所有地方;在默认情况下,该ttl的值为1。
使用MulticastSocket 进行多点广播时所有的通信实体都是平等的,它们都将自己的数据报发送到多点广播IP地址,并使用MulticastSocket接收其他人发送的广播数据报。下面【例15_07】展示了使用MulticastSocket实现多点广播。
【例15_07多点广播】
- import java.net.*;
- public class Exam15_07 {
- public static void main(String[] args) throws Exception {
- String IP = "239.255.255.254";//广播地址
- InetAddress broadcastAddress = InetAddress.getByName (IP) ;
- //创建两个接收消息的MulticastSocket
- MulticastSocket receiveSocket1 = new MulticastSocket (4325) ;
- MulticastSocket receiveSocket2 = new MulticastSocket (4325) ;
- //创建一个发送广播消息的MulticastSocket
- MulticastSocket sendSocket = new MulticastSocket () ;
- byte[] in1 = new byte[1024];
- DatagramPacket inPack1 = new DatagramPacket(in1,in1.length);
- byte[] in2 = new byte[1024];
- DatagramPacket inPack2 = new DatagramPacket(in2,in2.length);
- sendSocket.setLoopbackMode(false);//设置消息不会发送给自己
- //把Socket加入广播地址
- receiveSocket1.joinGroup(broadcastAddress);
- receiveSocket2.joinGroup(broadcastAddress);
- sendSocket.joinGroup(broadcastAddress);
- String message = "这是一条多点广播的消息";
- byte[] out = message.getBytes();
- //要发送的数据报
- DatagramPacket outPack = new DatagramPacket(out,out.length,broadcastAddress,4325);
- sendSocket.send(outPack);//发送
- receiveSocket1.receive(inPack1);//接收数据报
- receiveSocket2.receive(inPack2);//接收数据报
- String messageReceive1 = new String(in1,0,inPack1.getLength());
- System.out.println(messageReceive1);//打印接收的消息
- String messageReceive2 = new String(in2,0,inPack2.getLength());
- System.out.println(messageReceive2);//打印接收的消息
- }
- }
由于IPv6不支持多点广播,所以需要在运行多点广播程序之前手动设置一个虚拟机参数才能保证程序运行成功。设置虚拟机参数的方式是:首先找到IDEA的“Run”菜单,展开菜单后单击“Eidt Configurations”菜单项,出现如图15-9所示界面。
图15-9 设置虚拟机参数界面
打开这个界面后,先检查类名称,如果类名称不是当前程序,将会导致参数设置到其他程序上。检查类名称后,单击右上角方框中的“Modify options”超链接,在弹出的菜单中选择“Add VM options”菜单项,之后在参数文本框中填入“-Djava.net.preferIPv4Stack=true”,如图15-10所示。
图15-10 填入参数
填入以上参数后,单击“OK”按钮即可完成配置,之后就能正确的运行程序。【例15_07】的运行结果如图15-11所示。
图15-11【例15_07】运行结果
从图15-11可以看出:一个MulticastSocket发出消息后,在同一个广播地址的所有MulticastSocket都能收到消息。