文章目录
- 1. 简介
- 2. UDP客户端
- 3. UDP服务器
- 4. DatagramPacket类
1. 简介
Java中的UDP实现分为两个类:DatagramPacket和DatagramSocket。DatagramPacket类将数据字节填充到UDP包汇总,这称为数据报,由你来解包接收的数据报。DatagramSocket可以收发UDP数据报。为发送数据,要将数据放到DatagramPacket中,使用DatagramPacket来发送这个包。要接受数据,可以从DatagramSocket中接受一个DatagramSocket对象,然后检查这个包的内容。Socket本身非常简单,在UDP种,关于数据报的所有信息(包括发送的目标地址)都包含在包本身中。Socket只需要了机在哪个本地端口监听或发送。这种职责划分和TCP使用的Socket和ServerSocket有所不同,首先,UDP没有两台主机间唯一连接的概念。一个Socket会收发所有指向指定端口的数据,而不需要知道对方时哪一个远程主机。一个DatagramPacket可以从多个独立主机收发数据。与TCP不同这个Socket并不专用于一个连接。事实上,UDP没有任何两台主机之间连接的概念,它只知道单个数据报。要确定由谁发送什么数据,这个时应用程序的责任。其次,TCP socket把网络连接看作流:通过从Socket得到的输入和输出流来接受数据,其次TCP socket把网络节点看作是流:通过从Socket得到的输入和输出流来收发数据。UDP不支持这一点,你处理的总是单个数据报包,填充在一个数据报的所有数据会以一个包的形式进行发送,这些数据作为一个组要么全部接受,要么完全丢失。一个包不一定与下一个包相关。给定两个包,数据报回尽可能地传递到接收方。
2. UDP客户端
这里我们还是拿美国国家标准与技术研究院的daytime服务器举例子。这里使用的传输层协议是UDP。首先在端口0打开一个Socket
DatagramSocket socket= new DatagramSocket(0);
只需要指定一个本地端口,Socket并不知道远程服务器是什么。通过指定端口为0,就是在请求Java为你随机选择一个可用的端口。下面使用SetTimeout()方法在连接上设置一个超时时间。单位为毫秒
socket.setSoTimeout(10000);
超时对于UDP比TCP更重要,因此TCP中会导致IOException异常的很多问题在UDP中只会悄无声息地失败。接下来需要建立数据包。需要建立两个数据包,一个是要发送的数据包,另一个是需要接受的数据包。
InetAddress host=InetAddress.getByName("time.nist.nov");
DatagramPacket request=new DatagramPacket(new byte[1],1,host,13);
//如果接受的数据大小超过1kb,多出的数据会被自动截断
byte[] data=new byte[1024];
DatagramPacket response= new DatagramPacket(data,data.length);
现在已经准备就绪,首先在这个Socket发送数据包,然后接受响应:
socket.send(request);
socket.receive(response);
最后从响应中提取字节,将它们转换为可以显示给最终用户的字符串:
String daytime=new String(response.getData(),0,response.getLength,"US-ASCII");
System.out.println(daytime);
构造函数以及send()和receive()方法都可能抛出一个IOException,且DatagramSocket实现了Autocloseable,下面是完整代码:
public class QuizCardBuilder {
public static void main(String[] args) {
try(DatagramSocket socket=new DatagramSocket(0)){
socket.setSoTimeout(10000);
InetAddress address=InetAddress.getByName("time.nist.gov");
DatagramPacket request=new DatagramPacket(new byte[1],1,address,13);
DatagramPacket response=new DatagramPacket(new byte[1024],1024);
socket.send(request);
socket.receive(response);
String result=new String(response.getData(),0,response.getLength(),"US-ASCII");
System.out.println(result);
}catch (IOException e){
e.printStackTrace();
}
}
}
3. UDP服务器
UDP服务器几乎遵循与UDP客户端同样的模式,只不过通常在发送之前会先接收,而且不会选择绑定的匿名端口,与TCP不同,并没有单独的DatagramServerSocekt类。现在我们实现一个上面的简单的daytime服务器,首先在一个已知的端口上打开一个数据报Socket。对于daytime协议,这个端口为13:
DatagramSocket socket=new DatagramSocket(13);
接下来创建一个将接收请求的数据包,要提供一个将存储如站数据的byte数组,数组中的偏移量,以及要存储的字节数,
DatagramPacket request =new DatagramPacket(new byte[1024],0,1024);
然后接收这个数据包
socket.receive(request);
这个调用会被无限阻塞,直到一个UDP数据包到达13端口,如果有UDP数据包到达,java会将这个数据填充到byte数组,receive()方法返回。然后创建一个响应包,包括四个部分:要发送的原始数据、待发送的原始数据的字节数、要发送到的主机,以及发送到该主机上哪个端口。
String daytime= new Data().toString()+"\r\n";
byte[] data=daytime.getBytes("US-ASCII");
InetAddress host=request.getAddress();
int port = request.getPort();
DatagramPacket response=new DatagramPacket(data,data.length,host,port);
最后发送数据即可,下面是完整的服务器代码
public class QuizCardBuilder {
public static void main(String[] args) throws InterruptedException {
Thread server= new Thread(new server());
Thread client= new Thread(new client());
server.start();
Thread.sleep(1000);
client.start();
}
}
class client implements Runnable{
@Override
public void run() {
try(DatagramSocket socket=new DatagramSocket(0)){
socket.setSoTimeout(10000);
InetAddress address=InetAddress.getByName("localhost");
DatagramPacket request=new DatagramPacket(new byte[1],1,address,8080);
DatagramPacket response=new DatagramPacket(new byte[1024],1024);
socket.send(request);
socket.receive(response);
String result=new String(response.getData(),0,response.getLength(),"US-ASCII");
System.out.println(result);
}catch (IOException e){
e.printStackTrace();
}
}
}
class server implements Runnable{
@Override
public void run() {
try(DatagramSocket socket=new DatagramSocket(8080)){
while(true){
DatagramPacket request=new DatagramPacket(new byte[1024],1024);
socket.receive(request);
String daytime=new Date().toString();
byte[] data=daytime.getBytes("US-ASCII");
DatagramPacket datagramPacket=new DatagramPacket(data,data.length,request.getAddress(),request.getPort());
socket.send(datagramPacket);
}
}catch (IOException e){
e.printStackTrace();
}
}
}
这个例子可以看出,UDP服务器与TCP服务器不同,往往不是多线程的,它们通常不会对某一个客户做太多工作,而且不会阻塞来等待另一端响应,因为UDP从来不会报告错误。对于UDP服务器来说,除非为了准备响应需要做大量耗费时间的工作,否则使用一种迭代方法就可以了。
4. DatagramPacket类
UDP数据报是基于IP数据报建立的,只向其底层IP数据报添加了很少的一点内容。如下图,UDP首部只向IP首部天际了8个字节。UDP首部包括源和目标端口号,IP首部之后所有内容的长度,以及一个可选的校验和。由于端口号以2字节无符号整数给出,因此每台主机有65536个不同的UDP端口可以使用。它们与每台主机的65536不同的TCP端口截然不同。因为长度也是一2字节无符号整数给出,所以数据报中的字节数不能超过65536-8字节。不过,这与IP首部中的数据报的长度字段是冗余的,它将数据报限制为65467-65507之间(具体大小取决于IP首部)。检验和字段是可选的,应用层程序不使用这个校验和,页无法访问这个校验和。如果数据的校验失败,那么底层网络软件会悄悄丢掉这个数据报。发送方或接受方都不会得到这个通知。毕竟UDP是不可靠的。在Java中,UDP数据报用品DatagramPacket类的实例表示:
- 构造函数
取决于数据包用于发送数据还是接收数据。在这里6个构造函数都接受两个参数,一个时保存数据报数据的byte数组,另一个参数时该数组中用于数据报数据的字节数。希望接收数据报时,只需要提供这两个参数。当Socket从网络接收数据报时,它将数据报的数据存储在DatagramPacket对象的缓存区数组中,直到达到你指定的长度。第二组DatagramPacket构造函数用于创建通过网络发送的数据报。与前一组一样,这些构造函数需要一个缓冲区数组和一个长度,另外还需要指定数据包发去的地址和端口。
接收数据报的构造函数
public DatagramPacket(byte[] buffer, int length)
public DatagramPacket(byte[] buffer, int offset, int length)
构造函数不关心缓冲区多大,甚至它希望你创建几M得DatagramPacket。不过,底层网络软件却不那么宽容,大多数底层UDP实现都不支持超过8192字节数据的数据报。事实上,很多操作系统不支持超过8KB的UDP数据报,否则就会将更大的数据报截断、分解或丢掉。如果数据报太大,而导致网络将其截断或者丢弃,java会收不到任何通知。
发送数据的构造函数
public DatagramPacket(byte[] data, int length, InetAddress destination ,int port)
public DatagramPacket(byte[] data, int offset, int length, InetAddress destination , int port)
public DatagramPacket(byte[] data, int length, SocketAddress destination)
public DatagramPacket(byte[] data, int offset, SocketAddress destination)
每个构造函数都创建一个发往另一台主机的DatagramPacket。
在一个包中填充多少数据才合适?
这其实取决于实际情况,有些协议规定了包大小。如果网络非常不可靠,如分组无线电网络,则要选择较小的包,因为这样可以减少在传输中被破坏的可能性。另一方面,非常快速而可靠的LAN应当使用尽可能大的包。对于很多类型的网络,8KB字节往往是一个很好的折中方案。
- get方法
DatagramPacket有6个获取数据报不同部分的方法:这些部分包括具体的数据以及首部的几个字段。这些方法主要用于从网络接收的数据报:
public InetAddress getAddress()
该方法返回一个InetAddress对象,其中包含远程主机的地址。如果数据报时从Internet接收的,返回的地址是发送该数据报的机器地址(源地址)。另一方面,如果数据报是本地创建的,要发送到一个远程机器,那么这个返回会返回数据报将发往的那个主机地址。这个方法常用于确定发送UDP数据报的主机地址,使接收方可以回复
public int getPort()
该返回返回远程端口。如果数据从Internet接收,这就是发送包的主机上的端口。如果数据报是本地创建的,要发送一个远程主机,那么这就是远程机器上包发往的目标端口
public SocketAddress getSocketAddress()
该方法返回一个SocketAddress对象,包含远程主机的IP地址和端口。如果数据报从Internet接收的,返回的地址就是发送该数据报的机器的地址。如果是本地创建的,要发送到远程主机,这个返回数据报发往的主机地址。此外,如果你使用非阻塞I/O,DatagramChannel类可以接收一个SocketAddress,而不接收单独的InetAddress和端口
public byte[] getData()
返回一个byte数组,其中包含数据报中的数据。为了能够在你的程序中使用,通常必须将这些字节转换为其他的某种数据形式。一种方法是将byte数组转换为一个String。
String s=new String(dp.getData() ,"UTF-8")
如果数据报不包含文本,那么将它转换为java数据会更加困难。一种方法是将getData()返回的Byte数组转换一个ByteArrayInputStream。
//指定offset和length的原因是,返回的数组可能有额外的空间没利用到
InputStream in= new ByteArrayInputStream(packet.getData(),packet.getOffset(),packet.getLength());
然后ByteArrayInputStream可以串链到DataInputStream,接下来可以使用DataInputStream得readInt()、readLong()、readChar()及其他方法读取数据。
public int getLength()
该方法返回数据报中数据的字节数。
public int getOffset()
对于getData()返回的数组,这个方法会返回该数组的一个位置,即开始填充数据报数据的那个位置。
下面的程序使用了上面介绍的所有get方法
public static void main(String[] args) throws InterruptedException {
String s="This ia a test";
try{
byte[] data =s.getBytes("UTF-8");
InetAddress ia=InetAddress.getByName("www.ibiblio.org");
int port=7;
DatagramPacket dp=new DatagramPacket(data,data.length,ia,port);
System.out.println("This packet is adderssed to "+ dp.getAddress()+" on port "+dp.getPort());
System.out.println("There are "+dp.getLength()+" bytes of data in the packet");
System.out.println(new String(dp.getData(),dp.getOffset(),dp.getLength(),"UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
} catch (UnknownHostException e) {
throw new RuntimeException(e);
}
}
- Set方法
Java还提供了几个方法,可以在创建数据报之后改变数据、远程地址和远程端口。如果创建和垃圾回收新DatagramPacket对象的时间会严重影响性能,这些方法就很重要。
public void setData(byte[] data)
该方法可以改变UDP数据报的有效载荷。如果要向远程主机发送大文件,可能就用到这个方法。你可以重复地发送相同的DatagramPacket对象,每次只改变数据
public void setData(byte[] data, int offset, int length)
这个重载的setData方法提供了另一个途径来发送大量的数据。与发送大量新数组不同,可以将所有数据放到一个数组中,每次发送一个部分。如下:
int offset=0;
DatagramPacket dp= new DatagramPacket(bigarray, offset, 512);
int bytesSent=0;
while(bytesSent < bigarray,length){
socekt.send(dp);
bytesSent+=dp.getLength();
int bytesToSend=bigarray.length-bytesSent;
dp.setData(bigarray,bytesSent,size);
public void setAddress(InetAddress remote)
该方法会修改数据报发往的地址,这允许你将同一个数据报发送多个不同的接收方。
String s="Really Important Message";
byte[] data= s.getBytes("UTF-8);
DatagramPacket dp=new DatagramPacket(data ,data.length);
dp.setPort(2000);
int network="128.238.5.";
for (int host= 1;host <255 ;host++)
{
try{
InetAddress remote=InetAddress.getByName(network+host);
dp.setAddress(remote);
socket.send(dp);
}catch(IOException ex)
{
}
}
public void setPort(int port)
该方法会改变数据报发往的端口。
public void setAddress(SocketAddress remote)
该方法会改变数据包要发往的地址和端口,在回复时可以使用这个方法,例如下面代码将接收一个数据报包,用包含字符串的包响应同一个地址
DatagramPacket input= new DatagramPacket(new byte[8192] ,8192);
socekt.receive(input);
DatagramPacket output=new DatagramPacket(" hello there".getBytes("UTF-8"),11) ;
SocketAddress address=input.getSocketAddress();
output.setAddress(address);
socket.send(output);
public void setLength(int length)
该方法会改变内部缓冲区汇总包含实际数据报数据的字节数,而不包括未填充的数据的空间。这个方法在接收数据报时很有用,当接收到数据报时,其长度设置为入站数据的长度。这表示如果试图在同一个DatagramPacket中接收另一个数据报,那么会限制第二个数据报的字节数不能对于第一个数据报的字节数。也就是说,一旦接受了一个10字节的数据报,所有后续的数据报都将截断为10字节。一旦接受到9个字节数据报,所有后续的数据报都会截断到9字节。有了这个方法,我们可以修改缓冲区的长度,这样使用相同的Datagrampacket接受其他数据报时不会截断数据报