14.6 基于UDP协议的网络编程
UDP协议是一种不可靠的网络协议,它在通信实例的两端各建立一个Socket,但这两个Socket之间并没有虚拟链路,这两个Socket只负是发送、接受数据报的对象。Java提供了DatagramSocket对象作为基于UDP协议的Socket,使用DatagramPacket代表发送、接受的数据。
一、UDP协议基础
1、UDP协议是英文User Datagram Protocol的缩写,即用户数据报协议,主要用于支持那些需要在计算机之间传输数据的网络连接。
2、UDP协议是一种面向非连接的协议,面向非连接指的是在正式通信前不必与对方法先建立连接,不管对方状态就直接发送。至于对方是否可以接收到这些数据内容,UDP协议无法控制,因此说UDP协议是一种不可靠的协议。UDP协议适用于依一次只传送少量的数据、对可靠性要求不高的环境。
3、UDP协议直接位于IP协议之上,实际上IP协议属于OSI参考模型的网络层协议,而UDP,TCP属于网络传输层。
4、因为UDP协议是面向非连接的协议,没有建立连接过程,因此它的效率很高;但是可靠性不如TCP协议。
5、UDP协议主要作用是完成网络数据流和数据报之间的转换——在信息的发送端,UDP协议将网络数据流封装成数据报,然后将数据报发送出去;在信息接收端,UDP协议将数据报转换成实际的数据内容。
6、UDP协议、TCP协议简单对比
(1)TCP协议:可靠,传输大小无限制,但是需要连接建立时间,差错控制开销大。
(2)UDP协议:不可靠,差错控制开销小,传输大小控制在64K一下,不需要建立连接。
二、DatagramSocket发送、接受数据
DatagramSocket本身只是码头,不维护状态,不能产生IO流,它唯一的作用就是接受和发送数据报。
2.1 DatagramSocket构造器
(1)DatagramSocket(): 创建一个DatagramSocket实例,并将对象绑定到本地计算机默认的IP地址,本机所有可用端口中随机选择某个端口。
(2)DatagramSocket(int port) :创建一个DatagramSocket实例,并将该对象绑定到本机默认IP地址、本机指定端口。
(3)DatagramSocket(int port,InetAddress laddr):创建一个DatagramSocket实例,并将该对象绑定到指定IP地址、指定端口。
通常在创建服务器时,创建指定端口的DatagramSocket实例——这样保证其他客户端可以将数据发送到该服务器。
2.2 发送、接受数据的方法
(1)receive(DatagramPacket p):从该DatagramSocket对象接收数据报
(2)send(DatagramPacket p):以该DatagramSocket对象向外发送数据报
使用DatagramSocket发送数据时,DatagramSocket并不知道将该数据发送到哪里,二十有DatagramPacket自身决定数据报的目的地。就像码头不知道每个集装箱的目的地,码头只是将这些集装箱发送出去,而集装箱本身包含了该集装箱的目的地。
2.3 DatagramPacket的构造器
(1)DatagramPakcet(byte[] buf,int length):以一个空数组来创建DatagramPacket对象,该对象的作用是接收DatagramSocket中的数据放入buf中,最多放入length个字节。
(2)DatagramPacket(byte[] buf,int length,InetAddress addr,int port): 以一个包含数据的数组来创建DatagramPacket发送对象,创建该DatagramPacket对象时还指定了IP指定和端口---这就决定了该数据包的目的地。
(3)DatagramPacket(byte[] buf,int offset,int length): 以一个空数组来创建DatagramPacket对象,并指定接收到的数据放入buf数组中从offset开始,最多放length个字节。
(4)DatagramPacket(byte[] buf,int offset,int length,InetAddress addr,int port):创建DatagramPacket发送对象,指定发送buf数组中从offset开始,总共length字节的数组。
当Client/Server程序使用UDP协议时,实际上并没有明显的服务器端和客户端,因为双发都需要建立一个DatagramSocket对象,用来接收或发送数据报,然后使用DatagramPacket对象作为传输数据的载体。通常固定IP地址、固定端口的DatagramSocket对象所在的程序被称为服务器,因为该DatagramSocket可以主动接受客户端数据。
在接受数据之前,应该采用上面(1)/(3)构造器生成一个DatagramPacket对象,给出接受数据的字节数组和长度。然后调用DatagramSocket的receive()方法等待数据报到来,receive()将一直等待(该方法会阻塞调用该方法的线程),直到收到一个数据报为止。代码如下:
//创建一个接受数据的DatagramPacket对象
var packet=new DatagramPacket(buf,256);
//接受数据报
socket.receive();
在发送数据之前,调用(2)/(4)构造器,此时的字节数据里存放了想发送的数据。除此之外,话需要给出完整的目的地地址,包括IP地址和端口号。发送数据通过DatagramSocket的send()方法实现,send()方法更具数据报的目的地地址来寻径以传送数据。代码如下:
//创建一个发送数据的DatagramPacket对象
var packet=new DatagramPacket(buf,length,address,port);//此时buf已经装好数据
//发送数据
socket.send(packet);
注意:DatagramPacket还有一个getData()方法,该方法可以返回DatagramPacket对象里封装的字节数组。但是我们可以直接访问传给DatagramPacket构造器的字节数组,无需调用该方法,这样显得这个设计有点多余。
2.4 获取数据发送者的ip地址和端口
当服务器(或客户端)接收到了一个DatagramPacket对象之后,如果想向该数据报的发送者“反馈”一些信息,但由于UDP是相面非链接的,所以接收者并不知道每个数据报由谁发送过来,但程序可以调用DatagramPacket的如下三个方法来获取发送者的IP地址和端口
- InetAddress getAddress(): 当程序准备发送此数据报时,该方法返回此数据报对应目标机器的IP地址;当程序刚收到一份数据报时,该该方法返回该数据报发送者的IP地址。
- int getPort(): 与getAddress类似,不过getAddress返回的是IP地址,而getPort返回的是端口.
- SocketAddress getSocketAddress(): 当程序准备发送此数据报时,该方法返回此数据报的目标SocketAddress(ip+port);当程序该接收到一个数据报时,该方法返回该数据报的发送主机的SocketAddress.
2.5 程序实例
本程序的服务器端使用了循环1000次来读取DatagramSocket中的数据报,每当读取到内容之后便向该数据包的发送者送回一条消息。服务器端的程序代码如下:
package UDP_NET;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class UdpServer
{
public static final int PORT=30000;
//定义每个数据报的大小最大为4kB
private static final int DATA_LEN=4096;
//定义接受网络端数据的字节数组
byte[] inBuff=new byte[DATA_LEN];
//以指定字节数组创建准备接受数据的DatagramPacket对象
private DatagramPacket inPacket=new DatagramPacket(inBuff,DATA_LEN);
//定义一个用于发送的DatagramPacket对象
private DatagramPacket outPacket;
//定义一个字符串数组,服务器端发送该数组的元素
String[] books=new String[]{
"疯狂Java讲义",
"轻量级Java EE企业应用实战",
"疯狂Android讲义",
"疯狂Ajax讲义"
};
public void init() throws IOException
{
try(
//创建DtatgramSocket对象
var socket=new DatagramSocket(PORT))
{
//采用循环接受数据
for(var i=0;i<1000;i++)
{
//读取socket中的数据,将读取到的数据放入inPacket
socket.receive(inPacket);
//判断inPacket.getData()和inbuff是否为同一个数组
System.out.println(inBuff==inPacket.getData());
//将接收到的内容装欢为字符串输出
System.out.println(new String(inBuff,0,inPacket.getLength()));
//从字符串数组中取出一个元素作为发送数据
byte[] sendData=books[i%4].getBytes();
//以指定字节数组作为发送数据,以刚接收到的inPacket的源作为目标SocketAddress创建DatagramPacket
outPacket=new DatagramPacket(sendData,sendData.length,inPacket.getSocketAddress());
//发送数据
socket.send(outPacket);
}
}
}
public static void main(String[] args) throws IOException
{
new UdpServer().init();
}
}
该程序可以接受1000个客户端发送过来的消息。
客户端代码采用不断循环读取用户键盘输入,每当读到用户输入的内容后就将该内容封装成DatagramPacket数据报,再将该数据报发送出去;接着把DatagramSocket中的数据读入接收用的DatagramPacket中(实际上是读入该DatagramPacket所封装的字节数组中)。客户端的程序代码如下:
package UDP_NET;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class UdpServer
{
public static final int PORT=30000;
//定义每个数据报的大小最大为4kB
private static final int DATA_LEN=4096;
//定义接受网络端数据的字节数组
byte[] inBuff=new byte[DATA_LEN];
//以指定字节数组创建准备接受数据的DatagramPacket对象
private DatagramPacket inPacket=new DatagramPacket(inBuff,DATA_LEN);
//定义一个用于发送的DatagramPacket对象
private DatagramPacket outPacket;
//定义一个字符串数组,服务器端发送该数组的元素
String[] books=new String[]{
"疯狂Java讲义",
"轻量级Java EE企业应用实战",
"疯狂Android讲义",
"疯狂Ajax讲义"
};
public void init() throws IOException
{
try(
//创建DtatgramSocket对象
var socket=new DatagramSocket(PORT))
{
//采用循环接受数据
for(var i=0;i<1000;i++)
{
//读取socket中的数据,将读取到的数据放入inPacket
socket.receive(inPacket);
//判断inPacket.getData()和inbuff是否为同一个数组
System.out.println(inBuff==inPacket.getData());
//将接收到的内容装欢为字符串输出
System.out.println(new String(inBuff,0,inPacket.getLength()));
//从字符串数组中取出一个元素作为发送数据
byte[] sendData=books[i%4].getBytes();
//以指定字节数组作为发送数据,以刚接收到的inPacket的源作为目标SocketAddress创建DatagramPacket
outPacket=new DatagramPacket(sendData,sendData.length,inPacket.getSocketAddress());
//发送数据
socket.send(outPacket);
}
}
}
public static void main(String[] args) throws IOException
{
new UdpServer().init();
}
}
上面的程序同样使用DatagramSocket发送、接受DatagramPacket,这些代码与服务器端代码基本相似。而客户端于服务器端的唯一区别在于:服务器端的IP地址、端口号是固定的,所以客户端可以直接将该数据发送给服务端,而服务器择需要根据接收到的数据报来决定“反馈”数据报的目的地。
使用DatagramSocket进行网络通信时,服务器端也无需知道每个客户端的状态,客户端把数据报发送到服务器端后,完全有可能立即退出。但不管客户端是否退出,服务器端都无法直到客户端的状态。
上面程序运行结果:
三、使用MulticastSocket实现多点广播(或多点发送)
3.1 多点广播介绍
DatagramSocket只允许数据报发送给指定目标地址,而MulticastSocket可以将数据报以广播的方式发送到多个客户端。
MulticastSocket:该类是DatagramSocket的子类,作用是可以将数据报以广播方式发送到多个通信实体。若要使用多点发送(多点广播),则需要让一个数据报标有一组目标主机地址,当数据报发出后,整个组的所有主机都能收到该数据报,不仅如此,MulticastSocket既可以接收广播也可以发送广播。
IP多点发送(多点广播)实现了将单一信息发送到多个通信实体(接收者),其思想是设置一组特殊网络地址作为多点广播地址,每一个多点广播地址都被看做一个组,通信实体需要在发送或者接收广播信息之前,加入到该组即可。同时IP协议为多点广播提供了这批特殊的IP地址,这些IP地址的范围是224.0.0.0 至 239.255.255.255。
从上图可以看出,MulticastSocket类是实现多点广播的关键,当MulticastSocket把一个DatagramPacket发送到多点广播的IP地址时,该数据将自动广播到加入该地址的所有MulticastSocket,MulticastSocket既可以将数据发送到多点广播的地址,也可以接受其他主机的广播信息。
3.2 MulticastSocket的构造器
MulticastSocket是DatagramSocket大的子类,即MulticastSocket是特殊的DatagramSocket。当要发送一个数据报时,可以使用随机端口创建一个MulticastSocket,也可以在指定端口创建MulticastSocket。MulticastSocket提供了如下三个构造器:
- MulticastSocket() :使用本机默认IP地址,随机端口来创建MulticastSocket对象
- MulticastSocket(int port):使用本机默认IP地址以及指定端口来创建MulticastSocket对象
- MulticastSocket(SocketAddress socketAddress):使用指定IP地址以及指定端接口来创建MulticastSocket对象
**Ps:若创建仅用于发送数据报的MulticastSocket对象,则使用默认IP地址,随机端口即可;反之,若创建用于接收数据报的MulticastSocket对象,则必须指定端口,否则发送方无法确定发送数据报的目标端口 **
3.2 MulticastSocket对象加入/脱离指定的多点广播的地址
创建了MulticastSocket对象后,还需要将MulticastSocket加入到指定的多点广播的地址:
1.joinGroup(InetAddress multicastAddr):将该MulticastSocket加入指定的多点广播地址
2.leaveGroup(InetAddress multicastAddr):让MulticastSocket离开指定的多点广播地址
3.3 使用指定网络接口/查询监听的网络接口
在某些系统上可能有多个网络接口:
1.setInterface():强制MulticastSocket使用指定的网络端口
2.getInterface():查询MulticastSocket监听的网络端口
3.4 发送/接受数据和设置广播信息的ttl
MulticastSocket是DatagramSocket的子类,因此继承了DatagramSocket发送和接受数据的方法:
(1)receive(DatagramPacket p):从该MulticastSocket对象接收数据报
(2)send(DatagramPacket p):以该MulticastSockett对象向外发送数据报
支持多点广播的MulticastSocketh还多一个seTimeToLive(int ttl)方法,可设置广播信息的ttl(Time-To-Live),该ttl参数用于设置数据报最多可以跨过多少个网络:
当TTL的值为 0 时,指定数据报应停留在本地主机中;
当TTL的值为 1 时,指定将数据报发送到本地局域网中;
当TTL 的值为 32 时,意味着只能将数据报发送到本站点的网络上;
当TTL 的值为 64 时,意味着数据报应被保留在本地区;
当TTL 的值为 128 时,意味着数据报应被保留在本大洲;
当TTL 的值为 255 时,意味着数据报可被发送到所有地方;
在默认情况下,TTL 的值为1。
3.5 程序实例
使用MulticastSocket进行多点广播时所有的通信实体都是平等的,他们将自己的数据报发送到多点广播IP地址,并使用MulticastSocket接受其他其他人发送的广播数据报。下面程序使用MulticastSocket实现了一个基于广播的多人聊天室。程序只需要一个MulticastSocket,两个线程,其中MulticastSocket既用于发送也用于接受;一个线程负责接受用户的键盘输入,并向MulticastSocket发送数据,另一个线程负责从MulticastSocket中读取数据。
package Multicast;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.MulticastSocket;
import java.util.Scanner;
//让该类实现Runnable接口,该类的实例可以作为线程的target
public class MulticastSocketTest implements Runnable
{
//使用常量作为本程序的多点广播IP地址
private static final String BROADCAST_IP="230.0.0.1";
//使用常量作为本程序多点广播目的地的端口
public static final int BRAODCAST_PORT=30000;
//定义每个数据报大小最大为4KB
private static final int DATA_LEN=4096;
//定义本程序的MulticastSocket实例
private MulticastSocket socket=null;
private InetAddress broadcastAddress=null;
private Scanner scan=null;
//定义接收网络数据的字节数组
byte[] inBuff=new byte[DATA_LEN];
//以指定字节数组创建准备接收数据的DatagramPacket对象
private DatagramPacket inPacket=new DatagramPacket(inBuff,inBuff.length);
//定义一个用于发送的DatagramPacket
private DatagramPacket outPacket=null;
public void init() throws IOException
{
try {
//创建键盘输入流
scan = new Scanner(System.in);
//用于接受和发送数据报
//该对象需要接收数据,所以有指定端口
socket=new MulticastSocket(BRAODCAST_PORT);//1
broadcastAddress=InetAddress.getByName(BROADCAST_IP);
//将该socket加入指定的多点广播地址
socket.joinGroup(broadcastAddress);//2
//设置本MulticastSocket发送数据报会被回送到本身
socket.setLoopbackMode(false);//3
//初始化发送用的DatagramSocket,包含一个长度为0的字节数组
outPacket=new DatagramPacket(new byte[0],0,broadcastAddress,BRAODCAST_PORT);
//启动本实例的run()方法z作为线程的执行体
new Thread(this).start();
//不断读取键盘的输入
while(scan.hasNextLine())
{
//将键盘输入字符串转换为字节数组
byte[] buff=scan.nextLine().getBytes();
//设置用于发送用的DatagramPacket里的字节数据
outPacket.setData(buff);
//发送数据报
socket.send(outPacket);
}
}
finally {
scan.close();
socket.close();
}
}
@Override
public void run()
{
try
{
while(true)
{
//读取socket中的数据,读到的数据放在inPacket所封装的字节数组中
socket.receive(inPacket);
//打印输出从socket中读取到的数据
System.out.println(new String(inBuff,0,inPacket.getLength()));
}
}
catch (IOException e)
{
e.printStackTrace();
try
{
if(socket!=null)
{
//让该Socket离开多点广播地址
socket.leaveGroup(broadcastAddress);
//关闭该Socket对象
socket.close();
}
System.exit(1);
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
public static void main(String[] args) throws IOException
{
new MulticastSocketTest().init();
}
}
init()方法里的代码1处先创建了一个MulticastSocket对象,由于需要使用该接口接受数据,所以未该MulticastSocket对象设置使用固定端口;代码2处将该MulticastSocket对象添加到指定的多点广播IP地址,代码3设置本MulticastSocket发送数据报会被回送到本身(技改MulticastSocket可以接收自己发送的数据报),其他发送/接收数据报和DatagramSocket没有什么区别。
可以看到运行效果: