c++ c/s架构 多客户端请求服务器端视频(TCP on Windows With Hik

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

项目场景

 

为了模拟视频推流场景中 多用户终端请求拉取服务器端的视频流这里我在Windows平台下采用TCP协议在服务器和客户端之间传输视频流其中服务器端拉取视频流并将视频流以图像帧的形式发送给多路客户端。这里的网络视频流我采用的是海康威视的网络视频流rtsp://admin:abc.1234@192.168.0.64:554/h264/ch1/sub/av_stream


问题描述

多线程下资源共享问题

如果服务器对每个客户端都new一个拉流的对象并分别拉取海康网络视频流然后分别将图像帧发送给每个连接好的客户端不仅进行了不必要的重复的工作而且浪费的计算资源

以下代码为效率低的执行代码部分

std::string  url = "rtsp://admin:abc.1234@192.168.0.64:554/h264/ch1/sub/av_stream";
cv::VideoCapture cap1(url);

std::string  url = "rtsp://admin:abc.1234@192.168.0.64:554/h264/ch1/sub/av_stream";
cv::VideoCapture cap2(url);

原因分析

视频软解码是一个耗CPU的工作如果定义多个对象去拉相同的视频流的话那么必定浪费了不必要的计算资源

DWORD WINAPI VideoThread(LPVOID lpParameter)
{
	//海康威视子码流拉流地址  用户名 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::VideoCapture cap(0);
	while (1)
	{
		WaitForSingleObject(hMutex, INFINITE);
		cap >> cv_img;
		ReleaseMutex(hMutex);
	}
}


解决方案

定义一个拉流对象并将解析后的图像帧作为一个全局变量每个子线程取图像帧时通过互斥锁进行排他性资源获取这里我定义了两个线程一个是拉流的子线程另一个是发送TCP数据的子线程

。以下为服务器端发送数据子线程通过互斥锁进行资源获取的代码

WaitForSingleObject(hMutex, INFINITE);
//if (cv_img.size().width < 0 || cv_img.size().height < 0) { continue; }
if (cv_img.empty()) { Sleep(5);  ReleaseMutex(hMutex); continue; }
ReleaseMutex(hMutex);

以下为服务器端拉流子线程的工作逻辑代码

DWORD WINAPI VideoThread(LPVOID lpParameter)
{
	//海康威视子码流拉流地址  用户名 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::VideoCapture cap(0);
	while (1)
	{
		WaitForSingleObject(hMutex, INFINITE);
		cap >> cv_img;
		ReleaseMutex(hMutex);
	}
}

下面给出服务器端和客户端代码源码

chat_server.cpp 

// chat_server.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <stdio.h>
#include <winsock2.h>
#include <iostream>
#include <windows.h>

#include "opencv2\opencv.hpp"
#include "opencv2\imgproc\imgproc.hpp"
#pragma comment (lib, "ws2_32.lib")  //加载 ws2_32.dll
#pragma comment (lib, "opencv_world340.lib")  //加载 ws2_32.dll

HANDLE  hMutex = CreateMutex(NULL, FALSE, NULL);
cv::Mat cv_img; //cv_图像

DWORD WINAPI VideoThread(LPVOID lpParameter)
{
	//海康威视子码流拉流地址  用户名 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::VideoCapture cap(0);
	while (1)
	{
		WaitForSingleObject(hMutex, INFINITE);
		cap >> cv_img;
		ReleaseMutex(hMutex);
	}
}

DWORD WINAPI ServerThread(LPVOID lpParameter)
{
	std::vector<int> params;  // 压缩参数
	params.resize(3, 0);
	params[0] = CV_IMWRITE_JPEG_QUALITY; // 无损压缩
	params[1] = 30;//压缩的质量参数 该值越大 压缩后的图像质量越好
	SOCKET ClientSocket = *(SOCKET*)lpParameter;
	char serverBuffer[100] = { 0 };//缓冲区
	char frames_cnt[10] = { 0, };
	std::vector<uchar> data_encode;//用来从队列中提取编码后的数据
	
	while (1)
	{
		//由于编码耗时故将这部分操作放在取流线程做减小主线程的处理时间
		std::vector<uchar> data_encode;//保存从网络传输数据解码后的数据
		WaitForSingleObject(hMutex, INFINITE);
		//if (cv_img.size().width < 0 || cv_img.size().height < 0) { continue; }
		if (cv_img.empty()) { Sleep(5);  ReleaseMutex(hMutex); continue; }
		ReleaseMutex(hMutex);
		cv::imencode(".jpg", cv_img, data_encode, params);  // 对图像进行压缩
		int len_encoder = data_encode.size();//获取图像编码后的字节长度 方便后续通过TCP传输时  接收端知道此次传输的字节大小
		_itoa_s(len_encoder, frames_cnt, 10);// 
		send(ClientSocket, 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(ClientSocket, send_b, data_encode.size(), 0);//将图像字节数据传输到服务器端
		delete[]send_b;//销毁对象
		data_encode.clear();//将队列清空  方便下一次进行图像矩阵接收
	}
	//关闭监听套接字
	closesocket(ClientSocket);
	return 0;
}

int main() {
	//初始化 DLL
	WSADATA wsaData;
	WSAStartup(MAKEWORD(2, 2), &wsaData);

	//创建套接字
	SOCKET servSockToListen = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);

	//绑定套接字
	sockaddr_in sockAddr;
	memset(&sockAddr, 0, sizeof(sockAddr));  //每个字节都用0填充
	sockAddr.sin_family = PF_INET;  //使用IPv4地址
	sockAddr.sin_addr.s_addr = htonl(INADDR_ANY);  //具体的IP地址
	sockAddr.sin_port = htons(9999);  //端口
	bind(servSockToListen, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));

	//设置监听状态
	listen(servSockToListen, 20);
	HANDLE vThread = CreateThread(NULL, 0, &VideoThread, NULL, 0, NULL);
	CloseHandle(vThread);
	while (1)
	{
		//接收客户端请求
		SOCKADDR clntAddr;
		int nSize = sizeof(SOCKADDR);

		//accept阻塞
		SOCKET clntSock = accept(servSockToListen, (SOCKADDR*)&clntAddr, &nSize);

		std::cout << "一个客户端已连接到服务器socket是" << clntSock << std::endl;
		//为每一个连接创建一个线程
		HANDLE sThread = CreateThread(NULL, 0, &ServerThread, &clntSock, 0, NULL);
		CloseHandle(sThread);
	}

	//关闭监听套接字
	closesocket(servSockToListen);

	//终止 DLL 的使用
	WSACleanup();
	return 0;
}

 chat_client.cpp

// chat_client.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <stdio.h>
#include <winsock2.h>
#include <iostream>
#include <windows.h>
#include <vector>
#include "opencv2\opencv.hpp"
#include "opencv2\imgproc\imgproc.hpp"
#pragma comment (lib, "ws2_32.lib")  //加载 ws2_32.dll
#pragma comment (lib, "opencv_world340.lib")  //加载 ws2_32.dll
using namespace std;

int main() {
	//初始化 DLL
	WSADATA wsaData;
	WSAStartup(MAKEWORD(2, 2), &wsaData);

	//创建套接字
	SOCKET servSockToContect = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);

	//绑定套接字
	sockaddr_in sockAddr;
	memset(&sockAddr, 0, sizeof(sockAddr));
	sockAddr.sin_family = PF_INET;
	sockAddr.sin_addr.S_un.S_addr = inet_addr("192.168.0.110");
	sockAddr.sin_port = htons(9999);

	//connect阻塞
	connect(servSockToContect, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));

	char sendBuffer[100] = { 0 };
	char clientBuffer[100] = { 0 };
	char frams_cnt[10] = { 0, };
	std::vector<uchar> data_decode;//保存从网络传输数据解码后的数据
	cv::Mat frame;
	while (1)
	{
		int irecv = recv(servSockToContect, frams_cnt, 10, 0);
		int cnt = atoi(frams_cnt);
		//std::cout << " cnt= " << cnt << std::endl;
		data_decode.resize(cnt);//将队列大小重置为图片字节长度
		int index = 0;//表示接收数据长度计量
		int count = cnt;//表示的是要从接收缓冲区接收字节的数量
		char *recv_char = new char[cnt];//新建一个字节数组 数组长度为图片字节长度
		while (count > 0)//这里只能写count > 0 如果写count >= 0 那么while循环会陷入一个死循环
		{
			//在网络通信中  recv 函数一次性接收到的字节数可能小于等于设定的SIZE大小这时可能需要多次recv
			int iRet = recv(servSockToContect, 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())
			{
				cv::imshow("Server", frame);
				cv::waitKey(1);
				data_decode.clear();
			}
			else
			{
				std::cout << "####################################   " << std::endl;
				data_decode.clear();
				continue;
			}
		}
		catch (const char *msg)
		{
			data_decode.clear();
			continue;
		}
	}

	//关闭套接字
	closesocket(servSockToContect);

	//终止 DLL 的使用
	WSACleanup();

	return 0;
}

 下面是服务器端向两个客户端传输视频效果图

 这篇文章承接上一篇和上上篇文章的构造思路后续我会把该文章对应的Github源码链接附上来。

开启5个客户端同时拉取服务器视频流以验证可稳定运行24h以上。

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6