UDP协议
英文:User Datagram Protocol
即:用户数据报协议。
不可靠,传输少量的数据(限制在64KB下),效率高,在两端建立socket
(负责发送和接收,无服务端和客户端的概念),位于传输层,而IP为网络层。
使用场景:网络游戏,视频会议,实时性高的情况。
主要作用:完成网络数据流和数据报之间的转换。在信息的发送端,UDP协议将网络数据流封装成数据报,然后将数据报发送出去,接收端为逆过程。
传输层和网络层有什么区别呢?
网络层(IP层)提供点到点的连接即提供主机之间的逻辑通信,传输层提供端到端的连接——提供进程之间的逻辑通信。
那上面的端对端,即:什么是端口呢
为了使运行不同操作系统的计算机的应用进程能够互相通信,就必须用统一的方法对TCP/IP体系的应用进程进行标志。
解决这个问题的方法就是在运输层使用协议端口号(protocol port number),或通常简称为端口(port)。
简单理解,IP的地址负责的是点到点,而仅仅通过IP来连接到对方的电脑是不够的,因为电脑中要很多的应用程序,到底是将数据传输给那个应用程序呢?我们还要需要端口(TCP/IP)来区分是哪一个应用程序。两者结合达到网络通信。
与TCP协议的比较
TCP
(Transmission Control Protocol)可靠的、面向连接的协议(eg:打电话)、传输效率低全双工通信(发送缓存&接收缓存)、面向字节流。使用TCP的应用:Web浏览器;电子邮件、文件传输程序。
UDP
(User Datagram Protocol)不可靠的、无连接的服务,传输效率高(发送前时延小),一对一、一对多、多对一、多对多、面向报文,尽最大努力服务,无拥塞控制。使用UDP的应用:域名系统 (DNS);视频流;IP语音(VoIP)。
java是怎么使用UDP协议达到网络通信的呢?
java使用DaatagramSocket
代表UDP
协议的socket
,它本身只是码头,只负责接收和发送数据报,使用DatagramPacket
来代表数据报。发送的数据通过这个对象进行完成。
由DatagramPacket
来决定数据报的目的地。
实际上,还是可以区分出服务端和客户端的,下面展示服务端与客户端和客户端与客户端的传输。
服务端与客户端
在TCP
的socket
中,我们需要建立连接后再进行传输,所以有明显的服务端和客户端之分。而UDP
则不同,不需要建立连接,直接往目标进行发送数据。但还是可以人为的指定好客户端和服务端的。服务端的特性是有固定的IP和端口。
下面展示用法:
服务端
public class UDPServer {
public static final int PORT=30000;
//定义每个数据报的最大的大小为4KB
public static final int DATA_LEN=4096;
//定义接受网络数据的字节数组
byte[] inBuff=new byte[DATA_LEN];
//已指定字节数组创建准备接受数据的DatagramPacket对象
private DatagramPacket inPacket=new DatagramPacket(inBuff, inBuff.length);
//定义发送的DatagramPacket对象
private DatagramPacket outPacket;
//定义一个字符串数组,服务器发送该数组的元素
String[] book=new String[]{"I","am","Stu"};
public void init(){
try {
//创建datagramsocket对象
DatagramSocket socket=new DatagramSocket(PORT);
{
//采用循环接受数据
for(int i=0;i<1000;i++){
//读取inPacket的数据
socket.receive(inPacket);
//判断getData()和inbuf是否为同一个数组
System.out.println(inPacket.getData()==inBuff);
System.out.println(socket.getSoTimeout());
//将接受后的内容转化为字符串进行输出
System.out.println(new String(inBuff,0,inPacket.getLength()));
//从字符串中取出一个元素作为发送数据
byte[] sendData=book[i%4].getBytes();
//已指定的字符数组作为发送数据,以刚接手到的datagramPacket的
//源socketAddress作为目标socketAddress创建DatagramPacket
outPacket=new DatagramPacket(sendData,sendData.length, inPacket.getSocketAddress());//通过这个getSocketAddress就可以得到相应的IP地址和端口了
//发送数据
socket.send(outPacket);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new UDPServer().init();
}
}
相应的客户端:
public class UdpClient {
// 定义数据报的目的地
public static final int DEST_PORT = 30000;
public static final String DEST_IP = "127.0.0.1";
// 定义每个数据报的大小,最大为4kb
...//更UdpServer一样相应成员的声明
public void init(){
try {
//创建一个客户端DatagramSocket使用随机端口
DatagramSocket socket=new DatagramSocket();
outPacket=new DatagramPacket(new byte[0], 0,InetAddress.getByName(DEST_IP),DEST_PORT);
//创建键盘输入流
Scanner scan=new Scanner(System.in);
//不断读取键盘输入
while(scan.hasNextLine()){
//将键盘输入的一行字符串转换字节数组
byte[] buff=scan.nextLine().getBytes();
//设置发送用到的DatagramPacket中的字节数据
outPacket.setData(buff);
//发送数据报
socket.send(outPacket);
//读取Socket中的数据,读到的数据放在inPacket所封装的字节数组中
socket.receive(inPacket);
System.out.println(new String(inBuff,0,inPacket.getLength()));
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new UdpClient().init();
}
}
通过以上的代码,我们可以总结以下规律,即:使用步骤
- 定义好IP地址和端口,以及字节数组的大小(最终以此形式传输)并初始化输入的DatagramPacket
- 创建
DatagramSocket
实例(监听端口,S/C都各自不同) - S和C都有所不同
3.1 S则先进行socket.receive(inPacket);
3.2 而C则创建outPacket
(IP地址和端口,字节数组),并使用outPacket.setData(buff);
将需要发送的数据进行设置,再使用socket.send(outPacket);
进行发送 - 当发送后的就进行等待接受,接受完的进行处理并发送的一个循环里面,这优点像进程间通信的情况。
使用当中的注意点:
- 一定要注意
DatagramPacket
的设置,因为它指定发送给哪一个应用程序,通过接受的数据报可等到相应的ip地址和端口
。 -
socket.receive(inPacket);
是阻塞的。必要时需要对应启动一个线程。
针对注意的第一点,我们看客户端对客户端的传输
其实很简单,我们只要对每个客户端的DatagramSocket
监听不同的端口,而使用DatagramPacket
指定要传输的端口和IP地址即可
。我们只需要对上面的类UdpClient进行修改如下:
第一个客户端
DatagramSocket socket=new DatagramSocket(30001);//添加端口,而不是随机端口,
outPacket=new DatagramPacket(new byte[0],0,InetAddress.getByName(DEST_IP),30000);//将要传输放的端口和ip设置
第二个客户端则相反即可
DatagramSocket socket=new DatagramSocket(30000);//添加端口,而不是随机端口,
outPacket=new DatagramPacket(new byte[0],0,InetAddress.getByName(DEST_IP),30001);//将要传输放的端口和ip设置
针对注意第二点,当socket.receive(inPacket);
如何不阻塞主线程
下面我对相应的代码进行了封装和优化,并让类实现Runnable
接口
public class UdpClient implements Runnable {
// 定义数据报的目的地
public static final int DEST_PORT = 30001;
public static final String DEST_IP = "127.0.0.1";
...//相应成员的声明
public UdpClient() {
try {
// 创建一个客户端DatagramSocket使用随机端口
socket = new DatagramSocket(DEST_PORT);
// 初始化发送数据报,包含一个长度为0的字节数组
outPacket = new DatagramPacket(new byte[0], 0,
InetAddress.getByName(DEST_IP), 30000);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 发送字符串坐标strxy,用于使用ai先手的情况
*
* @param strXY
*/
public void sendPointXy(String strXY) {
byte[] buff = strXY.getBytes();
outPacket.setData(buff);// 设置数据报的字节数据
try {
// 发送数据报
socket.send(outPacket);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 进行接受坐标strXY,并进行处理进行下棋,最后进行发送
*
* @param strXY
*/
public void receiverPointXy(String content) {
String[] strXY = content.split(",");
...//对数据进行处理
sendPointXy(chComputer.getX() + "," + chComputer.getY());
}
/**
* 成为被动接受方
*/
public void acceptPointXy() {
try {
socket.receive(inPacket);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
receiverPointXy(new String(inBuff, 0, inPacket.getLength()));
}
@Override
public void run() {
//死循环不断地进行监听
while(true){
acceptPointXy();
}
}
}
从上面的代码可以看到,当我们发送后,再也不进行receive
,而是交给线程的run
中进行死循环地receive
监听。并把send
方法和receive
方法封装出来,是为了区分谁先发送,谁监听的情况。否则发送方就无法将数据到达目的地。
如何让receive
跳出?
也就是告诉等待的对方,我已经退出了,不需要继续等待呢?
这里分两种情况的方法,一种是正常退出,另一种是非正常退出。
正常退出:
传入特定的字符进行判断对方已经退出了,并主动停止等待。我根据上一个代码块进行修改如下:
public static boolean isRunning=true;//默认true;
public void receiverPointXy(String content) {
//首先进行判断是否关闭的情况
if(content.isEmpty()){//当正常关闭的时候,对方发送空字符来标示关闭
isRunning=false;//不再循环等待
if(socket!=null)
socket.close();
return ;
}
....
}
public void run() {
//isRunning进行标识是否等待
while(isRunning){
acceptPointXy();
}
}
非正常退出:
程序遇到异常,网络不正常的时候,我们可以设置timeout
来停止等待:
socket.setSoTimeout(60000);// 设置等待时间为1分钟
那么你可能会好奇,默认的Timeout
是多久呢?我们通过查看源码的注释
/**
* ...
* A timeout of zero is interpreted as an infinite timeout.
*/
public synchronized void setSoTimeout(int timeout) throws SocketException {
if (isClosed())
throw new SocketException("Socket is closed");
getImpl().setOption(SocketOptions.SO_TIMEOUT, new Integer(timeout));
}
DatagramSocketImpl getImpl() throws SocketException {
if (!created)
createImpl();
return impl;
}
/**
* Retrieve setting for SO_TIMEOUT. 0 returns implies that the
* option is disabled (i.e., timeout of infinity).
*/
public synchronized int getSoTimeout() throws SocketException {
if (isClosed())
throw new SocketException("Socket is closed");
if (getImpl() == null)
return 0;
...
}
通过查看源码可以发现,当我们执行setSoTimeout
的时候,将调用getImpl()
方法创建DatagramSocketImpl
,使得getImpl()!=null
,返回不为零的数字。所以,默认的timeout
为0
。但是为零的情况,我们通过看方法的注释可以发现,为零意味着无限等待。
使用MulticastSocket
实现多点广播
使用MulticastSocket
可以将数据报以广播方式发送到数量不等的多个客户端。
原理
若要使用多点广播时,则需要让一个数据报有一组目标主机地址,当数据报发出后,整个组的所有主机都能收到该数据报。
IP多点广播(或多点发送)实现了将单一信息发送到多个接收者的广播。
其思想是设置一组特殊网络地址作为多点广播地址,每一个多点广播地址都被看做一个组,当客户端需要发送、接收广播信息时,加入到该组即可。
IP协议为多点广播提供了这批特殊的IP地址,这些IP地址的范围是224.0.0.0至239.255.255.255
。
注意MulticastSocket
重要的属性:
使用
jionGroup()
方法来加入指定组;使用leaveGroup()
方法脱离一个组。
使用setTimeToLive(int ttl)
方法,通过参数ttl
来指定最多可以跨过多少个网络。(默认情况ttl=1
)
- ttl=0时,指定数据报应停留在本地主机
- ttl=1时,指定数据报发送到本地局域网
- ttl=32时,指定数据报只能发送到本站点的网络上
- ttl=64时,指定数据报应保留在本地区
- ttl=128时,指定数据报应保留在本大洲
- ttl=64时,指定数据报可发送到所有地方
当在某些系统中,可能有多个网络接口:(将会对多点广播带来问题)
通过调用
setInterface
可选择MulticastSocket
所使用的网络接口;也可以使用getInterface方法查询MulticastSocket监听的网络接口。
示例代码:
public class MulticastSocketTest implements Runnable {
// 使用常量作为本程序的多点广播IP地址
private static final String BROADCAST_IP = "230.0.0.1";
// 使用常量作为本程序的多点广播目的的端口
public static final int BROADCAST_PORT = 30000;
// 定义每个数据报的最大大小为4K
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 {
// 创建用于发送、接收数据的MulticastSocket对象
// 因为该MulticastSocket对象需要接收,所以有指定端口
socket = new MulticastSocket(BROADCAST_PORT);
broadcastAddress = InetAddress.getByName(BROADCAST_IP);
// 将该socket加入指定的多点广播地址
socket.joinGroup(broadcastAddress);
// 设置本MulticastSocket发送的数据报被回送到自身
socket.setLoopbackMode(false);
// 初始化发送用的DatagramSocket,它包含一个长度为0的字节数组
outPacket = new DatagramPacket(new byte[0], 0, broadcastAddress,
BROADCAST_PORT);
// 启动以本实例的run()方法作为线程体的线程
new Thread(this).start();
// 创建键盘输入流
scan = new Scanner(System.in);
// 不断读取键盘输入
while (scan.hasNextLine()) {
// 将键盘输入的一行字符串转换字节数组
byte[] buff = scan.nextLine().getBytes();
// 设置发送用的DatagramPacket里的字节数据
outPacket.setData(buff);
// 发送数据报
socket.send(outPacket);
}
} finally {
socket.close();
}
}
public void run() {
try {
while (true) {
// 读取Socket中的数据,读到的数据放在inPacket所封装的字节数组里。
socket.receive(inPacket);
// 打印输出从socket中读取的内容
System.out.println("聊天信息:"
+ new String(inBuff, 0, inPacket.getLength()));
}
}
// 捕捉异常
catch (IOException ex) {
ex.printStackTrace();
try {
if (socket != null) {
// 让该Socket离开该多点IP广播地址
socket.leaveGroup(broadcastAddress);
// 关闭该Socket对象
socket.close();
}
System.exit(1);
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws IOException {
new MulticastSocketTest().init();
}
}
多次按ctrl+f11
生成多个示例进行测试。可以发现,使用MulticastSocket
监听统一端口,多个示例并不会产生端口被占用的错误。再者监听的IP地址
要为IP协议
上的广播地址。
扩展:(聊天室的实现)
使用MulticastSocket实现多点广播(2)
参考资料
TCP、UDP详解
17.4.3 使用MulticastSocket实现多点广播(1),这个居然是《java疯狂讲义第二部》的全教程。
java.net.MulticastSocket Example