视频点播
该项目是一个共享的视频点播项目,用户通过浏览器对该网站进行访问,每个用户拥有增删查改该网站视频的权限。
技术与开发环境
开发环境:
系统:Ubuntu 20.04
编辑器:visual studio code(vscode)
编译器:gcc、g++
编译脚本:Makefile
所用技术:
C/C++、Linux、JSONCPP、MariaDB、httplib
JsonCpp
- 字符串:使⽤常规双引号 "" 括起来的表⽰⼀个字符串
- 数组:使⽤中括号 [] 括起来的表⽰⼀个数组
- 对象:使⽤花括号 {} 括起来的表⽰⼀个对象
- 数字:包括整形和浮点型,直接使⽤
5 #include <jsoncpp/json/json.h>
6
7 int main()
8 {
9 const char* name="西小明";
10 int age=19;
11 float score[]={77.5,88,99};
12 Json::Value val;
13 val["姓名"]=name;
14 val["年龄"]=age;
15 val["成绩"].append(score[0]);
16 val["成绩"].append(score[1]);
17 val["成绩"].append(score[2]);
18
19 Json::StreamWriterBuilder swb;
20 std::unique_ptr<Json::StreamWriter>sw(swb.newStreamWriter());
21
22 std::stringstream ss;
23 int ret=sw->write(val,&ss);
24 if(ret !=0)
25 {
26 std::cout<<"write failed!\n";
27 return -1;
28 }
29 std::cout<<ss.str()<<std::endl;
30 return 0;
31 }
输出结果为:
由于JSON版本的问题,汉字编码出现一点点问题。
1 #include<iostream>
2 #include<sstream>
3 #include<string>
4 #include<memory>
5 #include<jsoncpp/json/json.h>
6
7 void unserialize(const std::string &str)
8 {
9 Json::Value val;
10 Json::CharReaderBuilder crb;
11 std::unique_ptr<Json::CharReader>cr(crb.newCharReader());
12 std::string err;
13 bool ret=cr->parse(str.c_str(),str.c_str()+str.size(),&val,&err);
14 if(ret==false)
15 {
16 std::cout<<"parse failed!\n";
17 }
18 std::cout<<val["姓名"].asString()<<std::endl;
19 std::cout<<val["年龄"].asInt()<<std::endl;
20 int sz=val["成绩"].size();
21 for(int i=0;i<sz;i++)
22 {
23 std::cout<<val["成绩"][i]<<std::endl;
24 }
25 }
26 int main()
27 {
28 std::string str=R"({"姓名":"小黑","年龄":18,"成绩":[66,77,65]})";
29 unserialize(str);
}
反序列化结果:
MariaDB
MariaDB Server 是最流行的开源关系型数据库之一。它由 MySQL 的原始开发者制作,并保证保持开源。它是大多数云产品的一部分,也是大多数 Linux 发行版的默认配置。MariaDB 被设计为 MySQL 的直接替代产品,具有更多功能,新的存储引擎,更少的错误和更好的性能。
- MYSQL *mysql_init(MYSQL *mysql);
- MYSQL *mysql_real_connect(MYSQL *mysql, const char *host, const char *user, const char *passwd,const char *db, unsigned int port, const char *unix_socket, unsignedlong client_flag);
- int mysql_select_db(MYSQL *mysql, const char *db)
- int mysql_query(MYSQL *mysql, const char *stmt_str)
- MYSQL_RES *mysql_store_result(MYSQL *mysql)
- MYSQL_ROW mysql_fetch_row(MYSQL_RES *result)
- void mysql_free_result(MYSQL_RES *result)
- void mysql_close(MYSQL *mysql)
httplib
- 创建HTTP服务器:可以使用httplib库创建一个HTTP服务器,用于处理客户端的HTTP请求并返回相应的HTTP响应。这对于开发Web应用程序或者提供Web服务非常有用。
- 创建HTTP客户端:可以使用httplib库创建一个HTTP客户端,用于向其他服务器发送HTTP请求并接收相应的HTTP响应。这对于与其他服务器进行通信或者获取远程资源非常有用。
- 处理HTTP请求和响应:httplib库提供了一些方便的API和工具,用于处理HTTP请求和响应,包括解析HTTP请求参数、处理POST请求、设置HTTP响应头部等功能。
httplib::Request 类是 httplib
库中用于表示 HTTP 请求的类。它包含了 HTTP 请求的各种属性,例如请求方法、URL、请求头、请求体等信息。
httplib::Request 类一般包含以下成员:
- method: 表示 HTTP 请求的方法,如 “GET”、“POST”、“PUT” 等。
- path: 表示 HTTP 请求的路径,即请求的资源在服务器上的位置。
- headers: 表示 HTTP 请求的头部信息,通常包含诸如 Content-Type、Content-Length 等请求头。
- body: 表示 HTTP 请求的主体内容,例如 POST 请求中包含的表单数据或 JSON 数据等。
httplib::Response类是 httplib
库中用于表示 HTTP 响应的类。它包含了 HTTP 响应的各种属性,例如状态码、响应头、响应体等信息。
httplib::Response 类一般包含以下成员:
- version: 表示 HTTP 协议的版本,例如 “HTTP/1.1”。
- status: 表示 HTTP 响应的状态码,例如 200、404、500 等。默认值为 -1。
- reason: 表示 HTTP 响应状态码的原因短语,通常与状态码一起返回给客户端。
- headers: 表示 HTTP 响应的头部信息,是一个键值对形式的容器,包含了各种元数据。
- body: 表示 HTTP 响应的主体内容,即服务器返回给客户端的数据。
- location: 用于重定向的目标 URL 地址,当服务器返回 3xx 状态码时,通常会包含重定向的目标地址。
httplib::Server类,主要的成员变量和方法
路由设置:
- void Get(const std::string &pattern, Handler handler):设置对 GET 请求的处理函数。
- void Post(const std::string &pattern, Handler handler):设置对 POST 请求的处理函数。
- void Put(const std::string &pattern, Handler handler):设置对 PUT 请求的处理函数。
- void Delete(const std::string &pattern, Handler handler):设置对 DELETE 请求的处理函数。
- void Options(const std::string &pattern, Handler handler):设置对 OPTIONS 请求的处理函数。
- Handler 是一个函数对象,通常为 std::function<void(const Request&, Response&)> 类型。
服务器控制:
- bool listen(const char *host, int port, int socket_flags = 0):启动服务器,监听指定的主机和端口。
- void stop():停止服务器。
中间件:
- void set_logger(Logger logger):设置日志处理函数,用于记录请求和响应信息。
- void set_error_handler(ErrorHandler handler):设置错误处理函数,用于处理 HTTP 错误。
- Logger 是一个函数对象,通常为 std::function<void(const Request&, const Response&)> 类型。ErrorHandler 是一个函数对象,通常为 std::function<void(const Request&, Response&, int status_code)> 类型。
静态文件服务:
void set_mount_point(const char *mount_point, const char *dir):设置静态文件服务的挂载点和目录。
通过这个方法,服务器可以将某个 URL 路径映射到本地文件系统中的一个目录,从而提供静态文件服务。当用户请求静态资源的时候,则在指定的根目录下找,找到之后直接进行响应,不需要用户在外部进行额外的处理函数。
多线程操作
httplib 库中内置了一个简单的 TaskQueue 类,用于管理任务队列。这使得 httplib 能够在多线程环境下运行服务器并处理多个请求。TaskQueue 的实现隐藏在库内部,但我们可以通过使用 Server 类的 new_task_queue 方法来启用多线程支持。
工作流程
- 接受请求数据
- 进行解析,得到Request结构
- 检测映射表,根据请求的方法和资源路径查询有没有对应的处理函数。有,则调用,并且将Request和空Response对象传入。没有,则检测是否是静态资源。如果存在该静态资源,则直接返回;否则返回404页面。
- 当处理函数执行完毕之后的到一个填充完毕的Response对象。
- 根据Response对象的数据,组织http响应发送给客户端。
- 短连接则直接关闭,长连接等待超时后关闭。
httplib库搭建简单服务器:首先将一个index.html文件放置在www文件夹下,然后书写服务端代码
1 #include "./httplib.h"
2 using namespace httplib;
3
4 void HelloBit(const Request &req,Response &rsp)
5 {
6 rsp.body="HelloBit!";
7 rsp.status=200;//忽略,默认httplib会加上一个200的状态码
8 }
9 void Numbers(const Request &req,Response &rsp)
10 {
11 //matches:存放正则表达式匹配的规则数据 /numbers/123 matches[0]="/numbers/123" matches[1]="123"
12 std::string num=req.matches[1];
13 rsp.set_content(num,"text/plain");//给正文类型 向rsp.body里边添加数据,并且设置Content-Type类型
14 rsp.status=200;
15 }
16 void Multipart(const Request &req,Response &rsp)
17 {
18 if(req.has_file("file1")==false)
19 {
20 rsp.status=400;
21 return ;
22 }
23 MultipartFormData file=req.get_file_value("file1");//字段
24 std::cout<<file.filename<<std::endl;//区域文件名称
25 std::cout<<file.content<<std::endl;//区域文件内容
26 rsp.status=200;//打印
27 }
28 int main()
29 {
Server server;
31 //设置一个静态资源根目录
32 server.set_mount_point("/","./www");
33 //添加请求-处理函数映射信息
34 server.Get("/hi",HelloBit);
35 //在正则表达式中:d表示数字,+表示匹配前边的字符一次或多次,()表示单独捕捉数据
36 server.Get("/numbers/(\\d+)",Numbers);
37 server.Post("/multipart",Multipart);//先是路径 再是函数进行处理
38 server.listen("0.0.0.0",9090);
39 return 0;
40 }
项目实现工具类
文件实用工具类
- 获取⽂件⼤⼩(属性)
- 判断⽂件是否存在
- 向⽂件写⼊数据
- 从⽂件读取数据
- 针对⽬录⽂件多⼀个创建⽬录
util.hpp
#ifndef __MY_UTIL__
#define __MY_UTIL__
#include <iostream>
#include <fstream>
#include <string>
#include <unistd.h>
#include <sys/stat.h>
namespace Util
{
class FileUtil
{
private:
std::string _name;//文件路径名
public:
FileUtil(const std::string name):_name(name){}
//判断文件是否存在
bool Exists();
//获取文件的大小
size_t Size();
//读取文件数据到*body中
bool GetContent(std::string *body);
//向文件写入数据
bool SetContent(const std::string& body);
//针对目录时创建目录
bool CreateDirectory();
};
}
#endif
判断文件是否存在
int access(const char *pathname,int mode) 是用来专门检测文件是否存在
参数:
- pathname:表示要测试的文件的路径
- mode:表示测试的模式可能的值有:
- R_OK:是否具有读权限
- W_OK:是否具有可写权限
- X_OK:是否具有可执行权限
- F_OK:文件是否存在
返回值:若测试成功则返回0,否则返回-1
获取文件大小
获取文件数据向文件写入数据
针对文件创造目录
JSON实用工具类
序列化
反序列化
数据管理模块
- 视频ID
- 视频名称
- 视频描述信息
- 视频⽂件的 url 路径(加上相对根⽬录实际上就是实际存储路径)
- 视频封⾯图⽚的 URL 路径(只是链接,加上相对根⽬录才是实际的存储路径)
对该表要进行的操作有:
- 增加视频
- 删除视频
- 更新视频
- 查询所有视频
- 查询单个视频
- 模糊匹配查询视频
data.hpp
#ifndef__MY_DATA__
#define__MY_DATA__
#include "util.hpp"
#include <mutex>
#include <cstdlib>
#include <mysql/mysql.h>
namespace aod {
static MYSQL *MysqlInit();
static void MysqlDestroy(MYSQL *mysql);
static bool MysqlQuery(MYSQL *mysql, const std::string &sql);
class TableVideo {
private:
MYSQL *_mysql;//⼀个对象就是⼀个客⼾端,管理⼀张表
std::mutex _mutex;//防备操作对象在多线程中使⽤存在的线程安全 问题
public:
TableVideo();//完成mysql句柄初始化
~TableVideo();//释放msyql操作句柄
bool Insert(const Json::Value &video);//新增-传⼊视频信息
bool Update(int video_id, const Json::Value &video);//修改-传⼊视频
id,和信息
bool Delete(const int video_id);//删除-传⼊视频ID
bool SelectAll(Json::Value *videos);//查询所有--输出所有视频信息
bool SelectOne(int video_id, Json::Value *video);//查询单个-输⼊视频
id,输出信息
bool SelectLike(const std::string &key, Json::Value *videos);//模糊
匹配-输⼊名称关键字,输出视频信息
};
}
#endif
初始化接口:
销毁数据库接口:
执行数据库接口:
数据库表的管理
增加视频:
更新视频:
删除视频:
查询所有视频:
查询单个视频:
模糊信息匹配查询:
一个对象就是一个客户端,管理一张表
网络通信接口设计
该项目使用的收httlib库搭建服务器,相对比较简单。网络通信接口,主要是我们定义什么样的请求,就是一个什么样的请求。
restful风格建立在http协议,其中定义了,GET方法是查询,POST方法是新增,PUT方法是修改,DELETE方法是删除,并且资源数据采用json、xml数据格式。
- GET 表⽰查询获取
- POST 对应新增
- PUT 对应修改
- DELETE 对应删除
业务处理模块设计
- ⽹络通信功能的实现
- 业务功能处理的 实现
- 客户端的视频数据和信息上传
- 客户端的视频列表展⽰(视频信息查询)
- 客户端的视频观看请求(视频数据的获取)
- 客户端的视频其他管理(修改,删除)功能
server.hpp
#ifndef__MY_SERVERE__
#define__MY_SERVERE__
#include "httplib.h"
#include "data.hpp"
namespace aod
{
#define WWW_ROOT "./www"
#define VIDEO_ROOT "/video/"
#define IMAGE_ROOT "/image/"
//因为httplib基于多线程,因此数据管理对象需要在多线程中访问,为了便于访问定义全局变量
TableVideo *table_video = NULL;
//这⾥为了更加功能模块划分清晰⼀些,不使⽤lamda表达式完成,否则所有的功能实现集中到⼀
个函数中太过庞⼤
class Server {
private:
int _port;//服务器的 监听端⼝
httplib::Server _srv;//⽤于搭建http服务器
private:
//对应的业务处理接⼝
static void Insert(const httplib::Request &req, httplib::Response&rsp);
static void Update(const httplib::Request &req, httplib::Response &rsp);
static void Delete(const httplib::Request &req, httplib::Response &rsp);
static void GetOne(const httplib::Request &req, httplib::Response &rsp);
static void GetAll(const httplib::Request &req, httplib::Response &rsp);
public:
Server(int port):_port(port);
bool RunModule();//建⽴请求与处理函数的映射关系,设置静态资源根⽬录,启动
服务器,
};
}
新增视频
static void Insert(const httplib::Request &req,httplib::Response &rsp)
{
if(req.has_file("name")==false ||
req.has_file("info")==false ||
req.has_file("video")==false ||
req.has_file("image")==false)
{
rsp.status=400;
rsp.body=R"({"result":false,"reson":"上传的数据信息错误"})";
rsp.set_header("Content-Type","application/json");
return;
}
httplib::MultipartFormData name=req.get_file_value("name");//视频名称
httplib::MultipartFormData info=req.get_file_value("info");//视频简介
httplib::MultipartFormData video=req.get_file_value("video");//视频文件
httplib::MultipartFormData image=req.get_file_value("image");//图片文件
//content就是视频内容
std::string video_name=name.content;
std::string video_info=info.content;
std::string root=WWWROOT;
std::string video_path=root+VIDEO_ROOT+video_name+video.filename;//文件名
std::string image_path=root+IMAGE_ROOT+video_name+image.filename;//./www/image/变形金刚
//MultipartFormData{name,content_type,filename,content} content文件内容的数据
//文件存储
if(FileUtil(video_path).SetContent(video.content)==false)
{
rsp.status=500;
rsp.body=R"({"result":false,"reson":"视频文件存储失败"})";
rsp.set_header("Content-Type","application/json");
return;
}
if(FileUtil(image_path).SetContent(image.content)==false)
{
rsp.status=500;
rsp.body=R"({"result":false,"reson":"图片文件存储失败"})";
rsp.set_header("Content-Type","application/json");
return;
}
Json::Value video_json;
video_json["name"]=video_name;
video_json["info"]=video_info;
video_json["video"]=VIDEO_ROOT +video_name+video.filename;// /video/变形金刚robot.mp4
video_json["image"]=IMAGE_ROOT +video_name+image.filename;// /image/变形金刚robot.mp4
if(tb_video->Insert(video_json)==false)
{
rsp.status=500;
rsp.body=R"({"result":false,"reson":"数据库新增数据失败"})";
rsp.set_header("Content-Type","application/json");
return;
}
rsp.set_redirect("/index.html",303);
return ;
}
视频信息更新
static void Update(const httplib::Request &req,httplib::Response &rsp)
{
//1.获取要修改的视频信息 1.视频id 2.修改后的信息
int video_id=std::stoi(req.matches[1]);
Json::Value video;
if(JsonUtil::UnSerialize(req.body,&video)==false)
{
rsp.status=400;
rsp.body=R"({"result":false,"reson":"新的视频格式失败"})";
rsp.set_header("Content-Type","application/json");
return ;
}
//2.修改数据库数据
if(tb_video->Update(video_id,video)==false)
{
rsp.status=500;
rsp.body=R"({"result":false,"reson":"修改数据库信息失败"})";
rsp.set_header("Content-Type","application/json");
return;
}
return ;
}
视频删除
static void Delete(const httplib::Request &req,httplib::Response &rsp)
{
//1.获取要删除视频的ID
int video_id=std::stoi(req.matches[1]);//获取ID
//2.删除视频文件和图片文件
Json::Value video;
if(tb_video->SelectOne(video_id,&video)==false)
{
rsp.status=500;
rsp.body=R"({"result":false,"reson":"不存在视频信息"})";
rsp.set_header("Content-Type","application/json");
return;
}
std::string root=WWWROOT;
std::string video_path=root+video["video"].asString();
std::string image_path=root+video["image"].asString();
remove(video_path.c_str());
remove(image_path.c_str());
//3.删除数据库信息
if(tb_video->Delete(video_id)==false)
{
rsp.status=500;
rsp.body=R"({"result":false,"reson":"删除数据库信息失败"})";
rsp.set_header("Content-Type","application/json");
return;
}
return ;
}
查询所有视频与模糊匹配
static void SelectAll(const httplib::Request &req,httplib::Response &rsp)
{
// /video & /video?search="关键字"
bool select_flag=true;//默认查询所有
std::string search_key;
if(req.has_param("search")==true)
{
select_flag=false;//模糊匹配
search_key=req.get_param_value("search");
}
Json::Value videos;
if(select_flag==true)
{
if(tb_video->SelectAll(&videos)==false)
{
rsp.status=500;
rsp.body=R"({"result":false,"reson":"查询所有数据库失败"})";
rsp.set_header("Content-Type","application/json");
return;
}
}
else
{
if(tb_video->SelectLike(search_key,&videos)==false)
{
rsp.status=500;
rsp.body=R"({"result":false,"reson":"查询匹配数据库信息失败"})";
rsp.set_header("Content-Type","application/json");
return;
}
}
JsonUtil::Serialize(videos,&rsp.body);
rsp.set_header("Content-Type","application/json");
return ;
}
查询单个视频
static void SelectOne(const httplib::Request &req,httplib::Response &rsp)
{
//1.获取视频的ID
int video_id=std::stoi(req.matches[1]);//获取ID
//2.在数据库中查询指定信息
Json::Value video;
if(tb_video->SelectOne(video_id,&video)==false)
{
rsp.status=500;
rsp.body=R"({"result":false,"reson":"查询数据库单个失败"})";
rsp.set_header("Content-Type","application/json");
return;
}
//3.组织响应正文--json格式字符串
JsonUtil::Serialize(video,&rsp.body);
rsp.set_header("Content-Type","application/json");
}
使用postman进行接口测试
新增视频测试:
查看数据库中是否有新增视频
查询所有视频:
查询单个视频:
模糊匹配:
修改视频:
删除视频:
数据库查询:
前端界面
主界面
实现功能:
- 所有视频显示并可以播放
- 新增视频按钮
- 搜索框
主页面展示
点击播放按钮,跳转至播放页面
点击新增视频按钮,跳出一个界面
完成上述功能所需的函数:
<script src="js/jquery-1.12.1.min.js"></script>
<script src="js/bootstrap.min.js"></script>
<script src="js/lity.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
$(".nav .dropdown").hover(function () {
$(this).find(".dropdown-toggle").dropdown("toggle");
});
</script>
<script>
let app = new Vue({
el: '#myapp',
data: {
author: "dou",
videos:[]
},
methods:{
get_alivideos :function(){
$.ajax({
url:"/video",
type:"get",
context: this,
success : function(result,status,xhr){
this.videos=result;
}
})
}
}
});
app.get_alivideos();
</script>
</html>
播放页面
主要功能:
- 播放视频与暂停
- 修改视频
- 一键删除视频
播放与暂停:
点击视频修改按钮:
点击删除视频按钮,就会一键删除
完成上述功能所需的函数:
<script>
let app = new Vue({
el: '#myapp',
data: {
author:"dou",
video:{}
},
methods:
{
get_param: function (name) {
return decodeURIComponent((new RegExp('[?|&]' + name + '=' +'([^&;]+?)(&|#|;|$)').exec(location.href) || [, ""])[1].replace(/\+/g, '%20')) || null
},
get_video:function(){
var id=this.get_param("id");
$.ajax({
url:"/video/"+id,
type:"get",
context:this,
success :function(result,status,xhr)
{
this.video=result;
}
})
},
update_video:function(){
$.ajax({
url:"/video/"+this.video.id,
type:"put",
data:JSON.stringify(this.video),
context:this,
success :function(result,status,xhr)
{
alert("修改视频信息成功");
window.location.reload();
}
})
},
delete_video:function(){
$.ajax({
url:"/video/"+this.video.id,
type:"delete",
context:this,
success :function(result,status,xhr)
{
alert("删除视频成功");
window.location.href="/index.html";
}
})
}
}
});
app.get_video();
</script>
</html>
项目总结
- 数据管理模块:基于 MYSQL 进⾏数据管理,封装数据管理类进⾏数据统⼀访问
- 业务处理模块:基于 HTTPLIB 搭建 HTTP 服务器,使⽤ restful⻛格 进⾏接⼝设计处理客户端业务请求
- 前端界⾯模块:基于基础的 HTML+CSS+JS 完成基于简单模板前端界⾯的修改与功能完成
总结:
在本次项目中学习很多第三方库,并投入使用,也了解到网络传输中数据以什么样的格式是如何传送的,数据的存储,通过MariaDB存储数据,但视频和封面图片所占内存较大,存储在文件中,使用httplib搭建服务器,是本项目容易了很多,实现了前后端数据信息的交互。在本次项目中学习服务器的搭建遇到了一些困难,自己搭建的服务器稳定性和安全性不高,所以采用httplib第三方库。学习了一些简单的前端基础,对视频播放模板进行改造。