当前位置: 首页 > article >正文

【项目】视频点播

一、项目介绍

1. 对视频点播系统的认识

搭建视频共享点播服务器,可以让所有人通过浏览器访问服务器,实现视频的上传查看,以及管理并播放的功能。主要是完成服务器端的程序业务功能的实现以及前端访问界面 html 的编写,能够支持客户端浏览器针对服务器上的所有视频进行操作。

2. 服务端功能模块划分

该视频点播系统基本上包含四个模块:数据管理、网络通信、业务处理、前端界面,其功能如下:

  • 数据管理模块:负责针对客户端上传的视频信息进行管理。
  • 网络通信模块:搭建网络通信服务器,实现与客户端通信。
  • 业务处理模块:针对客户端的各个请求进行对应业务处理并响应结果。
  • 前端界面模块:完成前端浏览器上视频共享点播的各个 html 页面,在页面中支持增删改查以及观看功能。

二、环境搭建

2.1 升级GCC

由于在该项目中会引入许多第三方库,比如httplib库,该库就会要求gcc编译器必须是较新的版本。如果使用老版本的编译器要么编译不通过,要么就会运行报错。因此我们需要对gcc进行升级

2.2 安装JsonCpp库

JSON 是一种轻量级的数据交换格式。它可以代表数字、字符串、值的有序序列和名称/值的集合对。

JsonCpp 是一个C++库,允许操作 JSON 值,包括字符串的序列化和反序列化。它还可以保存反序列化/序列化步骤中的现有注释,方便
用于存储用户输入文件的格式。

sudo yum install epel-release
sudo yum install jsoncpp-devel

安装好的JsonCpp存放在/usr/include/jsoncpp/json目录下:

2.3 引入httplib库

cpp-httplib 是个开源的库,是一个c++封装的http库,使用这个库可以在linux、windows平台下完成http客户端、http服务端的搭建,这是一个多线程“阻塞”HTTP 库。使用起来非常方便,只需要包含头文件httplib.h即可。源码地址

获取httplib库:

git clone https://github.com/yhirose/cpp-httplib.git

2.4 MySQL数据库

此次项目的开发我们使用的数据库是MariaDB,MariaDB数据库管理系统是MySQL的一个分支,主要由开源社区在维护,采用GPL授权许可 MariaDB的目的是完全兼容MySQL,包括API和命令行,使之能轻松成为MySQL的代替品。

以下是安装MariaDB数据库的步骤:

  • 安装 MariaDB 服务
sudo yum install -y mariadb-server
  • 安装 MariaDB 命令行客户端
sudo yum install -y mariadb 
    • 安装 MariaDB C library
    sudo yum install -y mariadb-libs

    其实 MariaDB 的服务中包含了客户端和相关的C语言接口,因此只需要安装 mariadb-server即可。

    • 安装 MariaDB 开发包
    sudo yum install -y mariadb-devel

    安装完成后,以下是启动数据库的命令:

    • 启动服务
    systemctl start mariadb

    执行该命令启动服务之后,如果重启或者关机了,下次还需要重新启动,可以执行下列指令设置开启自启。

    • 设置服务开机自启
    systemctl enable mariadb
    • 查看服务状态
    systemctl status mariadb

    启动服务成功后,我们可以看到MariaDB 的状态是 active(running) 

    然后可以测试连接数据库:

    • 使用命令行客户端尝试连接
    mysql -uroot

    更改配置:

    为了在使用数据库时支持中文,还需要进行以下配置。

    • 在 /etc/my.cnf.d/client.cnf文件的 [client]下添加default-character-set = utf8

    • 在 /etc/my.cnf.d/mysql-client.cnf文件下的[mysql]下添加default-character-set = utf8。 

    • 在 /etc/my.cnf.d/server.cnf下的[mysqld]添加以下语句
    collation-server = utf8_general_ci
    init-connect = 'SET NAMES utf8'
    character-set-server = utf8
    sql-mode = TRADITIONAL
    

    三、第三方库的认识

    3.1 认识JsonCpp

    首先认识Json:

    Json 是一种数据交换格式,采用完全独立于编程语言的文本格式来存储和表示数据。

    例如:使用Json来表示张三同学的学生信息。

    const char* name1 = "张三";
    int age1 = 18;
    float scores1[3] = {60.0, 59.5, 61.0};
    
    const char* name2 = "李四";
    int age2 = 19;
    float scores2[3] = {69.0, 58.5, 64.0};
    // Json这种数据交换格式就是将这样的多种数据对象封装成一个字符串:
    [
    	{
    		"姓名" : "张三",
    		"年龄" : 18,
    		"成绩" : [860.0, 59.5, 61.0]
    	},
    	{
    		"姓名" : "李四",
    		"年龄" : 19,
    		"成绩" : [69.0, 58.5, 64.0]
    	},
    ] 
    

    Json可以封装的数据对象可以是:对象,数组,字符串,数字等等。

    认识JsonCpp:
    JsonCpp 库用于实现 Json 格式的序列化和反序列化,完成将多个数据对象组织成为 Json 格式字符串,以及将 Json
    格式字符串解析得到多个数据对象的功能。

    其中主要借助三个类以及对于的几个成员函数完成的。

    Json 数据对象:

    
    class Json::Value{
    	Value &operator=(const Value &other); // Value重载了[]和=,因此所有的赋值和获取数据都可以通过
    	Value& operator[](const std::string& key);// 简单的方式完成 val["姓名"] = "小明";
    	Value& operator[](const char* key);
    	Value removeMember(const char* key);// 移除元素
    	const Value& operator[](ArrayIndex index) const; // val["成绩"][0]
    	Value& append(const Value& value);// 添加数组元素val["成绩"].append(88);
    	ArrayIndex size() const;// 获取数组元素个数 val["成绩"].size();
    	std::string asString() const;// 转string string name = val["name"].asString();
    	const char* asCString() const;// 转char* char *name = val["name"].asCString();
    	Int asInt() const;// 转int int age = val["age"].asInt();
    	float asFloat() const;// 转float
    	bool asBool() const;// 转 bool
    }
    
    

    Json序列化类:

    // 建议低版本使用
    class JSON_API Writer {
    	virtual std::string write(const Value& root) = 0;
    }
    
    class JSON_API FastWriter : public Writer {
    	virtual std::string write(const Value& root);
    }
    
    class JSON_API StyledWriter : public Writer {
    	virtual std::string write(const Value& root);
    }
    
    // 建议较高版本使用,如果用低版本接口可能报错
    class JSON_API StreamWriter {
    	virtual int write(Value const& root, std::ostream* sout) = 0;
    }
    
    class JSON_API StreamWriterBuilder : public StreamWriter::Factory {
    	virtual StreamWriter* newStreamWriter() const;
    }
    
    

    Json反序列化类:

    // 低版本用起来更简单
    class JSON_API Reader {
    	bool parse(const std::string& document, Value& root, bool collectComments = true);
    }
    
    // 高版本更推荐
    class JSON_API CharReader {
    	virtual bool parse(char const* beginDoc, char const* endDoc,
    	Value* root, std::string* errs) = 0;
    }
    class JSON_API CharReaderBuilder : public CharReader::Factory {
    	virtual CharReader* newCharReader() const;
    }
    

    3.2 JsonCpp实现序列化

    #include <iostream>
    #include <memory>
    #include <sstream>
    #include <string>
    #include <jsoncpp/json/json.h>
    
    int main()
    {
    	const char* name = "张三";
    	int age = 18;
    	float scores[3] = {60.0, 59.5, 61.0};
    	Json::Value value;
    	value["姓名"] = name;
    	value["年龄"] = age;
    	value["成绩"].append(scores[0]);
    	value["成绩"].append(scores[1]);
    	value["成绩"].append(scores[2]);
    	
    	Json::StreamWriterBuilder swb;
    	std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());
    	
    	std::stringstream ss;
    	int ret = sw->write(value, &ss);
    	if(ret != 0)
    	{
    		 std::cout << "write falied!" << std::endl;
    		 return -1;
    	}
    	
    	std::cout << ss.str() << std::endl;
    	return 0;
    }
    
    

    3.3 JsonCpp实现反序列化

    #include <iostream>
    #include <memory>
    #include <sstream>
    #include <string>
    #include <jsoncpp/json/json.h>
    
    int main()
    {
        std::string str = R"({"姓名":"李四", "年龄":19, "成绩":[69.0, 58.5, 64.0]})";
        Json::Value root;
        Json::CharReaderBuilder crb;
        std::unique_ptr<Json::CharReader> cr(crb.newCharReader());
    
        std::string err;
        cr->parse(str.c_str(), str.c_str() + str.size(), &root, &err);
    
        std::cout << root["姓名"].asString() << std::endl;
        std::cout << root["年龄"].asInt() << std::endl;
    
        int sz = root["成绩"].size();
        for (int i = 0; i < sz; ++i)
            std::cout << root["成绩"][i].asFloat() << std::endl;
       
        for(auto it = root["成绩"].begin(); it != root["成绩"].end(); ++it)
            std::cout << it->asString() << std::endl;
        
        return 0;
    }
    
    

    3.4 认识MySQL数据库的API

    具体参考这篇。

    MySQL:函数式编程连接MySQL库_mysqlconnection.h mysql.h-CSDN博客

    编译指令:

    gcc -o mysql_test mysql_test.c -L/usr/lib64/mysql -lmysqlclient

    注意在进行编译的时候需要使用-L指定mysqlclient动态库所在的路径,因为它不是之间存在于/usr/lib64/路径下的。

    • 向数据库里面增加数据
    #include <stdio.h>
    #include <unistd.h>
    #include <string.h>
    #include <mysql/mysql.h>
    
    //添加数据
    int add(MYSQL* mysql)
    {
        const char* sql = "insert into test_tb values (null, 18, '张三', 61.5)";
        int ret = mysql_query(mysql, sql);
        if(ret != 0)
        {
            printf("query %s failed, error massage: %s\n", sql, mysql_error(mysql));
        	return -1;
        }
        return 0;
    }
    
    int main()
    {
    	//初始化操作句柄
    	MYSQL* mysql = mysql_init(NULL);
    	if(NULL == mysql)
    	{
    		printf("init mysql handle failed!\n");
    		return -1;
    	}
    	
    	//连接mysql服务器
    	if (mysql_real_connect(mysql, "127.0.0.1", "root", "", "test_db", 0, NULL, 0) == NULL)
    	{
    		printf("mysql connect failed!\n");
    		return -1;
    	}
    	
    	//设置客户端字符集
    	mysql_set_character_set(mysql, "utf8");
    	
    	add(mysql);
    	
    	//关闭句柄
    	mysql_close(mysql);
    	return 0;
    }
    
    
    • 修改数据库中的数据
    //修改
    int mod(MYSQL* mysql)
    {
        const char* sql = "update test_tb set name = '李四' where id = 2";
        int ret = mysql_query(mysql, sql);
        if(ret != 0)
        {
            printf("query %s failed, error massage: %s\n", sql, mysql_error(mysql));
            return -1;
        }
        return 0;
    }
    
    
    • 删除数据库中的数据
    //删除
    int del(MYSQL* mysql)
    {
        const char* sql = "delete from test_tb where name='张三'";
        int ret = mysql_query(mysql, sql);
        if(ret != 0)
        {
            printf("query %s failed, error massage: %s\n", sql, mysql_error(mysql));
            return -1;
        }
        return 0;
    }
    
    • 查询数据库中的数据
    //查询
    int get(MYSQL* mysql)
    {
        const char* sql ="select * from test_tb";
        int ret = mysql_query(mysql, sql);
        if(ret != 0)
        {
            printf("query %s failed, error massage: %s\n", sql, mysql_error(mysql));
            return -1;
        }
    
        //保存结果集到本地
        MYSQL_RES* result = mysql_store_result(mysql);
        if(NULL == result)
        {
            printf("mysql store result failed! error message: %s\n", mysql_error(mysql));
            return -1;
        }
    
        //获取结果集的行数
        int row = mysql_num_rows(result);
        //获取结果集的列数
        int col = mysql_num_fields(result);
        printf("%10s%10s%10s%10s\n", "ID", "姓名", "年龄", "成绩");
        for(int i = 0; i < row; ++i)
        {
            //获取当前行的数据
            MYSQL_ROW row_data = mysql_fetch_row(result);
            for(int j = 0; j < col; ++j)
            {
                printf("%10s", row_data[j]);
            }
            printf("\n");
        }
        //释放结果集
        mysql_free_result(result);
        return 0;
    }
    
    

    3.6 认识httplib库

    httplib库是一个基于C++11的跨平台的HTTP/HTTPS库,它的安装非常简单,只需要将httplib.h包含在代码中即可。

    httplib库实际上是用于搭建一个简单的HTTP服务器或者客户端的库,使用这种第三方网络库,可以帮助我们省去自己搭建服务器或者客户端的时间,把更多的精力投入到具体的业务处理当中,提高开发的效率。

    以下是对httplib库实现的简单剖析,该库中主要包含四个类:发送请求Request类,响应数据Response类,服务端Server类,客户端Client 类。

    发送请求Request类的组成:

    namespace httplib
    {
    	struct MultipartFormData {
    	std::string name;
    	std::string content;
    	std::string filename;
    	std::string content_type;
    	};
    	
    	using MultipartFormDataItems = std::vector<MultipartFormData>;
    	struct Request 
    	{
    		std::string method;//存放请求方法
    		std::string path;//存放请求资源路径
    		Headers headers;//存放头部字段的键值对map
    		std::string body;//存放请求正文
    		// for server
    		std::string version;//存放协议版本
    		Params params;//存放url中查询字符串 key=val&key=val的 键值对map
    		MultipartFormDataMap files;//存放文件上传时,正文中的文件信息
    		Ranges ranges;
    		bool has_header(const char *key) const;//判断是否有某个头部字段
    		std::string get_header_value(const char *key, size_t id = 0) const;//获取头部字段值
    		void set_header(const char *key, const char *val);//设置头部字段
    		bool has_file(const char *key) const;//文件上传中判断是否有某个文件的信息
    		MultipartFormData get_file_value(const char *key) const;//获取指定的文件信息
    	};
    
    }
    

    响应数据Response类:

    namespace httplib
    {
    	struct Response 
    	{
    		std::string version;//存放协议版本
    		int status = -1;//存放响应状态码
    		std::string reason;
    		Headers headers;//存放响应头部字段键值对的map
    		std::string body;//存放响应正文
    		std::string location; // Redirect location重定向位置
    		void set_header(const char *key, const char *val);//添加头部字段到headers中
    		void set_content(const std::string &s, const char *content_type);//添加正文到body中
    		void set_redirect(const std::string &url, int status = 302);//设置全套的重定向信息
    	};
    }
    
    

    服务端Server类

    namespace httplib
    {
    	class Server 
    	{
    		using Handler = std::function<void(const Request &, Response &)>;//函数指针类型
    		using Handlers = std::vector<std::pair<std::regex, Handler>>;//存放请求-处理函数映射
    		std::function<TaskQueue *(void)> new_task_queue;//线程池
    		Server &Get(const std::string &pattern, Handler handler);//添加指定GET方法的处理映射
    		Server &Post(const std::string &pattern, Handler handler);
    		Server &Put(const std::string &pattern, Handler handler);
    		Server &Patch(const std::string &pattern, Handler handler);
    		Server &Delete(const std::string &pattern, Handler handler);
    		Server &Options(const std::string &pattern, Handler handler);
    		bool listen(const char *host, int port, int socket_flags = 0);//开始服务器监听
    		bool set_mount_point(const std::string &mount_point, const std::string &dir,
    		Headers headers = Headers());//设置http服务器静态资源根目录
    	};
    }
    

    客户端Client 类:

    namespace httplib
    {
    	class Client
    	{
    		//创建client
    		Client(host,port);
    		Get()
    		Post()
    		Put()
    		...
    	};
    
    }
    

    3.7 使用httplib库搭建简单的服务器

    前端代码样例:

    <html>
        <head>
             <meta content="text/html; charset=utf-8" http-equiv="content-type" />
        </head>
        <body>
            <h1>Hello HTTP</h1>
            <form action="/multipart" method="post" enctype="multipart/form-data">
                <input type="file" name="file1">
                <input type="submit" value="上传">
            </form>
        </body>
    </html>
    

    这段代码是一个简单的HTML页面,包括一个标题"Hello HTTP"以及一个表单,允许用户上传文件。

    在表单中,使用了HTTP POST方法并将enctype属性设置为multipart/form-data。这是因为表单旨在上传文件,需要这种类型的编码。文件输入字段使用input标签创建,类型属性设置为file,名称属性设置为file1。最后,使用input标签创建一个提交按钮,类型属性设置为submit,值属性设置为"上传"。

    当用户单击提交按钮时,将拟定的表单数据,包括上传的文件,一起发送到表单标签中指定的服务器的文件中,即/multipart

    以下是使用httplib库实现的简单服务端代码:

    #include "./httplib.h"
    #include <iostream>
    using namespace httplib;
    
    void Handler(const Request &req, Response& rsp)
    {
        rsp.body = "Hello World!";
        rsp.status = 200; //可以忽略,httplib默认会加上一个200的状态码
    }
    
    void Numbers(const Request &req, Response& rsp)
    {
        //matches: 存放正则表达式匹配的规则数据 /numbers/123 matches[0] = "/numbers/123", matches[1] = "123"
        std::string num = req.matches[1];
        rsp.set_content(num, "text/plain");
        rsp.status = 200;
    }
    void Multipart(const Request &req, Response& rsp)
    {   if(req.has_file("file1") == false)
        {
            rsp.status = 400;
            return ;
        }
        MultipartFormData file =req.get_file_value("file1");
        std::cout << file.filename << std::endl; //区域文件名称
        std::cout << file.content << std::endl; //区域文件内容
    }
    int main()
    {
        Server server;
        //设置一个静态资源根目录---为当前的www目录
        server.set_mount_point("/", "./www");
    
        //添加请求---处理函数映射信息
        server.Get("/hi", Handler);
        //正则表达式中:\d表示数字,+表示匹配前面的字符一次或多次,()表示单独捕捉数据 /numbers/123
        server.Get("/numbers/(\\d+)", Numbers);
        
        server.Post("/multipart", Multipart);
        
        server.listen("0.0.0.0", 8080);
    
        return 0;
    }
    

    其中各个函数的作用:

    • Handler:回调函数,用于处理服务器的/hi路径的GET请求。当客户端向服务器发起GET请求并且请求路径是/hi时,该函数将被调用。并将响应的正文设置成 " Hello World!",响应状态设置为200。
    • Numbers:回调函数,用于处理服务器的/numbers/xxx路径的GET请求,其中xxx表示数字。当客户端向服务器发起GET请求并且请求路径是/numbers/xxx时,该函数将被调用。并且从请求对象的matches属性中提取数字并将其作为响应的正文,然后将响应状态码设置为200。
    • Multipart:回调函数,用于处理服务器的/multipart路径的POST请求。当客户端向服务器发起POST请求并且请求路径是/multipart时会调用该函数。该函数会检查请求是否包含file1的文件,如果不存在,则将响应状态码设置为400。如果文件存在则将文件名和文件内容输出到控制台。
    • 在主函数中,服务器被创建并配置三个请求处理函数:HandlerNumbersMultipart,并且指定服务端静态资源根目录为./www。然后监听8080的端口号等待客户端连接。

    四、服务端工具类的实现

    4.1 文件工具类的设计

    在视频点播系统中会涉及到文件的上传,需要对上传的文件进行备份存储,因此首先设计封装一个文件操作类,将这个类封装完成后,则在任意模块中对文件进行操作时都将得到简化。

    该类主要涉及到的功能是:获取文件的大小、判断文件是否存在、向文件中写入数据、从文件中读取数据、针对目标文件创建目录。具体实现如以下代码:

    #include <iostream>
    #include <fstream>
    #include <string>
    #include <unistd.h>
    #include <sys/stat.h>
    
    namespace aod
    {
      class FileUtil
      {
          private:
              std::string _name; //文件路径名称
          public:
              FileUtil(const std::string& name):_name(name){}
                
              //判断文件是否存在
              bool Exists()
              {
                  int ret = access(_name.c_str(), F_OK); //access的第一个参数是文件名,第二个参数如果传入F_OK用于判断文件是否存在
                  if(ret != 0)
                  {
                      std::cout << "file is not exist!" << std::endl;
                      return false;
                  }
                  return true;
              }
              
              //获取文件大小
              size_t Size()
              {
                  if(this->Exists() == false)
                  {
                      return 0;
                  }
    
                  //stat接口用于获取文件属性,结构体struct stat中的st_size就是文件大小
                  struct stat st;
                  int ret = stat(_name.c_str(), &st);
                  if(ret != 0)
                  {
                      std::cout << "get file size failed!" << std::endl;
                      return 0;
                  }
    
                  return st.st_size;
    
              }
          
              //获取文件内容
              bool GetContent(std::string* content)
              {
                  std::ifstream ifs;
                  ifs.open(_name.c_str(), std::ios::binary);
                  if(ifs.is_open() == false)
                  {
                      std::cout << "open file failed!" << std::endl;
                      return false;
                  }
                  size_t flen = this->Size();
                  content->resize(flen);
                  ifs.read(&(*content)[0], flen);
                  if(ifs.good() == false)
                  {
                      std::cout << "read file content failed!" << std::endl;
                      return false;
                  }
    
                  ifs.close();
                  return true;
              }
          
              //向文件中写入内容
              bool SetContent(const std::string& content)
              {
                  std::ofstream ofs;
                  ofs.open(_name.c_str(), std::ios::binary);
                  if(ofs.is_open() == false)
                  {
                      std::cout << "open file failed" << std::endl;
                      return false;
                  }
                  
                  ofs.write(content.c_str(), content.size());
                  if(ofs.good() == false)
                  {
                      std::cout << "write file content failed!" << std::endl;
                      return false; 
                  }
    
                  ofs.close();
    
                  return true;
              }
    
              //根据文件名称创建目录
              bool CreateDirectory()
              {
                  if(this->Exists()) 
                      return true;
                  
                 int ret = mkdir(_name.c_str(), 0777);
                 if(ret != 0)
                 {
                    std::cout << "create directory failed!" << std::endl;
                    return false;
                 }
                 return true;
    
              }
      };
    }
    
    
    #endif 
    
    

    【说明】

    • 在判断文件是否存在时使用的接口是access,调用成功返回 0 ,失败则返回 -1 ,其定义如下:
    #include <unistd.h>
    int access(const char *path, int amode);

    其中path是文件路径名称,amode用于指定access函数的作用,其取值如下:

    F_OK 值为0,判断文件是否存在
     
    X_OK 值为1,判断对文件是可执行权限
     
    W_OK 值为2,判断对文件是否有写权限
     
    R_OK 值为4,判断对文件是否有读权限
     
    注:后三种可以使用或“|”的方式,一起使用,如W_OK|R_OK
    
    • 在获取文件大小的函数中使用了一个接口stat,其功能是获取文件的属性,调用成功返回 0 ,失败则返回 - 1,定义如下:
    #include <sys/stat.h>
    int stat(const char *path, struct stat *buf);

    其中path代表文件的路径,struct stat是一个描述文件的结构体,其定义如下:

    struct stat {
        dev_t     st_dev;     /* ID of device containing file */
        ino_t     st_ino;     /* inode number */
        mode_t    st_mode;    /* protection */
        nlink_t   st_nlink;   /* number of hard links */
        uid_t     st_uid;     /* user ID of owner */
        gid_t     st_gid;     /* group ID of owner */
        dev_t     st_rdev;    /* device ID (if special file) */
        off_t     st_size;    /* total size, in bytes */
        blksize_t st_blksize; /* blocksize for file system I/O */
        blkcnt_t  st_blocks;  /* number of 512B blocks allocated */
        time_t    st_atime;   /* time of last access */
        time_t    st_mtime;   /* time of last modification */
        time_t    st_ctime;   /* time of last status change */
    };
    
    • 在创建文件目录时使用的函数是mkdir,调用成功返回 0, 失败则返回 -1,其定义如下:
    #include <sys/stat.h>
    int mkdir(const char *path, mode_t mode);

    4.2 Json 工具类的设计

    Json工具类包含的功能有两个,一个是将Json::Value对象序列化成为一个字符串,另一个是将字符串反序列化成为Json::Value对象。具体实现代码如下:

      //Json工具类
      class JsonUtil
      {
          public:
              //序列化
              static bool Serialize(const Json::Value& root, std::string* str)
              {
                  Json::StreamWriterBuilder swb;
                  std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());
                  
                  std::stringstream ss;
                  int ret = sw->write(root, &ss);
                  if(ret != 0)
                  {
                      std::cout << "Serialize failed!" << std::endl;
                      return false;
                  }
    
                  *str = ss.str();
    
                  return true; 
              }
              
              //反序列化
              static bool Deserialize(const std::string& str, Json::Value* root)
              {
                  Json::CharReaderBuilder crb;
                  std::unique_ptr<Json::CharReader> cr(crb.newCharReader());
                  
                  std::string err;
                  bool ret = cr->parse(str.c_str(), str.c_str() + str.size(), root, &err);
                  if(ret == false)
                  {
                      std::cout << "Deserialize failed! error message: " << err << std::endl;
                      return false;
                  }
                  return true;
              }
      };
    
    

    五、数据管理模块的实现

    5.1 视频数据表的设计

    在视频点播系统中,视频数据和图片数据都存储在文件中,所有需要使用数据库来管理用户上传的每个视频的属性信息。这里只需要创建一个简单的视频信息表即可,其属性如下:

    • 视频ID
    • 视频名称
    • 视频描述信息
    • 视频文件的URL路径 (加上静态资源根目录就是实际存储路径)
    • 数据封面图片的URL路径 (加上静态资源根目录就是实际存储路径)

    数据库的创建代码如下:

    drop database if exists aod_system;
    create database if not exists aod_system;
    use aod_system;
    create table if not exists tb_video(
    	id int primary key auto_increment comment '视频ID',
    	name varchar(32) comment '视频名称',
    	info text comment '视频描述',
    	video varchar(256) comment '视频文件url,加上静态资源根目录就是实际存储路径',
    	image varchar(256) comment '封面图片文件url,加上静态资源根目录就是实际存储路径'
    );
    

    5.2 数据管理类的设计

    数据管理模块负责统一对数据库中数据的增删改查管理,其他所有的模块要进行对数据的操作都要通过数据管理模块来完成。

    然而,数据库中可能存在多张表,每张表的数据又不相同,进行的数据操作也不同。因此,就需要为每张表中的数据操作都设计一个数据管理类,通过类的实例化对象来管理这张表中的数据。

    由于在视频点播系统中只涉及一张表,因此只设计一个类即可,该类包含的数据库操作有:新增、修改、删除、查询所有数据、查询单个数据、模糊匹配

    由于视频信息在接口之间的传递字段数量可能很多,因此使用 Json::Value 对象进行传递。以下是具体代码的实现:

    #ifndef __MY_DATA__ //防止头文件重复包含
    #define __MY_DATA__ 
    #include "util.hpp"
    #include <cstdlib>
    #include <mutex>
    #include <mysql/mysql.h>
    
    namespace aod
    {
    
    #define HSOT "127.0.0.1"
    #define USER "root"
    #define PASSWD ""
    #define DBNAME "aod_system"
    
    
    
      //MYSQL句柄初始化
        static MYSQL* MySQLInit()
        {
            MYSQL* mysql = mysql_init(NULL);
            if(NULL == mysql)
            {
                std::cout << "init mysql instance failed!" << std::endl;
                return NULL;
            }
    
            //连接数据库服务器
            if(mysql_real_connect(mysql, HSOT, USER, PASSWD, DBNAME, 0, NULL, 0) == NULL)
            {
                std::cout << "connect mysql server failed!" << std::endl; 
                mysql_close(mysql);
                return NULL;
            }
            
            mysql_set_character_set(mysql, "utf8");
            return mysql;
    
        }
    
        //释放MYSQL句柄
        static void MySQLDestroy(MYSQL* mysql)
        {
            if(mysql != NULL)
            {
                mysql_close(mysql);
            }
            return;
        }
    
        //执行sql语句
        static bool MySQLQuery(MYSQL* mysql, const std::string& sql)
        {
            int ret = mysql_query(mysql, sql.c_str());
            if(ret != 0)
            {
                std::cout << sql << std::endl;
                std::cout << "query sql failed!" << std::endl;
                return false;
            }
    
            return true;
        }
    
        class TableVideo
        {
            private:
                MYSQL* _mysql; //MYSQL句柄
                std::mutex _mutex; //解决操作对象在多线程中操作这张表的线程安全问题
            public:
                //完成对mysql句柄的初始化
                TableVideo()
                {
                    _mysql = MySQLInit();
                    if(NULL == _mysql)
                    {
                        exit(-1);
                    }
                }
    
                //释放mysql句柄
                ~TableVideo()
                {
                    MySQLDestroy(_mysql);
                }
    
                //新增---传入视频信息
                bool Insert(const Json::Value& video)
                {
                    //id name info video image
                    std::string sql;
                    sql.resize(4096 + video["info"].asString().size()); //防止视频简介内容过长
    
    #define INSERT_VIDEO "insert tb_video values(null, '%s', '%s', '%s', '%s');"
                    if(video["name"].asString().size() == 0 
                        || video["info"].asString().size() == 0 
                        || video["video"].asString().size() == 0
                        || video["image"].asString().size() == 0)
                    {
                        std::cout << "新增视频信息有误!" << std::endl;
                        return false;
                    }
    
                    sprintf(&sql[0], INSERT_VIDEO, video["name"].asCString(), video["info"].asCString(), video["video"].asCString(), video["image"].asCString());
                    
                    return MySQLQuery(_mysql, sql);
    
                }
    
                //修改---传入视频id和信息
                bool Update(int video_id, const Json::Value& video)
                {
                    std::string sql;
                    sql.resize(4096 + video["info"].asString().size()); //防止视频简介内容过长
    #define UPDATE_VIDEO "update tb_video set name='%s', info='%s' where id = %d;"
    
                    sprintf(&sql[0], UPDATE_VIDEO, video["name"].asCString(), video["info"].asCString(), video_id);
                    
                    return MySQLQuery(_mysql, sql);
    
                }
    
                //删除---传入视频id
                bool Delete(int video_id)
                {
    
                    std::string sql;
                    sql.resize(1024);
    #define DELETE_VIDEO "delete from tb_video where id=%d;"
                    
                    sprintf(&sql[0], DELETE_VIDEO, video_id);
    
                    return MySQLQuery(_mysql, sql);
                }
    
                //查询并输出所有视频信息
                bool SelectAll(Json::Value* videos)
                {
    #define SELECT_ALL "select * from tb_video;"
                    
                    _mutex.lock(); // 在多线程中,保护查询与保存结果到本地的过程
    
                    bool ret = MySQLQuery(_mysql, SELECT_ALL);
                    if(false == ret)
                    {
                        std::cout << "select all failed!" << std::endl;
                        _mutex.unlock();
                        return false;
                    }
                    
                    MYSQL_RES *res = mysql_store_result(_mysql);
                    if(NULL == res)
                    {
                        std::cout << "store result failed!" << std::endl;
                        _mutex.unlock();
                        return false;
                    }
                    
                    _mutex.unlock(); // 解锁
    
                    int num_rows = mysql_num_rows(res);
                    for(int i = 0; i < num_rows; ++i)
                    {
                        MYSQL_ROW row = mysql_fetch_row(res); 
                        Json::Value video;
                        video["id"] =atoi(row[0]);
                        video["name"] = row[1];
                        video["info"] = row[2];
                        video["video"] = row[3];
                        video["image"] = row[4];
                        
                        videos->append(video);
                    }
    
                    mysql_free_result(res); //释放结果集
                    return true;
                }
    
                //传入id,查询单个视频信息
                bool SelectOne(int video_id, Json::Value* video)
                {
                  
                  std::string sql;
                  sql.resize(1024);
    #define SELECT_ONE "select * from tb_video where id=%d;"
                  sprintf(&sql[0], SELECT_ONE, video_id);
                    
                    _mutex.lock(); // 在多线程中,保护查询与保存结果到本地的过程
    
                    bool ret = MySQLQuery(_mysql, sql);
                    if(false == ret)
                    {
                        std::cout << "select all failed!" << std::endl;
                        _mutex.unlock();
                        return false;
                    }
                    
                    MYSQL_RES *res = mysql_store_result(_mysql);
                    if(NULL == res)
                    {
                        std::cout << "store result failed!" << std::endl;
                        _mutex.unlock();
                        return false;
                    }
                    
                    _mutex.unlock(); // 解锁
    
                    int num_rows = mysql_num_rows(res);
                    if(num_rows != 1)
                    {
                        std::cout << "data is not exits!" << std::endl;
                        mysql_free_result(res);
                        return false;
                    }
    
                  
                     MYSQL_ROW row = mysql_fetch_row(res); 
                    
                     (*video)["id"] =atoi(row[0]);
                     (*video)["name"] = row[1];
                     (*video)["info"] = row[2];
                     (*video)["video"] = row[3];
                     (*video)["image"] = row[4];
    
                    mysql_free_result(res); //释放结果集
                    return true;
                }
    
                //模糊匹配---输入关键字
                bool SelectLike(const std::string& key, Json::Value* videos)
                {
                  std::string sql;
                  sql.resize(1024);
    #define SELECT_LIKE "select * from tb_video where name like '%%%s%%';"
                  sprintf(&sql[0], SELECT_LIKE, key.c_str());
                    
                    _mutex.lock(); // 在多线程中,保护查询与保存结果到本地的过程
    
                    bool ret = MySQLQuery(_mysql, SELECT_ALL);
                    if(false == ret)
                    {
                        std::cout << "select all failed!" << std::endl;
                        _mutex.unlock();
                        return false;
                    }
                    
                    MYSQL_RES *res = mysql_store_result(_mysql);
                    if(NULL == res)
                    {
                        std::cout << "store result failed!" << std::endl;
                        _mutex.unlock();
                        return false;
                    }
                    
                    _mutex.unlock(); // 解锁
    
                    int num_rows = mysql_num_rows(res);
                    for(int i = 0; i < num_rows; ++i)
                    {
                        MYSQL_ROW row = mysql_fetch_row(res); 
                        Json::Value video;
                        video["id"] =atoi(row[0]);
                        video["name"] = row[1];
                        video["info"] = row[2];
                        video["video"] = row[3];
                        video["image"] = row[4];
                        
                        videos->append(video);
                    }
    
                    mysql_free_result(res); //释放结果集
                    return true;
                }
        };
    }
    
    #endif 
    
    

    六、网络通信模块 — 网络通信接口设计

    首先要明确的是:

    • 网络通信接口设计其实就是定义好:什么样的请求是一个查询请求、什么样的请求是一个删除请求 

    • 服务端提高的功能包括:新增视频、删除视频、修改视频、查询所有视频、查询单个视频、模糊匹配查询。

    因此,要让不同的功能对应到不同的接口,在网络通信接口的设计中,就借助了 REST设计风格来设计网络接口。

    6.1 REST 设计风格

    REST 是 Representational State Transfer的缩写,中文名叫表现层状态转换。是Roy Thomas Fielding博士于2000年在他的博士论文中提出来的一种万维网软件架构风格,目的是便于不同软件或程序在网络(例如互联网)中互相传递信息。

    REST是基于HTTP协议之上而确定的一组约束和属性,可以充分利用HTTP协议的各种功能,是HTTP协议的最佳实践。RESTful API是一种软件架构风格,可以让软件更加的清晰、简介、富有层次感、提高可维护性。

    匹配于 REST这种架构风格的网络服务,允许客户端发出以URL访问和操作网络资源的请求,而与预先定义好的无状态操作集一致化。因此表现层状态转换提供了在互联网络的计算系统之间,彼此资源可交互使用的协作性质。

    REST风格中定义了:

    • GET方法:表示查询
    • POST方法:表示新增
    • PUT方法:表示修改
    • DELETE方法:表示删除
    • 资源正文数据采用JsonXML数据格式

    6.2 REST 风格下 CRUD 操作的 HTTP 格式

    获取所有视频信息:

    请求:
    GET /video HTTP/1.1
    Connection: keep-alive
    ......
    
    响应:
    HTTP/1.1 200 OK
    Content-Length: xxx
    Content-Type: application/json
    ......
    
    [
    	{
    		"id": 1,
    		"name": "电影1",
    		"info": "xxxxxx",
    		"video": "/video/movie1.mp4",
    		"image": "/img/thumbs/movie1.png",
    	},
    	{
    		"id": 2,
    		"name": "电影2",
    		"info": "xxxxxx",
    		"video": "/video/movie2.mp4",
    		"image": "/img/thumbs/movie2.png",
    	}
    ]
    

    搜索关键字获取视频信息:

    请求:
    GET /video?search="电影1" HTTP/1.1
    Connection: keep-alive
    ......
    
    响应:
    HTTP/1.1 200 OK
    Content-Length: xxx
    Content-Type: application/json
    ......
    
    [
    	{
    		"id": 1,
    		"name": "电影1",
    		"info": "xxxxxx",
    		"video": "/video/movie1.mp4",
    		"image": "/img/thumbs/movie1.png",
    	}
    ]
    
    

    获取指定视频信息:

    请求:
    GET /video/1 HTTP/1.1
    Connection: keep-alive
    ......
    
    响应:
    HTTP/1.1 200 OK
    Content-Length: xxx
    Content-Type: application/json
    ......
    
    [
    	{
    		"id": 1,
    		"name": "电影1",
    		"info": "xxxxxx",
    		"video": "/video/movie1.mp4",
    		"image": "/img/thumbs/movie1.png",
    	}
    ]
    

    删除指定视频信息:

    请求:
    DELETE /video/1 HTTP/1.1
    Connection: keep-alive
    ......
    
    响应:
    HTTP/1.1 200 OK
    ......
    

    修改指定视频信息:

    请求:
    PUT /video/1 HTTP/1.1
    Connection: keep-alive
    ......
    
    响应:
    HTTP/1.1 200 OK
    ......
    
    

    上传视频信息:

    因为在上传视频信息的时候,会携带有视频文件、封面图片文件的上传,而这些文件数据都是二进制的,所以使用Json格式就不再合适了。因此在上传视频的时候就使用HTTP协议默认的上传文件请求格式,而不使用REST风格。

    请求:
    PSOT /video HTTP/1.1
    Content-Type: video/form-data; boundary="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
    Content-Length: xxx
    ......
    
    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    Content-Disposition: form-data; name="name"
    name(视频的名称)
    
    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    Content-Disposition: form-data; name="info"
    info(视频的描述)
    
    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    Content-Disposition: form-data; name="video"; filename="video.mp4"
    Content-Type: text/plain
    video视频数据
    
    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    Content-Disposition: form-data; name="image"; filename="image.jpg"
    Content-Type: text/plain
    image封面图片数据
    
    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    Content-Disposition: form-data; name="submit"
    
    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    
    
    响应:
    HTTP/1.1 303 See Other
    Location: "/"
    
    
    

    七、业务处理模块的实现

    7.1 业务处理模块类的设计

    业务处理模块负责与客户端进行网络通信,接收客户端的请求,然后根据请求信息,明确客户端用户的意图进行业务处理,并返回相应的处理结果给客户端。

    由于在实现网络通信相关功能使用的是httplib库,大大减小了开发成本,因此这里将网络通信模块和业务处理模块合并在同一个类中。因此在视频点播系统中,业务处理模块主要包含两大功能:网络通信功能和业务处理功能

    业务处理模块主要完成的功能有:

    • 客户端的视频数据和信息的上传
    • 客户端的视频列表的展示
    • 客户端的观看视频请求
    • 客户端的视频管理(修改、删除)

    代码框架如下:

    #ifndef __MY_SERVER__
    #define __MY_SERVER__ 
    #include "data.hpp"
    #include "httplib.h"
    
    namespace aod
    {
    #define WWW_ROOT "./www"      //资源根目录
    #define VIDEO_ROOT "/video/"  //视频目录
    #define IMAGE_ROOT "/image/"  //图片目录
        // 因为 httplib 是基于多线程,因此数据管理模块需要在多线程被访问,为了便于访问定义全局变量
        aod::TableVideo * table_video = NULL;
        class Server 
        {
            private:
                int _port; //服务器监听的端口号
                httplib::Server _server; //用于搭建HTTP服务器
    
            public:
                //业务处理接口
                
                //新增
                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 SelectOne(const httplib::Request& req, httplib::Response& rsp);
                
                //查询所有或者模糊匹配
                static void SelectAll(const httplib::Request& req, httplib::Response& rsp);
    
            public:
    
                Server(int port) :_port(port){}
                
                //建立请求与处理函数之间的映射关系,设置静态资源根目录,启动服务器
                bool RunModule();
        };
    }
    
    #endif 
    
    

    RunModule的实现:

    
    bool RunModule()
    {
        //1. 初始化---初始化数据管理模块、创建指定的目录
        table_video = new TableVideo();
        if (aod::FileUtil(WWW_ROOT).CreateDirectory() == false)
        {
            std::cout << "create directory: " << WWW_ROOT << " failed!" << std::endl;
            return false;
        }
    
        std::string video_real_path = std::string(WWW_ROOT)+ std::string(VIDEO_ROOT);  // ./www/video/
        std::string image_real_path = std::string(WWW_ROOT)+ std::string(IMAGE_ROOT);  // ./www/image/
        if (aod::FileUtil(video_real_path).CreateDirectory() == false)
        {
            std::cout << "create directory: " << video_real_path << " failed!" << std::endl;
            return false;
        }
        if (aod::FileUtil(image_real_path).CreateDirectory() == false)
        {
            std::cout << "create directory: " << image_real_path << " failed!" << std::endl;
            return false;
        }
    
        // 2. 搭建HTTP服务器,开始运行
        // 2.1 设置静态资源根目录
        _server.set_mount_point("/", WWW_ROOT);
    
        // 2.2 建立请求与处理函数之间的映射关系
        _server.Post("/video", Insert);
        _server.Delete("/video/(\\d+)", Delete);
        _server.Put("/video/(\\d+)", Update);
        _server.Get("/video/(\\d+)", SelectOne);
        _server.Get("/video", SelectAll);
        
        // 3. 启动服务器
        _server.listen("0.0.0.0", _port);
        
        return true;
    }
    

    Insert的实现:

    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, "reason":"上传的数据信息错误"})";
            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");
    
        //保存视频和图片文件到磁盘
    
    	//MultipartFormData {name, content_type, filename, content}
    
        std::string video_name = name.content; //视频名称内容
    	std::string video_info = info.content; //视频描述内容
        
    	//视频和图片文件存储路径 例如:./www/video/视频1.mp4
        
        std::string root = WWW_ROOT;
    	std::string video_path = root + VIDEO_ROOT + video_name + video.filename;
    	std::string image_path = root + IMAGE_ROOT + video_name + image.filename;
    
    	if (aod::FileUtil(video_path).SetContent(video.content) == false)
        {
            rsp.status = 500;  //服务器错误,服务器在处理请求的过程中发生了错误
            rsp.body = R"({"result":false, "reason":"视频文件存储失败"})";
            rsp.set_header("Content-Type", "application/json");
            return;
        }
    
        if (aod::FileUtil(image_path).SetContent(image.content) == false)
        {
            rsp.status = 500;  //服务器错误,服务器在处理请求的过程中发生了错误
            rsp.body = R"({"result":false, "reason":"图片文件存储失败"})";
            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/视频1video.mp4
    	video_json["image"] = IMAGE_ROOT + video_name + image.filename; // /image/视频1image.jpg
    	
    	if (table_video->Insert(video_json) == false)
    	{
    	    rsp.status = 500;  //服务器错误,服务器在处理请求的过程中发生了错误
    	    rsp.body = R"({"result":false, "reason":"数据库新增数据失败"})";
    	    rsp.set_header("Content-Type", "application/json");
    	    return;
    	}
    	
    	return;
    }
    
    

    Update的实现:

     static void Update(const httplib::Request& req, httplib::Response& rsp)
     {
         // 1. 获取要修改的视频id和修改后的视频信息
         std::string num = req.matches[1];
         int video_id = atoi(num.c_str());
    
         Json::Value video;
         if(aod::JsonUtil::Deserialize(req.body, &video) == false)
         {
             rsp.status = 400;  //客户端错误,请求包含语法错误或无法完成请求
             rsp.body = R"({"result":false, "reason":"新的视频信息解析失败"})";
             rsp.set_header("Content-Type", "application/json");
             return;
         }
    
         // 2. 修改数据库视频信息
         
         if(table_video->Update(video_id, video) == false)
         {
             rsp.status = 500;  //服务器错误,服务器在处理请求的过程中发生了错误
             rsp.body = R"({"result":false, "reason":"修改数据库中的视频信息失败"})";
             rsp.set_header("Content-Type", "application/json");
             return;
         }
    
         return;
     }
    
    

    Delete的实现:

    static void Delete(const httplib::Request& req, httplib::Response& rsp)
    {
        //1. 获取要删除的视频id
        
        //matches: 存放正则表达式匹配的规则数据 /numbers/123 matches[0] = "/numbers/123", matches[1] = "123"
        std::string num = req.matches[1];
        int video_id = atoi(num.c_str());  
    
        //2. 删除视频文件和图片文件
        Json::Value video;
        if(table_video->SelectOne(video_id, &video) == false)
        {
            rsp.status = 500;  //服务器错误,服务器在处理请求的过程中发生了错误
            rsp.body = R"({"result":false, "reason":"数据库中不存在视频信息"})";
            rsp.set_header("Content-Type", "application/json");
            return;
        }
    
        std::string root = WWW_ROOT;
        //视频文件存放路径
        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(table_video->Delete(video_id) == false)
        {
            rsp.status = 500;  //服务器错误,服务器在处理请求的过程中发生了错误
            rsp.body = R"({"result":false, "reason":"删除数据库视频信息失败"})";
            rsp.set_header("Content-Type", "application/json");
            return;
        }
        
        return;
    }
    

    SelectOne的实现:

    static void SelectOne(const httplib::Request& req, httplib::Response& rsp)
    {
    
        //1. 获取要删除的视频id
        std::string num = req.matches[1];
        int video_id = atoi(num.c_str());  
    
        //2. 在数据库中查询指定视频信息
        Json::Value video;
        if(table_video->SelectOne(video_id, &video) == false)
        {
            rsp.status = 500;  //服务器错误,服务器在处理请求的过程中发生了错误
            rsp.body = R"({"result":false, "reason":"查询数据库指定视频信息失败"})";
            rsp.set_header("Content-Type", "application/json");
            return;
        }
    
        //3. 组织响应正文 --- json格式的字符串
        if (aod::JsonUtil::Serialize(video, &rsp.body) == false)
        {
            rsp.status = 500;  //服务器错误,服务器在处理请求的过程中发生了错误
            rsp.body = R"({"result":false, "reason":"序列化正文失败"})";
            rsp.set_header("Content-Type", "application/json");
            return;
        }
        rsp.set_header("Content-Type", "application/json");
        return;
    }
    

    SelectAll的实现:

    static void SelectAll(const httplib::Request& req, httplib::Response& rsp)
    {
        //存在两种可能: /video  和  /video?search="关键字"
        
        // 1. 判断查询类型
      
        bool select_flag = true; //默认查询所有
        std::string search_key;
        if (req.has_param("search") == true)
        {
            select_flag = false; //模糊匹配
            search_key = req.get_param_value("search");
        }
    
        
        //2. 查询视频信息 
        Json::Value videos;
        if(select_flag == true)
        {
            if (table_video->SelectAll(&videos) == false)
            {
                rsp.status = 500;  //服务器错误,服务器在处理请求的过程中发生了错误
                rsp.body = R"({"result":false, "reason":"查询数据库所有视频信息失败"})";
                rsp.set_header("Content-Type", "application/json");
                return;
            }
        }
        else 
        {
            if (table_video->SelectLike(search_key, &videos) == false)
            {
                rsp.status = 500;  //服务器错误,服务器在处理请求的过程中发生了错误
                rsp.body = R"({"result":false, "reason":"模糊匹配查询数据库视频信息失败"})";
                rsp.set_header("Content-Type", "application/json");
                return;
            }
        }
        
        //3. 组织响应正文
        if (aod::JsonUtil::Serialize(videos, &rsp.body) == false)
        {
    
            rsp.status = 500;  //服务器错误,服务器在处理请求的过程中发生了错误
            rsp.body = R"({"result":false, "reason":"序列化正文失败"})";
            rsp.set_header("Content-Type", "application/json");
            return;
        }
        
        rsp.set_header("Content-Type", "application/json");
        return;
    }
    

    【注意】

    SelectAll函数中将查询所有视频和模糊匹配两个功能包含在一起的,因为在httplib库中的Resuest类中有一个has_param函数,可用于判断请求中是否含义search关键字。利用has_param函数就可判断出此次查询请求是查询所有还是通过关键字查询。

    7.2 综合调试

    调试代码:

    #include "server.hpp"
    int main()
    {
    	aod::Server server(9090);
    	server.RunModule();
    	return 0;
    }
    

    服务器的功能测试借助一个工具 Postman 完成。Postman下载地址 

     

    八、前端界面的实现

    8.1 前端视频展示界面的实现

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
    	<meta charset="utf-8">
    	<meta name="viewport" content="width=device-width, initial-scale=1">
    	<meta name="description" content="">
    	<meta name="author" content="OrcasThemes">
    	<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
    	<title>Home</title>
    	<!-- Bootstrap core CSS -->
    	<link href="css/bootstrap.css" rel="stylesheet">
    	<!-- Custom styles for this template -->
    	<link rel="stylesheet" href="css/screen.css">
    	<link rel="stylesheet" href="css/animation.css">
    	<!--[if IE 7]>
    
    <![endif]-->
    	<link rel="stylesheet" href="css/font-awesome.css">
    	<!--[if lt IE 8]>
    <link rel="stylesheet" href="css/ie.css" type="text/css" media="screen, projection">
    <![endif]-->
    	<link href="css/lity.css" rel="stylesheet">
    
    	<style>
    		[v-cloak] {
    			display: none;
    		}
    	</style>
    </head>
    
    <body>
    	<div id="myapp">
    		<!-- HOME 1 -->
    		<div id="home1" class="container-fluid standard-bg">
    			<!-- HEADER -->
    			<div class="row header-top">
    				<div class="col-lg-3 col-md-6 col-sm-5 col-xs-8">
    					<a class="main-logo" href="#"><img src="img/main-logo.png" class="main-logo img-responsive"
    							alt="Muvee Reviews" title="Muvee Reviews"></a>
    				</div>
    				<div class="col-lg-6 hidden-md text-center hidden-sm hidden-xs">
    				</div>
    				<div class="col-lg-3 col-md-6 col-sm-7 hidden-xs">
    					<div class="right-box">
    						<button type="button" class="access-btn" data-toggle="modal" data-target="#enquirypopup">新增视频</button>
    					</div>
    				</div>
    			</div>
    			<!-- MENU -->
    			<div class="row home-mega-menu ">
    				<div class="col-md-12">
    					<nav class="navbar navbar-default">
    						<div class="navbar-header">
    							<button class="navbar-toggle" type="button" data-toggle="collapse"
    								data-target=".js-navbar-collapse">
    								<span class="sr-only">Toggle navigation</span>
    								<span class="icon-bar"></span>
    								<span class="icon-bar"></span>
    								<span class="icon-bar"></span>
    							</button>
    						</div>
    						<div class="collapse navbar-collapse js-navbar-collapse megabg dropshd ">
    							<ul class="nav navbar-nav">
    								<li><a href="index.html">视频点播</a></li>
    							</ul>
    
    							<div class="search-block">
    								<form>
    									<input type="search" placeholder="Search">
    								</form>
    							</div>
    						</div>
    						<!-- /.nav-collapse -->
    					</nav>
    				</div>
    			</div>
    			<!-- CORE -->
    			<div class="row">
    				<!-- SIDEBAR -->
    				<div class="col-lg-2 col-md-4 hidden-sm hidden-xs">
    				</div>
    				<!-- HOME MAIN POSTS -->
    				<div class="col-lg-10 col-md-8">
    					<section id="home-main">
    						<h2 class="icon"><i class="fa fa-television" aria-hidden="true"></i>视频列表</h2>
    						<div class="row">
    							<!-- ARTICLES -->
    							<div class="col-lg-9 col-md-12 col-sm-12">
    								<div class="row auto-clear">
    									<article class="col-lg-3 col-md-6 col-sm-4" v-for="video in videos">
    										<!-- POST L size -->
    										<div class="post post-medium">
    											<div class="thumbr">
    												<a class="afterglow post-thumb" v-bind:href="'/video.html?id='+video.id" target="_blank">
    													<span class="play-btn-border" title="Play"><i
    															class="fa fa-play-circle headline-round"
    															aria-hidden="true"></i></span>
    													<div class="cactus-note ct-time font-size-1"><span></span>
    													</div>
    													<img class="img-responsive" v-bind:src="video.image" alt="#"
    														v-cloak>
    												</a>
    											</div>
    											<div class="infor">
    												<h4>
    													<a class="title" href="#" v-cloak>{{video.name}}</video></a>
    												</h4>
    												<!-- <span class="posts-txt" title="Posts from Channel"><i
    														class="fa fa-thumbs-up" aria-hidden="true"></i>20.895</span>
    												<div class="ratings">
    													<i class="fa fa-star" aria-hidden="true"></i>
    													<i class="fa fa-star" aria-hidden="true"></i>
    													<i class="fa fa-star-half-o" aria-hidden="true"></i>
    													<i class="fa fa-star-o"></i>
    													<i class="fa fa-star-half"></i>
    												</div> -->
    											</div>
    										</div>
    									</article>
    
    								</div>
    								<div class="clearfix spacer"></div>
    							</div>
    							<!-- RIGHT ASIDE -->
    							<div class="col-lg-3 hidden-md col-sm-12 text-center top-sidebar">
    
    							</div>
    						</div>
    					</section>
    				</div>
    			</div>
    		</div>
    
    		<!-- CHANNELS -->
    		<div id="channels-block" class="container-fluid channels-bg">
    		</div>
    		<!-- BOTTOM BANNER -->
    		<div id="bottom-banner" class="container text-center">
    		</div>
    		<!-- FOOTER -->
    		<div id="footer" class="container-fluid footer-background">
    			<div class="container">
    				<footer>
    					<!-- SECTION FOOTER -->
    					<div class="row">
    						<!-- SOCIAL -->
    						<div class="col-lg-3 col-md-3 col-sm-6 col-xs-12">
    							<div class="row auto-clear">
    							</div>
    						</div>
    						<!-- TAGS -->
    						<div class="col-lg-3 col-md-3 col-sm-6 col-xs-12">
    						</div>
    						<!-- POST -->
    						<div class="col-lg-3 col-md-3 col-sm-6 col-xs-12">
    						</div>
    						<!-- LINKS -->
    						<div class="col-lg-3 col-md-3 col-sm-6 col-xs-12">
    						</div>
    					</div>
    					<div class="row copyright-bottom text-center">
    						<div class="col-md-12 text-center">
    							<a href="" class="footer-logo" title="Video Magazine Bootstrap HTML5 template">
    								<img src="img/footer-logo.png" class="img-responsive text-center"
    									alt="Video Magazine Bootstrap HTML5 template">
    							</a>
    							<p v-cloak>Copyright &copy; Author by {{author}}</p>
    						</div>
    					</div>
    				</footer>
    			</div>
    		</div>
    
    		<!-- MODAL -->
    		<div id="enquirypopup" class="modal fade in " role="dialog">
    			<div class="modal-dialog">
    				<!-- Modal content-->
    				<div class="modal-content row">
    					<div class="modal-header custom-modal-header">
    						<button type="button" class="close" data-dismiss="modal">×</button>
    						<h2 class="icon"><i class="fa fa-television" aria-hidden="true"></i>新增视频</h2>
    					</div>
    					<div class="modal-body">
    						<form name="info_form" class="form-inline" action="/video" method="post" enctype="multipart/form-data">
    							
    							<div class="form-group col-sm-12">
    								<input type="text" class="form-control" name="name" placeholder="输入视频名称">
    							</div>
    							
    							<div class="form-group col-sm-12">
    								<input type="text" class="form-control" name="info" placeholder="输入视频简介">
    							</div>
    
    							<div class="form-group col-sm-12">
    								<input type="file" class="form-control" name="video" placeholder="选择视频文件">
    							</div>
    							
    							<div class="form-group col-sm-12">
    								<input type="file" class="form-control" name="image" placeholder="选择封面图片">
    							</div>
    
    							<div class="form-group col-sm-12">
    								<button class="subscribe-btn pull-right" type="submit"title="Subscribe">上传</button>
    							</div>
    						</form>
    					</div>
    				</div>
    			</div>
    		</div>
    	</div>
    </body>
    
    <!-- JAVA SCRIPT -->
    <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
    <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: "Lihaifei",
    			videos: []
    		},
    		methods: {
    			get_allvideos: function () {
    				$.ajax({
    					url: "/video",
    					type: "get",
    					context: this, // 将vue传入ajax作为this对象
    					success: function (result, status, xhr) { //请求成功后的处理函数
    						this.videos = result;
    						// alert("获取结果成功!");
    					}
    				})
    
    			}
    		}
    	});
    
    	app.get_allvideos();
    </script>
    
    </html>
    

    8.2 前端视频观看页面的实现 

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
       <meta charset="utf-8">
       <meta name="viewport" content="width=device-width, initial-scale=1">
       <meta name="description" content="">
       <meta name="author" content="OrcasThemes">
       <meta http-equiv="X-UA-Compatible" content="IE=Edge" />
       <title></title>
       <!-- Bootstrap core CSS -->
       <link href="css/bootstrap.css" rel="stylesheet">
       <!-- Custom styles for this template -->
       <link rel="stylesheet" href="css/screen.css">
       <link rel="stylesheet" href="css/animation.css">
       <!--[if IE 7]>
          
          <![endif]-->
       <link rel="stylesheet" href="css/font-awesome.css">
       <!--[if lt IE 8]>
          <link rel="stylesheet" href="css/ie.css" type="text/css" media="screen, projection">
          <![endif]-->
       <link href="css/lity.css" rel="stylesheet">
    
       <style>
          [v-cloak] {
             display: none;
          }
       </style>
    </head>
    
    <body>
       <div id="myapp">
          <!-- SINGLE VIDEO -->
          <div id="single-video" class="container-fluid standard-bg">
             <!-- HEADER -->
             <div class="row header-top">
                <div class="col-lg-3 col-md-6 col-sm-5">
                   <a class="main-logo" href="#"><img src="img/main-logo.png" class="main-logo" alt="Muvee Reviews"
                         title="Muvee Reviews"></a>
                </div>
                <div class="col-lg-6 hidden-md text-center hidden-sm hidden-xs">
                </div>
                <div class="col-lg-3 col-md-6 col-sm-7 hidden-xs">
                   <div class="right-box">
                      <button type="button" class="access-btn" data-toggle="modal" v-on:click="delete_video()">视频删除</button>
                      <button type="button" class="access-btn" data-toggle="modal" data-target="#enquirypopup">视频修改</button>
                   </div>
                </div>
             </div>
             <!-- MENU -->
             <div class="row home-mega-menu ">
                <div class="col-md-12">
                   <nav class="navbar navbar-default">
                      <div class="navbar-header">
                         <button class="navbar-toggle" type="button" data-toggle="collapse"
                            data-target=".js-navbar-collapse">
                            <span class="sr-only">Toggle navigation</span>
                            <span class="icon-bar"></span>
                            <span class="icon-bar"></span>
                            <span class="icon-bar"></span>
                         </button>
                      </div>
                      <div class="collapse navbar-collapse js-navbar-collapse megabg dropshd ">
                         <ul class="nav navbar-nav">
                            <li><a href="index.html">视频点播</a></li>
                         </ul>
                         <div class="search-block">
                            <form>
                               <input type="search" placeholder="Search">
                            </form>
                         </div>
                      </div>
                      <!-- /.nav-collapse -->
                   </nav>
                </div>
             </div>
             <!-- SINGLE VIDEO -->
             <div class="row">
                <!-- SIDEBAR -->
                <div class="col-lg-2 col-md-4 hidden-sm hidden-xs">
    
                </div>
                <!-- SINGLE VIDEO -->
                <div id="single-video-wrapper" class="col-lg-10 col-md-8">
                   <div class="row">
                      <!-- VIDEO SINGLE POST -->
                      <div class="col-lg-9 col-md-12 col-sm-12">
                         <!-- POST L size -->
                         <article class="post-video">
                            <!-- VIDEO INFO -->
                            <div class="video-info">
                               <!-- 16:9 aspect ratio -->
                               <div class="embed-responsive embed-responsive-16by9 video-embed-box">
                                  <iframe v-bind:src="video.video" class="embed-responsive-item"></iframe>
                               </div>
                               <!-- <div class="metabox">
                                  <span class="meta-i">
                                     <i class="fa fa-thumbs-up" aria-hidden="true"></i>20.895
                                  </span>
                                  <span class="meta-i">
                                     <i class="fa fa-thumbs-down" aria-hidden="true"></i>3.981
                                  </span>
                                  <span class="meta-i">
                                     <i class="fa fa-user"></i><a href="#" class="author" title="John Doe">John Doe</a>
                                  </span>
                                  <span class="meta-i">
                                     <i class="fa fa-clock-o"></i>March 16. 2017
                                  </span>
                                  <span class="meta-i">
                                     <i class="fa fa-eye"></i>1,347,912 views
                                  </span>
                                  <div class="ratings">
                                     <i class="fa fa-star" aria-hidden="true"></i>
                                     <i class="fa fa-star" aria-hidden="true"></i>
                                     <i class="fa fa-star-half-o" aria-hidden="true"></i>
                                     <i class="fa fa-star-o"></i>
                                     <i class="fa fa-star-half"></i>
                                  </div>
                               </div> -->
                            </div>
                            <div class="clearfix spacer"></div>
                            <!-- DETAILS -->
                            <div class="video-content">
                               <h2 class="title main-head-title">视频描述</h2>
                               <p v-cloak>{{video.info}}</p>
                            </div>
                            <div class="clearfix spacer"></div>
                         </article>
    
    
                      </div>
    
                      <!-- VIDEO SIDE BANNERS -->
                      <div class="col-lg-3 hidden-md hidden-sm">
    
                      </div>
                   </div>
                   <div class="clearfix spacer"></div>
                   <div class="row">
                   </div>
                </div>
             </div>
          </div>
          <!-- CHANNELS -->
          <div id="channels-block" class="container-fluid channels-bg">
             <div class="container-fluid ">
                <div class="col-md-12">
    
                   <div class="clearfix"></div>
                </div>
             </div>
          </div>
    
          <!-- FOOTER -->
          <div id="footer" class="container-fluid footer-background">
             <div class="container">
                <footer>
    
                   <div class="row copyright-bottom text-center">
                      <div class="col-md-12 text-center">
                         <a href="" class="footer-logo" title="Video Magazine Bootstrap HTML5 template">
                            <img src="img/footer-logo.png" class="img-responsive text-center"
                               alt="Video Magazine Bootstrap HTML5 template">
                         </a>
                         <p v-cloak>Copyright &copy; Author by {{author}}</p>
    
                      </div>
                   </div>
                </footer>
             </div>
          </div>
    
    
          <!-- MODAL -->
          <div id="enquirypopup" class="modal fade in " role="dialog">
             <div class="modal-dialog">
                <!-- Modal content-->
                <div class="modal-content row">
                   <div class="modal-header custom-modal-header">
                      <button type="button" class="close" data-dismiss="modal">×</button>
                      <h2 class="icon"><i class="fa fa-television" aria-hidden="true"></i>视频信息修改</h2>
                   </div>
                   <div class="modal-body">
                      <form name="info_form" class="form-inline" action="#" method="post">
                         <div class="form-group col-sm-12">
                            <input type="text" class="form-control" name="name" v-model="video.name">
                         </div>
                         <div class="form-group col-sm-12">
                            <input type="text" class="form-control" name="info" v-model="video.info">
                         </div>
                         <div class="form-group col-sm-12">
                            <button class="subscribe-btn pull-right" type="submit" title="Subscribe"
                               v-on:click.prevent="update_video()">提交</button>
                         </div>
                      </form>
                   </div>
                </div>
             </div>
          </div>
       </div>
    </body>
    
    
    <!-- JAVA SCRIPT -->
    <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
    <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: "Lihaifei",
             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, // 将vue传入ajax作为this对象
                   success: function (result, status, xhr) { //请求成功后的处理函数
                      this.video = result;
                      // alert("获取结果成功!");
                   }
                })
             },
             update_video: function () {
                $.ajax({
                   type: "put",
                   url: "/video/" + this.video.id,
                   data: JSON.stringify(this.video),
                   context: this,
                   success: function (result, status, xhr) {
                      alert("修改视频信息成功!");
                      window.location.reload();
                   }
                })
             },
             delete_video: function () {
                $.ajax({
                   type: "delete",
                   url: "/video/" + this.video.id,
                   data: JSON.stringify(this.video),
                   context: this,
                   success: function (result, status, xhr) {
                      alert("删除视频成功!");
                      window.location.href="/index.html";
                   }
                })
    
             }
          }
       });
    
       app.get_video();
    </script>
    
    </html>
    

    九、项目总结

    • 项目名称:视频共享点播系统

    • 项目功能:搭建一个共享点播系统,服务器支持用户通过前端浏览器访问服务器,获取展示与观看和操作的界面,最
      终实现视频的上传以及观看和删改查等基础管理功能。

    • 开发环境及工具: centos7.6vimg++gdbmakefilevscode等。

    • 技术特点: HTTP 服务器搭建, RESTful 风格接口设计, Json 序列化,线程池, HTML+CSS+JS 基础。

    • 项目模块:
      1.数据管理模块:基于 MYSQL 进行数据管理,封装数据管理类进行数据统一访问

    • 2.业务处理模块:基于 HTTPLIB 搭建 HTTP 服务器,使用 restful风格 进行接口设计处理客户端业务请求
      3.前端界面模块:基于基础的 HTML+CSS+JS 完成基于简单模板前端界面的修改与功能完成。

    • 项目扩展方向:
      1.添加用户管理以及视频分类管理
      2.添加视频的评论,打分功能。
      3.服务器上的视频或图片采用压缩处理节省空间,进行热点与非热点的管理

    • 源码:fengxi740/学习项目


    http://www.kler.cn/a/574236.html

    相关文章:

  • PTA 7-6 列出连通集
  • 在MWC2025,读懂华为如何以行践言
  • 考研初试需要准备多少时间用于复习备考?
  • Hadoop、Hive、Spark的关系
  • RecyclerView与ListView的优化
  • Android Studio Gradle 8.0 适配指南
  • 在大型语言模型的提示词设计中,system、user和assistant三个角色的区别与联系
  • 网络安全 | 密码基础知识介绍
  • django中路由配置规则的详细说明
  • Vue2 的生命周期有哪些
  • MyBatis-Plus开发流程:Spring Boot + MyBatis-Plus 实现对 book_tab 表的增删改查及Redis缓存
  • Tomcat 新手入门指南
  • AI技术赋能增长,新一代呼叫中心系统,完美适配企业转型
  • Spring WebFlux 中 WebSocket 使用 DataBuffer 的注意事项
  • ORACLE导入导出
  • 搭建一个跳板服务器的全过程
  • TP-LINK图像处理工程师(深圳)内推
  • 质量属性场景描述
  • 计算机视觉算法实战——表面缺陷检测(表面缺陷检测)
  • XGBClassifiler函数介绍