项目场景:
在无线局域网里采用TCP协议传输海康威视网络视频:
上一篇文章中采用UDP协议传输网络视频,由于事先不知道图像字节长度,导致每次传输视频之前都需要根据图像大小更改UDP接收缓冲区,同时,上一篇文章中涉及到的只是在局域网中传输USB摄像头视频,如何快速解码网络摄像头并且高质量传输。这里我用到了多线程对快速解码这一要求进行了响应,采用TCP协议,在传输图像字节之前,先传输图像字节长度,在接收端根据发送端发送的长度信息,实时new一个字节数组作为缓冲区对图像字节数据进行保存。
问题描述
1、服务器端从接收缓冲区接收图像字节时,将图像队列复制给字节数组,如果采用在循环中进行赋值,那么当队列长度很大时 , 耗时很长,请看代码:
以下为未优化之前的代码
char send_char[SIZE] = { 0, };
int index = 0;
bool flag = false;
for (int i = 0; i < len_encoder / SIZE + 1; ++i) {
for (int k = 0; k < SIZE; ++k) {
if (index >= data_encode.size()) {
flag = true;
break;
}
send_char[k] = data_encode[index++];
}
send(m_server, send_char, SIZE, 0);
}
原因分析:
在循环中将data_encode队列中的每一个元素赋值为
send_char,如果这个循环很大,是很耗时的.
解决方案:
memcpy函数的功能是从源src所指的内存地址的起始位置开始拷贝n个字节到目标dest所指的内存地址的起始位置中。
memcpy是内存拷贝,当一段连续的空间很长时,采用内存拷贝效率高,而采用在循环中对数组元素进行单个赋值 效率很低。采用memcpy拷贝时 是一次性将源src 拷贝到目标dst
以下为优化后的代码
char *send_b = new char[data_encode.size()];
memcpy(send_b, &data_encode[0], data_encode.size());
优化后 , 代码的执行速度比优化前至少加快了10倍
问题描述
2、采用opencv软解码海康威视摄像头视频流时,如果解码速度太慢,这样的状态持续一段时间后,会导致解码模块出bug,报类似于以下错误:
[h264 @ 000000000ef76940] cabac decode of qscale diff failed at 84 17 [h264 @ 000000000ef76940] error while decoding MB 84 17, bytestream 507ffmpeg
原因分析:
这种错误,我在网上查了一下,是由于解码模块在相邻两帧解码速度太慢导致的错误
之前代码逻辑是在一个线程里 解码视频 + 发送视频 ,这个过程太耗时,超过了40ms,久而久之,码流得不到解析,就会报错
解决方案:
那么这里可以采用多线程:一个线程解码视频,一个线程发送视频
以下为未优化之前的代码
while (m_cap.read(frame)) {
imencode(".jpg", frame, data_encode, params); // 对图像进行压缩
int len_encoder = data_encode.size();
_itoa_s(len_encoder, frames_cnt, 10);
send(m_server, frames_cnt, 10, 0);
_itoa_s(SIZE, frames_cnt, 10);
send(m_server, frames_cnt, 10, 0);
// 发送
char send_char[SIZE] = { 0, };
int index = 0;
bool flag = false;
char *send_b = new char[data_encode.size()];
for (int i = 0; i<data_encode.size(); i++)
{
//data_encode.size()数据装换成字符数组
send_b[i] = data_encode[i];
}
int iSend = send(m_server, send_b, data_encode.size(), 0);
delete[]send_b;
data_encode.clear();
++j;
}
以下为优化后的代码
myMutex.lock();
if (queueInput.empty()) {//如果队列中没有数据 说明队列为空 此时应该等待生产者向队列中输入数据
myMutex.unlock();//释放锁
Sleep(3);//睡眠三秒钟 把锁让给生产者
continue;
}
else {
frame = queueInput.front();//从队列中取出图像矩阵
queueInput.pop();
myMutex.unlock();//释放锁
}
imencode(".jpg", frame, data_encode, params); // 对图像进行压缩
int len_encoder = data_encode.size();//获取图像编码后的字节长度 方便后续通过TCP传输时 接收端知道此次传输的字节大小
_itoa_s(len_encoder, frames_cnt, 10);//
send(m_server, frames_cnt, 10, 0);//将图像字节长度 进行传输
// 发送
int index = 0;//标志实时接收图像字节的长度 方便程序中判断还有多少字节尚未接收到
char *send_b = new char[data_encode.size()];// 创建一个字节数组 开启大小为图像字节长度的字符数组空间
//这里是将data_encode首地址且长度为图片字节长度 通过内存拷贝复制到send_b数组中,相比于采用循环单个元素赋值,速度快了至少10倍
memcpy(send_b, &data_encode[0], data_encode.size());
int iSend = send(m_server, send_b, data_encode.size(), 0);//将图像字节数据传输到服务器端
delete[]send_b;//销毁对象
data_encode.clear();//将队列清空 方便下一次进行图像矩阵接收
++j;
优化后 , 代码的执行速度比优化前至少加快了10倍
问题描述
3、在局域网中进行数据传输时,假如客户端传输100个字节长度的数据,在服务器端可能是先接收到53个字节,然后再接收剩下的47个字节的数据。如果我把客户端和服务器端都放在一个终端上运行,则不会出现这种情况。由于前期是在一个终端(也就是客户端和服务器端都在一台终端上)上面进行代码开发,代码没有出现问题,但是将代码移植到局域网中,出现了上述所说的现象:
原因分析:
这种现象,可能是由于网络传输导致。既然不能避免,那么在服务器端接收数据时,就进行数据长度校验,客户端首先会向服务器发送一个待接收的数据长度值,服务器端按照这个长度值进行数据接收
解决方案:
在服务器端接收数据时,就进行数据长度校验,客户端首先会向服务器发送一个待接收的数据长度值,服务器端按照这个长度值进行数据接收
以下为解决方案代码
while (count > 0)//这里只能写count > 0 如果写count >= 0 那么while循环会陷入一个死循环
{
//在网络通信中 recv 函数一次性接收到的字节数可能小于等于设定的SIZE大小,这时可能需要多次recv
int iRet = recv(m_accept, recv_char, count, 0);
int tmp = 0;//用来保存当前接收的数据长度
for (int k = 0; k < iRet; k++)
{
tmp = k+1;
index++;
if (index >= cnt) { break; }
}
memcpy(&data_decode[index - tmp ], recv_char , tmp);//内存拷贝函数
if (!iRet) { return -1; }
count -= iRet;//更新余下需要从接收缓冲区接收的字节数量
}
delete[]recv_char;
下面贴出客户端代码
// tcp_video_client.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include "stdafx.h"
#include "opencv2\opencv.hpp"
#include "opencv2\imgproc\imgproc.hpp"
#include<WinSock2.h>
#include<iostream>
#include<mutex>
#include<thread>
#pragma comment(lib,"ws2_32.lib")
#pragma comment(lib,"opencv_world340.lib")
std::mutex myMutex;
std::queue<cv::Mat> queueInput;//存储图像的队列
void get_online_video()
{
//海康威视子码流拉流地址 用户名 admin 密码abc.1234 需要修改为对应的用户名和密码
std::string url = "rtsp://admin:abc.1234@192.168.0.64:554/h264/ch1/sub/av_stream";
cv::VideoCapture cap(url);
cv::Mat frame;//保存抽帧的图像矩阵
while (1)
{
cap >> frame;
myMutex.lock();
if (queueInput.size() > 3) {
queueInput.pop();
}
else {
queueInput.push(frame);
}
myMutex.unlock();
}
}
int send_online_video()
{
WORD w_req = MAKEWORD(2, 2);//版本号
WSADATA wsadata;
int err;
err = WSAStartup(w_req, &wsadata);
if (err != 0) {
std::cout << "初始化套接字库失败!" << std::endl;
return false;
}
else {
std::cout << "初始化套接字库成功!" << std::endl;
}
//检测版本号
if (LOBYTE(wsadata.wVersion) != 2 || HIBYTE(wsadata.wHighVersion) != 2) {
std::cout << "套接字库版本号不符!" << std::endl;
WSACleanup();
return false;
}
else {
std::cout << "套接字库版本正确!" << std::endl;
}
SOCKADDR_IN server_addr;
SOCKADDR_IN accept_addr;
//填充服务端信息
server_addr.sin_family = AF_INET; // 用来定义那种地址族,AF_INET:IPV4
std::string m_ip = "192.168.0.111";
server_addr.sin_addr.S_un.S_addr = inet_addr(m_ip.c_str()); // 保存ip地址,htonl将一个无符号长整型转换为TCP/IP协议网络的大端
// INADDR_ANY表示一个服务器上的所有网卡
server_addr.sin_port = htons(7777); // 端口号
//创建套接字
SOCKET m_server = socket(AF_INET, SOCK_STREAM, 0);
if (connect(m_server, (SOCKADDR*)&server_addr, sizeof(SOCKADDR)) == SOCKET_ERROR) {
std::cout << "服务器连接失败!" << std::endl;
WSACleanup();
return false;
}
else {
std::cout << "服务器连接成功!" << std::endl;
}
cv::Mat frame;
std::vector<uchar> data_encode;//保存从网络传输数据解码后的数据
std::vector<int> params; // 压缩参数
params.resize(3, 0);
params[0] = cv::IMWRITE_JPEG_QUALITY; // 无损压缩
params[1] = 30;//压缩的质量参数 该值越大 压缩后的图像质量越好
char frames_cnt[10] = { 0, };
std::cout << "开始发送" << std::endl;
int j = 0;
while (1) {
/* 这里采用多线程 从队列中存取数据 主要是防止单线程解码网络视频速度太慢导致的网络拥塞*/
myMutex.lock();
if (queueInput.empty()) {//如果队列中没有数据 说明队列为空 此时应该等待生产者向队列中输入数据
myMutex.unlock();//释放锁
Sleep(3);//睡眠三秒钟 把锁让给生产者
continue;
}
else {
frame = queueInput.front();//从队列中取出图像矩阵
queueInput.pop();
myMutex.unlock();//释放锁
}
imencode(".jpg", frame, data_encode, params); // 对图像进行压缩
int len_encoder = data_encode.size();//获取图像编码后的字节长度 方便后续通过TCP传输时 接收端知道此次传输的字节大小
_itoa_s(len_encoder, frames_cnt, 10);//
send(m_server, frames_cnt, 10, 0);//将图像字节长度 进行传输
// 发送
int index = 0;//标志实时接收图像字节的长度 方便程序中判断还有多少字节尚未接收到
char *send_b = new char[data_encode.size()];// 创建一个字节数组 开启大小为图像字节长度的字符数组空间
//这里是将data_encode首地址且长度为图片字节长度 通过内存拷贝复制到send_b数组中,相比于采用循环单个元素赋值,速度快了至少10倍
memcpy(send_b, &data_encode[0], data_encode.size());
int iSend = send(m_server, send_b, data_encode.size(), 0);//将图像字节数据传输到服务器端
delete[]send_b;//销毁对象
data_encode.clear();//将队列清空 方便下一次进行图像矩阵接收
++j;
}
std::cout << "发送完成";
closesocket(m_server);//关闭发送端套接字
WSACleanup();//释放初始化Ws2_32.dll所分配的资源。
}
int main()
{
std::thread Get(get_online_video);
std::thread Send(send_online_video);
Get.join();
Send.join();
return 0;
}
下面贴出服务器端代码
bool Server::receive_data() {
Mat frame;
vector<uchar> data_decode;
std::vector<int> params; // 压缩参数
params.resize(3, 0);
params[0] = IMWRITE_JPEG_QUALITY; // 无损压缩
params[1] = 50;
cv::namedWindow("Server", cv::WINDOW_NORMAL);
char frams_cnt[10] = { 0, };
// 解析总帧数
int count = atoi(frams_cnt);
int idx = 0;
while (1) {
// 解析图片字节长度
int irecv = recv(m_accept, frams_cnt, 10, 0);
int cnt = atoi(frams_cnt);
data_decode.resize(cnt);//将队列大小重置为图片字节长度
int index = 0;//表示接收数据长度计量
count = cnt;//表示的是要从接收缓冲区接收字节的数量
char *recv_char = new char[cnt];//新建一个字节数组 数组长度为图片字节长度
while (count > 0)//这里只能写count > 0 如果写count >= 0 那么while循环会陷入一个死循环
{
//在网络通信中 recv 函数一次性接收到的字节数可能小于等于设定的SIZE大小,这时可能需要多次recv
int iRet = recv(m_accept, recv_char, count, 0);
int tmp = 0;
for (int k = 0; k < iRet; k++)
{
tmp = k+1;
index++;
if (index >= cnt) { break; }
}
memcpy(&data_decode[index - tmp ], recv_char , tmp);
if (!iRet) { return -1; }
count -= iRet;//更新余下需要从接收缓冲区接收的字节数量
}
delete[]recv_char;
try {
frame = cv::imdecode(data_decode, CV_LOAD_IMAGE_COLOR);
if (!frame.empty())
{
imshow("Server", frame);
waitKey(1);
data_decode.clear();
}
else
{
std::cout << "#################################### " << std::endl;
data_decode.clear();
continue;
}
}
catch (const char *msg)
{
data_decode.clear();
continue;
}
}
cout << "接受完成";
return true;
}
以上为客户端和服务器端核心代码
完成代码可见github链接