文章目录

  • 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{@Overridepublic 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{@Overridepublic 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接受其他数据报时不会截断数据报