udp 传输图像

在 Ubuntu 上用 C++ 实现 UDP 传输图像,核心依赖 OpenCV(处理图像编解码)和 C++ 标准 Socket 库(实现 UDP 通信)。以下是完整的实现方案,包含单包传输(适合小图像)和分片传输(适合大图像)两种场景。

一、环境准备:安装 OpenCV 开发库

首先安装 OpenCV 的 C++ 开发依赖(用于编译链接):

bash

运行

sudo apt-get update
sudo apt-get install libopencv-dev  # 包含头文件和编译所需的库

二、核心原理

  1. 图像处理:用 OpenCV 读取图像,通过 imencode 压缩为 JPEG 格式(减少数据量),存储为字节流;接收端用 imdecode 还原为图像。
  2. UDP 通信:通过 Socket 函数(socketbindsendtorecvfrom)实现无连接的数据传输。
  3. 限制与解决:UDP 单个数据包最大约 65507 字节(MTU 限制),大图像需分片传输,接收端合并后解码。

三、方案 1:单包传输(适合小图像)

直接将编码后的图像字节流一次性发送,适合编码后小于 65507 字节的图像(如 640x480 分辨率的 JPEG)。

1. 发送端代码(sender.cpp

cpp

运行

#include <iostream>
#include <vector>
#include <opencv2/opencv.hpp>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

using namespace std;
using namespace cv;

int main() {
    // 1. 配置 UDP 地址(接收端的 IP 和端口)
    const char* RECV_IP = "127.0.0.1";  // 接收端 IP(本地测试用 127.0.0.1,局域网用实际 IP)
    const int RECV_PORT = 8888;         // 接收端端口
    struct sockaddr_in recv_addr;
    recv_addr.sin_family = AF_INET;
    recv_addr.sin_port = htons(RECV_PORT);  // 字节序转换(主机字节序 → 网络字节序)
    inet_pton(AF_INET, RECV_IP, &recv_addr.sin_addr);  // IP 字符串 → 二进制格式

    // 2. 创建 UDP Socket
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("socket create failed");
        return -1;
    }

    // 3. 读取并编码图像(压缩为 JPEG)
    Mat image = imread("test.jpg");  // 替换为你的图像路径
    if (image.empty()) {
        cout << "image read failed" << endl;
        close(sockfd);
        return -1;
    }

    vector<uchar> img_buf;  // 存储编码后的图像字节流
    vector<int> encode_params = {IMWRITE_JPEG_QUALITY, 90};  // JPEG 质量(0-100)
    bool encode_success = imencode(".jpg", image, img_buf, encode_params);
    if (!encode_success) {
        cout << "image encode failed" << endl;
        close(sockfd);
        return -1;
    }

    // 4. 发送图像数据
    int send_len = sendto(
        sockfd,
        img_buf.data(),  // 数据起始地址
        img_buf.size(),  // 数据长度
        0,
        (struct sockaddr*)&recv_addr,
        sizeof(recv_addr)
    );
    if (send_len < 0) {
        perror("sendto failed");
    } else {
        cout << "send success! size: " << send_len << " bytes" << endl;
    }

    // 5. 关闭 Socket
    close(sockfd);
    return 0;
}
2. 接收端代码(receiver.cpp

cpp

运行

#include <iostream>
#include <vector>
#include <opencv2/opencv.hpp>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

using namespace std;
using namespace cv;

int main() {
    // 1. 配置 UDP 地址(监听的 IP 和端口)
    const int LISTEN_PORT = 8888;  // 与发送端端口一致
    struct sockaddr_in listen_addr;
    listen_addr.sin_family = AF_INET;
    listen_addr.sin_port = htons(LISTEN_PORT);
    listen_addr.sin_addr.s_addr = INADDR_ANY;  // 监听所有网络接口(本地/局域网)

    // 2. 创建 UDP Socket
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("socket create failed");
        return -1;
    }

    // 3. 绑定端口(接收端必须绑定端口才能接收数据)
    int bind_ret = bind(sockfd, (struct sockaddr*)&listen_addr, sizeof(listen_addr));
    if (bind_ret < 0) {
        perror("bind failed");
        close(sockfd);
        return -1;
    }
    cout << "waiting for data on port " << LISTEN_PORT << "..." << endl;

    // 4. 接收图像数据(缓冲区设为 UDP 最大包大小 65507)
    const int BUF_SIZE = 65507;
    vector<uchar> recv_buf(BUF_SIZE);
    struct sockaddr_in sender_addr;
    socklen_t sender_addr_len = sizeof(sender_addr);

    int recv_len = recvfrom(
        sockfd,
        recv_buf.data(),
        BUF_SIZE,
        0,
        (struct sockaddr*)&sender_addr,
        &sender_addr_len
    );
    if (recv_len < 0) {
        perror("recvfrom failed");
        close(sockfd);
        return -1;
    }
    cout << "receive success! size: " << recv_len << " bytes from " 
         << inet_ntoa(sender_addr.sin_addr) << ":" << ntohs(sender_addr.sin_port) << endl;

    // 5. 解码图像并显示/保存
    recv_buf.resize(recv_len);  // 截取实际接收的字节
    Mat image = imdecode(Mat(recv_buf), IMREAD_COLOR);  // 解码为彩色图像
    if (image.empty()) {
        cout << "image decode failed" << endl;
        close(sockfd);
        return -1;
    }

    imshow("Received Image", image);  // 显示图像
    imwrite("received.jpg", image);   // 保存图像
    cout << "image saved as received.jpg" << endl;

    waitKey(0);  // 等待按键关闭窗口
    destroyAllWindows();

    // 6. 关闭 Socket
    close(sockfd);
    return 0;
}

四、编译与运行

1. 编译命令

g++ 编译,通过 pkg-config 自动引入 OpenCV 的头文件和库路径(兼容 OpenCV 3/4 版本):

bash

运行

# 编译发送端
g++ sender.cpp -o sender `pkg-config --cflags --libs opencv4`

# 编译接收端
g++ receiver.cpp -o receiver `pkg-config --cflags --libs opencv4`
  • 若安装的是 OpenCV 3,将 opencv4 改为 opencv 即可。
2. 运行步骤
  1. 准备一张名为 test.jpg 的图像(与发送端代码同目录)。
  2. 先启动接收端(必须先绑定端口):

    bash

运行

  • ./receiver
    
  • 再启动发送端:

    bash

运行

  1. ./sender
    
  2. 接收端会显示图像,并保存为 received.jpg

五、方案 2:分片传输(适合大图像)

若图像编码后超过 65507 字节(如 1080P 图像),需分片发送,接收端合并后解码。核心逻辑:

  1. 发送端:先发送「分片总数」,再逐个发送分片(每个分片 ≤ 65507 字节)。
  2. 接收端:先接收「分片总数」,再接收所有分片,合并后解码。
1. 发送端代码(sender_fragment.cpp

cpp

运行

#include <iostream>
#include <vector>
#include <opencv2/opencv.hpp>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

using namespace std;
using namespace cv;

int main() {
    // 1. 配置 UDP 地址(同方案 1)
    const char* RECV_IP = "127.0.0.1";
    const int RECV_PORT = 8888;
    struct sockaddr_in recv_addr;
    recv_addr.sin_family = AF_INET;
    recv_addr.sin_port = htons(RECV_PORT);
    inet_pton(AF_INET, RECV_IP, &recv_addr.sin_addr);

    // 2. 创建 Socket(同方案 1)
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) { perror("socket failed"); return -1; }

    // 3. 读取并编码图像(同方案 1)
    Mat image = imread("test.jpg");
    if (image.empty()) { cout << "image read failed"; close(sockfd); return -1; }

    vector<uchar> img_buf;
    vector<int> encode_params = {IMWRITE_JPEG_QUALITY, 90};
    imencode(".jpg", image, img_buf, encode_params);

    // 4. 分片配置(UDP 最大包大小 65507)
    const int FRAG_SIZE = 65507;
    int total_frags = (img_buf.size() + FRAG_SIZE - 1) / FRAG_SIZE;  // 向上取整计算分片数
    cout << "total fragments: " << total_frags << ", total size: " << img_buf.size() << " bytes" << endl;

    // 5. 发送「分片总数」(先发送控制信息)
    int send_frag_cnt = sendto(
        sockfd,
        &total_frags,
        sizeof(total_frags),
        0,
        (struct sockaddr*)&recv_addr,
        sizeof(recv_addr)
    );
    if (send_frag_cnt < 0) { perror("send frag count failed"); close(sockfd); return -1; }

    // 6. 逐个发送分片
    for (int i = 0; i < total_frags; i++) {
        int start_idx = i * FRAG_SIZE;
        int end_idx = min((i + 1) * FRAG_SIZE, (int)img_buf.size());
        int frag_len = end_idx - start_idx;

        int send_len = sendto(
            sockfd,
            img_buf.data() + start_idx,
            frag_len,
            0,
            (struct sockaddr*)&recv_addr,
            sizeof(recv_addr)
        );
        if (send_len < 0) {
            perror("send fragment failed");
        } else {
            cout << "send fragment " << i + 1 << "/" << total_frags << ", size: " << send_len << " bytes" << endl;
        }
    }

    close(sockfd);
    cout << "all fragments sent" << endl;
    return 0;
}
2. 接收端代码(receiver_fragment.cpp

cpp

运行

#include <iostream>
#include <vector>
#include <opencv2/opencv.hpp>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

using namespace std;
using namespace cv;

int main() {
    // 1. 配置 UDP 地址(同方案 1)
    const int LISTEN_PORT = 8888;
    struct sockaddr_in listen_addr;
    listen_addr.sin_family = AF_INET;
    listen_addr.sin_port = htons(LISTEN_PORT);
    listen_addr.sin_addr.s_addr = INADDR_ANY;

    // 2. 创建并绑定 Socket(同方案 1)
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) { perror("socket failed"); return -1; }
    if (bind(sockfd, (struct sockaddr*)&listen_addr, sizeof(listen_addr)) < 0) {
        perror("bind failed"); close(sockfd); return -1;
    }
    cout << "waiting for fragments on port " << LISTEN_PORT << "..." << endl;

    // 3. 接收「分片总数」
    int total_frags = 0;
    struct sockaddr_in sender_addr;
    socklen_t sender_addr_len = sizeof(sender_addr);

    int recv_cnt = recvfrom(
        sockfd,
        &total_frags,
        sizeof(total_frags),
        0,
        (struct sockaddr*)&sender_addr,
        &sender_addr_len
    );
    if (recv_cnt < 0) {
        perror("recv frag count failed");
        close(sockfd);
        return -1;
    }
    cout << "total fragments to receive: " << total_frags << endl;

    // 4. 接收所有分片并合并
    vector<uchar> img_buf;
    const int FRAG_SIZE = 65507;
    vector<uchar> frag_buf(FRAG_SIZE);

    for (int i = 0; i < total_frags; i++) {
        int recv_len = recvfrom(
            sockfd,
            frag_buf.data(),
            FRAG_SIZE,
            0,
            (struct sockaddr*)&sender_addr,
            &sender_addr_len
        );
        if (recv_len < 0) {
            perror("recv fragment failed");
            close(sockfd);
            return -1;
        }

        // 将分片数据追加到总缓冲区
        img_buf.insert(img_buf.end(), frag_buf.begin(), frag_buf.begin() + recv_len);
        cout << "received fragment " << i + 1 << "/" << total_frags << ", size: " << recv_len << " bytes" << endl;
    }

    // 5. 解码并显示/保存图像(同方案 1)
    Mat image = imdecode(Mat(img_buf), IMREAD_COLOR);
    if (image.empty()) {
        cout << "image decode failed" << endl;
        close(sockfd);
        return -1;
    }

    imshow("Received Image (Fragmented)", image);
    imwrite("received_fragment.jpg", image);
    cout << "fragmented image saved as received_fragment.jpg" << endl;

    waitKey(0);
    destroyAllWindows();
    close(sockfd);
    return 0;
}
3. 编译与运行

编译命令与方案 1 一致(替换文件名即可):

bash

运行

g++ sender_fragment.cpp -o sender_frag `pkg-config --cflags --libs opencv4`
g++ receiver_fragment.cpp -o receiver_frag `pkg-config --cflags --libs opencv4`

运行步骤:先启动 ./receiver_frag,再启动 ./sender_frag

六、关键注意事项

  1. IP 地址配置:局域网传输时,发送端需填写接收端的「局域网 IP」(可通过 ifconfigip addr 查看),而非 127.0.0.1
  2. 端口占用:若提示「bind failed: Address already in use」,更换一个未被占用的端口(如 8889)。
  3. 图像路径:确保发送端的 test.jpg 路径正确(相对路径 / 绝对路径均可)。
  4. UDP 可靠性:UDP 不保证数据完整性,若需可靠传输,需自行实现「重传机制」(如超时重传、确认应答)。
  5. 实时视频流:若需传输实时视频,可在发送端循环读取摄像头帧(VideoCapture),并连续发送;接收端循环接收并显示。

七、扩展:实时视频流(基于分片传输)

只需修改发送端,用 VideoCapture 读取摄像头,循环发送帧;接收端循环接收并显示。以下是发送端修改示例(sender_video.cpp):

cpp

运行

// 其他代码同 sender_fragment.cpp,仅修改「读取图像」部分
int main() {
    // ... 前序 Socket 配置 ...

    // 读取摄像头(0 为默认摄像头,可改为视频文件路径)
    VideoCapture cap(0);
    if (!cap.isOpened()) {
        cout << "camera open failed" << endl;
        close(sockfd);
        return -1;
    }

    Mat frame;
    while (true) {
        cap >> frame;  // 读取一帧
        if (frame.empty()) break;

        // 编码帧(同之前的逻辑)
        vector<uchar> img_buf;
        vector<int> encode_params = {IMWRITE_JPEG_QUALITY, 80};
        imencode(".jpg", frame, img_buf, encode_params);

        // 分片发送(同之前的逻辑)
        const int FRAG_SIZE = 65507;
        int total_frags = (img_buf.size() + FRAG_SIZE - 1) / FRAG_SIZE;
        sendto(sockfd, &total_frags, sizeof(total_frags), 0, (struct sockaddr*)&recv_addr, sizeof(recv_addr));

        for (int i = 0; i < total_frags; i++) {
            int start_idx = i * FRAG_SIZE;
            int end_idx = min((i + 1) * FRAG_SIZE, (int)img_buf.size());
            sendto(sockfd, img_buf.data() + start_idx, end_idx - start_idx, 0, (struct sockaddr*)&recv_addr, sizeof(recv_addr));
        }

        cout << "sent one frame, size: " << img_buf.size() << " bytes" << endl;
        waitKey(1);  // 延时 1ms,避免CPU占用过高
    }

    cap.release();
    close(sockfd);
    return 0;
}

接收端无需大幅修改,只需在 imshow 后添加 waitKey(1) 实现实时刷新:

cpp

运行

// 接收完一帧后修改
imshow("Real-Time Video", image);
waitKey(1);  // 必须调用,否则窗口无响应

编译运行后,即可实现实时视频流的 UDP 传输。

#include <iostream> #include <vector> #include <opencv2/opencv.hpp> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> using namespace std; using namespace cv; int main() { // 1. 配置 UDP 地址(接收端的 IP 和端口) const char* RECV_IP = "127.0.0.1"; // 接收端 IP(本地测试用 127.0.0.1,局域网用实际 IP) const int RECV_PORT = 8888; // 接收端端口 struct sockaddr_in recv_addr; recv_addr.sin_family = AF_INET; recv_addr.sin_port = htons(RECV_PORT); // 字节序转换(主机字节序 → 网络字节序) inet_pton(AF_INET, RECV_IP, &recv_addr.sin_addr); // IP 字符串 → 二进制格式 // 2. 创建 UDP Socket int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd < 0) { perror("socket create failed"); return -1; } // 3. 读取并编码图像(压缩为 JPEG) Mat image = imread("test.jpg"); // 替换为你的图像路径 if (image.empty()) { cout << "image read failed" << endl; close(sockfd); return -1; } vector<uchar> img_buf; // 存储编码后的图像字节流 vector<int> encode_params = {IMWRITE_JPEG_QUALITY, 90}; // JPEG 质量(0-100) bool encode_success = imencode(".jpg", image, img_buf, encode_params); if (!encode_success) { cout << "image encode failed" << endl; close(sockfd); return -1; } // 4. 发送图像数据 int send_len = sendto( sockfd, img_buf.data(), // 数据起始地址 img_buf.size(), // 数据长度 0, (struct sockaddr*)&recv_addr, sizeof(recv_addr) ); if (send_len < 0) { perror("sendto failed"); } else { cout << "send success! size: " << send_len << " bytes" << endl; } // 5. 关闭 Socket close(sockfd); return 0; }

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值