Java 网络通信编程(8):完善 UDP 协议

在本次 Java UDP 视频传输项目的初期实现中,我们虽然完成了摄像头采集、图片压缩、UDP 数据包发送与接收的基础功能,但未考虑 UDP 协议本身无连接、不可靠、无流控、数据包大小受限 的特性。而在实际通信应用中,往往会对 UDP 进行改进,可以理解为在程序层面上去模仿 TCP 的安全机制。因此,本期文章我们将会对数据包进行拆包 ,并自定义确认机制,让 UDP 视频通信更加稳定、可靠。

1.发送方

在网络传输中,MTU(Maximum Transmission Unit,最大传输单元) 是指链路层一次能传输的最大数据包大小 ,局域网以太网的标准 MTU 为 1500 字节 。也就是说,UDP 数据包总大小超过 1500 字节时,会被网络自动分片,而这将大幅增加丢包率 。我们可以对原来的数据包进行手动拆包,即把一帧图片分开发送。为了使传输更加安全稳定,我们将单包有效数据大小设定为 1400 字节。

对于每个数据包,我们可以加上编号值作为它的 ACK 值,ACK(Acknowledgement)即确认应答 ,是网络通信中接收方向发送方返回的接收确认信号,作用是告知发送方指定数据已成功接收,这在之前的文章中也有所介绍。这里以序号为 ACK 值,让接收方接收数据包后回传,可确保数据包无丢失、有序传输。

理解以上两个核心操作后,我们可以一步步细化封装的发送视频方法。

首先,和之前的操作一样,我们需要把数据长度单独打包发送,这可以让接收方知晓应该接收几个数据包便于验证。

java 复制代码
//将数据长度转成byte 数组
int lenData = imageData.length;
byte[] lenBuf = new byte[4];
intToBytes(lenBuf, lenData);

//发送长度数据包
DatagramPacket lenPacket = new DatagramPacket(lenBuf, 0, lenBuf.length, address, port);
clientA.send(lenPacket);

接下来,我们需要将原数据拆分,并且加上 ACK 值重新打包。我们分别定义 offset 和 packetIndex 作为当前的数据位置和数据包序号(ACK)。在循环内以 final 变量 PACKET_SIZE(大小1400)为标准分割 imageData[] (原数据数组)。考虑到数组末尾的数据长度可能不足1400,我们用 int size = Math.min(PACKET_SIZE, lenData - offset);处理末尾数据。处理好数组大小后,再用 Arrays 类方法中的 copyOfRange(要复制的数组,起始位置,结尾位置) 方法把数据拷贝到新数组 packetArr 中。

然后,我们需要一个新数组 sendPacketArr 来拼接 packetArr 和 ACK 值,在前四位加上序号信息后,在第四位之后用 arraycopy 拼上 packetArr 的全部数据。

java 复制代码
//最后数据包不到PACKET_SIZE时,调整size大小
int size = Math.min(PACKET_SIZE, lenData - offset);
//从imageData 中拷贝数据
byte[] packetArr = Arrays.copyOfRange(imageData, offset, offset + size);
//当前数据位置改变
offset += size;

//重新定义byte 数组,用于拼装序号和数据
byte[] sendPacketArr = new byte[packetArr.length + 4];
//给前四位拼上序号信息(ack值)
intToBytes(sendPacketArr, packetIndex);
//第四位后面复制packetArr 中的数据
System.arraycopy(packetArr, 0, sendPacketArr, 4, size);

最后,我们加上 ACK 验证重传机制。设置初始值为 false 的标志位,当标志位不为真时进入发送数据包的循环。用 ackBuf 数组接收回传的 ACK 值,只有 ACK 值符合条件,即传输成功时才退出循环。

java 复制代码
//ack标志位,初始置为false
boolean ackFlag = false;
while (!ackFlag) {
    //发送数据包
    DatagramPacket imagePacket = new DatagramPacket(sendPacketArr, 0, sendPacketArr.length,
            address, port);
    clientA.send(imagePacket);

    //定义byte 数组用于接收回传的ack值
    byte[] ackBuf = new byte[4];
    DatagramPacket ackPacket = new DatagramPacket(ackBuf, 0, ackBuf.length);
    clientA.receive(ackPacket);

    int ack = byteToInt(ackBuf);

    System.out.println("ack=" + ack);

    if (ack == packetIndex) {
        //ack值正确,则将标志位置为true,不需要重传,数据包序号加1
        ackFlag = true;
        packetIndex++;
    }
}

以下是发送方完整的发送视频方法参考代码。

java 复制代码
public void sendVideo(byte[] imageData) throws Exception {
    //将数据长度转成byte 数组
    int lenData = imageData.length;
    byte[] lenBuf = new byte[4];
    intToBytes(lenBuf, lenData);

    //发送长度数据包
    DatagramPacket lenPacket = new DatagramPacket(lenBuf, 0, lenBuf.length, address, port);
    clientA.send(lenPacket);

    System.out.println(lenData);

    int offset = 0;//记录当前数据位置
    int packetIndex = 0;//记录拆包个数(序号)

    while (offset < lenData) {
        //最后数据包不到PACKET_SIZE时,调整size大小
        int size = Math.min(PACKET_SIZE, lenData - offset);
        //从imageData 中拷贝数据
        byte[] packetArr = Arrays.copyOfRange(imageData, offset, offset + size);
        //当前数据位置改变
        offset += size;

        //重新定义byte 数组,用于拼装序号和数据
        byte[] sendPacketArr = new byte[packetArr.length + 4];
        //给前四位拼上序号信息(ack值)
        intToBytes(sendPacketArr, packetIndex);
        //第四位后面复制packetArr 中的数据
        System.arraycopy(packetArr, 0, sendPacketArr, 4, size);

        //ack标志位,初始置为false
        boolean ackFlag = false;
        while (!ackFlag) {
            //发送数据包
            DatagramPacket imagePacket = new DatagramPacket(sendPacketArr, 0, sendPacketArr.length,
                    address, port);
            clientA.send(imagePacket);

            //定义byte 数组用于接收回传的ack值
            byte[] ackBuf = new byte[4];
            DatagramPacket ackPacket = new DatagramPacket(ackBuf, 0, ackBuf.length);
            clientA.receive(ackPacket);

            int ack = byteToInt(ackBuf);

            System.out.println("ack=" + ack);

            if (ack == packetIndex) {
                //ack值正确,则将标志位置为true,不需要重传,数据包序号加1
                ackFlag = true;
                packetIndex++;
            }
        }
    }
}

2.接收方

接收方首先会接收到整个图片的数据长度,我们可以用这个数据得到有多少个数据包要接收。

java 复制代码
//读取总长度
byte[] lenBuf = new byte[4];
DatagramPacket receiveLen = new DatagramPacket(lenBuf, 0, lenBuf.length);
clientB.receive(receiveLen);
int len = byteToInt(lenBuf);

System.out.println(len);
//存储还原后的数据
byte[] imageData = new byte[len];
int maxPacket = (len + PACKET_SIZE - 1) / PACKET_SIZE;  //计算有多少个数据包要接收

计算出 maxPacket 后,新建二维数组 buffPacket 按包序号缓存各个数据包、布尔数组 receiveFlag 标记包是否接收。再在循环内持续接收数据包,每次收到一个数据包,先解析出前 4 字节的包序号 ACK,提取后面的图片数据,并立即将 ACK 序号回传给发送端作为接收确认。接着通过 if 条件校验,确保包序号合法、未越界且未重复接收,再将数据存入二维数组对应序号位置并更新接收状态与计数,直到所有分包全部收齐。

最后循环遍历二维数组,按包序号顺序将所有缓存的数据包拼接成完整的字节数组 imageData ,返回给上层用于图片还原与界面绘制。

java 复制代码
//用二维数组缓存每个数据包,二维的默认值是null
byte[][] buffPacket = new byte[maxPacket][];
boolean[] receiveFlag = new boolean[maxPacket];
int receiveCount = 0;

while (receiveCount < maxPacket) {
    //读取一个数据包
    byte[] packetBuf = new byte[PACKET_SIZE + 4];
    DatagramPacket packet = new DatagramPacket(packetBuf, 0,
            packetBuf.length);
    clientB.receive(packet);
    int readLen = packet.getLength();
    System.out.println("packet.getLength() = " + packet.getLength());

    //从packetBuf 解析出ACK用于回传
    int ack = byteToInt(packetBuf);
    System.out.println("ack = " + ack);

    //提取从 packetBuf数组的图像数据
    byte[] imageBuf = new byte[readLen - 4];
    System.arraycopy(packetBuf, 4, imageBuf, 0, imageBuf.length);

    //回传 ACK
    byte[] ackBuf = new byte[4];
    intToBytes(ackBuf, ack);
    DatagramPacket ackPacket = new DatagramPacket(ackBuf, 0,
            ackBuf.length, packet.getAddress(), packet.getPort());
    System.out.println("ip:" + packet.getAddress().getHostName() + "port:"+packet.getPort());
    clientB.send(ackPacket);

    //把读取到数据保存到缓冲数组中
    if (ack >= receiveCount && ack < maxPacket && !receiveFlag[ack]) {
        buffPacket[ack] = imageBuf;
        receiveFlag[ack] = true;
        receiveCount++;
    }
}
//拼接缓冲数组数据
int index = 0;
for (byte[] b : buffPacket) {
    System.arraycopy(b, 0, imageData, index, b.length);
    index += b.length;
}
return imageData;

以下是接收方完整的接收视频方法参考代码。

java 复制代码
public byte[] receiveVideo() throws Exception {
    //读取总长度
    byte[] lenBuf = new byte[4];
    DatagramPacket receiveLen = new DatagramPacket(lenBuf, 0, lenBuf.length);
    clientB.receive(receiveLen);
    int len = byteToInt(lenBuf);

    System.out.println(len);
    //存储还原后的数据
    byte[] imageData = new byte[len];
    int maxPacket = (len + PACKET_SIZE - 1) / PACKET_SIZE;  //计算有多少个数据包要接收

    //用二维数组缓存每个数据包,二维的默认值是null
    byte[][] buffPacket = new byte[maxPacket][];
    boolean[] receiveFlag = new boolean[maxPacket];
    int receiveCount = 0;

    while (receiveCount < maxPacket) {
        //读取一个数据包
        byte[] packetBuf = new byte[PACKET_SIZE + 4];
        DatagramPacket packet = new DatagramPacket(packetBuf, 0,
                packetBuf.length);
        clientB.receive(packet);
        int readLen = packet.getLength();
        System.out.println("packet.getLength() = " + packet.getLength());

        //从packetBuf 解析出ACK用于回传
        int ack = byteToInt(packetBuf);
        System.out.println("ack = " + ack);

        //提取从 packetBuf数组的图像数据
        byte[] imageBuf = new byte[readLen - 4];
        System.arraycopy(packetBuf, 4, imageBuf, 0, imageBuf.length);

        //回传 ACK
        byte[] ackBuf = new byte[4];
        intToBytes(ackBuf, ack);
        DatagramPacket ackPacket = new DatagramPacket(ackBuf, 0,
                ackBuf.length, packet.getAddress(), packet.getPort());
        System.out.println("ip:" + packet.getAddress().getHostName() + "port:"+packet.getPort());
        clientB.send(ackPacket);

        //把读取到数据保存到缓冲数组中
        if (ack >= receiveCount && ack < maxPacket && !receiveFlag[ack]) {
            buffPacket[ack] = imageBuf;
            receiveFlag[ack] = true;
            receiveCount++;
        }
    }
    //拼接缓冲数组数据
    int index = 0;
    for (byte[] b : buffPacket) {
        System.arraycopy(b, 0, imageData, index, b.length);
        index += b.length;
    }
    return imageData;
}
相关推荐
Fortune792 小时前
实时操作系统中的C++
开发语言·c++·算法
夫礼者2 小时前
【极简监控】打破中间件黑盒:用 Micrometer 打造“SLF4J式”的降维打击Metrics监控体系
java·中间件·监控·metrics·micrometer
yashuk2 小时前
Spring Boot 3.4 正式发布,结构化日志!
java·spring boot·后端
吠品2 小时前
PyTorch张量维度不匹配?实战排查与修复指南
开发语言·oracle·php
AI科技星2 小时前
基于v≡c光速螺旋理论的正确性证明:严格遵循科学方法论的完整路径
c语言·开发语言·人工智能·线性代数·算法·机器学习·数学建模
ALex_zry4 小时前
C++ ORM与数据库访问层设计:Repository模式实战
开发语言·数据库·c++
潜创微科技--高清音视频芯片方案开发10 小时前
2026年C转DP芯片方案深度分析:从适配场景到成本性能的优选指南
c语言·开发语言