前面几章讨论了在TCP传输层协议之上运行的网络应用程序,TCP是为数据的可靠传输而设计的。用户数据报协议(User Datagram Protocal,UDP)是在IP之上发送数据的另一种传输层协议,速度很快,但不可靠。当发送UDP数据时,无法知道数据是否会到达,也不知道数据的各个部分是否会以发送时的顺序到达。
1、UDP协议
类似FTP的应用程序,需要通过网络进行可靠的数据传输,UDP不是一个好的选择。不过还有其他类型的应用程序,保持最快的速度比保证每一位数据都正确更为重要。例如在实时音频或视频中,丢失或交换数据包只是带来一些干扰而已。如果客户端像服务器发送一个短的UDP请求,倘若指定时间内没有响应返回,它会认为这个包已丢失。域名系统(Domain Name System,DNS)就采取这样的工作方式(也可以基于TCP)。用UDP也可以实现一个可靠的文件传输协议,如网络文件系统(Network File System,NFS)、简单FTP(Trivial FTP,TFTP)和FSP(与FTP关系比较远的一种协议)都使用了UDP。在这些协议中,由应用程序复杂可靠性,也就是说应用程序必须处理丢失或乱序的包。
TCP和UDP就像电话系统和邮局的关系。电话已接通,双方可以按顺序听到讲话。相反邮局寄包裹,接收方并不能保证按顺序收到邮件,而且有丢失的概率。电话系统和邮局都有各自的用处,尽管它们都是用来通信的,但在某些特定情况,二者之间肯定有优劣之分。UDP和TCP也是这样。TCP应用比UDP应用更常见,不过UDP也有自己的位置。本章我们将看到用UDP能做什么,进一步深入,下一章会介绍UDP之上的组播。组播socket是标准UDP socket的一种相当简单的变体。
Java中UDP的实现分为两个类:DatagramPacket和DatagramSocket。DatagramPacket类将数据自己填充到UDP包中,这称为数据报(datagram),由你来解包接收中的数据报。DatagramSocket可以收发UDP数据报。这种职责划分与TCP使用的Socket和ServerSocket有所不同,UDP没有两台主机之间唯一连接的概念。TCP socket把网络连接看作是流,UDP让你处理总是单个数据报包。
1、UDP客户端
先来看一个简单的例子,还是用第8章的例子,我们将连接daytime服务器,不过这次使用UDP而不是TCP:
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class UDPClient {
public static void main(String[] args) {
try (DatagramSocket socket = new DatagramSocket(0)) {
socket.setSoTimeout(10000);// 超时时间很重要
InetAddress host = InetAddress.getByName("121.40.47.132");
DatagramPacket request = new DatagramPacket(new byte[1], 1, host, 13);
DatagramPacket response = new DatagramPacket(new byte[1024], 1024);// 分配1KB的空间
socket.send(request);
socket.receive(response);
String result = new String(response.getData(), 0, response.getLength());
System.out.println(result);
} catch (IOException e) {
e.printStackTrace();
}
}
}
2、UDP服务器
UDP服务器几乎遵循与UDP客户端同样的模式,只不过通常在发送之前会接收,而且不会选择要绑定的匿名端口。
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.util.Date;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DayTimeUDPServer {
private static final Logger logger = LoggerFactory.getLogger(DayTimeUDPServer.class.getCanonicalName());
public static void main(String[] args) {
try (DatagramSocket socket = new DatagramSocket(13)) {
while (true) {
try {
DatagramPacket request = new DatagramPacket(new byte[1024], 1024);
socket.receive(request);
String daytime = new Date().toString();
byte[] data = daytime.getBytes();
DatagramPacket response = new DatagramPacket(data, data.length, request.getAddress(),
request.getPort());
socket.send(response);
logger.info(daytime + " " + request.getAddress());
} catch (IOException | RuntimeException e) {
logger.error("", e);
}
}
} catch (Exception e) {
logger.error("", e);
}
}
}
从这个例子可以看出,UDP服务器往往不是多线程的,它们通常不会对某一个客户做太多工作,而且不会阻塞来等待另一端的响应,因为UDP从来不会报告错误。
3、DatagramPacket类
UDP数据报是基于IP数据报建立的,只向其底层IP数据报添加了很少的一点内容。
很多平台限制UDP包中的数据8192字节(8KB),多余的会被截取。为保证最大的安全性,UDP包的数据部分应当保持为512字节或更少。TCP数据报也存在这个问题,但Socket和ServerSocket提供的是基于流的API,对程序员隐藏了这些细节。
在Java中,UDP数据报用DatagramPacket类的实例表示,这个类提供了一些方法来获取和设置IP首部中的源或目标地址、获取和设置源或目标端口、获取和设置数据,以及获取和设置数据长度。其余首部字段无法通过纯Java代码访问。
4、DatagramSocket类
要收发DatagramPacket,必须打开一个数据报Socket,在Java中,数据报Socket通过DatagramSocket类创建和访问。客户端和服务器使用的Socket是一样的,区别只在于使用匿名端口(系统分配的端口)还是已知端口。与TCP中不同,不存在诸如DatagramServerSocket之类的东西。
Java支持6个UDP Socket选项:
- SO_TIMEOUT:receive()在抛出IngterruptIOException异常前等待入站数据报的时间,serSoTimeout()方法可以改变这个值。
- SO_RCVBUF:确定了用于网络I/O的缓冲区大小。setReceiveBufferSize()方法会建议对来自这个Socket的输入进行缓冲时使用的字节数,依赖于平台的限制。
- SO_SNDBUF:用于网络输出的发送缓冲区大小,setSendBufferSize()。
- SO_REUSEADDR:控制是否允许多个数据报Socket同时绑定到相同的端口和地址。
- SO_BROADCAST:控制是否允许一个Socket向广播地址收发包。
- IP_TOS:业务流类型由各个IP数据报首部中的IP_TOS字段值来确定,所以它对于UDP与TCP一样重要。DatagramSocket中的setTrafficClass()和getTrafficClass()方法与Socket中的相应方法实际上没有分别。业务流类型用0到255之间的整数指定,详细请参考“IP_TOS服务类型”相关知识。
5、一些有用的应用程序
UDP客户端:一些Internet服务只需要知道客户端的地址和端口,它们会忽略客户端在数据报中发送的数据。因此可以向服务器发送一个UDP数据报,读取传回的响应。
UDP服务端:如echo服务器,你可以在一台机器上运行echo客户端,来验证两台机器之间的网络是否正常。
6、DatagramChannel
DatagramChannel类用于非阻塞UDP应用程序,就像SocketChannel和ServerSocketChannel用于非阻塞TCP应用程序一样。不过,UDP天生就比TCP更具异步性,因而实际效果没有那么明显,在UDP中,一个数据报Socket可以处理多个客户端的输入和输出请求,DatagramChannel类所增加的就是能够以非阻塞方式来做到这一点,这样一来,如果网络没有立即准备好收发数据,这些方法可以迅速返回。
对于UDP,DatagramChannel是一个近乎完备的候选API,在Java 6及之前版本中,仍需使用DatagramSocket类将通道绑定一个端口。不过在Java 7及以后版本中就不一定非得使用这个类了,也不必使用DatagramPacket,读/写字节缓冲区,就像对SocketChannel的操作一样。