2.3.2 UDP客户端
UDP客户端首先向被动等待联系的服务器端发送一个数据报文。一个典型的UDP客户端主要执行以下三步:
1.创建一个DatagramSocket实例,可以选择对本地地址和端口号进行设置。
2.使用DatagramSocket类的send()和receive()方法来发送和接收DatagramPacket实例,进行通信。
3.通信完成后,使用DatagramSocket类的close()方法来销毁该套接字。
与Socket类不同,DatagramSocket实例在创建时并不需要指定目的地址。这也是TCP协议和UDP协议的最大不同点之一。在进行数据交换前,TCP套接字必须跟特定主机和另一个端口号上的TCP套接字建立连接,之后,在连接关闭前,该套接字就只能与相连接的那个套接字通信。而UDP套接字在进行通信前则不需要建立连接,每个数据报文都可以发送到或接收于不同的目的地址(DatagramSocket类的connect()方法确实允许指定远程地址和端口,但该功能是可选的)。
我们的UDP回馈客户端示例程序UDPEchoClientTimeout.java,发送一个带有回馈字符串的数据报文,并打印出从服务器收到的所有信息。一个UDP回馈服务器只是简单地将其收到的数据报文返回给客户端。当然,一个UDP客户端只与一个UDP服务器进行通信。许多系统都集成了UDP回馈服务程序,用于调试和测试。
使用UDP协议的一个后果是数据报文可能丢失。在我们的回馈协议中,客户端的回馈请求信息和服务器端的响应信息都有可能在网络中丢失。回顾前面所介绍的TCP回馈客户端,其发送了一个回馈字符串后,将在read()方法上阻塞等待响应。如果试图在我们的UDP回馈客户端上使用相同的策略,数据报文丢失后,我们的客户端就会永远阻塞在receive()方法上。为了避免这个问题,我们在客户端使用DatagramSocket类的setSoTimeout()方法来指定receive()方法的最长阻塞时间,因此,如果超过了指定时间仍未得到响应,客户端就会重发回馈请求。我们的回馈客户端执行以下步骤:
1.向服务器端发送回馈字符串。
2.在receive()方法上最多阻塞等待3秒钟,在超时前若没有收到响应,则重发请求(最多重发5次)。
3.终止客户端。
UDPEchoClientTimeout.java
1.应用程序设置和参数处理:第0~20行
2.创建UDP套接字:第22行
该DatagramSocket实例能够将数据报文发送给任何UDP套接字。我们没有指定本地地址和端口号,因此程序将自动选择本地地址和可用端口号。如果需要的话,我们也可以通过setLocalAddress()和setLocalPort()方法或构造函数,来显式地设置本地地址和端口。
3.设置套接字超时时间:第24行
数据报文套接字的超时时间,用来控制调用receive()方法的最长阻塞时间(毫秒)。本例中我们设置超时时间为3秒。注意,超时时间是不精确的,receive()方法的调用可能会阻塞比这更长的时间(但不会少于超时时间)。
4.创建发送的数据报文:第26~27行
创建一个要发送的数据报文,我们需要指定三项:数据、目的地址以及目的端口。对于目的地址,我们可以使用主机名或IP地址来确定一个回馈服务器。若使用的是主机名,它将在构造函数中转换成实际的IP地址。
5.创建接收的数据报文:第29~30行
创建一个要接收的数据报文,我们只需要定义一个用来存放报文数据的字节数组。而数据报文的源地址和端口号将从receive()方法获得。
6.发送数据报文:第32~47行
由于数据报文可能丢失,我们必须准备好重新传输数据。本例中,我们最多循环5次,来发送数据报文并尝试接收响应信息。
·发送数据报文:第35行
send()方法将数据报文传输到其指定的地址和端口号。
·处理数据报文的接收:第36~46行
receive()方法将阻塞等待,直到收到一个数据报文或等待超时。超时信息由InterruptedIOException异常指示。一旦超时,发送尝试计数器(tries)加1,并重新发送。若尝试了最大次数后,仍没有接收到数据报文,循环将退出。如果receive()方法成功接收了数据,我们将循环标记receivedResponse设为true,以退出循环。由于数据报文可能发送自任何地址,我们需要验证所接收的数据报文,检查其源地址和端口号是否与所指定的回馈服务器地址和端口号相匹配。
7.打印接收结果:第49~53行
如果接收到了一个数据报文,即receivedResponse为true,我们就可以打印出数据报文中的数据信息。
8.关闭套接字:第54行
在学习服务器端代码之前,我们先看看DatagramSocket类的主要方法。
DatagramSocket:创建
以上构造函数将创建一个UDP套接字。可以分别或同时设置本地端口和地址。如果没有指定本地端口,或将其设置为0,该套接字将与任何可用的本地端口绑定。如果没有指定本地地址,数据包(packet)可以接收发送向任何本地地址的数据报文。
DatagramSocket:连接与关闭
connect()方法用来设置套接字的远程地址和端口。一旦连接成功,该套接字就只能与指定的地址和端口进行通信,任何向其他地址和端口发送数据报文的尝试都将抛出一个异常。套接字也将只接收从指定地址和端口发送来的数据报文,从其他地址或端口发送来的数据报文将被忽略。重点提示:连接到多播地址或广播地址的套接字只能发送数据报文,因为数据报文的源地址总是一个单播地址(见第4.3节)。注意,连接仅仅是本地操作,因为与TCP协议不同,UDP中没有端对端的数据包交换。disconnect()方法用来清空远程地址和端口号,若存在的话。close()方法表明该套接字不再使用,之后任何发送或接收数据的尝试都将抛出异常。
DatagramSocket:地址处理
第一个方法返回一个代表所连接的远程套接字地址的InetAddress实例,如果没有连接,则返回null。与之类似,getPort()方法返回所连接的套接字的端口号,若没有连接则返回-1。第三个方法一个SocketAddress实例,其中包含了所连接的远程套接字的地址和端口号,如果没有连接,则返回null。
后面三个方法为本地地址和端口提供了类似的服务。如果该套接字没有与本地地址绑定,getLocalAddress()方法将返回通配符地址(“任何本地地址”)。getLocalPort()方法总是会返回一个本地端口号。如果调用这个方法前该套接字还没有绑定端口号,getLocalPort()方法将选择任意一个可以本地端口与之绑定。getLocalSocketAddress()在套接字没有绑定本地地址时返回null。
DatagramSocket:发送和接收
send()方法用来发送DatagramPacket实例。一旦建立连接,数据包将发送到该套接字所连接的地址,除非DatagramPacket实例中已经指定了不同目的地址,这将抛出一个异常。如果没有创建连接,数据包将发送到DatagramPacket实例中指定的目的地址。该方法不阻塞等待。
receive()方法将阻塞等待,直到接收到数据报文,并将报文中的数据复制到指定的DatagramPacket实例中。如果套接字已经创建了连接,该方法也阻塞等待,直到接收到从所连接的远程套接字发来的数据报文。
DatagramSocket:选项
以上方法分别获取和设置该套接字中receive()方法调用的最长阻塞时间。如果在接收到数据之前超时,则抛出InterruptedIOException异常。超时时间以毫秒为单位。
与Socket类和ServerSocket类一样,DatagramSocket类也还有许多其他选项,这些内容将在第4.4节更加完整地介绍。