UDP协议提供了一种不同与TCP协议的端到端服务。实际上UDP协议只实现了两个功能:
(1)在IP协议的基础上添加了另一层地址(端口)
(2)对数据传输过程中可能产生的数据进行了检测,并抛弃已经损坏的数据。
UDP套接字具有与我们之前所看到的TCP套接字不同的特征。例如
1.连接方式不同:
TCP套接字与打电话相似,在进行通信前必须先建立连接,在连接关闭前,该套接字就只能与相连接的那个套接字通信;
而UDP套接字与邮件相似,在进行通信前无需建立连接,在UDP套接字中每条消息(即数据报文)负载了自己的地址信息。
在接收信息时,UDP套接字扮演的角色就像一个信箱,从不同地址发送来的信件和包裹都可以放到里面。 一旦被创建,
UDP套接字就可以用来连续的向不同的地址发送信息,或从任何地址接收信息。
2.消息边界处理方式不同:
UDP套接字与TCP套接字的另一个不同点在于它们对信息边界的处理方式不同,UDP套接字将保留边界信息,而TCP则不保留边界信息,
这个特性使应用程序在接收信息时,从某些方面来说比使用TCP套接字更简单。
3.安全性不同:
UDP套接字将尽可能的传送信息,但并不保证信息一定能成功到达目的地,而且信息到达的顺序与其发送顺序不一定一致(就像通过邮政
部门寄信一样),因此,使用UDP套接字的程序必须准备好处理信息的丢失和重排;
使用UDP套接字的原因:
既然UDP套接字为程序带来这个多额外的负担,怎么还要使用它呢?原因有2个:
(1)效率:如果应用程序只交换非常少量的数据,TCP连接的建立阶段就至少要传输其2倍的信息量(还有2倍的往返延迟时间);
(2)灵活性:UDP套接字提供了一个最小开销平台来满足任何需求的实现;
Java程序员通过DatagramPacket类和DatagramSocket类来使用UDP套接字。客户端和服务端都使用DatagramSocket来发送
数据,使用DatagramPacket来接收数据。
1.DatagramPacket类
UDP终端交换的是一种称为数据报文的自包含信息。这种信息在Java中表示为DatagramPacket类的实例。发送信息时,Java
程序创建一个包含了待发送信息的DatagramPacket实例,并将其作为参数传递给DatagramSocket类的send()方法。接收信息时,
Java程序首先创建一个DatagramPacket实例,该实例中预先分配了一些空间(一个字节数组byte[]),并将接收到的信息存放在该空间
中。然后把该实例作为参数传递给DatagramSocket类的receive()方法。
2.UDP客户端
UDP客户端首先向被动等待联接的服务器端发送一个数据报文。
第一步:创建一个DatagramSocket实例;
第二步:使用DatagramSocket类的send()和receive()方法来发送和接收DatagramPacket实例,进行通信;
第三步:使用DatagramSocket类的close()方法来销毁该套接字;
UDP客户端类:
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketTimeoutException;
import java.util.Arrays;
public class UDPEchoClientTimeout
{
// 最多重发次数
private static final int MAXTRIES = 5;
// 最大超时时间
private static final int TIMEOUT = 3000;
public static void main(String[] args) throws IOException
{
byte[] msg = new String("Hello job").getBytes();
InetAddress serverAddress = InetAddress.getLocalHost();
DatagramSocket socket = new DatagramSocket();
//设置超时时间
socket.setSoTimeout(TIMEOUT);
DatagramPacket sendPacket = new DatagramPacket(msg, msg.length,
serverAddress, 8850);
DatagramPacket receivePacket = new DatagramPacket(new byte[1024], 1024);
boolean receivedResponse = false;
int tries = 0;
do
{
socket.send(sendPacket);
try
{
socket.receive(receivePacket);
// 判断发送与接收的数据报包是否不属于同一个IP地址
if (!receivePacket.getAddress().equals(serverAddress))
{
throw new IOException(
"Received pakcet from a unknown source");
}
// 用于标识数据包接收是否成功
receivedResponse = true;
}
catch (SocketTimeoutException e)
{
tries += 1;
System.out.println("Timed out," + (MAXTRIES - tries)
+ "more tries......");
}
}
while ((!receivedResponse) && (tries < MAXTRIES));
if (receivedResponse)
{
/*
* 返回的缓存数组的长度可能比接收的数据报文内部长度更长 (也就是说:在接收的byte[]数组缓冲区中,有可能
* 没有被发送的数据报文消息填满)
*/
byte[] receiveMsg = Arrays.copyOfRange(receivePacket.getData(),
0, receivePacket.getOffset()
+ receivePacket.getLength());
System.out.println("Client receive data:" + new String(receiveMsg));
}
socket.close();
}
}
UDP服务端的工作是建立一个通信终端,并被动等待客户端发起连接。
第一步:创建一个DatagramSocket类实例,指定本地端口号,此时,服务器已经准备好从任何客户端接收数据报文;
第二步:使用DatagramSocket类的receive()方法来接收一个DatagramPacket实例,当receive()方法返回时,数据报文就包含了
客户端的地址,这样我们就知道了回复信息应该发送到什么地方。
第三步:使用DatagramSocket类的send()、receive()方法来发送和接收DatagramPacket实例,进行通信。
UDP服务端类:
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.util.Arrays;
public class UDPEchoServerTimeout
{
public static void main(String[] args) throws IOException
{
DatagramSocket socket = new DatagramSocket(8850);
DatagramPacket packet = new DatagramPacket(new byte[255], 255);
while (true)
{
/*
* 因为在DatagramPacket.setData()的内部,是将msg(字节数组)赋值给buff(数据缓冲区),
* 这样的话两个字节数组就会指向同一个内存地址的引用
* ,如果我们把buff(数据缓冲区)中的内容改变了,相应的msg(字节数组)内容也会改变,
* 所以,每次接收一个请求都必须要实例化一次要发送的字节消息。
*/
byte[] msg = new String("Hello sone").getBytes();
socket.receive(packet);
System.out.println("Handing client at"
+ packet.getAddress().getHostAddress() + " on port:"
+ packet.getPort());
byte[] receiveMsg = Arrays.copyOfRange(packet.getData(), 0,
packet.getLength());
System.out.println("Server receive data:" + new String(receiveMsg));
// 设置数据报包的数据缓冲区
packet.setData(msg, 0, msg.length);
socket.send(packet);
}
}
}
4.关于DatagramSocket构造函数
DatagramSocket():构造数据报套接字并将其绑定到本地主机上任何可用的端口,通常用于客户端编程;
DatagramSocket(int port):创建数据报套接字并将其绑定到本地主机上的指定端口,通常用于服务
端编程;
DatagramSocket(int port, InetAddress localAddr):创建数据报套接字,将其绑定到指定的本地地址。通常用于
一台主机上拥有多个IP地址的时,可以设置要绑定的本地IP地址;
1.消息为什么会丢失?
UDP套接字保留了消息边界,所以DatagramSocket的每一次receive()的调用最多只接收一次send()方法所发送的数据。但是,
需要注意的是,一个UDP套接字所接收的数据存放在一个消息队列中,每个消息都关联了其源地址信息。每次receive()调用只
返回一条消息。然而,如果receive( )方法传入的数据报包的数据缓冲区长度为n,而接收队列中第一条消息长度大于n,则receive()
方法直返回这条信息的前n个字节。超出部分的其他字节都将自动被丢弃,并且也没有任何消息丢失的提示!
(通俗的说就是:当receive( )传入的数据报包的数据缓冲区长度为n,如果接收的消息长度大于n时,那么receive()方法只返回这条消息的前n个字节,
超出部分将自动的被丢弃,并没有任何提示)
出于这个原因,接收者应该提供一个有足够大的数据缓冲区的DatagramPacket实例,当调用receive( )方法时以完整的接收所允许的最大长度消息。
一个DatagramPacket实例中所运行传输的最大数据量为65507个字节(即UDP数据报文包所能负载的最多数据)。
2.如何才能正确的接收消息?
UDP其中有最重要的一点,就是每一个DatagramPacket实例都包含一个内部消息长度值,而该实例只要一接收到新消息,这个内部消息长度值就会改变
(用来反映实际接收的消息字节数)。如果一个应用程序使用同一个DatagramPacket实例并多次调用receive( )方法,每次调用前必须将内部消息长度重置为
数据缓冲区的实际长度(即调用DatagramPacket.setLength( )方法,这里好像如果不重置也没有什么影响)。
对于新手来说一个潜在的问题就是:DatagramPacket类的getData( )方法,该方法总是返回缓冲区的原始大小,忽略了实际数据的内部偏移量和内部长度信息。
例如,假设buf是一个长度为20的字节数据,其在初始化时已使每个字节中存放了该字节在数组中的索引:
同时假设dg是一个DatagramPacket实例,我们将dg的缓冲区设置为buf数组中间10个字节:
dg.setData(buf,5,10);
假设dgSocket是一个DatagramSocket的实例,某人向dgSocket发送了一个包含以下内容的8个字节信息:
此时调用dg.getData( )方法将返回buf字节数组的原始引用,其内容变为:
大家可以看到buf数组中只有索引5~12的字节被修改,一般而言,我们需要同时使用getOffset( )和getData( )方法来访问刚接收到的数据,我们可以将
接收到的数据复制到一个单独的字节数组中。
在Java 1.6中,我们可以使用:
byte[] receive = Array.copyOfRange(dg.getData( ) , dg.getOffset( ), dg.getOffset( )+dg.getLength( ) )
以下是我写的一个关于正确接收UDP套接字消息的例子:
客户端类:
public class RealMsgClient
{
public static void main(String[] args) throws IOException
{
//发送的消息字节
byte[] msg = new String("1234").getBytes();
InetAddress inetAddr = InetAddress.getLocalHost();
DatagramSocket client = new DatagramSocket();
DatagramPacket sendPacket = new DatagramPacket(msg, msg.length,
inetAddr, 8888);
client.send(sendPacket);
}
}
服务端类:
public class RealMsgServer
{
public static void main(String[] args) throws IOException
{
byte[] msg = new String("ABCDEFGHIG").getBytes();
DatagramSocket server = new DatagramSocket(8888);
DatagramPacket receivePacket = new DatagramPacket(msg, msg.length);
// 设置接收数据报包的缓冲区
receivePacket.setData(msg, 3, 4);
server.receive(receivePacket);
/*
* 场景一: 使用错误的方式接收数据
* 原因:getData()方法忽略了消息内部偏移量(offset)和内部长度(length)
*/
byte[] receiveByte1 = receivePacket.getData();
/*
* 场景二: 使用正确的方式接收数据
*/
byte[] receiveByte2 = Arrays.copyOfRange(receivePacket.getData(),
receivePacket.getOffset(), receivePacket.getOffset()
+ receivePacket.getLength());
System.out.println("使用错误的方式接收数据的结果:"+new String(receiveByte1));
System.out.println("使用正确的方式接收数据的结果:"+new String(receiveByte2));
}
}
以上就是我对TCP和UDP的一些总结。总体来说,唯一遗憾的就是:关于UPD重置内部消息长度问题,我自己实验了以下,感觉不重置也没有什么问题,如果有知道的同学,可以给我留言,谢谢!