今天UDP组播丢包问题,可把我害惨了,130个包,接收端总是只接受到121个包,稳定丢9个包,我一直以为是代码逻辑问题,但是通过130个单步调试发现,单步调试就是不丢包。
后来去复习了一下UDP。豁然开朗,UDP发送过快就是会导致丢包的,难怪我单步调试就不丢包。心累。源码如下
UDP丢包原因
一、主要丢包原因
1、接收端处理时间过长导致丢包:调用recv方法接收端收到数据后,处理数据花了一些时间,处理完后再次调用recv方法,在这二次调用间隔里,发过来的包可能丢失。对于这种情况可以修改接收端,将包接收后存入一个缓冲区,然后迅速返回继续recv。
2、发送的包巨大丢包:虽然send方法会帮你做大包切割成小包发送的事情,但包太大也不行。例如超过50K的一个udp包,不切割直接通过send方法发送也会导致这个包丢失。这种情况需要切割成小包再逐个send。
3、发送的包较大,超过接受者缓存导致丢包:包超过mtu size数倍,几个大的udp包可能会超过接收者的缓冲,导致丢包。这种情况可以设置socket接收缓冲。以前遇到过这种问题,我把接收缓冲设置成64K就解决了。
4、发送的包频率太快:虽然每个包的大小都小于mtu size 但是频率太快,例如40多个mut size的包连续发送中间不sleep,也有可能导致丢包。这种情况也有时可以通过设置socket接收缓冲解决,但有时解决不了。所以在发送频率过快的时候还是考虑sleep一下吧。
5、局域网内不丢包,公网上丢包。这个问题我也是通过切割小包并sleep发送解决的。如果流量太大,这个办法也不灵了。总之udp丢包总是会有的,如果出现了用我的方法解决不了,还有这个几个方法: 要么减小流量,要么换tcp协议传输,要么做丢包重传的工作。
二、具体问题分析
1.发送频率过高导致丢包
很多人会不理解发送速度过快为什么会产生丢包,原因就是UDP的SendTo不会造成线程阻塞,也就是说,UDP的SentTo不会像TCP中的SendTo那样,直到数据完全发送才会return回调用函数,它不保证当执行下一条语句时数据是否被发送。(SendTo方法是异步的)这样,如果要发送的数据过多或者过大,那么在缓冲区满的那个瞬间要发送的报文就很有可能被丢失。至于对“过快”的解释,作者这样说:“A few packets a second are not an issue; hundreds or thousands may be an issue.”(一秒钟几个数据包不算什么,但是一秒钟成百上千的数据包就不好办了)。 要解决接收方丢包的问题很简单,首先要保证程序执行后马上开始监听(如果数据包不确定什么时候发过来的话),其次,要在收到一个数据包后最短的时间内重新回到监听状态,其间要尽量避免复杂的操作(比较好的解决办法是使用多线程回调机制)。
2.报文过大丢包
至于报文过大的问题,可以通过控制报文大小来解决,使得每个报文的长度小于MTU。以太网的MTU通常是1500 bytes,其他一些诸如拨号连接的网络MTU值为1280 bytes,如果使用speaking这样很难得到MTU的网络,那么最好将报文长度控制在1280 bytes以下。
3.发送方丢包
发送方丢包:内部缓冲区(internal buffers)已满,并且发送速度过快(即发送两个报文之间的间隔过短); 接收方丢包:Socket未开始监听; 虽然UDP的报文长度最大可以达到64 kb,但是当报文过大时,稳定性会大大减弱。这是因为当报文过大时会被分割,使得每个分割块(翻译可能有误差,原文是fragmentation)的长度小于MTU,然后分别发送,并在接收方重新组合(reassemble),但是如果其中一个报文丢失,那么其他已收到的报文都无法返回给程序,也就无法得到完整的数据了。
Receive.java
package com.lzj.recive;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.util.List;
public class ReceiveFile {
private static String receivePath = "C://Users//LZJ//Desktop//";
private static String multicastHost = "224.224.10.0";
private InetAddress address;
private MulticastSocket recSocket;// 发送服务多播组
private int recPort = 9988;
private BufferedOutputStream fos;
private ReceiveFileDialog dialog;
private String head = "<html><body>";
private String body;
private String end = "</body></html>";
public ReceiveFile(ReceiveFileDialog dialog) {
super();
this.dialog = dialog;
}
class ReceiveThread implements Runnable {
// 接受文件字节数组
byte[] buf = new byte[1024 * 60];
byte[] fileByte = new byte[1024];
public ReceiveThread() {
try {
address = InetAddress.getByName(multicastHost);
recSocket = new MulticastSocket(recPort);
recSocket.joinGroup(address);
recSocket.setTimeToLive(1);
} catch (final IOException e) {
} // end of try-catch
}
public void run() {
try {
while (true) {
DatagramPacket packet = new DatagramPacket(fileByte, fileByte.length); // 待接收的数据包
// 文件长度
long fileSize = 0;
// 文件包数量
int n = 0;
// 接收File集合
// 客户端DatagramSocket 接收 DatagramPacket数据包(字节)
recSocket.receive(packet);
// 将字节数组转换成对象(反序列化)
// 基类流 :内存流 读
ByteArrayInputStream bais = new ByteArrayInputStream(fileByte);
// 包装流 :对象流
ObjectInputStream ois = new ObjectInputStream(bais);
// 内存输入流 读取对象信息
Object object = ois.readObject();
body = "";
// 向下转型 获取对象信息
List<File> files = (List<File>) object;
for (File file : files) {
body += file.getName() + "<br>";
fileSize += fileSize + file.length();
}
dialog.getLblNewLabel().setText(head + body + end);
dialog.getProgressBar().setMaximum((int) (fileSize / (1024 * 60)) + 1);// 设置进度条的长度
dialog.getProgressBar().setMinimum(0);// 设置进度条的长度
dialog.getProgressBar().setValue(0);
for (File file : files) {
int fileByteSize = 0;
int c = 0;
fos = new BufferedOutputStream(new FileOutputStream(new File(receivePath + file.getName())));
packet = new DatagramPacket(buf,buf.length);
while (true) {
dialog.getProgressBar().setValue(++n);
try {
recSocket.receive(packet);// 接收数据包
} catch (Exception e) {
e.printStackTrace();
}
fileByteSize += packet.getLength();
fos.write(packet.getData(), 0, packet.getLength());
System.out.println("第" +(++c)+ "次写入成功" + "这次写入的大小为" + packet.getLength() +"B");
if(fileByteSize == file.length()){
System.out.println("文件"+file.getName()+"接收完毕");
break;
}
}
// System.out.println("文件总大小"+fileByteSize/1024+"KB");
fos.flush();
fos.close();
}
dialog.getProgressBar().setValue(dialog.getProgressBar().getMaximum());
}
} catch (Exception e) {
e.printStackTrace();
}
}// end of run()
}
}
send.java
package com.lzj.send;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.net.UnknownHostException;
import java.util.List;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
public class SendThread {
private static String multicastHost = "224.224.10.0";
private static MulticastSocket sendSocket;// 发送服务多播组
private static InetAddress address;// 发送服务多播组
private static int serverPort = 9988;
private SendFileDialog dialog;
ObjectInputStream is = null;
ObjectOutputStream os = null;
public SendThread(SendFileDialog dialog) {
this.dialog = dialog;
}
static {
try {
address = InetAddress.getByName(multicastHost);
sendSocket = new MulticastSocket(serverPort);
sendSocket.joinGroup(address);
sendSocket.setTimeToLive(1);
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
public void beginSend() {
dialog.getProgressBar().setValue(0);
SendThread1 send = new SendThread1();
new Thread(send).start();
}
class SendThread1 implements Runnable {
public void run() {
try {
DatagramPacket packet = null; // 声明DatagramPacket对象
List<File> files = dialog.getFiles();
long fileSize = 0;
for (File file : files) {
fileSize += file.length();
}
int n = 0;
dialog.getProgressBar().setMaximum((int) (fileSize/(1024*60))+1);//设置进度条的长度
dialog.getProgressBar().setMinimum(0);//设置进度条的长度
dialog.getProgressBar().setValue(0);
System.out.println(dialog.getProgressBar().getValue());
System.out.println(dialog.getProgressBar().getMaximum());
System.out.println(dialog.getProgressBar().getMinimum());
//对象流 写 对象输出流
//内存流(基类流)
ByteArrayOutputStream baos = new ByteArrayOutputStream();
//对象流(包装流 )因为传输对象
ObjectOutputStream oos = new ObjectOutputStream(baos);
//将对象写入对象流
oos.writeObject(files);
//将对象转化为字节数组 因为数据包需要字节数组 该方法属于字节流 很重要!
byte[] buf = baos.toByteArray();
//将字节数组的数据放入数据包
packet = new DatagramPacket(buf, buf.length, address, serverPort);
//向客户端传输数据包
sendSocket.send(packet );
for (File file : files) {
System.out.println("即将发送"+file.getName()+"\n");
BufferedInputStream fis = new BufferedInputStream(new FileInputStream(file));
//send file
byte[] data = new byte[1024*60];
int fileByteSize = 0;
try {
int len = 0;
len = fis.read(data);
while(len!=-1){
dialog.getProgressBar().setValue(++n);
System.out.println("第"+n+"次发送数据包,数据包大小"+len);
fileByteSize += len;
packet = new DatagramPacket(data,len, address, serverPort); // 将数据打包
sendSocket.send(packet);// 发送数据
Thread.sleep(100);
len = fis.read(data);
}//end of while
fis.close();//关闭文件流
data=new String("over").getBytes();//告诉接受端发送结束
System.out.println(file.getName()+"发送完毕,文件总大小为"+fileByteSize/1024+"KB");
System.out.println(dialog.getProgressBar().getValue());
int value=dialog.getProgressBar().getValue();
if(value<dialog.getProgressBar().getMaximum()){
dialog.getProgressBar().setValue(dialog.getProgressBar().getMaximum());
}
} catch (IOException e) {
System.out.println("发送失败:"+e.getMessage());// 输出异常信息
}//end of try-catch
}}catch( Exception e){
System.out.println("发送失败:"+e.getMessage());// 输出异常信息
}
}// end of run
}// end of thread
}