项目场景:
在无线局域网里采用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
<WinSock
2.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:IPV
4
-
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();
/
/释放初始化Ws
2_
32.dll所分配的资源。
-
}
-
-
int ma
in()
-
{
-
std
::thread
Get(
get_online_video);
-
std
::thread
Send(
send_online_video);
-
Get.join();
-
Send.join();
-
return
0;
-
}
下面贴出服务器端代码
-
bool Server
::
receive_dat
a() {
-
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链接
https://github.com/linxizi/TCP_Online_Video.git
转载:https://blog.csdn.net/linxizi0622/article/details/128868324