Java 网络通信编程(7):完善视频通信

上期文章中,我们利用 Socket 在客户端之间连续发送图片,初步达到了视频通信的效果。然而,这种传输方式连接开销大、实时性差,带来的画面延迟与卡顿很难满足用户的现实需求。因此,本期文章我们会压缩图片并利用 UDP 协议传输图片数据,实现低延迟的流畅视频通信。

1.DatagramSocket

不同于 TCP 协议的 Soket ,这里我们要使用的 UDP 套接字是 DatagramSocket ,它采用无连接设计,客户端创建时无需手动指定端口,仅负责收发 DatagramPacket 数据包。由于省去了连接建立和断开的流程,系统开销显著降低,特别适合视频传输等对实时性要求高的应用场景。

此处有必要介绍一下 DatagramPacket 数据包,它是 UDP 的数据载体,必须包含 "数据字节数组 + 目标 IP + 目标端口",我们可以在后文的示例代码中看到它的具体使用方法。

我们之前使用的 TCP 是基于字节流的,在通过 Soket 建立连接后,会获取字节流进行读写。但是 UDP 没有"流"的概念,仅支持 "数据包 + 字节数组" 传输,DatagramPacket 这个类的构造方法必须接收字节数组作为数据参数。

2.压缩图片

上文提到 UDP 仅支持 "数据包 + 字节数组" 传输,单个数据包的大小是有限的,同时考虑到提升传输效率,我们需要将图片压缩并转为字节数组,这会用到 ByteArrayOutputStream

ByteArrayOutputStream Java IO 体系中内存型字节输出流 ,可以把数据写入**内存中的字节数组。**它的特点是无磁盘 IO 开销、自动扩容。它的toByteArray() 方法会把流中存储的所有字节一次性提取出来,返回完整的 byte[] 数组,这便是我们实现图片转字节数组的关键。

对于压缩,我们会用到ImageIO ,一个 JDK 内置的图片 IO 工具类。它的 write() 方法可以实现图片格式转换与压缩。通过指定输出图片格式,它可以把对于图片数据写入内存流。

java 复制代码
// 1. 创建内存输出流(内存临时容器)
ByteArrayOutputStream bao = new ByteArrayOutputStream();
// 2. 将BufferedImage压缩为JPG格式,写入内存流
ImageIO.write(bufferedImage,"JPG",bao);
//3.将图片数据转为字节数组
byte[] imageData = bao.toByteArray();

我们的图片格式采用 JPG,因为JPG 是有损压缩格式,会舍弃图片中视觉不敏感的像素细节,以大幅减小数据体积。而 PNG 则是无损压缩,体积大,不适合 UDP 视频传输

在这三行代码中,我们首先创建 bao,相当于在内存中开了一个 "临时缓冲区",用来装压缩后的 JPG 数据;再利用 ImageIO 内部调用的 JPG 压缩算法,把 bufferedImage 的原始像素数据转换为 JPG 编码的二进制数据,并写入 bao;最后 toByteArray() 把缓冲区里的所有 JPG 二进制数据打包成一个连续的 byte[],即 UDP 能传输的唯一格式。

3.视频发送

之前使用 TCP 协议时,我们先发送图片的宽高再发送像素点的 RGB 值,接收方即可接收并正确转换图片数据。TCP 基于字节流传输,数据是连续的,然而 UDP 是离散的数据包传输,服务端无法判断一个图片的数据包何时结束。因此,我们必须先传长度,再传数据,让接收方先获取边界信息并创建对应大小的缓冲数组。

java 复制代码
//获取数组长度
int dataLen = imageData.length;
System.out.println(dataLen);

String str = String.valueOf(dataLen);
byte[] len = str.getBytes();
//打包成数据包,先发送数组长度
DatagramPacket lenPacket = new DatagramPacket(len,0,len.length,
        address,port);
clientA.send(lenPacket);

//再发送数组数据
DatagramPacket imagePacket = new DatagramPacket(imageData,0,dataLen,
        address,port);
clientA.send(imagePacket);

我们利用 String 类的.getBytes() 方法可将字符串转为字符数组。

DatagramPacket 构造方法参数含义:(字节数组, 起始偏移量, 传输长度, 目标IP, 目标端口),这里 offset=0、length=len.length 表示传输整个长度字节数组。

4.视频接收

完成发送端代码后,接收端代码就比较容易理解了。

首先创建 UDP 接收端,绑定指定端口(8888),与发送端的目标端口一致。用 DatagramSocket 的阻塞方法.receive() 接收图片长度,创建对应大小的缓存数组。再接收图片的数据包,用内存型字节输入流 ByteArrayInputStream 将字节数组转为输入流,利用 ImageIO 的.read() 方法读取输入流中的 JPG 格式数据,还原为 BufferedImage 对象,最后再把图片绘制出来。

java 复制代码
public void readImage(Graphics g) {
        new Thread(new Runnable() {
            @Override
            public void run() {

                //创建客户端
                DatagramSocket clientB = null;
                try {
                    clientB = new DatagramSocket(8888);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }

                while (true) {
                    try {
                        //读取image 长度
                        byte[] bLen = new byte[8];
                        DatagramPacket receiveLen = new DatagramPacket(bLen, 0, bLen.length);
                        clientB.receive(receiveLen);

                        //把 byte 数组转成int
                        String str = new String(bLen);
                        int len = Integer.parseInt(str.trim());
                        System.out.println("len = " + len);

                        //定义缓冲数组大小
                        byte[] imageData = new byte[len];
                        DatagramPacket receiveImage = new DatagramPacket(imageData, 0, imageData.length);
                        clientB.receive(receiveImage);

                        //把字节数组转成图片绘制出来
                        ByteArrayInputStream bis = new ByteArrayInputStream(imageData);
                        //读取输入流中的数据
                        BufferedImage bufferedImage = ImageIO.read(bis);
                        g.drawImage(bufferedImage, 0, 0, null);

                        Thread.sleep(70);
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }).start();

    }
相关推荐
fengpan20042 小时前
ubuntu下vscode使用串口
linux·运维·服务器
后青春期的诗go2 小时前
泛微OA-E9与第三方系统集成开发企业级实战记录(九)
java·金蝶·erp·泛微·oa·集成开发·e9
IMPYLH2 小时前
Linux 的 cut 命令
linux·运维·服务器·数据库
先跑起来再说2 小时前
HTTP到底是什么?一次讲清楚
网络·网络协议·计算机网络·http
逸Y 仙X2 小时前
文章十:ElasticSearch索引字段高级属性
java·大数据·elasticsearch·搜索引擎·全文检索
就叫飞六吧2 小时前
Tomcat /hvm类加载机制
java·笔记
闻道且行之2 小时前
ESP32 搭建 HTTP 服务:接收图片并实时显示
网络·网络协议·http·esp32·tft_espi
共享家95272 小时前
Java入门( 日期类与 BigDecimal 工具类 )
java·开发语言
Wzx1980122 小时前
HTTP深度解析
网络·网络协议·http