【项目设计】基于Httplib和Mysql的视频播放

项目源码(绝对可以直接运行)

一、项目介绍

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

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

2. 服务端功能模块划分

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

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

二、环境搭建

1. 升级GCC

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

2. 安装JsonCpp库

JSON 是一种轻量级的数据交换格式。它可以代表数字、字符串、值的有序序列和名称/值的集合对。JsonCpp 是一个C++库,允许操作 JSON 值,包括字符串的序列化和反序列化。它还可以保存反序列化/序列化步骤中的现有注释,方便用于存储用户输入文件的格式。

3. 引入httplib库

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

4. MySQL数据库及开发包安装

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

三、第三方库的认识

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;// 转floatbool 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;
}

JsonCpp实现序列化和反序列化

#include <iostream>
#include <jsoncpp/json/json.h>
#include <sstream>
#include <memory>
using namespace std;void serialize()
{const char *name = "张三";int age = 18;float scores[] = {88, 88.5, 90.8};Json::Value root;root["姓名"] = name;root["年龄"] = age;root["成绩"].append(scores[0]);root["成绩"].append(scores[1]);root["成绩"].append(scores[2]);Json::StreamWriterBuilder swb;unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());stringstream s;int res = sw->write(root, &s);if (res != 0){cout << "serialize failed!" << endl;return;}cout << s.str() << endl;
}void deserialize(string s)
{Json::Value root;Json::CharReaderBuilder crb;unique_ptr<Json::CharReader> cr(crb.newCharReader());string errs;bool res = cr->parse(s.c_str(), s.c_str() + s.size(), &root, &errs);if (!res){cout << "deserialize failed!" << endl;return;}cout << root["姓名"].asString() << endl;cout << root["年龄"].asInt() << endl;for (int i = 0; i < root["成绩"].size(); i++){cout << root["成绩"][i].asFloat() << endl;}
}int main()
{// serialize();string s = R"({"姓名":"李四", "年龄":20, "成绩":[100, 0, 60]})";deserialize(s);return 0;
}

2. 认识MySQL数据库的API

MySQL C语言API就是用C语言编写的MySQL编程接口,使用这些接口函数可以实现对MySQL数据库的查询等操作。以下是MySQL的C语言API接口:

MySQL句柄初始化

MYSQL* mysql_init(MYSQL* mysql);
  • 参数为空是动态申请句柄空间进行初始化,如果调用成功则返回MySQL句柄,失败则返回NULL

句柄是什么?

句柄(handle)是C++程序设计中经常提及的一个术语。它并不是一种具体的、固定不变的数据类型或实体,而是代表了程序设计中的一个广义的概念。句柄一般是指获取另一个对象的方法——一个广义的指针,它的具体形式可能是一个整数、一个对象或就是一个真实的指针,而它的目的就是建立起与被访问对象之间的唯一的联系。

在C++中,要访问一个对象,通常可以建立一个指向对象的指针。但是在很多具体的应用中,直接用指针代表对象并不是一个好的解决方案,因此引入了句柄的概念。

连接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, unsigned long client_flag);
  • mysql:初始化完成的句柄
  • host:连接的MySQL服务器的地址
  • user:连接的数据库的用户名
  • passwd:该用户连接数据库的密码
  • db:默认选择的数据库名称
  • port:连接的MySQL服务器的端口,默认0是3306端口
  • unix_socket:通信管道文件或者socket文件,通常置为NULL
  • client_flag:客户端的标志位,通常置为0
  • 连接成功返回MySQL句柄,失败则返回NULL

设置当前客户端的字符集

int mysql_set_character_set(MYSQL* mysql, const char* csname);
  • mysql:初始化完成的句柄
  • csname:字符集名称,通常为utf8
  • 调用成功返回0,失败则返回非0

选择操作的数据库

int mysql_select_db(MYSQL* mysql, const char* db);
  • mysql:初始化完成的句柄
  • db:要进行操作的数据库名称
  • 调用成功返回0,失败则返回非0

执行SQL语句

int mysql_query(MYSQL* mysql, const char* stmt_str);
  • mysql:初始化完成的句柄
  • stmt_str:要执行的sql语句
  • 调用成功返回0,失败则返回非0

保存查询结果到本地

MYSQL_RES* mysql_store_result(MYSQL* mysql);
  • mysql:初始化完成的句柄
  • 调用成功返回结果集的首地址,失败则返回NULL

获取结果集中的行数

uint64_t mysql_num_rows(MYSQL_RES* result);
  • result:保存到本地的结果集
  • 返回结果集中数据的条数,也是是行数

获取每一条结果集的列数

unsigned int mysql_num_fields(MYSQL_RES* result);
  • result:保存到本地的结果集
  • 返回结果每一条集中数据的列数

遍历结果集

MYSQL_ROW mysql_fecth_row(MYSQL_RES* result);
  • result:保存到本地的结果集
  • MYSQL_ROW实际上是一个char** 类型的二级指针将每一条数据表示成了字符串指针数组。比如row[0]表示第0行,row[1]表示第一行,并且这个接口会保存当前读取的结果集位置,每次获取的都是下一条数据。

释放结果集

void mysql_free_result(MYSQL_RES* result);
  • result是保存到本地的结果集,无返回值。

注意一定要释放结果集,否则会造成内存泄漏。

关闭数据库客户端的连接,销毁MySQL句柄

void mysql_close(MYSQL* mysql);

获取MySQL接口中执行错误的原因

const char* mysql(MYSQL* mysql);
  • 该函数会返回执行sql语句失败的原因。

使用MySQL的API实现对数据的增删改查

  • 创建一个测试所用数据库和表
    create database if not exists test_db;
    use test_db;
    create table if not exists test_tb(id int primary key auto_increment,age int,name varchar(32),score decimal(4, 2)
    );
    
  • 对数据库进行增删改查的代码样例:
    #include <stdio.h>
    #include <stdlib.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 res = mysql_query(mysql, sql);if (res != 0){printf("add failed, %s\n", mysql_error(mysql));return -1;}return 0;
    }int mod(MYSQL *mysql)
    {const char *sql = "update test_tb set score=80 where id=2;";int res = mysql_query(mysql, sql);if (res != 0){printf("mod failed, %s\n", mysql_error(mysql));return -1;}return 0;
    }int del(MYSQL *mysql)
    {const char *sql = "delete from test_tb where id=2;";int res = mysql_query(mysql, sql);if (res != 0){printf("del failed, %s\n", mysql_error(mysql));return -1;}return 0;
    }int sel(MYSQL *mysql)
    {const char *sql = "select * from test_tb;";int res = mysql_query(mysql, sql);if (res != 0){printf("sel failed, %s\n", mysql_error(mysql));return -1;}MYSQL_RES *ret = mysql_store_result(mysql);if (ret == NULL){printf("mysql_store_result failed, %s\n", mysql_error(mysql));return -1;}int rows = mysql_num_rows(ret);int cols = mysql_num_fields(ret);printf("%10s%10s%10s%10s\n", "ID", "年龄", "姓名", "成绩");for (int i = 0; i < rows; i++){MYSQL_ROW rows_data = mysql_fetch_row(ret);for (int j = 0; j < cols; j++){printf("%9s", rows_data[j]);}printf("\n");}mysql_free_result(ret);return 0;
    }int main()
    {MYSQL *mysql = mysql_init(NULL);if (mysql == NULL){printf("mysql init failed\n");return -1;}if (mysql_real_connect(mysql, "127.0.0.1", "root", "654321", "test_db", 3306, NULL, 0) == NULL){printf("mysql connect failed\n");return -1;}mysql_set_character_set(mysql, "utf8");// add(mysql);// mod(mysql);// del(mysql);sel(mysql);mysql_close(mysql);return 0;
    }

    编译指令:

    gcc -o mysql_test mysql_test.c -L/usr/include/mysql/mysql.h -lmysqlclient
    

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

3. 认识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;//存放头部字段的键值对mapstd::string body;//存放请求正文// for serverstd::string version;//存放协议版本Params params;//存放url中查询字符串 key=val&key=val的 键值对mapMultipartFormDataMap 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;//存放响应头部字段键值对的mapstd::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服务器静态资源根目录};
}

使用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/123server.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。如果文件存在则将文件名和文件内容输出到控制台。

在主函数中,服务器被创建并配置三个请求处理函数:Handler、Numbers、Multipart,并且指定服务端静态资源根目录为./www。然后监听8080的端口号等待客户端连接。

四、服务端工具类的实现

1. 文件工具类的设计

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

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

class FileUtil{private:string _name;public:FileUtil(const string &name) : _name(name) {}// 判断文件是否存在bool Exist(){int ret = access(_name.c_str(), F_OK);if (ret != 0){cout << "file is not exist" << endl;return false;}return true;}// 获得文件大小size_t Size(){if (!this->Exist()){return 0;}struct stat st;int ret = stat(_name.c_str(), &st);if (ret != 0){cout << "file is empty" << endl;return 0;}return st.st_size;}// 读取文件bool Reader(string *s){ifstream ifs;ifs.open(_name, ios::binary);if (ifs.is_open() == false){cout << "file open failed" << endl;return false;}s->resize(this->Size());ifs.read(&(*s)[0], s->size());if (!ifs.good()){cout << "file read failed" << endl;ifs.close();return false;}ifs.close();return true;}// 写入文件bool Writer(const string &s){ofstream ofs;ofs.open(_name, ios::binary);if (ofs.is_open() == false){cout << "file open failed" << endl;return false;}ofs.write(s.c_str(), s.size());if (!ofs.good()){cout << "file write failed" << endl;ofs.close();return false;}ofs.close();return true;}// 根据文件创建目录bool CreatDict(){if (this->Exist()){return true;}int ret = mkdir(_name.c_str(), 0777);if (ret != 0){cout << "mkdir failed" << endl;return false;}return true;}};

在判断文件是否存在时使用的接口是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);

其中path表示要创建的文件名,mode表示赋予给新创建的文件权限。

2. Json 工具类的设计

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

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

五、数据管理模块的实现

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,加上静态资源根目录就是实际存储路径'
);

2. 数据管理类的设计

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

然而,数据库中可能存在多张表,每张表的数据又不相同,进行的数据操作也不同。因此,就需要为每张表中的数据操作都设计一个数据管理类,通过类的实例化对象来管理这张表中的数据。由于在视频播放系统中只涉及一张表,因此只设计一个类即可,该类包含的数据库操作有:新增、修改、删除、查询所有数据、查询单个数据、模糊匹配。

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

#include <iostream>
#include <string>
#include <mutex>
#include <mysql/mysql.h>
#include "util.hpp"using namespace std;namespace data
{#define HOST "127.0.0.1"
#define USER "root"
#define PASS "654321"
#define DATABASE "aod_system"
#define PORT 3306static MYSQL *Init(){MYSQL *mysql = mysql_init(nullptr);if (mysql == NULL){cout << "mysql init failed" << endl;return NULL;}if (mysql_real_connect(mysql, HOST, USER, PASS, DATABASE, PORT, NULL, 0) == NULL){cout << "mysql connect failed" << endl;mysql_close(mysql);return NULL;}mysql_set_character_set(mysql, "utf8");return mysql;}static void Destory(MYSQL *mysql){if (mysql == NULL){cout << "mysql is not exist" << endl;return;}mysql_close(mysql);}static bool Excute(MYSQL *mysql, const string &sql){int ret = mysql_query(mysql, sql.c_str());if (ret != 0){cout << sql << endl;cout << mysql_error(mysql) << endl;return false;}return true;}class TableVideo{private:MYSQL *_mysql;mutex _mutex;public:TableVideo(){_mysql = Init();if (_mysql == nullptr){exit(-1);}}~TableVideo(){Destory(_mysql);}bool Insert(const Json::Value &value){
#define INSERT "insert into tb_video values (null,'%s','%s','%s','%s');"string sql;sql.resize(4096 + value["info"].asString().size());sprintf(&sql[0], INSERT, value["name"].asCString(), value["info"].asCString(), value["video"].asCString(), value["image"].asCString());return Excute(_mysql, sql);}bool Delete(int id){
#define DELETE "delete from tb_video where id=%d;"char sql[1024] = {0};sprintf(sql, DELETE, id);return Excute(_mysql, sql);}bool Update(const Json::Value &value, int id){
#define UPDATE "update tb_video set name='%s' , info='%s' where id=%d;"string sql;sql.resize(4096 + value["info"].asString().size());sprintf(&sql[0], UPDATE, value["name"].asCString(), value["info"].asCString(), id);return Excute(_mysql, sql);}bool SelectAll(Json::Value *values){
#define SELECTALL "select * from tb_video;"_mutex.lock();bool ret = Excute(_mysql, SELECTALL);if (ret == false){cout << "select all failed" << endl;_mutex.unlock();return false;}MYSQL_RES *result = mysql_store_result(_mysql);if (result == NULL){cout << "mysql_store_result failed" << endl;_mutex.unlock();return false;}_mutex.unlock();int rows = mysql_num_rows(result);for (int i = 0; i < rows; i++){MYSQL_ROW row_data = mysql_fetch_row(result);Json::Value value;value["id"] = atoi(row_data[0]);value["name"] = row_data[1];value["info"] = row_data[2];value["video"] = row_data[3];value["image"] = row_data[4];values->append(value);}mysql_free_result(result);return true;}bool SelectOne(Json::Value *value, int id){
#define SELECTONE "select * from tb_video where id=%d;"char sql[1024] = {0};sprintf(sql, SELECTONE, id);_mutex.lock();bool ret = Excute(_mysql, sql);if (ret == false){cout << "select one failed" << endl;_mutex.unlock();return false;}MYSQL_RES *result = mysql_store_result(_mysql);if (result == NULL){cout << "mysql_store_result failed" << endl;_mutex.unlock();return false;}_mutex.unlock();int rows = mysql_num_rows(result);if (rows != 1){cout << "select one over" << endl;return false;}MYSQL_ROW row_data = mysql_fetch_row(result);(*value)["id"] = atoi(row_data[0]);(*value)["name"] = row_data[1];(*value)["info"] = row_data[2];(*value)["video"] = row_data[3];(*value)["image"] = row_data[4];mysql_free_result(result);return true;}bool SelectLike(Json::Value *values, const string &s){
#define SELECTLIKE "select * from tb_video where name like '%%%s%%';"string sql;sql.resize(4096);sprintf(&sql[0], SELECTLIKE, s.c_str());_mutex.lock();bool ret = Excute(_mysql, sql);if (ret == false){cout << "select one failed" << endl;_mutex.unlock();return false;}MYSQL_RES *result = mysql_store_result(_mysql);if (result == NULL){cout << "mysql_store_result failed" << endl;_mutex.unlock();return false;}_mutex.unlock();int rows = mysql_num_rows(result);for (int i = 0; i < rows; i++){MYSQL_ROW row_data = mysql_fetch_row(result);Json::Value value;value["id"] = atoi(row_data[0]);value["name"] = row_data[1];value["info"] = row_data[2];value["video"] = row_data[3];value["image"] = row_data[4];values->append(value);}mysql_free_result(result);return true;}};}

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

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

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

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

1. REST 设计风格

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

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

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

在REST风格中定义了:

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

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
......[{"id": 1,"name": "电影1","info": "yyyyyyyy","video": "/video/movie1.mp4","image": "/img/thumbs/movie1.png",}
]响应:
HTTP/1.1 200 OK
......

上传视频信息:

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

请求:
PSOT /video HTTP/1.1
Content-Type: multipart/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: "/"

七、业务处理模块的实现

1. 业务处理模块类的设计

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

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

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

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

代码框架如下:

#define WWWROOT "./www"
#define VIDEO_ROOT "/video/"
#define IMAGE_ROOT "/image/"data::TableVideo *tableVideo = nullptr;class Server{private:int _port;httplib::Server _server;public:Server(int port) : _port(port) {}~Server() {}// 业务处理接口static void Insert(const httplib::Request &req, httplib::Response &resp){if (!req.has_file("name") || !req.has_file("info") || !req.has_file("video") || !req.has_file("image")){resp.status = 400; // 客户端错误 请求资源有误resp.body = R"({"result":false, "reson":"上传的数据信息有误"})";resp.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");string videoName = name.content;string imageName = name.content;string videoPath = string(WWWROOT) + string(VIDEO_ROOT) + videoName + video.filename;string imagePath = string(WWWROOT) + string(IMAGE_ROOT) + imageName + image.filename;if (!Util::FileUtil(videoPath).Writer(video.content)){resp.status = 500; // 服务器错误,服务器在处理请求的过程中发生了错误resp.body = R"({"result":false, "reason":"视频文件存储失败"})";resp.set_header("Content-Type", "application/json");return;}if (!Util::FileUtil(imagePath).Writer(image.content)){resp.status = 500; // 服务器错误,服务器在处理请求的过程中发生了错误resp.body = R"({"result":false, "reason":"图像文件存储失败"})";resp.set_header("Content-Type", "application/json");return;}Json::Value value;value["name"] = name.content;value["info"] = info.content;value["video"] = string(VIDEO_ROOT) + videoName + video.filename;value["image"] = string(IMAGE_ROOT) + imageName + image.filename;if (tableVideo->Insert(value) == false){resp.status = 500; // 服务器错误,服务器在处理请求的过程中发生了错误resp.body = R"({"result":false, "reason":"数据库新增数据失败"})";resp.set_header("Content-Type", "application/json");return;}}static void Delete(const httplib::Request &req, httplib::Response &resp){string nums = req.matches[1];int id = atoi(nums.c_str());Json::Value value;if (!tableVideo->SelectOne(&value, id)){resp.status = 500; // 服务器错误,服务器在处理请求的过程中发生了错误resp.body = R"({"result":false, "reason":"数据库不存在视频数据"})";resp.set_header("Content-Type", "application/json");return;}string video_path = string(WWWROOT) + value["video"].asString();string image_path = string(WWWROOT) + value["image"].asString();remove(video_path.c_str());remove(image_path.c_str());if (!tableVideo->Delete(id)){resp.status = 500; // 服务器错误,服务器在处理请求的过程中发生了错误resp.body = R"({"result":false, "reason":"数据库删除视频数据失败"})";resp.set_header("Content-Type", "application/json");return;}}static void Update(const httplib::Request &req, httplib::Response &resp){string nums = req.matches[1];int id = atoi(nums.c_str());Json::Value value;if (!Util::JsonUtil::Deserialize(req.body, &value)){resp.status = 500; // 服务器错误,服务器在处理请求的过程中发生了错误resp.body = R"({"result":false, "reason":"反序列化请求数据失败"})";resp.set_header("Content-Type", "application/json");return;}if (!tableVideo->Update(value, id)){resp.status = 500; // 服务器错误,服务器在处理请求的过程中发生了错误resp.body = R"({"result":false, "reason":"数据库修改视频数据失败"})";resp.set_header("Content-Type", "application/json");return;}}static void SelectAllAndLike(const httplib::Request &req, httplib::Response &resp){bool flag = true; // 默认查询所有string select_key;if (req.has_param("search")){select_key = req.get_param_value("search");flag = false; // 查询相似}Json::Value values;if (flag){if (!tableVideo->SelectAll(&values)){resp.status = 500; // 服务器错误,服务器在处理请求的过程中发生了错误resp.body = R"({"result":false, "reason":"数据库查询所有视频数据失败"})";resp.set_header("Content-Type", "application/json");return;}}else{if (!tableVideo->SelectLike(&values, select_key)){resp.status = 500; // 服务器错误,服务器在处理请求的过程中发生了错误resp.body = R"({"result":false, "reason":"数据库查询相似视频数据失败"})";resp.set_header("Content-Type", "application/json");return;}}if (!Util::JsonUtil::Serialize(values, &resp.body)){resp.status = 500; // 服务器错误,服务器在处理请求的过程中发生了错误resp.body = R"({"result":false, "reason":"序列化请求数据失败"})";resp.set_header("Content-Type", "application/json");return;}resp.set_header("Content-Type", "application/json");}static void SelectOne(const httplib::Request &req, httplib::Response &resp){string nums = req.matches[1];int id = atoi(nums.c_str());Json::Value value;if (!tableVideo->SelectOne(&value, id)){resp.status = 500; // 服务器错误,服务器在处理请求的过程中发生了错误resp.body = R"({"result":false, "reason":"数据库查询指定视频数据失败"})";resp.set_header("Content-Type", "application/json");return;}if (!Util::JsonUtil::Serialize(value, &resp.body)){resp.status = 500; // 服务器错误,服务器在处理请求的过程中发生了错误resp.body = R"({"result":false, "reason":"序列化请求数据失败"})";resp.set_header("Content-Type", "application/json");return;}resp.set_header("Content-Type", "application/json");}// 初始化数据管理模块  设置静态资源根目录 建立请求与处理函数(回调函数)之间的关系 启动服务器bool RunModule(){tableVideo = new data::TableVideo();string RootPath = WWWROOT;if (Util::FileUtil(RootPath).CreatDict() == false){cout << "create RootPath failed" << endl;return false;}string videoRealPath = RootPath + string(VIDEO_ROOT);string imageRealPath = RootPath + string(IMAGE_ROOT);if (Util::FileUtil(videoRealPath).CreatDict() == false){cout << "create videoRealPath failed" << endl;return false;}if (Util::FileUtil(imageRealPath).CreatDict() == false){cout << "create imageRealPath failed" << endl;return false;}_server.set_mount_point("/", RootPath);_server.Get("/video", SelectAllAndLike);_server.Get("/video/(\\d+)", SelectOne);_server.Post("/video", Insert);_server.Delete("/video/(\\d+)", Delete);_server.Put("/video/(\\d+)", Update);_server.listen("0.0.0.0", _port);return true;}};

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

2. 综合调试

调试代码:

#include "network.hpp"void fileUtilTest()
{Util::FileUtil("./www").CreatDict();Util::FileUtil("./www/index.html").Writer("<html>");string body;Util::FileUtil("./www/index.html").Reader(&body);cout << body << endl;cout << Util::FileUtil("./www/index.html").Size() << endl;
}void jsonUtilTest()
{Json::Value root;root["姓名"] = "张三";root["年龄"] = 18;root["性别"] = "男";root["成绩"].append(88.5);root["成绩"].append(100);root["成绩"].append(90.5);string body;Util::JsonUtil::Serialize(root, &body);cout << body << endl;Util::JsonUtil::Deserialize(body, &root);cout << root["姓名"].asString() << endl;cout << root["性别"].asString() << endl;cout << root["年龄"].asInt() << endl;for (auto &e : root["成绩"]){cout << e.asFloat() << endl;}
}void dataTest()
{Json::Value root;// root["name"] = "星际迷航";// root["info"] = "一个宇宙探索的美国科幻电影";// root["video"] = "/video/star.mp4";// root["image"] = "/image/star.png";// root["name"] = "沙丘";// root["info"] = "一个意义深奥的英国科幻电影";// root["video"] = "/video/mountain.mp4";// root["image"] = "/image/mountain.png";// data::TableVideo().Insert(root);// data::TableVideo().Update(root,2);// data::TableVideo().SelectAll(&root);// data::TableVideo().SelectOne(&root,1);// data::TableVideo().SelectLike(&root,"迷航");// string body;// Util::JsonUtil::Serialize(root, &body);// cout << body << endl;data::TableVideo().Delete(2);
}void serverTest()
{network::Server server(8080);server.RunModule();
}int main()
{// fileUtilTest();// jsonUtilTest();// dataTest();serverTest();return 0;
}

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

八、前端界面的实现

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"><iclass="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"><iclass="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>

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>

九、项目总结

项目名称:

视频共享播放系统

项目功能:

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

开发环境及工具:

Ubuntu 16.04.10 、vim、g++、gdb、makefile、vscode等。

技术特点:

HTTP 服务器搭建、RESTful风格接口设计、Json 序列化与反序列化、线程池、HTML+CSS+JS 基础。

项目模块:

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

项目扩展方向:

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

项目源码(绝对可以直接运行)icon-default.png?t=N7T8https://gitee.com/swbb1023/video-on-demand-system

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/276435.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vue 引用百度地图

address.vue <template><div><!-- 地图 --><el-drawer:visible.sync"type1"direction"rtl"size"50%"append-to-bodyclass"map-drawer":before-close"beforeClose"><div style"width: 100%…

永热爱 敢向前 | Paraverse平行云的2023 年终总结

永热爱&#xff0c;敢向前 值此新年&#xff0c;回顾2023&#xff0c;仅以此句&#xff0c;献给所有XR产业信仰者 2023 年&#xff0c;是XR产业技术和场景承上启下的关键之年 在这场波澜壮阔的技术潮中 「Paraverse平行云」踏浪前行 已是第八个年头&#xff0c;让我们一起…

基于YOLOv8/YOLOv7/YOLOv6/YOLOv5的远距离停车位检测系统(深度学习代码+UI界面+训练数据集)

摘要&#xff1a;开发远距离停车位检测系统对于提高停车效率具有关键作用。本篇博客详细介绍了如何运用深度学习构建一个远距离停车位检测系统&#xff0c;并提供了完整的实现代码。该系统基于强大的YOLOv8算法&#xff0c;并对比了YOLOv7、YOLOv6、YOLOv5&#xff0c;展示了不…

2024 MCM数学建模美赛2024年A题复盘,思路与经验分享:资源可用性与性别比例 | 七鳃鳗的性别比例变化对生态系统稳定性的影响(四)

审题 第三问要我们评估七鳃鳗的性别比例变化对生态系统稳定性的影响。 这里我们就要去查一下生态系统稳定性的定义。 通过查资料我们知道&#xff0c;生态系统稳定性包括生态系统的抵抗力和恢复力。 OK&#xff0c;到这里问题就变成了&#xff0c;七鳃鳗的性别比例对生态系…

LeetCode2115. 从给定原材料中找到所有可以做出的菜

拓扑排序 题面 题目链接&#xff1a;2115. 从给定原材料中找到所有可以做出的菜 - 力扣&#xff08;LeetCode&#xff09; 你有 n 道不同菜的信息。给你一个字符串数组 recipes 和一个二维字符串数组 ingredients 。第 i 道菜的名字为 recipes[i] &#xff0c;如果你有它 所有…

游戏免费下载平台模板源码

功能介绍 此游戏网站模板源码是专门为游戏下载站而设计的&#xff0c;旨在为网站开发者提供一个高效、易于维护和扩展的解决方案。 特点&#xff1a; 响应式设计&#xff1a;我们的模板可以自适应不同设备屏幕大小&#xff0c;从而为不同平台的用户提供最佳的浏览体验。 …

Qt中进行客户端开发框架

在Qt中进行客户端开发是一种常见的做法&#xff0c;Qt是一个跨平台的C框架&#xff0c;提供了丰富的工具和类库&#xff0c;用于开发图形用户界面&#xff08;GUI&#xff09;应用程序、网络应用程序以及其他类型的软件。以下是一些常用的Qt客户端开发框架和技术&#xff0c;希…

php.exe运行时,提示缺少VCRUNTIME140.dll

php.exe运行时&#xff0c;提示缺少VCRUNTIME140.dll 下载地址 https://www.microsoft.com/zh-cn/download/details.aspx?id48145根据需要选择下载3.运行安装后&#xff0c;再次运行php.exe。

已解决:android SDK安装时点击SDK Manager出现闪退

1、首先确保电脑里边安装了JDK&#xff0c;并且要把安装路径配置在环境变量里边&#xff0c;避免使用绝对路径 推荐%JAVA_HOME%\bin 2、在C:\Users\huanhuan\Desktop\android-sdk-windows\tools路径下找到android.bat文件打开&#xff0c;把set java_exe后改为jdk中java.exe的路…

TCP的三次握手和4次挥手

一、首先讲一下TCP的由来 最开始&#xff0c;人们考虑到将网络信息的呼唤与回应进行规范&#xff0c;达成一种公认的协议&#xff0c;就好像没有交通规则的路口设定交通规则。 人们设计出完美的OSI协议&#xff0c;这个协议包含七个层次由下到上分别是&#xff1a; 物理层&…

7-Eleven用工数字化:零售哲学下的人效管理实践

2014年&#xff0c;一本《零售的哲学》在中国掀起热潮&#xff0c;揭示了7-Eleven便利店的新零售坪效管理秘诀。而对大部分零售企业来说&#xff0c;劳动力效率是坪效背后的主要支柱。近期&#xff0c;国内领先的劳动力管理云服务提供商盖雅工场发布了《聚焦人效、重塑组织&…

计算机毕业设计-神经网络算法及对未来一月的天气状况预测系统

概要 随着对气象各项数据的观测手段、技术上的提升&#xff0c;对于各项或取得数据种类&#xff0c;精度上都有着更好的超越&#xff0c;而对于气象温度进行预测是目前预测数据中最重要的需要解决的问题之一。 针对如何选择预测一个月内的天气情况&#xff0c;本次利用神经网络…

最新骨传导耳机热门评测!南卡、韶音和墨觉哪个最好?

最近有不少朋友向我寻求建议&#xff0c;关于如何挑选优质的骨传导耳机。作为一名经验丰富的骨传导耳机爱好者&#xff0c;我自然愿意为大家分享一期详尽的评测指南。在选择骨传导耳机时&#xff0c;音质表现和佩戴的舒适度是最为关键的评价标准。 如今市场上存在很多劣质产品&…

基于有限状态机开发健壮的Nodejs/TCP客户端

有限状态机是一种数学计算模型&#xff0c;它描述了在任何给定时间只能处于一种状态的系统的行为。形式上&#xff0c;有限状态机有五个部分&#xff1a; 初始状态值 (initial state)有限的一组状态 (states)有限的一组事件 (events)由事件驱动的一组状态转移关系 (transition…

分析型数据库的主要使用场景有哪些?

如今数据已经成为了企业和组织的核心资产。如何有效地管理和利用这些数据&#xff0c;成为了决定竞争力的关键。分析型数据库作为数据处理领域的重要工具&#xff0c;为各行各业提供了强大的数据分析和洞察能力。基于分析型数据库&#xff08;Apache Doris &#xff09;构建的现…

Linux中GPU相关命令

Linux查看显卡信息&#xff1a; lspci | grep -i vga 使用nvidia GPU可以&#xff1a; lspci | grep -i nvidia1 前边的序号 "00:0f.0"是显卡的代号(这里是用的虚拟机); 查看指定显卡的详细信息用以下指令&#xff1a; lspci -v -s 00:0f.01 Linux查看Nvidia显…

MongoDB实战面试指南:常见问题一网打尽

码到三十五 &#xff1a; 个人主页 心中有诗画&#xff0c;指尖舞代码&#xff0c;目光览世界&#xff0c;步履越千山&#xff0c;人间尽值得 ! MongoDB是一款流行的非关系型数据库&#xff0c;以其高效、可扩展的特性受到开发者的青睐。了解MongoDB的架构、存储引擎和数据结…

Jmeter进行http接口测试

&#x1f345; 视频学习&#xff1a;文末有免费的配套视频可观看 &#x1f345; 关注公众号【互联网杂货铺】&#xff0c;回复 1 &#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 本文主要针对http接口进行测试&#xff0c;使用 jmeter工具实现…

vue3+ts props定义识别为unknown

"vue": "^3.3.4", "typescript": "5.0.4", 确保agriculturalPollution引入成功确保PropType引入成功details获得类型推断defineProps传参正确props的detail为unknown 这就很奇怪&#xff0c;一步步都是按照规范写的&#xff0c;但是…

目前研一,是选 FPGA 还是 Linux 嵌入式?

目前研一&#xff0c;是选 FPGA 还是 Linux 嵌入式? 在开始前我有一些资料&#xff0c;是我根据网友给的问题精心整理了一份「Linux 的资料从专业入门到高级教程」&#xff0c; 点个关注在评论区回复“888”之后私信回复“888”&#xff0c;全部无偿共享给大家&#xff01;&a…