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
0 import java.net.DatagramSocket;
1 import java.net.DatagramPacket;
2 import java.net.InetAddress;
3 import java.io.IOException;
4 import java.io.InterruptedIOException;
5
6 public class UDPEchoClientTimeout {
7
8 private static final int TIMEOUT = 3000; // Resend timeout
(milliseconds)
9 private static final int MAXTRIES = 5; // Maximum
retransmissions
10
11 public static void main(String[] args) throws
IOException {
12
13 if ((args.length < 2) || (args.length > 3)) { // Test
for correct # of args
14 throw new IllegalArgumentException("Parameter(s):
<Server> <Word> [<Port>]");
15 }
16 InetAddress serverAddress =
InetAddress.getByName(args[0]); // Server address
17 // Convert the argument String to bytes using the default
encoding
18 byte[] bytesToSend = args[1].getBytes();
19
20 int servPort = (args.length == 3) ?
Integer.parseInt(args[2]) : 7;
21
22 DatagramSocket socket = new DatagramSocket();
23
24 socket.setSoTimeout(TIMEOUT); // Maximum receive
blocking time (milliseconds)
25
26 DatagramPacket sendPacket = new
DatagramPacket(bytesToSend, // Sending packet
27 bytesToSend.length, serverAddress, servPort);
28
29 DatagramPacket receivePacket = // Receiving packet
30 new DatagramPacket(new byte[bytesToSend.length],
bytesToSend.length);
31
32 int tries = 0; // Packets may be lost, so we have to
keep trying
33 boolean receivedResponse = false;
34 do {
35 socket.send(sendPacket); // Send the echo string
36 try {
37 socket.receive(receivePacket); // Attempt echo reply
reception
38
39 if
(!receivePacket.getAddress().equals(serverAddress)) {//
Check source
40 throw new IOException("Received packet from an unknownsource");
41 }
42 receivedResponse = true;
43 } catch (InterruptedIOException e) { // We did not get
anything
44 tries += 1;
45 System.out.println("Timed out, " + (MAXTRIES - tries)
+ " more tries...");
46 }
47 } while ((!receivedResponse) && (tries < MAXTRIES));
48
49 if (receivedResponse) {
50 System.out.println("Received: " + new
String(receivePacket.getData()));
51 } else {
52 System.out.println("No response -- giving up.");
53 }
54 socket.close();
55 }
56 }
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:创建
DatagramSocket()
DatagramSocket(int localPort)
DatagramSocket(int localPort, InetAddress localAddr)
以上构造函数将创建一个UDP套接字。可以分别或同时设置本地端口和地址。如果没有指定本地端口,或将其设置为0,该套接字将与任何可用的本地端口绑定。如果没有指定本地地址,数据包(packet)可以接收发送向任何本地地址的数据报文。
DatagramSocket:连接与关闭
void connect(InetAddress remoteAddr, int remotePort)
void connect(SocketAddress remoteSockAddr)
void disconnect()
void close()
connect()方法用来设置套接字的远程地址和端口。一旦连接成功,该套接字就只能与指定的地址和端口进行通信,任何向其他地址和端口发送数据报文的尝试都将抛出一个异常。套接字也将只接收从指定地址和端口发送来的数据报文,从其他地址或端口发送来的数据报文将被忽略。重点提示:连接到多播地址或广播地址的套接字只能发送数据报文,因为数据报文的源地址总是一个单播地址(见第4.3节)。注意,连接仅仅是本地操作,因为与TCP协议不同,UDP中没有端对端的数据包交换。disconnect()方法用来清空远程地址和端口号,若存在的话。close()方法表明该套接字不再使用,之后任何发送或接收数据的尝试都将抛出异常。
DatagramSocket:地址处理
InetAddress getInetAddress()
int getPort()
SocketAddress getRemoteSocketAddress()
InetAddress getLocalAddress()
int getLocalPort()
SocketAddress getLocalSocketAddress()
第一个方法返回一个代表所连接的远程套接字地址的InetAddress实例,如果没有连接,则返回null。与之类似,getPort()方法返回所连接的套接字的端口号,若没有连接则返回-1。第三个方法一个SocketAddress实例,其中包含了所连接的远程套接字的地址和端口号,如果没有连接,则返回null。
后面三个方法为本地地址和端口提供了类似的服务。如果该套接字没有与本地地址绑定,getLocalAddress()方法将返回通配符地址("任何本地地址")。getLocalPort()方法总是会返回一个本地端口号。如果调用这个方法前该套接字还没有绑定端口号,getLocalPort()方法将选择任意一个可以本地端口与之绑定。getLocalSocketAddress()在套接字没有绑定本地地址时返回null。
DatagramSocket:发送和接收
void send(DatagramPacket packet)
void receive(DatagramPacket packet)
send()方法用来发送DatagramPacket实例。一旦建立连接,数据包将发送到该套接字所连接的地址,除非DatagramPacket实例中已经指定了不同目的地址,这将抛出一个异常。如果没有创建连接,数据包将发送到DatagramPacket实例中指定的目的地址。该方法不阻塞等待。
receive()方法将阻塞等待,直到接收到数据报文,并将报文中的数据复制到指定的DatagramPacket实例中。如果套接字已经创建了连接,该方法也阻塞等待,直到接收到从所连接的远程套接字发来的数据报文。
DatagramSocket:选项
int getSoTimeout()
void setSoTimeout(int timeoutMillis)
以上方法分别获取和设置该套接字中receive()方法调用的最长阻塞时间。如果在接收到数据之前超时,则抛出InterruptedIOException异常。超时时间以毫秒为单位。
与Socket类和ServerSocket类一样,DatagramSocket类也还有许多其他选项,这些内容将在第4.4节更加完整地介绍。
2.3.3 UDP服务器端
与TCP服务器一样,UDP服务器的工作是建立一个通信终端,并被动等待客户端发起
连接。但由于UDP是无连接的,UDP通信通过客户端的数据报文初始化,并没有TCP中
建立连接那一步。典型的UDP服务器要执行以下三步:
1. 创建一个DatagramSocket实例,指定本地端口号,并可以选择指定本地地址。此时,服务器已经准备好从任何客户端接收数据报文。
2. 使用DatagramSocket类的receive()方法来接收一个DatagramPacket实例。当receive()方法返回时,数据报文就包含了客户端的地址,这样我们就知道了回复信息应该发送到什么地方。
3. 使用DatagramSocket类的send() 和receive()方法来发送和接收DatagramPackets实例,进行通信。
下一个示例程序,UDPEchoServer.java,实现了一个UDP版本的回馈服务器。这个服务器非常简单:它不停地循环,接收数据报文后将相同的数据报文返回给客户端。实际上我们的服务器只接收和发送数据报文中的前255(ECHOMAX)个字符,超出的部分将在套接字的具体实现中无提示地丢弃。
UDPEchoServer.java
0 import java.io.IOException;
1 import java.net.DatagramPacket;
2 import java.net.DatagramSocket;
3
4 public class UDPEchoServer {
5
6 private static final int ECHOMAX = 255; // Maximum size
of echo datagram
7
8 public static void main(String[] args) throws
IOException {
9
10 if (args.length != 1) { // Test for correct argument
list
11 throw new IllegalArgumentException("Parameter(s):
<Port>");
12 }
13
14 int servPort = Integer.parseInt(args[0]);
15
16 DatagramSocket socket = new DatagramSocket(servPort);
17 DatagramPacket packet = new DatagramPacket(new
byte[ECHOMAX], ECHOMAX);
18
19 while (true) { // Run forever, receiving and echoing
datagrams
20 socket.receive(packet); // Receive packet from client
21 System.out.println("Handling client at " +
packet.getAddress().getHostAddress()
22 + " on port " + packet.getPort());
23 socket.send(packet); // Send the same packet back to
client
24 packet.setLength(ECHOMAX); // Reset length to avoid
shrinking buffer
25 }
26 /* NOT REACHED */
27 }
28 }
UDPEchoServer.java
1. 应用程序设置和参数解析:第0-14行
UDPEchoServer只接收一个参数,即该回馈服务器套接字的本地端口号。
2. 创建和设置数据报文套接字:第16行
与UDP客户端不同的是,UDP服务器必须显式地设置它的本地端口号,并使客户端知道该端口,否则客户端将不知道应该把回馈请求数据报文发送到什么目的地址。服务器从客户端接收到了回馈数据报文后,能从中获取客户端的地址和端口号。
3. 创建数据报文:第17行
UDP消息包含在数据报文中。我们构建了一个DatagramPacket实例,其缓存区最多(ECHOMAX)可容纳255个字节。这个数据报文将同时用来接收回馈请求和发送回馈信息。
4. 迭代处理收到的回馈请求:第19-25行
UDP服务器为所有的通信使用同一个套接字,这点与TCP服务器不同,TCP服务器为每个成功返回的accept()方法创建一个新的套接字。
接收回馈请求数据报文,打印其源地址信息:第20-22行
DatagramSocket类的receive()方法将阻塞等待,直到接收到从客户端发来的数据报文(或超时)。由于没有连接,每个数据报文都可能发送自不同的客户端。而数据报文自身就包含了其发送者的(客户端的)源地址和端口号。
发送回馈信息:第23行
数据包(packet)中已经包含了回馈字符串和回馈目的地址及端口,因此DatagramSocket类的send()方法只是简单地传输之前接收到的数据报文。注意,当我们接收数据报文时,将其地址和端口解释为源地址和端口,而在发送数据报文时,则将其地址和端口称为目的地址和端口。
重置缓存区大小:第24行
处理了接收到的消息后,数据包的内部长度将设置为刚处理过的消息的长度,而这可能比缓冲区的原始长度短。如果接收新消息前不对内部长度进行重置,后续的消息一旦长于之前消息,就会被截断。
相关下载:
Java_TCPIP_Socket编程(doc)
http://download.csdn.net/detail/undoner/4940239
文献来源:
UNDONER(小杰博客) :http://blog.csdn.net/undoner
LSOFT.CN(琅软中国) :http://www.lsoft.cn