Java UDP Socket编程
UDP协议
UDP协议提供的服务不同于TCP协议的端到端服务,它是面向非连接的,属不可靠协议,UDP套接字在使用前不需要进行连接。实际上,UDP协议实现了两个功能:
1)在IP协议的基础上添加了端口;
2)对传输过程中可能产生的数据错误进行了检测,并抛弃已经损坏的数据。
UDP的Java支持
Java通过DatagramPacket类和DatagramSocket类来使用UDP套接字,客户端和服务器端都通过DatagramSocket的send()方法和receive()方法来发送和接收数据,用DatagramPacket来包装需要发送或者接收到的数据。发送信息时,Java创建一个包含待发送信息的DatagramPacket实例,并将其作为参数传递给DatagramSocket实例的send()方法;接收信息时,Java程序首先创建一个DatagramPacket实例,该实例预先分配了一些空间,并将接收到的信息存放在该空间中,然后把该实例作为参数传递给DatagramSocket实例的receive()方法。在创建DatagramPacket实例时,要注意:如果该实例用来包装待接收的数据,则不指定数据来源的远程主机和端口,只需指定一个缓存数据的byte数组即可(在调用receive()方法接收到数据后,源地址和端口等信息会自动包含在DatagramPacket实例中),而如果该实例用来包装待发送的数据,则要指定要发送到的目的主机和端口。
UDP的通信建立的步骤
UDP客户端首先向被动等待联系的服务器发送一个数据报文。一个典型的UDP客户端要经过下面三步操作:
1、创建一个DatagramSocket实例,可以有选择地对本地地址和端口号进行设置,如果设置了端口号,则客户端会在该端口号上监听从服务器端发送来的数据;
2、使用DatagramSocket实例的send()和receive()方法来发送和接收DatagramPacket实例,进行通信;
3、通信完成后,调用DatagramSocket实例的close()方法来关闭该套接字。
由于UDP是无连接的,因此UDP服务端不需要等待客户端的请求以建立连接。另外,UDP服务器为所有通信使用同一套接字,这点与TCP服务器不同,TCP服务器则为每个成功返回的accept()方法创建一个新的套接字。一个典型的UDP服务端要经过下面三步操作:
1、创建一个DatagramSocket实例,指定本地端口号,并可以有选择地指定本地地址,此时,服务器已经准备好从任何客户端接收数据报文;
2、使用DatagramSocket实例的receive()方法接收一个DatagramPacket实例,当receive()方法返回时,数据报文就包含了客户端的地址,这样就知道了回复信息应该发送到什么地方;
3、使用DatagramSocket实例的send()方法向服务器端返回DatagramPacket实例。
这里有一点需要注意:
UDP程序在receive()方法处阻塞,直到收到一个数据报文或等待超时。由于UDP协议是不可靠协议,如果没有收到DatagramPacket,那么程序将会一直阻塞在receive()方法处,这样客户端将永远都接收不到服务器端发送回来的数据,但是又没有任何提示。为了避免这个问题,我们在客户端使用DatagramSocket类的setSoTimeout()方法来制定receive()方法的最长阻塞时间,并指定重发数据报的次数,如果每次阻塞都超时,并且重发次数达到了设置的上限,则关闭客户端。
下面给出一个客户端服务端UDP通信的Demo(没有用多线程),该客户端在本地2222端口监听接收到的数据,并将字符串"Hello UDPserver"发送到本地服务器的3222端口,服务端在本地3222端口监听接收到的数据,如果接收到数据,则返回字符串"Hello UDPclient"到该客户端的2222端口。在客户端,由于程序可能会一直阻塞在receive()方法处,因此这里我们在客户端用DatagramSocket实例的setSoTimeout()方法来指定receive()的最长阻塞时间TIMEOUT ,并设置重发数据的次数MAXNUM ,如果最终依然没有接收到从服务端发送回来的数据,我们就关闭客户端。
1 import java.io.IOException; 2 import java.io.InterruptedIOException; 3 import java.net.DatagramPacket; 4 import java.net.DatagramSocket; 5 import java.net.InetAddress; 6 7 public class UdpClient { 8 private static final int MAXNUM = 5; // 设置重发数据的最多次数 9 private static final int TIMEOUT = 5000; //设置接收数据的超时时间 10 private static final int CLIENT_PORT = 2222; 11 private static final int SERVER_PORT = 3222; 12 private static final int REV_SIZE = 1024; //接收数据的存储空间大小 13 14 public static void main(String[] args) throws IOException { 15 String str_send = "Hello UDPserver"; //要发送的字串 16 byte[] buf_rev = new byte[REV_SIZE]; //要接收的存储空间 17 18 /*第一步 实例化DatagramSocket*/ 19 DatagramSocket mSoc = new DatagramSocket(CLIENT_PORT); 20 mSoc.setSoTimeout(TIMEOUT); //设置接收数据时阻塞的最长时间 21 22 /*第二步 实例化用于发送的DatagramPacket和用于接收的DatagramPacket*/ 23 InetAddress inetAddress = InetAddress.getLocalHost(); 24 DatagramPacket data_send = new DatagramPacket(str_send.getBytes(), 25 str_send.length(), inetAddress, SERVER_PORT); 26 27 DatagramPacket data_rev = new DatagramPacket(buf_rev, REV_SIZE); 28 29 30 /*第三步 DatagramPacket send发送数据,receive接收数据*/ 31 int send_count = 0; // 重发数据的次数 32 boolean revResponse = false; // 是否接收到数据的标志位 33 while (!revResponse && send_count < MAXNUM) { 34 try { 35 mSoc.send(data_send); //发送数据 36 mSoc.receive(data_rev);//接收数据 37 if (!data_rev.getAddress().getHostAddress() 38 .equals(InetAddress.getLocalHost().getHostAddress())) { 39 throw new IOException( 40 "Received packet from an umknown source"); 41 } 42 revResponse = true; 43 } catch (InterruptedIOException e) { 44 // 如果接收数据时阻塞超时,重发并减少一次重发的次数 45 send_count += 1; 46 System.out.println("Time out," + (MAXNUM - send_count) 47 + " more tries..."); 48 } 49 } 50 if (revResponse) { 51 // 如果收到数据,则打印出来 52 System.out.println("client received data from server:"); 53 String str_receive = new String(data_rev.getData(), 0, 54 data_rev.getLength()) 55 + " from " 56 + data_rev.getAddress().getHostAddress() 57 + ":" 58 + data_rev.getPort(); 59 System.out.println(str_receive); 60 // 由于dp_receive在接收了数据之后,其内部消息长度值会变为实际接收的消息的字节数, 61 // 所以这里要将dp_receive的内部消息长度重新置为1024 62 data_rev.setLength(REV_SIZE); 63 } else { 64 // 如果重发MAXNUM次数据后,仍未获得服务器发送回来的数据,则打印如下信息 65 System.out.println("No response -- give up."); 66 } 67 68 /*第四步 关闭DatagramPacket*/ 69 mSoc.close(); 70 } 71 72 }
1 import java.io.IOException; 2 import java.net.DatagramPacket; 3 import java.net.DatagramSocket; 4 import java.net.InetAddress; 5 6 public class UdpServer { 7 8 private static final int SERVER_PORT = 3222; 9 private static final int REV_SIZE = 1024; // 接收数据的存储空间大小 10 11 public static void main(String[] args) throws IOException { 12 byte[] buf_rev = new byte[REV_SIZE]; 13 String str_send = "Hello UDPclient"; 14 /* 第一步 实例化DatagramSocket */ 15 DatagramSocket mSoc = new DatagramSocket(SERVER_PORT); 16 17 /* 第二步 实例化用于接收的DatagramPacket 并从DatagramSocket接收数据 */ 18 DatagramPacket data_rev = new DatagramPacket(buf_rev, REV_SIZE); 19 boolean f = true; 20 while (f) { 21 mSoc.receive(data_rev); 22 InetAddress inetAddress = data_rev.getAddress(); 23 int port = data_rev.getPort(); 24 System.out.println("server received data from client:"); 25 String str_rev = new String(data_rev.getData(), 0, 26 data_rev.getLength()) 27 + " from " + inetAddress.getHostAddress() + ":" + port; 28 System.out.println(str_rev); 29 30 /* 第三步 实例化用于发送的DatagramPacket,并从DatagramSocket中发送出去 */ 31 DatagramPacket data_send = new DatagramPacket(str_send.getBytes(), 32 str_send.length(), inetAddress, port); 33 mSoc.send(data_send); 34 35 /* 36 * 由于dp_receive在接收了数据之后,其内部消息长度值会变为实际接收的消息的字节数, 37 * 所以这里要将dp_receive的内部消息长度重新置为1024 38 */ 39 data_rev.setLength(REV_SIZE); 40 } 41 mSoc.close(); 42 43 } 44 }
如果服务器端没有运行,则receive()会失败,此时运行结果如下图所示:
如果服务端先运行,客户端后运行,客户端将向服务端发送数据,并接受从服务端发送回来的数据,此时运行结果如下图所示:
几个需要注意的地方
1、UDP套接字和TCP套接字的一个微小但重要的差别:UDP协议保留了消息的边界信息。
DatagramSocket的每一次receive()调用最多只能接收调用一次send()方法所发送的数据,而且,不同的receive()方法调用绝对不会返回同一个send()方法所发送的额数据。
当在TCP套接字的输出流上调用write()方法返回后,所有调用者都知道数据已经被复制到一个传输缓存区中,实际上此时数据可能已经被发送,也有可能还没有被传送,而UDP协议没有提供从网络错误中恢复的机制,因此,并不对可能需要重传的数据进行缓存。这就意味着,当send()方法调用返回时,消息已经被发送到了底层的传输信道中。
2、UDP数据报文所能负载的最多数据,亦及一次传送的最大数据为65507个字节
当消息从网络中到达后,其所包含的数据被TCP的read()方法或UDP的receive()方法返回前,数据存储在一个先进先出的接收数据队列中。对于已经建立连接的TCP套接字来说,所有已接受但还未传送的字节都看作是一个连续的字节序列。然而,对于UDP套接字来说,接收到的数据可能来自不同的发送者,一个UDP套接字所接受的数据存放在一个消息队列中,每个消息都关联了其源地址信息,每次receive()调用只返回一条消息。如果receive()方法在一个缓存区大小为n的DatagramPacket实例中调用,而接受队里中的第一条消息的长度大于n,则receive()方法只返回这条消息的前n个字节,超出部分会被自动放弃,而且对接收程序没有任何消息丢失的提示!
出于这个原因,接受者应该提供一个有足够大的缓存空间的DatagramPacket实例,以完整地存放调用receive()方法时应用程序协议所允许的最大长度的消息。一个DatagramPacket实例中所允许传输的最大数据量为65507个字节,也即是UDP数据报文所能负载的最多数据。因此,可以用一个65600字节左右的缓存数组来接受数据。
3、DatagramPacket的内部消息长度值在接收数据后会发生改变,变为实际接收到的数据的长度值。
每一个DatagramPacket实例都包含一个内部消息长度值,其初始值为byte缓存数组的长度值,而该实例一旦接受到消息,这个长度值便会变为接收到的消息的实际长度值,这一点可以用DatagramPacket类的getLength()方法来测试。如果一个应用程序使用同一个DatagramPacket实例多次调用receive()方法,每次调用前就必须显式地将其内部消息长度重置为缓存区的实际长度,以免接受的数据发生丢失。
4、DatagramPacket的getData()方法总是返回缓冲区的原始大小,忽略了实际数据的内部偏移量和长度信息。
由于DatagramPacket的getData()方法总是返回缓冲数组的原始大小,即刚开始创建缓冲数组时指定的大小,在上面程序中,该长度为1024,因此如果我们要获取接收到的数据,就必须截取getData()方法返回的数组中只含接收到的数据的那一部分。
我们可以使用Arrays.copyOfRange()方法来实现,只需一步便可实现以上功能:
byte[] destbuf = Arrays.copyOfRange(data_rev.getData(),data_rev.getOffset(),
data_rev.getOffset() + data_rev.getLength());
当然,如果要将接收到的字节数组转换为字符串的话,也可以采用本程序中直接new一个String对象的方法:
new String(data_rev.getData(),data_rev.getOffset(),
data_rev.getOffset() + data_rev.getLength());
参考文章:
【Java TCP/IP Socket】UDP Socket(含代码) http://blog.csdn.net/ns_code/article/details/14128987
《Java TCP/IP Socket 编程》