树莓派搭建http服务器实现网页监控摄像头

目录


前言

在树莓派部署yolov5lite的基础上,搭建http服务器,添加网页监控功能


一、HTTP协议

http(hyper text transfer prtocol)即超文本传输协议,利用该协议能够实现浏览器加载本地文件的功能,本文在http1.0版本的基础上实现功能。

1.客户端请求

客户端请求的格式一般由:请求行、请求头、空行和请求数据组成,具体格式如下:

请求行:        请求方法|空格|URL|空格|协议版本|回车符|换行符|

请求头:        字段名|||回车符|换行符|

                       ........

                       字段名||值|回车符|换行符|

空行:            回车符|换行符|

请求数据:     xxxxxxxx

 2.服务器响应

服务器响应的格式一般由:状态行、消息头、空行和响应正文组成,具体格式如下:

状态行:        协议版本|空格|响应代号|空格|代号描述|回车符|换行符|

消息头:        字段名|||回车符|换行符|

                       ........

                       字段名|||回车符|换行符|

空行:            回车符|换行符|

响应正文:     xxxxxxxx

3.常见响应代号和对应描述

响应代号 代号描述
服务器上存在请求内容并可以响应给客户端 200 OK
客户端请求异常,方法有问题 501 METHOD NOT IMPLEMENTED
请求内容不存在 404 NOT FOUND
客户端发送的请求格式有问题 400 BAD REQUEST

二、服务器搭建

1.思路

http协议工作模式为客户端发送请求,服务器响应请求,客户端为浏览器,因此只需要解析客户端发来的请求并响应即可,由于yolov5在c++环境下部署,因此服务器采用c/c++语言。

2.获取请求头部

简历socket连接,read客户端发来的消息并解析

/*
作用:获取请求行
形参:套接字id,读取内容缓存buff
返回值:-1表示出错,0表示读取一个空行, >0表示成功读取一行
*/
int HTTP::getline(int socketid, string &buff)
{
	while(true)//遇到换行说明一行结束
	{
		char ch = '\0';
		int len = read(socketid, &ch, 1);

		if(len == 1)//正常读取
		{
			if(ch == '\r') 			continue;
			else if (ch == '\n') 	break;

			buff += ch;//string添加
		}
		else if (len == -1)//读取出错
		{
			perror("read error");
			return -1;
		}
		else if (len == 0)//无数据
		{
			std::cerr<<"关闭客户端"<<endl;
			return 0;
		}
	}
	
	return buff.size();
}

/*
作用:获取请求头,遇到两次回车换行代表请求头结束
形参:套接字id, 头部内容缓存
返回值:0表示成功, -1表示错误
*/
int HTTP::gethead(int socketid, string &header)
{
	int len = 0;
	do
	{
		header.clear();
		len = getline(socketid, header);
		cout<<"head: "<<header<<endl;
	}while(len > 0);
	cout<<endl;
	return len;
}

 3.解析头部获取URL和请求

根据请求头格式,先读取一行请求行,利用空格划分字符串并获取URL和请求,如果URL格式不正确,比如url=index.htm?xxxxxxxxx,只取?前的内容

*
作用:解析请求行内容
形参:请求行内容buff,请求方法缓存request,url路径缓存
返回值:解析成功返回true,否则false
*/
bool HTTP::get_request_url(string buff, string &request, string &url)
{
	if(buff == "")
		return false;

	std::vector<string> strlist;
	strlist.clear();
	buff += " ";
	size_t pos = buff.find(' ');

	while(pos != buff.npos)
	{
		strlist.push_back(buff.substr(0, pos));
		buff = buff.substr(pos+1, buff.size());
		pos = buff.find(' ');
	}

	request = strlist[0];
	url = strlist[1];
	cout<<"request: "<<request<<"\turl:"<<url<<endl;
	return true;
}

/*
作用:获取真正的url并定位到本地文件
参数: url缓存
返回值:返回url文件大小
*/
long int HTTP::get_realurl(string &url, int &status)
{
	url = url.substr(0, url.find('?'));

	struct stat info;
	if(stat((work_dir+url).c_str(), &info) == -1)//文件不存在或者出错,响应404
	{
		cerr<<"stat "<<(work_dir+url)<<" failed, reason:"<<strerror(errno)<<endl;
	}
	else
	{
		if(S_ISDIR(info.st_mode))//如果是目录
		{
			url = work_dir + "/200.html";
		}
		else
		{
			url = work_dir + url;

		}
		status = 200;
		return info.st_size;
	}

	url = work_dir + "/404.html";
	stat(url.c_str(), &info);
	status = 404;
	return info.st_size;
}

4.服务器响应 

根据请求方法和URL进行响应,由于本文只用到了GET方法,故只需要解决GET和非GET

/*
作用:响应200 and 404
参数:套接字id, url路径, 响应代号status
*/
void HTTP::do_response(int socketid, std::string url, int status)
{
	cout<<"==============================do_response========================"<<endl;
	int size = get_realurl(url, status);
	cout<<"status: "<<status<<endl;
	cout<<"url: "<<url.c_str()<<endl;
	cout<<"size: "<<size<<endl;
	string header;
	gethead(socketid, header);
	if(url.find("jpg") != url.npos || (url.find("png")) != url.npos || (url.find("ico")) != url.npos)
		header = img_header;
	else
		header = text_header;
	if(status == 200 )//url文件存在
	{
		header = status_200 + header + to_string(size) + "\r\n\r\n";
	}
	else if(status == 404)//文件不存在
	{
		header = status_404 + header + to_string(size) + "\r\n\r\n";;
	}
	cout<<"header: "<<header<<endl;
	//1.发送头部
	int len = write(socketid, header.c_str(), header.size());
	if(len < 0)
	{
		cerr<<"socket write error"<<endl;
		return;
	}
	//2.发送html文件
	FILE* urlfd = fopen(url.c_str(), "rb");
	do
	{
		char buff[1024] = {0};
		len = fread(buff, 1, 1024, urlfd);
		cout<<buff;
		len = write(socketid, buff, len);

		if(len < 0)
		{
			cerr<<"socket write error"<<endl;
			return;
		}
	}while(!feof(urlfd));
	fclose(urlfd);
}

/*
作用:响应400 and 501
参数:套接字id, 响应代号status
*/
void HTTP::do_response(int socketid, int status)
{
	cout<<"==============================do_response========================"<<endl;
	struct stat info;
	string header, url;
	gethead(socketid, header);
	if(status == 400 )//url文件存在
	{
		url = work_dir + "/400.html";
		stat(url.c_str(), &info);
		header = status_400 + text_header + to_string(info.st_size) + "\r\n\r\n";
	}
	else if(status == 501)//文件不存在
	{
		url = work_dir + "/501.html";
		stat(url.c_str(), &info);
		header = status_501 + text_header + to_string(info.st_size) + "\r\n\r\n";;
	}
	cout<<"header: "<<header<<endl;
	//1.发送头部
	int len = write(socketid, header.c_str(), header.size());
	if(len < 0)
	{
		cerr<<"socket write error"<<endl;
		return;
	}

	//2.发送html文件
	FILE* urlfd = fopen(url.c_str(), "rb");
	do
	{
		char buff[1024] = {0};
		len = fread(buff, 1, 1024, urlfd);
		cout<<buff;
		len = write(socketid, buff, len);

		if(len < 0)
		{
			cerr<<"socket write error"<<endl;
			return;
		}
	}while(!feof(urlfd));
	fclose(urlfd);
}

/*
作用:处理客户端请求
参数:套接字id
*/
void HTTP::do_request(int socketid)
{
	string buff, request, url;
	//1.读取请求行并解析方法
	int len = getline(socketid, buff);
	if(len > 0)//读取正常
	{
		
		if(!get_request_url(buff, request, url))//解析错误
			return;
		if(request == "GET")
		{
			int status = 0;
			do_response(socketid, url, status);
		}
		else//非GET请求,读取http头部,并响应客户端501 Mehtod Not Implemented
		{

			do_response(socketid, 501);
		}
	}
	else//读取异常
	{
		do_response(socketid, 400);
	}
}

 5.http.h

#ifndef __HTTP_H
#define __HTTP_H

#include <iostream>
#include "tcp.h"
#include "http.h"
#include "sys/stat.h"
#include <errno.h>
#include <string>
#include <vector>

class HTTP
{
	private:
		const std::string status_200 = "HTTP/1.0 200 OK\r\n";
		const std::string status_501 = "HTTP/1.0 501 METHOD NOT IMPLEMENTED\r\n";
		const std::string status_404 = "HTTP/1.0 404 NOT FOUND\r\n";
		const std::string status_400 = "HTTP/1.0 400 BAD REQUEST\r\n";
		const std::string text_header = "Server: Run Server\r\nContent-Type: text/html\r\nConnection: close\r\nContent-Length: ";
		const std::string img_header = "Server: Run Server\r\nContent-Type: image/jpeg\r\nConnection: close\r\nContent-Length: ";
		const std::string work_dir = "/home/sy/HTTPCPP/html_docs";
    public:
		int getline(int socketid, std::string &buff);
		int gethead(int socketid, std::string &header);
		bool get_request_url(std::string buff, std::string &request,std::string &url);
		long int get_realurl(std::string &url, int &status);
		void do_request(int socketid);
		void do_response(int socketid, std::string url, int status);
		void do_response(int socketid, int status);
};

#endif

6.tcp.cpp 和tcp.h 

#include "tcp.h"

//domain 指定使用何种的地址类型
//PF_INET/AF_INET  Ipv4 网络协议
//PF_INET6/AF_INET6  Ipv6 网络协议
int TCP::Socket(int domain,int type,int protocol)
{
	int socketid = socket(domain,type,protocol);
	if(socketid == -1)
	{
		perror("socket");
		exit(1);
	}
	return socketid;
}

int TCP::Bind(int sockfd,struct sockaddr * my_addr,int addrlen)
{
	int val = bind(sockfd,my_addr,addrlen);
	if(val == -1)
	{
		perror("bind");
		exit(0);
	}
	return val;
}

int TCP::Listen(int s,int backlog)
{
	int val = listen(s,backlog);
	if(val == -1)
	{
		perror("listen");
		exit(0);
	}
	return val;
}

int TCP::Accept(int s,struct sockaddr * addr,socklen_t * addrlen)
{
	int newfd = accept(s,addr,addrlen);
	if(newfd < 0)
	{
		perror("accept");
		exit(0);
	}
	return newfd;
}

int TCP::Connect(int s,struct sockaddr * addr,socklen_t addrlen)
{
	int newfd = connect(s, addr, addrlen);
	if(newfd < 0)
	{
		perror("connet");
		exit(0);
	}
	return newfd;
}

#ifndef TCP_H
#define TCP_H

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdarg.h>
#include <sys/stat.h>
#include <fcntl.h>


class TCP{
public:
		int Socket(int domain,int type,int protocol);
		int Bind(int sockfd,struct sockaddr * my_addr,int addrlen);
		int Listen(int s,int backlog);
		int Accept(int s,struct sockaddr * addr,socklen_t * addrlen);
		int Connect(int s,struct sockaddr * addr,socklen_t addrlen);
};

#endif

 三、主程序的实现

树莓派搭建服务器采用多线程的方式,主程序负责人脸识别并保存jpg格式图片,创建线程用来实现图片和html文件传输,设置全局变量标志位,当图像文件写入完成后,线程才能发送图片,而发送图片时不能写入。

#include <iostream>
#include <string>
#include <ctime>

#include <MNN/MNNDefine.h>
#include <MNN/MNNForwardType.h>
#include <MNN/Interpreter.hpp>
#include <opencv2/opencv.hpp>

#include "Yolo.h"
#include "base64.h"
#include "tcp.h"
#include "http.h"
#include <pthread.h>

bool sendok = false;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

void show_shape(std::vector<int> shape)
{
    std::cout<<shape[0]<<" "<<shape[1]<<" "<<shape[2]<<" "<<shape[3]<<" "<<shape[4]<<" "<<std::endl;
    
}

void scale_coords(std::vector<BoxInfo> &boxes, int w_from, int h_from, int w_to, int h_to)
{
    float w_ratio = float(w_to)/float(w_from);
    float h_ratio = float(h_to)/float(h_from);
    
    
    for(auto &box: boxes)
    {
        box.x1 *= w_ratio;
        box.x2 *= w_ratio;
        box.y1 *= h_ratio;
        box.y2 *= h_ratio;
    }
    return ;
}

cv::Mat draw_box(cv::Mat & cv_mat, std::vector<BoxInfo> &boxes)
{
    int CNUM = 80;
    static const char* class_names[] = {
        "face", "face_mask"
    };
    cv::RNG rng(0xFFFFFFFF);
	cv::Scalar_<int> randColor[CNUM];
	for (int i = 0; i < CNUM; i++)
		rng.fill(randColor[i], cv::RNG::UNIFORM, 0, 256);
    
    for(auto box : boxes)
    {
        int width = box.x2-box.x1;
        int height = box.y2-box.y1;
        char text[256];
        cv::Point p = cv::Point(box.x1, box.y1-5);
        cv::Rect rect = cv::Rect(box.x1, box.y1, width, height);
        cv::rectangle(cv_mat, rect, cv::Scalar(0, 0, 255), 2);
        sprintf(text, "%s %.1f%%", class_names[box.label], box.score * 100);
        cv::putText(cv_mat, text, p, cv::FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(0, 0, 255), 2);
    }
    return cv_mat;
}

void* pthread_listen(void* arg)
{
	TCP tcp;
	HTTP http;

	int socketid = tcp.Socket(AF_INET, SOCK_STREAM, 0);

	struct sockaddr_in httpserver;
	httpserver.sin_family = AF_INET;
	httpserver.sin_port = htons(80);
	httpserver.sin_addr.s_addr = htonl(INADDR_ANY);
	tcp.Bind(socketid, (struct sockaddr *)&httpserver, sizeof(struct sockaddr));
	tcp.Listen(socketid, 100);

	while (true)
	{
		struct sockaddr_in client;
		socklen_t clientlen = sizeof(client);
		char client_ip[64] = {0};

		
		int client_sockfd = tcp.Accept(socketid, (struct sockaddr *)&client, &clientlen);
		cout << "========================================================" << endl;
		cout << "client ip: " << inet_ntop(AF_INET, &client.sin_addr.s_addr, client_ip, sizeof(client_ip)) << "\tport : " << ntohs(client.sin_port) << endl;
		while(!sendok);
		http.do_request(client_sockfd);
		sendok = false;
		close(client_sockfd);
	}
	close(socketid);
	pthread_exit(NULL);
}

int main(int argc, char* argv[])
{
	Base64 base64;
    std::string model_name = "weights/yolov5s.mnn";

    int num_classes=2;
    std::vector<YoloLayerData> yolov5s_layers{
			{"461",    32, {
   
   {116, 90}, {156, 198}, {373, 326}}},	
            {"403",    16, {
   
   {30,  61}, {62,  45},  {59,  119}}},	
            {"345", 	8,  {
   
   {10,  13}, {16,  30},  {33,  23}}},	
			
    };
    std::vector<YoloLayerData> & layers = yolov5s_layers;

    int net_size =640;
    // get output data
    std::string output_tensor_name0 = layers[2].name ;
    std::string output_tensor_name1 = layers[1].name ;
    std::string output_tensor_name2 = layers[0].name ;
    
    std::shared_ptr<MNN::Interpreter> net = std::shared_ptr<MNN::Interpreter>(MNN::Interpreter::createFromFile(model_name.c_str()));
    if (nullptr == net) {
        return 0;
    }

    MNN::ScheduleConfig config;
    config.numThread = 4;
    config.type      = static_cast<MNNForwardType>(MNN_FORWARD_CPU);
    MNN::BackendConfig backendConfig;
    backendConfig.precision = (MNN::BackendConfig::PrecisionMode)2;
    // backendConfig.precision =  MNN::PrecisionMode Precision_Normal; // static_cast<PrecisionMode>(Precision_Normal);
    config.backendConfig = &backendConfig;
    
    std::vector<std::string> saveNamesVector;
    saveNamesVector.push_back(output_tensor_name0);
    saveNamesVector.push_back(output_tensor_name1);
    saveNamesVector.push_back(output_tensor_name2);
    
    config.saveTensors = saveNamesVector;
    
    MNN::Session *session = net->createSession(config);
    
	cv::VideoCapture capture;
    capture.open(0);  //修改这个参数可以选择打开想要用的摄像头

    cv::Mat frame;
	int INPUT_SIZE = 320;
	std::string img;
	pthread_mutex_init(&lock, NULL);
	pthread_t id;
	pthread_create(&id, NULL, pthread_listen, NULL);
	while(1)
	{ 
		capture >> frame;
		cv::Mat image;
		cv::flip(frame, image, 1);
		cv::Mat raw_image = image;

		cv::resize(raw_image, image, cv::Size(INPUT_SIZE, INPUT_SIZE));
		// preprocessing
		image.convertTo(image, CV_32FC3);
		// image = (image * 2 / 255.0f) - 1;
		image = image /255.0f;

		// wrapping input tensor, convert nhwc to nchw    
		std::vector<int> dims{1, INPUT_SIZE, INPUT_SIZE, 3};
		auto nhwc_Tensor = MNN::Tensor::create<float>(dims, NULL, MNN::Tensor::TENSORFLOW);
		auto nhwc_data   = nhwc_Tensor->host<float>();
		auto nhwc_size   = nhwc_Tensor->size();
		std::memcpy(nhwc_data, image.data, nhwc_size);

		auto inputTensor = net->getSessionInput(session, nullptr);
		inputTensor->copyFromHostTensor(nhwc_Tensor);

		// run network
		clock_t startTime,endTime;
		startTime = clock();//计时开始
		net->runSession(session);
		
		
		MNN::Tensor *tensor_scores  = net->getSessionOutput(session, output_tensor_name0.c_str());
		MNN::Tensor *tensor_boxes   = net->getSessionOutput(session, output_tensor_name1.c_str());
		MNN::Tensor *tensor_anchors = net->getSessionOutput(session, output_tensor_name2.c_str());

		MNN::Tensor tensor_scores_host(tensor_scores, tensor_scores->getDimensionType());
		MNN::Tensor tensor_boxes_host(tensor_boxes, tensor_boxes->getDimensionType());
		MNN::Tensor tensor_anchors_host(tensor_anchors, tensor_anchors->getDimensionType());

		tensor_scores->copyToHostTensor(&tensor_scores_host);
		tensor_boxes->copyToHostTensor(&tensor_boxes_host);
		tensor_anchors->copyToHostTensor(&tensor_anchors_host);

		std::vector<BoxInfo> result;
		std::vector<BoxInfo> boxes;
		
		yolocv::YoloSize yolosize = yolocv::YoloSize{INPUT_SIZE,INPUT_SIZE};
		
		float threshold = 0.45;
		float nms_threshold = 0.5;

		// show_shape(tensor_scores_host.shape());
		// show_shape(tensor_boxes_host.shape());
		// show_shape(tensor_anchors_host.shape());

		
		boxes = decode_infer(tensor_scores_host, layers[2].stride,  yolosize, net_size, num_classes, layers[2].anchors, threshold);
		result.insert(result.begin(), boxes.begin(), boxes.end());

		boxes = decode_infer(tensor_boxes_host, layers[1].stride,  yolosize, net_size, num_classes, layers[1].anchors, threshold);
		result.insert(result.begin(), boxes.begin(), boxes.end());

		boxes = decode_infer(tensor_anchors_host, layers[0].stride,  yolosize, net_size, num_classes, layers[0].anchors, threshold);
		result.insert(result.begin(), boxes.begin(), boxes.end());

		nms(result, nms_threshold);

		std::cout<<result.size()<<std::endl;

		endTime = clock();//计时结束
		cout << "The forward time is: " <<(double)(endTime - startTime) / 1000.0 << "ms" << endl;
		//cout << "The fps is: " <<1000 / (double)(endTime - startTime)<< "frame/s" << endl;
		scale_coords(result, INPUT_SIZE, INPUT_SIZE, raw_image.cols, raw_image.rows);

		cv::Mat frame_show = draw_box(raw_image, result);
		std::vector<unsigned char> vecImg;
		std::vector<int> quality;
		quality.push_back(CV_IMWRITE_JPEG_QUALITY);
		quality.push_back(30);
		cv::imencode(".jpg",frame_show, vecImg, quality);
			
		if(!sendok)
			cv::imwrite("html_docs/output.jpg", frame_show);
		sendok = true;

		//cv::imshow("outpt", frame_show);
		//if (cv::waitKey(30) >= 0)
		//	break;
    }
    return 0;
}

 四、本地html文件

接收到客户端请求后,会将本地的html文件进行传输,从而实现网页,利用刷新机制,设置图片为200ms刷新一次,从而实现视频效果。

  • 响应代码为200的html
<html lang="zh-CN" refresh=6>
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<title>Raspi</title>
</head>
<script>
function refreshimg(){
document.getElementById("myimg").src="output.jpg?time="+(new Date()).getTime();
}
setInterval("refreshimg()",200);
</script>
<body>
<div align="center">
<h1>Raspi Camera</h1>
<img class="myimg"id="myimg"src="output.jpg"vertical-align="middle"width="640px"height="480px">
</div>
</body>
</html>
  • 响应代码为400的html
<html lang="zh-CN">
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<title>400 BAD REQUEST</title>
</head>
<body>
<div align="center">
<h1>400 BAD REQUEST</h1>
<h1>请求格式有问题!</h1>
</div>
</body>
</html>
  • 响应代码为404的html
<html lang="zh-CN">
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<title>404 NOT FOUND</title>
</head>
<body>
<div align="center">
<h1>404 NOT FOUND</h1>
<h1>File is not existed!</h1>
</div>
</body>
</html>
  •  响应代码为501的html
<html lang="zh-CN">
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<title>501 METHOD NOT IMPLEMENTED</title>
</head>
<body>
<div align="center">
<h1>501 METHOD NOT IMPLEMENTED</h1>
<h1>请求方法异常!</h1>
</div>
</body>
</html>

总结

最终实现效果如图

猜你喜欢

转载自blog.csdn.net/deku_desi/article/details/125586233