0 引言
TCP是一种可靠的、面向连接的传输协议,它提供了流控制、拥塞控制和可靠的数据传输机制,适用于各种应用场景,特别是对数据传输的可靠性要求较高的场景,如文件传输、Web浏览和电子邮件等。但TCP协议的开销较大、时延较高、不适用于低宽带高延迟网络。
UDP(用户数据报协议)是一种面向无连接的传输层协议。与TCP相比,UDP更加轻量级,不提供可靠性和拥塞控制机制,但具有较低的延迟和较少的开销。由于其简单性和低延迟的特点,UDP常用于需要快速传输和实时性较高的数据,但对数据丢失和乱序具有一定容忍度的场景,在使用UDP时,应用程序需要自行处理数据的可靠性和丢失恢复等问题。
本次基于以上两种协议,来实现视频网络传输。易知,在每一种协议下,都需要有收发两端,才可以完成传输。此外,本次实现视频的实时传输,也即直播的简易版。本质是对摄像头捕捉到的每一帧图像进行传输,在建立连接的前提下,摄像头每捕捉一帧画面,就对其进行压缩编码,然后传输。
1 实验目标
编程实现视频网络传输:利用socket接口,实现压缩视频的网络传输,尝试传输层协议tcp和udp时视频传输质量(时延、卡顿等)。
2 实验内容
2.1 TCP协议
2.1.1 Client端
TCP协议下的客户端算法流程如图2-1-1所示。具体如下:

(1)使用函数socket()创建一个socket。
(2)使用函数setsockopt()设置socket属性。
(3)使用函数bind()绑定IP地址、端口等信息到socket上。
(4)设置要连接的对方的IP地址和端口等属性。
(5)使用函数connect()连接服务器。
(6)设置摄像头参数并开启摄像头。
(7)将摄像头捕获到的图像进行压缩处理并发送。
(8)释放空间并关闭网络连接。
2.1.2 Server端
TCP协议下的服务器端算法流程如图2-1-2所示。具体如下:

(1)使用函数socket()创建一个socket。
(2)使用函数setsockopt()设置socket属性。
(3)使用函数bind()绑定IP地址、端口等信息到socket上。
(4)使函数listen()开启监听。
(5)使用函数accept()接收客户端上来的连接,。
(6)接收数据,将收到的数据解压缩并显示。
(7)关闭网络连接。
(8)关闭监听。
2.2 UDP协议
2.2.1 Client端
UDP协议下的客户端算法流程如图2-2-1所示。具体流程如下:

(1)引入所需的头文件。
(2)使用 WSAStartup() 函数初始化 Winsock 库。
(3)创建套接字 sclient,使用 socket() 函数。
(4)设置服务器地址 serAddr,包括服务器的 IP 地址和端口号。
(5)创建 VideoCapture 对象 capture,用于打开摄像头并捕获视频帧。
(6)设置摄像头参数,如分辨率和视频大小。
(7)创建一个大小为 MAX_BUFFER_SIZE 的字符数组 sendData,用于存储图像数据。
(8)创建名为 "Client" 的窗口,用于显示捕获的视频帧。
(9)在循环中,读取摄像头的每一帧图像数据,并将每帧图像进行90JPEG压缩处理。
(10)显示视频帧,并等待按键。
(11)当按下 ESC 键时,退出循环。
(12)使用 sendto() 函数将 sendData 数组中的数据通过 UDP 发送给服务器。
(13)释放摄像头资源,释放内存,关闭窗口,关闭套接字,并清理 Winsock 库。
2.2.2 Server端
UDP协议下的服务器端算法流程如图2-2-2所示。具体如下:

(1)初始化 Winsock 库:通过调用 WSAStartup 函数初始化 Winsock 库。
(2)创建套接字:使用 socket 函数创建一个 UDP 套接字。
(3)绑定 IP 地址和端口号:通过设置 sockaddr_in 结构体的相关字段,使用 bind 函数将套接字绑定到指定的 IP 地址和端口号。
(4)创建接收缓冲区和图像对象:定义一个接收缓冲区用于接收来自客户端的图像数据,并创建一个Mat 对象用于存储接收到的图像。
(5)进入循环等待:通过一个无限循环,服务器端不断等待来自客户端的图像数据。
(6)接收数据:使用 recvfrom 函数从客户端接收图像数据,并将数据存储在接收缓冲区中。
(7)解析图像数据:将接收到的图像数据解压缩并存储。
(8)显示图像:使用 cv::imshow 函数显示接收到的图像。
(9)继续等待下一次数据接收:回到循环的开头,继续等待下一次来自客户端的图像数据。
(10)清理资源:在退出循环后,释放所使用的资源,包括关闭套接字和清理 Winsock 库。
3 实验结果
3.1 TCP协议


由上述结果可知,对视频进行JPEG压缩并传输,发送端摄像头捕获的帧率为30FPS,且发送的时延大致稳定在0.06ms;接收端视频的帧率约为14FPS,接收处理时延约为0.15ms且不稳定。而且在实际的过程中,肉眼能够明显看到接收端视频的卡顿,效果较差。我看到接收端处理时延会突然出现8ms跳变,因此我怀疑是解压缩解压缩的问题。
3.2 UDP协议


由上述结果可知,对视频进行JPEG压缩并传输,发送端摄像头捕获的帧率为30FPS,且发送的时延平均在0.05ms;接收端视频的帧率约为13FPS,接收处理时延约为8ms,没有较大波动。在实际的过程中,肉眼能够明显看到接收端视频的不太流畅,但是不会出现明显卡顿,是由于帧率较低且处理时延较稳定的原因。
4 代码部分
4.1 TCP协议
4.1.1 Client端
//进行JPEG压缩后传输
#include <WINSOCK2.H>
#include <iostream>
#include <stdio.h>
#include <cv.h>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <vector>
#include <chrono>
#pragma comment(lib,"ws2_32.lib")
using namespace cv;
int main(int argc, char* argv[])
{
WORD sockVersion = MAKEWORD(2, 2);
WSADATA data;
if (WSAStartup(sockVersion, &data) != 0)
{
return 0;
}
SOCKET sclient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sclient == INVALID_SOCKET)
{
printf("invalid socket !\n");
return 0;
}
sockaddr_in serAddr;
serAddr.sin_family = AF_INET;
serAddr.sin_port = htons(8888);
serAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); //服务器的IP地址,可以是:①连接外网后分配的②手动设置的
if (connect(sclient, (sockaddr*)&serAddr, sizeof(serAddr)) == SOCKET_ERROR)
{
printf("connect error !\n");
closesocket(sclient);
return 0;
}
//摄像头
VideoCapture capture(0);
//摄像头参数设置
capture.set(CV_CAP_PROP_FRAME_WIDTH, 640);//宽度
capture.set(CV_CAP_PROP_FRAME_HEIGHT, 480);//高度
//视频大小
Size S = Size((int)capture.get(CV_CAP_PROP_FRAME_WIDTH),
(int)capture.get(CV_CAP_PROP_FRAME_HEIGHT));
//初始化压缩
std::vector<int> compressionParams;
compressionParams.push_back(CV_IMWRITE_JPEG_QUALITY);//JPEG压缩
compressionParams.push_back(90); // 压缩质量,0 - 100
// 变量用于计算时延
std::chrono::steady_clock::time_point startTime1, endTime1;
int i, j;
int key;
char* sendData = new char[1000000];
namedWindow("Client", CV_WINDOW_AUTOSIZE);
Mat frame, gray;
while (capture.read(frame))
{
// 压缩图像到frame
std::vector<uchar> compressedData;
imencode(".jpg", frame, compressedData, compressionParams);
// 发送数据前记录时间戳
startTime1 = std::chrono::steady_clock::now();
// 发送数据
send(sclient, reinterpret_cast<const char*>(compressedData.data()), compressedData.size(), 0);
// 发送数据后记录时间戳
endTime1 = std::chrono::steady_clock::now();
// 计算时延
std::chrono::duration<double> timeDiff = endTime1 - startTime1;
printf("发送时延: %.2f ms\n", timeDiff.count() * 1000);
//摄像头捕获的FPS
int fps = capture.get(CV_CAP_PROP_FPS);
printf("current fps : %d \n", fps);
imshow("Client", frame);
cvWaitKey(30);//如果服务端收到的视频比较卡,此处延时适当改大一点
send(sclient, sendData, 1000000, 0);
//等待按键
key = cvWaitKey(30);
//按ESC键直接退出
if (key == 27) {
return 0;
}
}
capture.release();
delete[] sendData;
cvDestroyWindow("Client");
closesocket(sclient);
WSACleanup();
return 0;
}
4.1.2 Server端
//进行JPEG压缩后传输
#include <stdio.h>
#include <winsock2.h>
#include <cv.h>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <vector>
#include <chrono>
#pragma comment(lib,"ws2_32.lib")
int main(void)
{
//初始化WSA
WORD sockVersion = MAKEWORD(2, 2);
WSADATA wsaData;
if (WSAStartup(sockVersion, &wsaData) != 0)
{
return 0;
}
//创建套接字
SOCKET slisten = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (slisten == INVALID_SOCKET)
{
printf("socket error !");
return 0;
}
//绑定IP和端口
sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(8888);//端口8888
sin.sin_addr.S_un.S_addr = INADDR_ANY;
if (bind(slisten, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR)
{
printf("bind error !");
}
//开始监听
if (listen(slisten, 5) == SOCKET_ERROR)
{
printf("listen error !");
return 0;
}
//循环接收数据
SOCKET sClient;
sockaddr_in remoteAddr;
int nAddrlen = sizeof(remoteAddr);
printf("等待连接...\n");
do
{
sClient = accept(slisten, (SOCKADDR*)&remoteAddr, &nAddrlen);
} while (sClient == INVALID_SOCKET);
printf("接受到一个连接:%s \r\n", inet_ntoa(remoteAddr.sin_addr));
cv::Mat image_src(cv::Size(640, 480), CV_8UC3);
// 变量用于计算帧率时延
std::chrono::steady_clock::time_point startTime = std::chrono::steady_clock::now();
int frameCount = 0;
double fps = 0.0;
std::chrono::steady_clock::time_point startTime1, endTime1;
char* revData = new char[1000000];
int i, j;
int ret;
int key;
cv::namedWindow("server", cv::WINDOW_AUTOSIZE);
while (true)
{
// 接收数据
ret = recv(sClient, revData, 1000000, 0);
// 接收数据前记录时间戳
startTime1 = std::chrono::steady_clock::now();
if (ret > 0)
{
revData[ret] = 0x00;
// 创建向量存储接收数据
std::vector<uchar> receivedData(revData, revData + ret);
// 解码收到的压缩图像
cv::Mat receivedImage = cv::imdecode(receivedData, cv::IMREAD_COLOR);
// Display the received image
//cvWaitKey(1);
if (!receivedImage.empty())
{
cv::imshow("server", receivedImage);
}
// 接收数据后记录时间戳
endTime1 = std::chrono::steady_clock::now();
// 计算接收时延
std::chrono::duration<double> timeDiff = endTime1 - startTime1;
printf("接收处理时延: %.2f ms\n", timeDiff.count() * 1000);
// 重置
ret = 0;
//等待按键
key = cvWaitKey(30);
//按ESC键直接退出
if (key == 27) {
return 0;
}
}
// 计算帧率
frameCount++;
std::chrono::steady_clock::time_point endTime = std::chrono::steady_clock::now();
std::chrono::duration<double> elapsedTime = endTime - startTime;
if (elapsedTime.count() >= 1.0)
{
fps = frameCount / elapsedTime.count();
printf("帧率: %.2f\n", fps);
frameCount = 0;
startTime = endTime;
}
//等待按键
key = cvWaitKey(30);
//按ESC键直接退出
if (key == 27) {
return 0;
}
}
cvDestroyWindow("server");
delete[] revData;
closesocket(slisten);
WSACleanup();
return 0;
}
4.2 UDP协议
4.2.1 Client端
#include <WINSOCK2.H>
#include <iostream>
#include <stdio.h>
#include <cv.h>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <chrono>
#pragma comment(lib,"ws2_32.lib")
#define MAX_BUFFER_SIZE 1000000
using namespace cv;
int main()
{
WORD sockVersion = MAKEWORD(2, 2);
WSADATA data;
if (WSAStartup(sockVersion, &data) != 0)
{
return 0;
}
// 创建套接字
SOCKET sclient = socket(AF_INET, SOCK_DGRAM, 0);
if (sclient == -1)
{
printf("invalid socket !\n");
return 0;
}
// 服务器地址
sockaddr_in serAddr;
serAddr.sin_family = AF_INET;
serAddr.sin_port = htons(8888);
serAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); // 服务器的IP地址,可以根据实际情况修改
// 摄像头
VideoCapture capture(0);
// 摄像头参数设置
// 分辨率
capture.set(CV_CAP_PROP_FRAME_WIDTH, 640); // 宽度
capture.set(CV_CAP_PROP_FRAME_HEIGHT, 480); // 高度
// 视频大小
Size S = Size((int)capture.get(CV_CAP_PROP_FRAME_WIDTH),
(int)capture.get(CV_CAP_PROP_FRAME_HEIGHT));
std::vector<uchar> sendData;
namedWindow("Client", CV_WINDOW_AUTOSIZE);
Mat frame;
int key;
// 变量用于计算时延
std::chrono::steady_clock::time_point startTime1, endTime1;
while (capture.read(frame))
{
// 图像压缩
std::vector<int> compression_params;
compression_params.push_back(CV_IMWRITE_JPEG_QUALITY); // JPEG压缩
compression_params.push_back(90); // 压缩质量,0 - 100
// 压缩图像到sendData中
imencode(".jpg", frame, sendData, compression_params);
imshow("Client", frame);
//等待按键
key = cvWaitKey(30);
//按ESC键直接退出
if (key == 27) {
break;
}
// 发送数据前记录时间戳
startTime1 = std::chrono::steady_clock::now();
//摄像头捕获的FPS
int fps = capture.get(CV_CAP_PROP_FPS);
printf("current fps : %d \n", fps);
// 发送数据
int sendResult = sendto(sclient, reinterpret_cast<char*>(sendData.data()), sendData.size(), 0, (struct sockaddr*)&serAddr, sizeof(serAddr));
if (sendResult == -1)
{
printf("Failed to send data\n");
}
// 发送数据后记录时间戳
endTime1 = std::chrono::steady_clock::now();
// 计算时延
std::chrono::duration<double> timeDiff = endTime1 - startTime1;
printf("发送时延: %.2f ms\n", timeDiff.count() * 1000);
}
capture.release();
cvDestroyWindow("Client");
closesocket(sclient);
WSACleanup();
return 0;
}
4.2.2 Server端
#include <stdio.h>
#include <winsock2.h>
#include <opencv2/opencv.hpp>
#include <cv.h>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <Ws2tcpip.h>
#include <chrono>
#pragma comment(lib, "ws2_32.lib")
#define MAX_BUFFER_SIZE 1000000
int main()
{
// 初始化WSA
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
printf("Failed to initialize winsock");
return 0;
}
// 创建套接字
SOCKET sServer = socket(AF_INET, SOCK_DGRAM, 0);
if (sServer == INVALID_SOCKET)
{
printf("Failed to create socket");
return 0;
}
// 绑定IP和端口
sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(8888); // 端口8888
serverAddr.sin_addr.S_un.S_addr = INADDR_ANY;
if (bind(sServer, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == -1)
{
printf("Failed to bind socket");
closesocket(sServer);
return 0;
}
cv::Mat image_dst;
std::vector<uchar> imageData;
cv::namedWindow("Server", cv::WINDOW_NORMAL);
printf("等待连接...\n");
// 变量用于计算帧率时延
std::chrono::steady_clock::time_point startTime = std::chrono::steady_clock::now();
int frameCount = 0;
double fps = 0.0;
std::chrono::steady_clock::time_point startTime1, endTime1;
while (1)
{
// 接收数据前记录时间戳
startTime1 = std::chrono::steady_clock::now();
// 接收数据
struct sockaddr_in clientAddr;
socklen_t nLen = sizeof(clientAddr);
char revData[MAX_BUFFER_SIZE];
int ret = recvfrom(sServer, revData, MAX_BUFFER_SIZE, 0, (struct sockaddr*)&clientAddr, &nLen);
if (ret > 0)
{
revData[ret] = '\0';
// 将接收到的数据存储在imageData中
imageData.assign(revData, revData + ret);
// 解码并显示图像
image_dst = cv::imdecode(imageData, cv::IMREAD_COLOR);
if (!image_dst.empty())
{
cv::imshow("Server", image_dst);
}
// 接收数据后记录时间戳
endTime1 = std::chrono::steady_clock::now();
// 计算接收时延
std::chrono::duration<double> timeDiff = endTime1 - startTime1;
printf("接收处理时延: %.2f ms\n", timeDiff.count() * 1000);
//等待按键
int key = cvWaitKey(30);
//按ESC键直接退出
if (key == 27) {
break;
}
// 计算帧率
frameCount++;
std::chrono::steady_clock::time_point endTime = std::chrono::steady_clock::now();
std::chrono::duration<double> elapsedTime = endTime - startTime;
if (elapsedTime.count() >= 1.0)
{
fps = frameCount / elapsedTime.count();
printf("帧率: %.2f\n", fps);
frameCount = 0;
startTime = endTime;
}
}
//等待按键
int key = cvWaitKey(30);
//按ESC键直接退出
if (key == 27) {
break;
}
}
cv::destroyWindow("Server");
closesocket(sServer);
WSACleanup();
return 0;
}