Datagram(数据报)是一种尽力而为的传送数据的方式,它只是把数据的目的地记录在数据包中,然后就直接放在网络上,系统不保证数据是否能安全送到,或者什么时候可以送到,也就是说它并不保证传送质量。
1 UDP套接字
数据报(Datagram)是网络层数据单元在介质上传输信息的一种逻辑分组格式,它是一种在网络中传播的、独立的、自身包含地址信息的消息,它能否到达目的地、到达的时间、到达时内容是否会变化不能准确地知道。它的通信双方是不需要建立连接的,对于一些不需要很高质量的应用程序来说,数据报通信是一个非常好的选择。还有就是对实时性要求很高的情况,比如在实时音频和视频应用中,数据包的丢失和位置错乱是静态的,是可以被人们所忍受的,但是如果在数据包位置错乱或丢失时要求数据包重传,就是用户所不能忍受的,这时就可以利用UDP协议传输数据包。在Java的java.net包中有两个类DatagramSocket和DatagramPacket,为应用程序中采用数据报通信方式进行网络通信。
使用数据包方式首先将数据打包,Java.net包中的DategramPacket类用来创建数据包。数据包有两种,一种用来传递数据包,该数据包有要传递到的目的地址;另一种数据包用来接收传递过来的数据包中的数据。要创建接收的数据包,通过DatagramPackett 类的方法构造:
public DatagramPacket(byte ibuft[],int ilength)
public DatagramPacket( byte ibuft[],int offset ,int ilength)
ibuf[]为接受数据包的存储数据的缓冲区的长度,ilength为从传递过来的数据包中读取的字节数。当采用第一种构造方法时,接收到的数据从ibuft[0]开始存放,直到整个数据包接收完毕或者将ilength的字节写入ibuft为止。采用第二种构造方法时,接收到的数据从ibuft[offset]开始存放。如果数据包长度超出了ilength,则触发IllegalArgument-Exception。不过这是 RuntimeException,不需要用户代码捕获。示范代码如下:
byte[ ] buffer=new byte[8912];
DatagramPacket datap=new DatagramPacket(buffer ,buffer.length( ));
创建发送数据包的构造方法为:
public DatagramPacket(byt ibuf[],int ilength,InetAddrss iaddr,int port)
public DatagramPacket(byt ibuf[],int offset , int ilength,InetAddrss iaddr,int port)
iaddr为数据包要传递到的目标地址,port为目标地址的程序接受数据包的端口号(即目标地址的计算机上运行的客户程序是在哪一个端口接收服务器发送过来的数据包)。ibuf[]为要发送数据的存储区,以ibuf数组的offset位置开始填充数据包ilength字节,如果没有offset,则从ibuf数组的0位置开始填充。以下示范代码是要发送一串字符串:
数据包也是对象,也有操作方法用来获取数据包的信息,这是很有用的。其方法如下:
- public InetAddress getAddress() 如果是发送数据包,则获得数据包要发送的目标地址,但是如果是接收数据包则返回发送此数据包的源地址。
- public byte[]getData()
返回一个字节数组,其中是数据包的数据。如果想把字节数组转换成别的类型就要进行转化。如果想转化成String类型,可以进行以下的处理,设DatagramPacket datap为:
String s = new String(datap.getbytes());
- public int getLength() 获得数据包中数据的字节数。
- pubic int getPort( ) 返回数据包中的目标地址的主机端口号。
发送和接收数据包还需要发送和接收数据包的套接字,即DatagramSocket对象,DatagramSocket套接字在本地机器端口监听是否有数据到达或者将数据包发送出去。其构造方法如下。
- public DatagramSocket() 用本地机上任何一个可用的端口创建一个套接字,这个端口号是由系统随机产生的。使用方法如下:
这种构造方法没有指定端口号,可以用在客户端。如果构造不成功则触发SocketException异常。- public DatagramSocket(int port) 用一个指定的端口号port创建一个套接字。 当不能创建套接字时就抛出SocketException异常,其原因是指定的端口已被占用或者是试图连接低于1024的端口,但是又没有具备权限。 2 实例:利用DatagramSocket查询端口占用情况 我们可以利用这个异常探查本地机的端口号有没有服务。见示例12-9。 【程序源代码】
- 【程序输出结果】
【程序注解】
在第11~19行我们用for循环以端口号为参数实例化DatagramSocket,其中端口号从1024到65535。如果在实例过程中出错,会抛出SocketException异常。我们根据这个异常就可以判断出哪些端口被占用,哪些还是空闲的。值得一提的是,我们在实例化了DatagramSocket后,调用了close()关闭它。作为一种好的作风,应该遵循。端口号在1024以下的系统可能会用到,比如HTTP默认为80端口,FTP默认为21端口,等等,所以我们从1024端口开始探查。
套接字对象也有相应的方法,例如发送数据包的方法还有接收数据包的方法,介绍如下。- pubic void close() 当我们创建一个套接字后,用该方法关闭套接字。
- public int getLocalPort() 返回本地套接字的正在监听的端口号。
- public void receive(DatagramPacket p) 从网络上接收数据包并将其存储在DatagramPacket对象p中。p中的数据缓冲区必须足够大,receive()把尽可能多的数据存放在p中,如果装不下,就把其余的部分丢弃。接收数据出错时会抛出IOException异常。
- public Void Send(DatagramPacket p) 发送数据包,出错时会发生IOException异常。
下面,我们详细解释在Java中实现客户端与服务器之间数据报通信的方法。
应用程序的工作流程如下:
(1)首先要建立数据报通信的Socket,我们可以通过创建一个DatagramSocket对象实现它,在Java中DatagramSocket类有如下两种构造方法:
- public DatagramSocket() 构造一个数据报socket,并使其与本地主机任一可用的端口连接。若打不开socket则抛出SocketException异常。
- public DatagramSocket(int port) 构造一个数据报socket,并使其与本地主机指定的端口连接。若打不开socket或socket无法与指定的端口连接则抛出SocketException异常。
(2)创建一个数据报文包,用来实现无连接的包传送服务。每个数据报文包用DatagramPacket类创建, DatagramPacket对象封装了数据报包数据、包长度、目标地址和目标端口。客户端要发送数据报文包,要调用DatagramPacket类以如下形式的构造函数创建DatagramPacket对象,将要发送的数据和包文目的地址信息放入对象之中。DatagramPacket(byte bufferedarray[],int length,InetAddress address,int port)即构造一个包长度为length的包传送到指定主机指定端口号上的数据报文包,参数length必须小于等于 bufferedarry.length。
DatagramPacket类提供了4个类获取信息:
- public byte[] getData() 返回一个字节数组,包含收到或要发送的数据报中的数据。
- public int getLength() 返回发送或接收到的数据的长度。
- public InetAddress getAddress() 返回一个发送或接收此数据报包文的机器的IP地址。
- public int getPort() 返回发送或接收数据报的远程主机的端口号。 (3)创建完DatagramSocket和DatagramPacket对象,就可以发送数据报文包了。发送是通过调用 DatagramSocket对象的send方法实现,它需要以DatagramPacket对象为参数,将刚才封装进DatagramPacket对象中的数据组成数据报发出。 (4)当然,我们也可以接收数据报文包。为了接收从服务器返回的结果数据报文包,我们需要创建一个新的 DatagramPacket对象,这就需要用到DatagramPacket的另一种构造方式DatagramPacket(byte bufferedarray[],int length),即只需指明存放接收的数据报的缓冲区和长度。调用DatagramSocket对象的receive()方法完成接收数据报的工作,此时需要将上面创建的DatagramPacket对象作为参数,该方法会一直阻塞直到收到一个数据报文包,此时DatagramPacket的缓冲区中包含的就是接收到的数据,数据报文包中也包含发送者的IP地址,发送者机器上的端口号等信息。 (5)处理接收缓冲区内的数据,获取服务结果。 (6)当通信完成后,可以使用DatagramSocket对象的close()方法关闭数据报通信Socket。当然, Java会自动关闭Socket,释放DatagramSocket和DatagramPacket所占用的资源。但是作为一种良好的编程习惯,还是要显式地予以关闭。 3 实例:利用数据报通信的C/S程序 示例12-10给出了一个简单的利用数据报通信的客户端程序,它能够完成与服务器简单的通信。 【程序源代码】
- 【程序输出结果】
- 【程序注解】 第13行和第15行分别实例化了一个DatagramSocket对象receiveSocket 和一个DatagramPacket对象receivePacket,都是通过调用各自的构造函数实现的,为建立服务器做好准备。在while这个永久循环中,receiveSocket这个套接字始终尝试receive()方法接收DatagramPacket数据包,当接收到数据包后,就调用 DatagramPacket的一些成员方法显示一些数据包的信息。在程序中调用了getAddress()获得地址,getPort()方法获得客户端套接字的端口,getData()获得客户端传输的数据。注意getData( )返回的是字节数组,我们把它转化为字符串显示。在第27~33行我们对程序中发生的SocketException和IOException异常进行了处理。 示例12-11是UDP客户端的程序。 【程序源代码】
【程序输出结果】
send the data: hello !this is the clientsend the data: hello !this is the client
【程序注解】
第13行用DatagramSocket的构造函数实例化一个发送数据的套接字 sendSocket。第17~18行实例化了一个DatagramPacket,其中数据包要发往的目的地是163.121.139.20,端口是 5000。当构造完数据包后,就调用send( )方法将数据包发送出去。
4 组播套接字
在Java中,可以用java.net.MulticastSocket类组播数据。组播套接字是DatagramSocket的子类,定义如下:
public class MulticastSocket extends DatagramSocket
构造方法有两个:
public MulticastSocket ( ) throws SocketException
public MulticastSocket (int port ) throws SocketException
以上两个方法都是创建组播套接字,第一个方法没有端口号,第二个指定了端口号。
常用的方法如下:- public void joinGroup(InetAddress address) throws IOException
建立了MulticastSocket对象后,为了发送或者接收组播包,必须用joinGroup方法加入一个组播组。若加入的不是组播地址将触发IOException异常。
- public void leaveGroup(InetAddress address)throws IOException
如果不想接收组播包了,就调用leaveGroup方法。程序就发信息到组播路由器,通知它向此用户发送数据。若想离开的地址不是组播地址就触发IOException异常。
- public void send(DatagramPacket packet, byte, ttl) throws IOExceptin
发送组播包的方法与DatagramSocket发送数据相似。其中ttl是生存时间,大小在0~255之间。
- public void receive(DatagramPacket p) 与DatagramSocket的接收方法没有差别。
- public void setTimeToLive(int ttl )throws IOException 设置套接字发出的组播包中的默认ttl数值。
- public int getTimeToLive( ) throws IOException 返回ttl数值。 使用组播套接字发送数据的过程是首先用MulticastSocket()构造器创建MulticastSocket类,然后利用MulticastSocket类的joinGroup()方法加入一个组播组,之后创建DatagramPacket数据包,最后调用 MulticastSocket类的send()方法发送组播包。 发送组播包的代码如下:
- 使用组播套接字接收数据的过程是首先用MulticastSocket()构造器创建MulticastSocket类,然后利用MulticastSocket类的joinGroup()方法加入一个组播组,之后用receive()方法接收组播包。我们发现其过程与UDP包的过程很相似,区别是要加入一个组播组。 5 实例:组播套接字C/S程序 下面的程序示例12-12说明了组播套接字的基本用法。 【程序源代码】
- 【程序注解】 服务器程序由3个类组成:QuoteServerThread, MulticastServerThread和MulticastServer。它们的关系是:QuoteServerThread继承自线程类,而 MulticastServerThread类继承自类QuoteServerThread。这个程序主要的部分在QuoteServerThread和 MulticastServerThread。QuoteServerThread类有两个构造函数,其中在构造函数QuoteServerThread (String name)中,初始化了DatagramSocket套接字并打开了文件one-liners.txt,在这个文件中存有服务器发送的字符串。 在QuoteServerThread类的run()函数中,服务器端套接字接收来自客户端的数据包,并从文件中读取数据,把信息发给客户端。 MulticastServerThread类中重载了run( )方法,实现的功能基本相同,在发完服务器的信息后,用sleep( )函数停止处理了一个随机的时间。 在MultiServer类中,用 new MulticastServerThread().start()开始服务器线程。我们现在只是关注其基本思想。 示例12-13是UDP组播的客户端程序。 【程序源代码】
- 【程序输出结果】
【程序注解】
在客户端的main()方法中,第13行实例化了一个MulticastSocket对象 socket,然后用join()方法加入了组播组136.122.133.1。在for循环中接收了5个数据包,并把数据包中的内容显示出来(第 18~25行)。最后在第27行离开组播组(leaveGroup()),第28行关闭套接字。