在 Ubuntu 上用 C++ 实现 UDP 传输图像,核心依赖 OpenCV(处理图像编解码)和 C++ 标准 Socket 库(实现 UDP 通信)。以下是完整的实现方案,包含单包传输(适合小图像)和分片传输(适合大图像)两种场景。
一、环境准备:安装 OpenCV 开发库
首先安装 OpenCV 的 C++ 开发依赖(用于编译链接):
bash
运行
sudo apt-get update
sudo apt-get install libopencv-dev # 包含头文件和编译所需的库
二、核心原理
- 图像处理:用 OpenCV 读取图像,通过
imencode压缩为 JPEG 格式(减少数据量),存储为字节流;接收端用imdecode还原为图像。 - UDP 通信:通过 Socket 函数(
socket、bind、sendto、recvfrom)实现无连接的数据传输。 - 限制与解决: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. 运行步骤
- 准备一张名为
test.jpg的图像(与发送端代码同目录)。 - 先启动接收端(必须先绑定端口):
bash
运行
-
./receiver - 再启动发送端:
bash
运行
-
./sender - 接收端会显示图像,并保存为
received.jpg。
五、方案 2:分片传输(适合大图像)
若图像编码后超过 65507 字节(如 1080P 图像),需分片发送,接收端合并后解码。核心逻辑:
- 发送端:先发送「分片总数」,再逐个发送分片(每个分片 ≤ 65507 字节)。
- 接收端:先接收「分片总数」,再接收所有分片,合并后解码。
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。
六、关键注意事项
- IP 地址配置:局域网传输时,发送端需填写接收端的「局域网 IP」(可通过
ifconfig或ip addr查看),而非127.0.0.1。 - 端口占用:若提示「bind failed: Address already in use」,更换一个未被占用的端口(如 8889)。
- 图像路径:确保发送端的
test.jpg路径正确(相对路径 / 绝对路径均可)。 - UDP 可靠性:UDP 不保证数据完整性,若需可靠传输,需自行实现「重传机制」(如超时重传、确认应答)。
- 实时视频流:若需传输实时视频,可在发送端循环读取摄像头帧(
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; }
4783

被折叠的 条评论
为什么被折叠?



