基于UDP协议的Socket编程--UDP文件传输优化

任务要求

试完善文件传输功能,可选择 1.使用基于TCP的Socket进行改写;2.优化基于UDP文件传输,包括有序发送、接收端细粒度校验和发送端数据重传。

这里我们来进行UDP的优化

首先我们知道的是在传输大量数据时,UDP协议的传输容易出现数据的缺失(这里我们可以采用计算文件的MD5值来进行验证),因此我们需要进行优化,优化的点为有序发送、接收端细粒度校验和发送端数据重传。

为了完成上述优化,我们需要对发送的数据包进行一定地封装,这里我采用以下方案

1.每次传输的数据包大小为512bytes

2.前8位存储两个整型,分别时数据包编号(即第几个数据包),还有传输内容的长度(不包括修饰的前缀)

3.接下来34位存储MD5码,理论来说计算得到的MD5码应该为32位,但是笔者这里采用的写入字符串的方式来打包入数据包,这导致MD5占了34位,具体原因还未搞清楚,可能是因为前两位是用于标记16进制的,但是无妨,不影响我们后续的进行

在修改接收器和发送器的代码前,我们需要对于MD5工具类进行修改,使其可以对于byte[]型的数据进行计算

import java.security.MessageDigest;

public class MD5Util_byte {
    public static String getMD5(byte[] input) {
        try {
            MessageDigest MD5 = MessageDigest.getInstance("MD5");
            byte[] data = input;
            MD5.update(data);
            return new String(byte2hex(MD5.digest()));
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    private static String byte2hex(byte[] b) {
        String hs = "";
        String stmp = "";
        for (int n = 0; n < b.length; n++) {
            stmp = (java.lang.Integer.toHexString(b[n] & 0xFF));
            if (stmp.length() == 1)
                hs = hs + "0" + stmp;
            else
                hs = hs + stmp;
        }
        return hs;
    }
}

这样我们就可以利用MD5Util_byte.getMD5()来计算MD5值了。

接下来我们需要完成UDPFileSender

import java.io.*;
import java.net.*;
import java.security.*;
import java.util.Arrays;
import java.util.Random;
public class UDPFileSender {
    public static void main(String[] args) throws IOException, NoSuchAlgorithmException {
        // 生成并写入发送文件
        try (FileWriter fileWriter = new FileWriter("checksum.txt")) {
            Random r = new Random(2023);
            // 尝试 1e3 and 1e8
            for (int i = 0; i < 1e8; i++) {
                fileWriter.write(r.nextInt());
            }
        }

        File file = new File("checksum.txt");
        System.out.println("发送文件生成完毕");
        System.out.println("发送文件的md5为: " + MD5Util.getMD5(file));

        FileInputStream fis = new FileInputStream(file);
        DatagramSocket socket = new DatagramSocket();
        byte[] bytes = new byte[512-42];
        DatagramPacket packet;
        boolean resend = false;

        int sequenceNumber = 0; // 添加序列号变量

        // 不断从文件读取字节并将其组装成数据报发送
        int len;
        DatagramPacket lastPacket = null;
        while ((len = fis.read(bytes)) != -1) {
            if (resend) {
                // 重新发送上一个数据包
                socket.send(lastPacket);
                resend = false; // 重置重新发送标记
            } else {
                // 计算校验和
                String checksum = MD5Util_byte.getMD5(bytes);
                if (len < 470) {
                    checksum = MD5Util_byte.getMD5(Arrays.copyOfRange(bytes, 0, len));
                }
                // 构造带有序列号和校验和的数据包
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                DataOutputStream dos = new DataOutputStream(baos);
                dos.writeInt(sequenceNumber);
                dos.writeUTF(checksum);
                System.out.println(len);
                dos.writeInt(len);
                dos.write(bytes, 0, len);
                dos.flush();
                byte[] sendData = baos.toByteArray();
                System.out.println(sendData.length);
                System.out.println("sendData: " + Arrays.toString(sendData));
                packet = new DatagramPacket(sendData, sendData.length, InetAddress.getLocalHost(), 9092);
                socket.send(packet);
                lastPacket = packet; // 保存上一个数据包,以便在接收方报告错误时重新发送
            }
            // 等待接收方的响应
            DatagramPacket responsePacket = new DatagramPacket(new byte[512], 512);
            socket.receive(responsePacket);
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength());

            // 检查接收方的响应是否表示接收错误,如果是,则重新发送上一个数据包
            if (response.equals("ERROR")) {
                System.out.println("接收方报告接收错误,重新发送上一个数据包");
                resend = true;
            }
            else if (response.equals("OK")) {
                System.out.println("接收方报告接收正确");
            }
            else {
                System.out.println("接收方报告未知错误");
                resend = true;
            }
            sequenceNumber++; // 更新序列号
        }

        // 发送终止符
        byte[] a = new byte[0];
        System.out.println("flag");
        packet = new DatagramPacket(a, a.length, InetAddress.getLocalHost(), 9092);
        socket.send(packet);

        fis.close();
        socket.close();
        System.out.println("向" + packet.getAddress().toString() + "发送文件完毕!端口号为:" + packet.getPort());
    }
}

可以看到,我们的UDPFileSender首先随机生成数据写入文件,然后从文件不断读入并且按照我们希望的方式对数据进行打包,同时等待接受器的回写消息,若发生错误则重新发送出现错误的数据包。

而UDPFileReceiver也是相同的逻辑

import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;

public class UDPFileReceiver {
    public static void main(String[] args) throws IOException {
        File file = new File("checksum_recv.txt"); //要接收的文件存放路径
        FileOutputStream output = new FileOutputStream(file);
        byte[] data = new byte[512];
        DatagramSocket ds = new DatagramSocket(9092);
        int len=470;
        // 使用一个循环来接收数据报,并根据序列号将数据写入文件
        DatagramPacket dp = new DatagramPacket(data, data.length);
        while (true) {
            ds.receive(dp);
            ByteArrayInputStream bais = new ByteArrayInputStream(dp.getData());
            DataInputStream dis = new DataInputStream(bais);
            int sequenceNumber = dis.readInt();
            if (len!=470) // 收到终止符则退出循环
                break;

            byte[] checksum = new byte[34];
            dis.readFully(checksum);
            len = dis.readInt();
            System.out.println("len: " + len);
            byte[] receivedData = new byte[512-42]; // 数据长度减去序列号和校验和的长度
            dis.read(receivedData, 0,len);
            if(len < 470){
                byte[] temp = new byte[len];
                System.arraycopy(receivedData, 0, temp, 0, len);
                receivedData = temp;
            }
            // 计算接收到的数据包的校验和
            String receivedChecksum = MD5Util_byte.getMD5(receivedData);
            // System.out.println("receivechecksum: " + receivedChecksum);
            byte[] receivedChecksum_byte = receivedChecksum.getBytes(StandardCharsets.UTF_8);
            byte[] stantardChecksum = new byte[checksum.length-2];
            System.arraycopy(checksum, 2, stantardChecksum, 0, checksum.length-2);
            // 比较校验和

            if (!Arrays.equals(stantardChecksum, receivedChecksum_byte)){
                System.out.println("接收到的数据包校验和与发送端不一致,丢弃该数据包!"+ "sequenceNumber: " + sequenceNumber+ " receivedChecksum: " + Arrays.toString(receivedChecksum_byte)+ " checksum: " + Arrays.toString(checksum));
                // 向发送端报告错误
                InetAddress senderAddress = dp.getAddress();
                int senderPort = dp.getPort();
                String errorMessage = "ERROR"; // 错误消息
                byte[] errorData = errorMessage.getBytes(StandardCharsets.UTF_8);
                DatagramPacket errorPacket = new DatagramPacket(errorData, errorData.length, senderAddress, senderPort);
                ds.send(errorPacket);
                continue;
            }
            InetAddress senderAddress = dp.getAddress();
            int senderPort = dp.getPort();
            String errorMessage = "OK"; // 错误消息
            byte[] errorData = errorMessage.getBytes(StandardCharsets.UTF_8);
            DatagramPacket errorPacket = new DatagramPacket(errorData, errorData.length, senderAddress, senderPort);
            ds.send(errorPacket);
            System.out.println("接收到的数据包校验和与发送端一致,写入文件!"+ "sequenceNumber: " + sequenceNumber);
            output.write(receivedData); // 将数据写入文件
        }

        output.close();
        ds.close();
        System.out.println("接收来自" + dp.getAddress().toString() + "的文件已完成!对方所使用的端口号为:" + dp.getPort());
        file = new File("checksum_recv.txt");
        System.out.println("接收文件的md5为: " + MD5Util.getMD5(file));
    }
}

我们需要对接收的数据包进行解析,比对MD5码,最后根据结果选择写入还是向Sender报错。

最后我们进行验证

Receiver

Sender

当然我们也可以编写一个小程序来检验文件的完整性

这样我们完成了对于UDP文件传输的优化

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值