视屏点播项目

项目背景

大家应该在电脑上刷过视频吧,这个项目就是模拟一下我们刷视频的整个流程,我们要做的是一个类似B站的网页,这里面包含视频的上传修改和观看以及删除,注意我这个是一个简易版本的,在后面我会做一个升级,增加其他的功能.

基本原理

下面我们说一下我们项目的基本原理.我们这里做的是服务器客户端类型的项目.当客户端发起请求之后,我们服务端分析请求,看他是做什么的,例如请求资源,还是推送资源,我们对每一个请求都做一个处理让后把响应发送给客户端.

image-20230915163821147

服务端程序负责功能

  • 针对客户端上传的视频文件以及封面进行存储
  • 针对客户端上传的视频完成增删查改内容
  • 支持浏览器进行视频观看内容

服务端功能模块划分

  • 数据管理模块: 负责针对客户端上传的视频信息进行管理
  • 网络通信模块: 搭建网络服务器,与客户端进行通信
  • 业务处理模块: 针对客户端的请求处理各项业务并进行相应
  • 前端界面模块: 完成前端浏览器上的各个html页面,并支持增删改查及其观看功能

技术栈与环境

这里说下我们的技术栈与环境.

技术栈

  • 后端: C/C++, C++11,STL, Jsoncpp, cpp-httplib, MySQL
  • 前端: html5,css,js、jQuery, Ajax

环境

  • Centos7虚拟机,vim,gcc(g++),Makefile,Vscode

环境准备

环境准备包含下面几个方面的内容,我的服务器是centos 7系列的.

  • 编译器的升级
  • 三方库的下载

Gcc 升级7.3版本

[qkj@Qkj ~]$ sudo yum install centos-release-scl-rh centos-release-scl
[qkj@Qkj ~]$ sudo yum install devtoolset-7-gcc devtoolset-7-gcc-c++
[qkj@Qkj ~]$ source /opt/rh/devtoolset-7/enable
[qkj@Qkj ~]$ echo "source /opt/rh/devtoolset-7/enable" >> ~/.bashrc[qkj@Qkj ~]$ gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/opt/rh/devtoolset-8/root/usr/libexec/gcc/x86_64-redhat-linux/8/lto-wrapper
Target: x86_64-redhat-linux
Configured with: ../configure --enable-bootstrap --enable-languages=c,c++,fortran,lto --prefix=/opt/rh/devtoolset-8/root/usr --mandir=/opt/rh/devtoolset-8/root/usr/share/man --infodir=/opt/rh/devtoolset-8/root/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-shared --enable-threads=posix --enable-checking=release --enable-multilib --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-linker-build-id --with-gcc-major-version-only --with-linker-hash-style=gnu --with-default-libstdcxx-abi=gcc4-compatible --enable-plugin --enable-initfini-array --with-isl=/builddir/build/BUILD/gcc-8.3.1-20190311/obj-x86_64-redhat-linux/isl-install --disable-libmpx --enable-gnu-indirect-function --with-tune=generic --with-arch_32=x86-64 --build=x86_64-redhat-linux
Thread model: posix
gcc version 8.3.1 20190311 (Red Hat 8.3.1-3) (GCC) 
[qkj@Qkj ~]$ 

安装Jsoncpp库

[qkj@Qkj ~]$ sudo yum install epel-release
[qkj@Qkj ~]$ sudo yum install jsoncpp-devel
[qkj@Qkj ~]$ ll /usr/include/jsoncpp/json/
total 80
-rw-r--r-- 1 root root  2203 Jul 23  2015 assertions.h
-rw-r--r-- 1 root root   662 Jul 23  2015 autolink.h
-rw-r--r-- 1 root root  3860 Jul 23  2015 config.h
-rw-r--r-- 1 root root  1509 Jul 23  2015 features.h
-rw-r--r-- 1 root root   758 Jul 23  2015 forwards.h
-rw-r--r-- 1 root root   420 Jul 23  2015 json.h
-rw-r--r-- 1 root root 11482 Jul 23  2015 reader.h
-rw-r--r-- 1 root root 26101 Jul 23  2015 value.h
-rw-r--r-- 1 root root   509 Jul 23  2015 version.h
-rw-r--r-- 1 root root 10298 Jul 23  2015 writer.h
[qkj@Qkj ~]$ 

下载httplib库

[qkj@Qkj ~]$ git clone https://github.com/yhirose/cpp-httplib.git

MySQL数据库

安装与卸载中,用户全部切换成为root,一旦 安装,普通用户能使用的 初期练习,mysql不进行用户管理,全部使用root进行,尽快适应mysql语句,后面学了用户管理,在考虑新建普通用户

卸载旧环境

首先检测我们系统是否存在mariadb在运行存在,这个也是一个数据库,是MySQL的分支.如果存在这个软件,那么按照下面的步骤停止服务.

[root@Qkj ~]# ps ajx |grep mariadb # 先检查是否有mariadb存在
13134 14844 14843 13134 pts/0 14843 S+ 1005 0:00 grep --color=auto mariadb
19010 19187 19010 19010 ? -1 Sl 27 16:55 /usr/libexec/mysqld --basedir=/usr --datadir=/var/lib/mysql --plugin-dir=/usr/lib64/mysql/plugin --log-error=/var/log/mariadb/mariadb.log --pid-file=/var/run/mariadb/mariadb.pid--socket=/var/lib/mysql/mysql.sock
[root@Qkj ~]# systemctl stop mariadb.service # 停止mariadb 服务
==== AUTHENTICATING FOR org.freedesktop.systemd1.manage-units ===
Authentication is required to manage system services or units.
Authenticating as: root
Password:
==== AUTHENTICATION COMPLETE ===
[root@Qkj ~]# ps axj |grep mariadb # 停止完成
13134 14976 14975 13134 pts/0 14975 S+ 1005 0:00 grep --color=auto mariadb

下面我们继续检测我们mariadb是否存在.

[root@Qkj ~]# rpm -qa | grep mariadb  # 或者使用这个指令rpm -qa | grep mysql
....

如果出现了这样的结果.

[root@Qkj ~]# rpm -qa | grep mysql
mysql-community-common-5.7.41-1.el7.x86_64
mysql-community-server-5.7.41-1.el7.x86_64
mysql57-community-release-el7-11.noarch
mysql-community-client-5.7.41-1.el7.x86_64
mysql-community-libs-5.7.41-1.el7.x86_64

那么此时我们需要卸载这些安装包.下面我们说两个方式.

可以一个一个删除.

#卸载显示出来的mariadb/mysql安装包
[whb@VM-0-3-centos ~]$ sudo yum remove mysql-community-common-5.7.41-1.el7.x86_64

也可以通过一个指令,一次性删除所有的安装包.

获取MySQL数据库yum源

下面我们开始获得官方的yum源.这里我们有官网.

获取mysql官方yum源 http://repo.mysql.com/

我们进入之后,然后我们发现他的页面实在是太简单了.

image-20230912144532509

这里我们右键,选择查看网页源代码,此时我们这里详细一些.

image-20230912144714872

我们这里会发现,此时这里有太多的版本了,此时我们需要关注我们红色的框里面的内容.

image-20230912144926710

让后我们查看一下我们系统的版本.

[root@Qkj ~]# cat /etc/redhat-release
CentOS Linux release 7.9.2009 (Core)
[root@Qkj ~]# 

我们发现自己的版本是CentOS 7版本,这里我们选择这一类的.

image-20230912145553529

这里需要注意的,我们是7.9版本的,我们最好选择7.9以上的,例如7.10.如果没有7.10,可以去下载这个el7.rpm文件.我们把他下载到Windows,让拖到我们服务器上就可以了.

[root@Qkj mysql]# rz -E[root@Qkj mysql]# ll
total 28
-rw-r--r-- 1 root root 25548 Sep 12 14:58 mysql57-community-release-el7-10.noarch.rpm
[root@Qkj mysql]# 

安装

下面我们可以查看本地的yum源.我的应该和大家的不同.

[root@Qkj mysql]# ls /etc/yum.repos.d/ -al
total 32
drwxr-xr-x.  2 root root 4096 Sep  1 00:43 .
drwxr-xr-x. 73 root root 4096 Sep 12 14:42 ..
-rw-r--r--   1 root root 2523 Sep  1 00:34 CentOS-Base.repo
-rw-r--r--   1 root root  675 Sep  1 00:33 CentOS-Base.repo.backup
-rw-r--r--   1 root root  971 Oct 29  2018 CentOS-SCLo-scl-rh.repo
-rw-r--r--   1 root root  230 Sep  1 00:10 epel.repo
-rw-r--r--   1 root root 1358 Sep  5  2021 epel.repo.rpmnew
-rw-r--r--   1 root root 1457 Sep  5  2021 epel-testing.repo
[root@Qkj mysql]# 

下面我们开始安装.

[root@Qkj mysql]# rpm -Uvh mysql57-community-release-el7-10.noarch.rpm 
warning: mysql57-community-release-el7-10.noarch.rpm: Header V3 DSA/SHA1 Signature, key ID 5072e1f5: NOKEY
Preparing...                          ################################# [100%]
Updating / installing...1:mysql57-community-release-el7-10 ################################# [100%]
[root@Qkj mysql]# 

安装结束后,我们继续查看我们的yum源.

[root@Qkj mysql]# ls /etc/yum.repos.d/ -al
total 40
drwxr-xr-x.  2 root root 4096 Sep 12 15:02 .
drwxr-xr-x. 73 root root 4096 Sep 12 14:42 ..
-rw-r--r--   1 root root 2523 Sep  1 00:34 CentOS-Base.repo
-rw-r--r--   1 root root  675 Sep  1 00:33 CentOS-Base.repo.backup
-rw-r--r--   1 root root  971 Oct 29  2018 CentOS-SCLo-scl-rh.repo
-rw-r--r--   1 root root  230 Sep  1 00:10 epel.repo
-rw-r--r--   1 root root 1358 Sep  5  2021 epel.repo.rpmnew
-rw-r--r--   1 root root 1457 Sep  5  2021 epel-testing.repo
-rw-r--r--   1 root root 1627 Apr  5  2017 mysql-community.repo
-rw-r--r--   1 root root 1663 Apr  5  2017 mysql-community-source.repo
[root@Qkj mysql]# 

这里可以检测一下我们的yum源是不是可以使用.

[root@Qkj mysql]# yum list |grep mysql
mysql57-community-release.noarch           el7-10                 installed     
akonadi-mysql.x86_64                       1.9.2-4.el7            base          
anope-mysql.x86_64                         2.0.14-1.el7           epel          
apr-util-mysql.x86_64                      1.5.2-6.el7_9.1        updates       
calligra-kexi-driver-mysql.x86_64          2.9.10-2.el7           epel          
collectd-mysql.x86_64                      5.8.1-1.el7            epel          
dmlite-plugins-mysql.x86_64                1.15.2-15.el7          epel          
dovecot-mysql.x86_64                       1:2.2.36-8.el7         base          
dpm-copy-server-mysql.x86_64               1.13.0-1.el7           epel          
dpm-name-server-mysql.x86_64               1.13.0-1.el7           epel
....

下面我们就可以安装我们的MySQL了.

安装服务端

下面我们开始安装MySQL的服务端,直接执行下面的指令.

[root@Qkj mysql]# sudo yum install -y mysql-community-server

但是如果我们出现了安装错误,我们例如下面的情况.

#安装遇到秘钥过期的问题:
#Failing package is: mysql-community-client-5.7.39-1.el7.x86_64
#GPG Keys are configured as: file:///etc/pki/rpm-gpg/RPM-GPG-KEY-mysql
#解决方案:
[root@Qkj mysql]# rpm --import https://repo.mysql.com/RPM-GPG-KEY-mysql-2022

安装客户端

注意,如果我们执行了上面的指令,我们的客户端已经安装了.

image-20230912150940060

查看配置文件

安装完成之后,我们去查看一下我们的配置文件.需要保证这两个文件是存在的.

[root@Qkj mysql]# ls /etc/my.cnf       # 这是配置文件
/etc/my.cnf
[root@Qkj mysql]# ls /var/lib/mysql    # 这是我们数据存储的文件
auto.cnf    client-cert.pem  ibdata1      ibtmp1      mysql.sock.lock     public_key.pem   sys
ca-key.pem  client-key.pem   ib_logfile0  mysql       performance_schema  server-cert.pem  t1_db
ca.pem      ib_buffer_pool   ib_logfile1  mysql.sock  private_key.pem     server-key.pem
[root@Qkj mysql]#

启动服务

在我们安装好了MySQL之后,这里我们就可以启动服务了.

[root@Qkj mysql]# systemctl start mysqld.service
==== AUTHENTICATING FOR org.freedesktop.systemd1.manage-units ===
Authentication is required to manage system services or units.
Authenticating as: root
Password:
==== AUTHENTICATION COMPLETE ===
[root@Qkj mysql]# ps axj |grep mysqld1   1579   1578   1578 ?            -1 Sl      27   0:01 /usr/sbin/mysqld --daemonize --pid-file=/var/run/mysqld/mysqld.pid1513   1608   1607   1481 pts/0      1607 S+       0   0:00 grep --color=auto mysqld
[root@Qkj mysql]#

登陆MySQL

注意,MySQL支持密码登陆,不过这里我们先暂时不说.我们这里使用两个方式登陆,总有一个方法可行的.

直接使用MySQL客户端登陆,如果不行,那么就使用下一个方法.

如果你安装的最新的mysql,没有所谓的临时密码,root默认没有密码
# 试着直接client登陆一下

我们这里修改配置文件,然后重启服务,注意,一定要重启,在最后一行

[root@Qkj mysql]# vim /etc/my.cnf

image-20230912154125796

[qukangjie@localhost ~]$ mysql -uroot
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 2
Server version: 5.7.41 MySQL Community Server (GPL)Copyright (c) 2000, 2023, Oracle and/or its affiliates.Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.mysql> 

设置开机自启

我们这里可以设置MySQL服务开机自启动.

[root@Qkj mysql]# systemctl enable mysqld
[root@Qkj mysql]# systemctl daemon-reload

配置文件

我们配置文件的修改主要分为两个方面.

修改编码格式

这里我们要修改编码格式.

[root@Qkj mysql]# vim /etc/my.cnf

image-20230912154506505

修改存储引擎

[root@Qkj mysql]# vim /etc/my.cnf

数据库管理系统如何存储数据,如何为存储的数据建立索引和如何更新,查询数据等技术的实现方法,就是我们的存储引擎,这里我们使用innodb.

image-20230912154538967

常见问题

如果我们已经配置好了编码,但是我们还是不支持中文,我们确保你在终端命令行中可以输入中文

[root@Qkj mysql]# env |grep LANG
LANG=en_US.UTF-8
[root@Qkj mysql]# 

第三方库认识

在正式开始之前,我们需要一些前置的知识,这里我们要学习下面几个东西

  • jsoncpp库认识和学习
  • http-lib库的认识

jsoncpp库认识和学习

json 是一种数据交换格式,采用完全独立于编程语言的文本格式来存储和表示数据。例如:小明同学的学生信息.

char name = "小明";
int age = 18;
float score[3] = {88.5, 99, 58};
则json这种数据交换格式是将这多种数据对象组织成为一个字符串:
[
{
"姓名" : "小明",
"年龄" : 18,
"成绩" : [88.5, 99, 58]
},
{
"姓名" : "小黑",
"年龄" : 18,
"成绩" : [88.5, 99, 58]
}
]

我们这里分析一下,我们可以把我们的信息看作一个结构体,其中[]就是一个数组.{}是一个结构体,里面是我们内容,以键值对的形式经行存储.

json 数据类型:对象,数组,字符串,数字
对象:使用花括号{} 括起来的表示一个对象。
数组:使用中括号[] 括起来的表示一个数组。
字符串:使用常规双引号"" 括起来的表示一个字符串
数字:包括整形和浮点型,直接使用。

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数据对象类,这里可以认为他是一个KV类型的结构,其中V可以可以理解为一个数组.

序列化

这个是我们序列化的类,里面最重要的就是write函数,我们发现FastWriter和StyledWriter都继承了抽象类Writer并且都重写了write纯虚函数.

//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 Writer 的命名格式,为何这里是JSON_API Writer,实际上我们看一下源码就可以知道了,这里JSON_API只是一个简单的宏.

image-20230807153723323

下面我们使用一下,我们的逻辑应该是实例化一个Value对象,然后把这个对象给序列化.

#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
using namespace std;int main()
{Json::Value root;string name1 = "张三";int age1 = 18;double socre1 = 99.5;Json::Value v1;v1["name"] = name1;v1["age"] = age1;v1["score"] = socre1;string name2 = "李四";int age2 = 18;double socre2 = 99.5;Json::Value v2;v2["name"] = name2;v2["age"] = age2;v2["score"] = socre2;root.append(v1);root.append(v2);Json::FastWriter writer;cout << writer.write(root) << endl;return 0;
}

image-20230915165303613

StyledWriter这个类打印的结果比较美观一些.

int main()
{Json::Value root;string name1 = "张三";int age1 = 18;double socre1 = 99.5;Json::Value v1;v1["name"] = name1;v1["age"] = age1;v1["score"] = socre1;string name2 = "李四";int age2 = 18;double socre2 = 99.5;Json::Value v2;v2["name"] = name2;v2["age"] = age2;v2["score"] = socre2;root.append(v1);root.append(v2);// Json::FastWriter writer;Json::StyledWriter writer;cout << writer.write(root) << endl;return 0;
}

image-20230915165435353

上面使用比较简单,不过它是低版本的,如果用低版本的接口可能会有警告,那么此时我们需要使用高版本的.

//json序列化类,高版本推荐,如果用低版本的接口可能会有警告
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;
}
int main()
{// 序列化 -- 高版本Json::StreamWriterBuilder swb; // 他的作用就是new出来一个对象,可以实现多态std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());return 0;
}

下面看一下我们的序列化是具体如何使用的.

int main()
{const char *name = "小明";int age = 19;float score[] = {77.5, 88, 99.5};// 构造对象Json::Value val;val["姓名"] = name;val["年龄"] = 19;val["成绩"].append(score[0]);val["成绩"].append(score[1]);val["成绩"].append(score[2]);// 序列化 -- 高版本Json::StreamWriterBuilder swb;std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());std::stringstream ss;int ret = sw->write(val, &ss);if (ret != 0){std::cout << "write failed!\n";return -1;}std::cout << ss.str() << std::endl;return 0;
}

image-20230915170033165

反序列化

这里是低版本的.

//json反序列化类,低版本用起来更简单
class JSON_API Reader {
bool parse(const std::string& document, Value& root, bool collectComments = true);
}
#include <iostream>
#include <sstream>
#include <string>
#include <memory>
#include <jsoncpp/json/json.h>
int main()
{const char *name = "小明";int age = 19;float score[] = {77.5, 88, 99.5};// 构造对象Json::Value val;val["姓名"] = name;val["年龄"] = 19;val["成绩"].append(score[0]);val["成绩"].append(score[1]);val["成绩"].append(score[2]);// 序列化Json::Writer *wr = new Json::StyledWriter;std::string str = wr->write(val);// 反序列化Json::Value val2;Json::Reader read;read.parse(str, val2);// 打印结果std::cout << val2["姓名"] << std::endl;std::cout << val2["年龄"].asInt() << std::endl;int sz = val2["成绩"].size();for (int i = 0; i < sz; i++){std::cout << val2["成绩"][i].asFloat() << " ";}std::cout << std::endl;return 0;
}

image-20230915170201949

同样的,这个是低版本的,我们也是需要学习一下高版本的.

int main()
{std::string str = R"({"姓名":"小明", "年龄":18, "成绩":[76.5, 55, 88]})"; // 这个是允许的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->asFloat() << std::endl;}return 0;
}

image-20230915170306303

MySQL API认识

我们这里使用MySQL的API,直接通过代码链接我们的MySQL.下面我们用一下.

#include <iostream>
#include <mysql/mysql.h>
using namespace std;int main()
{cout << "版本: " << mysql_get_client_info() << endl;return 0;

这里直接演示.

[qkj@localhost example]$ g++ mysql_test.cc -L/lib64/mysql -lmysqlclient
[qkj@localhost example]$ ./a.out 
版本: 5.7.43
[qkj@localhost example]$ 

访问数据库

访问数据库的第一步,我们首先要创建一个句柄.

int main()
{// cout << "版本: " << mysql_get_client_info() << endl;MYSQL *msql = mysql_init(nullptr);if (nullptr == msql){return 0;}mysql_close(msql);return 0;
}

当我们创建好句柄之后,这里我们就可以链接我们的MySQL了,这是一个函数.

MYSQL *		STDCALL 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 clientflag);

下面说一下这些参数.

  • mysql: 就是我们的句柄
  • host: 我们要连接服务器的IP地址
  • user: 用户名
  • passwd: 密码
  • db: 数据库名
  • unix_socket: 不关心,直接为null
  • clientflag: 不关心,直接为0

下面我们链接一下,注意要设置字符集.

int main()
{MYSQL *msql = mysql_init(nullptr);if (nullptr == msql){cerr << "创建句柄失败" << endl;return 0;}// 1 . 登录认证if (mysql_real_connect(msql, host.c_str(), user.c_str(), password.c_str(), db.c_str(), port, nullptr, 0) == nullptr){cerr << "链接数据库失败" << endl;}cerr << "链接数据库成功" << endl;// 2. 设置字符集mysql_set_character_set(msql, "utf8");mysql_close(msql);return 0;
}

image-20230915191828752

当我们链接上数据库的时候,此时我们就可以使用sql语句了,这里非常简答.

int main()
{MYSQL *msql = mysql_init(nullptr);if (nullptr == msql){cerr << "创建句柄失败" << endl;return 0;}// 1 . 登录认证if (mysql_real_connect(msql, host.c_str(), user.c_str(), password.c_str(), db.c_str(), port, nullptr, 0) == nullptr){cerr << "链接数据库失败" << endl;}cerr << "链接数据库成功" << endl;// 2. 设置字符集mysql_set_character_set(msql, "utf8");string sql = "insert  emp values (31, '吕布', 9999.10)";// 3. sql操作int n = mysql_query(msql, sql.c_str());mysql_close(msql);return 0;
}

后面我们所有的SQL语句都可以这样做,这里唯一存在一个小的问题,就是我们插入,删除,修改都可以,但是这里测查找就有问题了.此时我们需要继续认识一下接口.对于select而言,我们所有的结果都被保存好了,此时我们手动拿出来就可以了.

#include <iostream>
#include <mysql/mysql.h>
using namespace std;
string host = "127.0.0.1";
string user = "root";
string password;
string db = "test_vi_db";
uint16_t port = 3306;int main()
{MYSQL *msql = mysql_init(nullptr);if (nullptr == msql){cerr << "创建句柄失败" << endl;return 0;}// 1 . 登录认证if (mysql_real_connect(msql, host.c_str(), user.c_str(), password.c_str(), db.c_str(), port, nullptr, 0) == nullptr){cerr << "链接数据库失败" << endl;}cerr << "链接数据库成功" << endl;// 2. 设置字符集mysql_set_character_set(msql, "utf8");// string sql = "insert  emp values (31, '吕布', 9999.10)";string sql = "select * from emp";// 3. sql操作int n = mysql_query(msql, sql.c_str());if (0 == n){// sql语句执行成功MYSQL_RES *res = mysql_store_result(msql); // 所有的结果int row = mysql_num_rows(res);int fields = mysql_num_fields(res);MYSQL_FIELD *field = mysql_fetch_fields(res); // 得到所有的字段名int i = 0;for (; i < fields; i++){cout << field[i].name << "\t|\t";}cout << endl;MYSQL_ROW line;for (int i = 0; i < row; i++){line = mysql_fetch_row(res); // 得到一行数据for (int j = 0; j < fields; j++){// 解析每一行cout << line[j] << "\t|\t";}cout << endl;}}mysql_close(msql);return 0;
}

image-20230915193229165

httplib库认识

httplib 库,一个C++11 单文件头的跨平台HTTP/HTTPS 库。安装起来非常容易。只需包含httplib.h 在你的代码中即可。httplib 库实际上是用于搭建一个简单的http 服务器或者客户端的库,这种第三方网络库,可以让我们免去搭建服务器或客户端的时间,把更多的精力投入到具体的业务处理中,提高开发效率。

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;            // 获取指定的文件信息};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);      // 设置全套的重定向信息};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的处理流程.这里重点认识一下Server类.

  • Handler: 函数指针
  • Handlers: 一个数组,保存的正则表达式,保存的是请求信息,后面的函数指针是对应的处理函数.
  • new_task_queue: 线程池,处理任务
  • set_mount_point: 设置根目录
  • listen: 启动服务器
  • Get: 这些接口就是给我们Handlers添加信息

下面我们使用他来简单的测试一下.我们直接使用它.

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

image-20230915195823813

#include <string>
#include <iostream>
#include "../cpp-httplib/httplib.h"
using namespace httplib;
void HelloBit(const Request &rep, Response &rsp)
{rsp.body = "hello bit";rsp.status = 200;
}void Numbers(const Request &rep, Response &rsp)
{// 这个是捕捉的数据 /numbers/123 -> matches[0] = "/numbers/123"  matches[1] = "123"std::string num = rep.matches[1];rsp.set_content(num, "text/plain"); // 设置正文rsp.status = 200;
}void Multipart(const Request &rep, Response &rsp)
{// 文件上传的的if (rep.has_file("file1") == false){rsp.status = 400;return;}MultipartFormData file = rep.get_file_value("file1");std::cout << file.filename << std::endl; // 区域文件名称std::cout << file.content << std::endl;  // 区域文件内容rsp.status = 200;
}int main()
{Server server;// 设置静态根目录 另外一个用法server.set_mount_point("/", "./www");// 条件请求server.Get("/hi", HelloBit);// 正则表达式 ,在正则表达式中// \d 表示数字,// + 表示一次或者多次// ()--表示捕捉数据sserver.Get("/numbers/(\\d+)", Numbers);server.Post("/multipart", Multipart);server.listen("0.0.0.0", 8081);return 0;
}

这是请求动态的.

image-20230915200915805

这是请求静态的.

image-20230915201054506

这里我一点上串,这是因为我们的这里是二进制文件,所有的乱码,不过我们不用担心.

image-20230915201019370

项目开始

下面我们正式开始编写我们的项目代码.

文件工具类

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

  • 获取文件大小(属性)
  • 判断文件是否存在
  • 向文件写入数据
  • 从文件读取数据
  • 针对目录文件多一个创建目录
[qkj@localhost source]$ touch util.hpp 

这里我们看框架.

namespace aod
{class FileUtil{public:FileUtil(const std::string &name): _name(name){}public:/// @brief 针对目录是创建目录/// @returnbool CreateDirectory(){}/// @brief 向文件中写入数据/// @param body/// @returnbool SetContent(const std::string &body){}/// @brief 获取文件数据到body中/// @param body/// @returnbool GetContent(std::string *body){}/// @brief 获取文件大小/// @returnstd::size_t Size(){}/// @brief 判断文件是否存在/// @returnbool Exists(){}private:std::string _name; // 文件路径名称};
}

实现

这里我们一个一个的实现.

如何判断文件存在,这里存在一个接口.

int access(const char *pathname, int mode);

那么这里就可以使用它了.

bool Exists()
{// F_OK 检测是否存在,存在返回0,不存在返回-1,并且错误码被设置int ret = access(_name.c_str(), F_OK);if (ret != 0){std::cout << "文件不存在" << std::endl;return false;}return true;
}

拿到文件的大小

int stat(const char *restrict path, struct stat *restrict buf);
std::size_t Size()
{if (Exists() == false)return 0;// 获取文件的属性信息struct stat st; // 保存文件属性信息的int ret = stat(_name.c_str(), &st);  if (ret != 0){return 0;}// 这里成功了// long int 就是一个 长整型return st.st_size;
}

读取数据

 bool GetContent(std::string *body)
{std::ifstream ifs;ifs.open(_name, std::ios::binary); // 二进制方式打开if (ifs.is_open() == false){std::cerr << "打开文件 " << _name << "失败" << std::endl;return false;}// 开始读取文件的数据std::size_t flen = Size();body->resize(flen);ifs.read(&((*body)[0]), flen); // 禁止使用 c_str() 这是constif (ifs.good() == false){std::cerr << "读取文件失败" << std::endl;ifs.close();return false;}ifs.close();return true;
}

写文件

 bool SetContent(const std::string &body)
{std::ofstream ofs;ofs.open(_name, std::ios::binary); // 二进制方式打开if (ofs.is_open() == false){std::cerr << "打开文件 " << _name << "失败" << std::endl;return false;}ofs.write(body.c_str(), body.size());if (ofs.good() == false){std::cerr << "保存文件失败" << std::endl;ofs.close();return false;}ofs.close();return true;
}

创建一个文件.

 bool CreateDirectory()
{if (Exists() == true)return true;mkdir(_name.c_str(), 0777);return true;
}

测试

下面我们开始测试.

[qkj@localhost source]$ touch aod.cpp

这是测试

void FileTset()
{aod::FileUtil("./www").CreateDirectory();aod::FileUtil("./www/index.html").SetContent("aaaaaaaaaaaaaaaaaaaaaaa");std::string body;aod::FileUtil("./www/index.html").GetContent(&body);std::cout << body << std::endl;std::cout << aod::FileUtil("./www/index.html").Size() << std::endl;
}
int main()
{FileTset();return 0;
}

下面是我们的Makefile

aod:aod.cppg++ -std=c++11 -o $@ $^
.PHONY:clean
clean:rm -f aod
[qkj@localhost source]$ make
g++ -std=c++11 -o aod aod.cpp
[qkj@localhost source]$ ll
total 44
-rwxrwxr-x. 1 qkj qkj 20176 Sep 15 20:36 aod
-rw-rw-r--. 1 qkj qkj  2930 Sep 15 20:34 aod.cpp
-rw-rw-r--. 1 qkj qkj    69 Sep 15 20:36 Makefile
-rw-rw-r--. 1 qkj qkj  8253 Sep 15 20:32 server.hpp
-rw-rw-r--. 1 qkj qkj  3811 Sep 15 20:27 util.hpp
[qkj@localhost source]$ ./aod 
文件不存在
aaaaaaaaaaaaaaaaaaaaaaa
23
[qkj@localhost source]$ ll
total 44
-rwxrwxr-x. 1 qkj qkj 20176 Sep 15 20:36 aod
-rw-rw-r--. 1 qkj qkj  2930 Sep 15 20:34 aod.cpp
-rw-rw-r--. 1 qkj qkj    69 Sep 15 20:36 Makefile
-rw-rw-r--. 1 qkj qkj  8253 Sep 15 20:32 server.hpp
-rw-rw-r--. 1 qkj qkj  3811 Sep 15 20:27 util.hpp
drwxrwxr-x. 2 qkj qkj    24 Sep 15 20:37 www
[qkj@localhost source]$ cat www/index.html 
aaaaaaaaaaaaaaaaaaaaaaa[qkj@localhost source]$ 

Json工具类实现

下面我们开始实现另外一个工具了,Json的,主要有两个功能

  • 序列化
  • 反序列

实现

class JsonUtil
{
public:/// @brief 序列化/// @param val/// @param body/// @returnstatic bool Serialize(const Json::Value &val, std::string *body){Json::StreamWriterBuilder swb;std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());std::stringstream ss;int ret = sw->write(val, &ss);if (ret != 0){std::cerr << "序列化失败" << std::endl;return false;}*body = ss.str();return true;}/// @brief 反序列化/// @param val/// @param body/// @returnstatic bool UnSerialize(const std::string &body, Json::Value *val){Json::CharReaderBuilder crb;std::unique_ptr<Json::CharReader> cr(crb.newCharReader());std::string err;bool ret = cr->parse(body.c_str(), body.c_str() + body.size(), val, &err);if (ret == false){std::cerr << "反序列化失败" << std::endl;return false;}return true;}
};

测试

下面是测试

void JsonTset()
{const char *name = "四小明";int age = 18;float score[] = {77.5, 88, 99.5};Json::Value val;val["姓名"] = name;val["年龄"] = 19;val["成绩"].append(score[0]);val["成绩"].append(score[1]);val["成绩"].append(score[2]);std::string body;aod::JsonUtil::Serialize(val, &body);std::cout << body << std::endl;std::cout << "=====================================" << std::endl;Json::Value stu;aod::JsonUtil::UnSerialize(body, &stu);std::cout << stu["姓名"].asString() << std::endl;std::cout << stu["年龄"].asString() << std::endl;// 换一种用法for (auto &e : stu["成绩"]){std::cout << e.asFloat() << std::endl;}}

注意修改Makefile

[qkj@localhost source]$ make
g++ -std=c++11 -o aod aod.cpp -ljsoncpp
[qkj@localhost source]$ ./aod 
{"姓名" : "四小明","年龄" : 19,"成绩" : [77.5,88,99.5]
}
=====================================
四小明
19
77.5
88
99.5
[qkj@localhost source]$ 

数据库的设计

下面我们就要设计我们的数据课了.

数据表设计

这里的的数据表是指我们存储文件的数据表.在视频共享点播系统中,视频数据和图片数据都存储在文件中,而我们需要在数据库中管理用户上传的每个视频信息。

  • 视频ID
  • 视频名称
  • 视频描述信息
  • 视频文件的url 路径(加上相对根目录实际上就是实际存储路径)
  • 视频封面图片的URL 路径(只是链接,加上相对根目录才是实际的存储路径)

下面就是我们的数据表,注意,这里我们在aod_system数据库下设计.

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

image-20230915204743139

数据管理类设计

数据管理模块负责统一对于数据库中数据的增删改查管理,其他所有模块要进行数据的操作都通过数据管理模块完成。然而,数据库中有可能存在很多张表,每张表中数据又有不同,要进行的数据操也各不相同,因此咱们将数据的操作分摊到每一张表上,为每一张表中的数据操作都设计一个类,通过类实例化的对象来访问这张数据库表中的数据,这样的话当我们要访问哪张表的时候,使用哪个类实例化的对象即可。那么对于我们的数据,我们就要管理一下我们的数据表,也就是SQL语句,这里包含:

  • 新增
  • 修改
  • 删除
  • 查询所有
  • 查询单个
  • 模糊匹配

注意, 视频信息在接口之间的 传递因为字段数量可能很多,因此使用Json::Value 对象进行传递

[qkj@localhost source]$ touch data.hpp 
#include <mysql/mysql.h>
#include <mutex>
#include <jsoncpp/json/json.h>namespace aod
{static MYSQL *MysqlInit();static void MysqlDestroy(MYSQL *mysql);static bool MysqlQuery(MYSQL *mysql, const std::string &sql);
}

下面我们开始编写每一个功能编写.

数据库的初始化

static MYSQL *MysqlInit(){
#define HOST "127.0.0.1"
#define USER "root"
#define PASSWARD ""
#define DB "aod_system"
#define PORT 3306MYSQL *mysql = mysql_init(nullptr);if (nullptr == mysql){std::cerr << "创建句柄失败" << std::endl;return nullptr;}// 1 . 登录认证if (mysql_real_connect(mysql, HOST, USER, PASSWARD, DB, PORT, nullptr, 0) == nullptr){std::cerr << "链接数据库失败" << std::endl;mysql_close(mysql);return nullptr;}std::cerr << "链接数据库成功" << std::endl;// 2. 设置字符集mysql_set_character_set(mysql, "utf8");return mysql;}

销毁

  static void MysqlDestroy(MYSQL *mysql){if (nullptr == mysql)return;mysql_close(mysql);}

语句执行

  static bool MysqlQuery(MYSQL *mysql, const std::string &sql){int ret = mysql_query(mysql, sql.c_str());if (ret != 0){std::cerr << "sql: " << sql << std::endl;std::cerr << mysql_errno(mysql) << std::endl;return false;}return true;}

下面我们开始编写我们的操作SQL语句,这里我们是这样做了的.

 class TableVideo{private:MYSQL *_mysql;     // 一个对象就是一个客户端,管理一张表std::mutex _mutex; // 防备操作对象在多线程中使用存在的线程安全 问题public:TableVideo();                                                 // 完成mysql句柄初始化~TableVideo();                                                // 释放msyql操作句柄bool Insert(const Json::Value &video);                        // 新增-传入视频信息bool Update(int video_id, const Json::Value &video);          // 修改-传入视频id,和信息bool Delete(const int video_id);                              // 删除-传入视频IDbool SelectAll(Json::Value *videos);                          // 查询所有--输出所有视频信息bool SelectOne(int video_id, Json::Value *video);             // 查询单个-输入视频id,输出信息bool SelectLike(const std::string &key, Json::Value *videos); // 模糊匹配-输入名称关键字,输出视频信息};

这里先完成初始化.

TableVideo() // 完成mysql句柄初始化
{_mysql = MysqlInit();if (_mysql == NULL)exit(-1);
}
~TableVideo()
{MysqlDestroy(_mysql);
}

新增、修改、删除

这里编写这三个功能的语句.

// 释放msyql操作句柄
bool Insert(const Json::Value &video) // 新增-传入视频信息
{std::string sql;sql.resize(4096 + video["info"].asString().size());
#define INSERT_VIDEO "insert tb_video values(null, '%s','%s','%s','%s');"sprintf(&sql[0], INSERT_VIDEO, video["name"].asCString(), video["info"].asCString(), 	video["video"].asCString(), video["image"].asCString());return MysqlQuery(_mysql, sql);
}bool Update(int video_id, const Json::Value &video) // 修改-传入视频id,和信息
{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);
}
bool Delete(const int video_id) // 删除-传入视频ID
{
#define DELETE_VIDEO "delete from tb_video where id=%d;"char sql[1024] = {0};sprintf(sql, DELETE_VIDEO, video_id);return MysqlQuery(_mysql, sql);
}

全列查找、查找一个、模糊查找

这里我们需要说一下,我们的查找语句很好的执行,保存查找结果到本地也是可以的,但是这里存在一个线程不安全的问题,注意,我们的访问数据库可以理解是可以多个进程访问的,此时这里就是一个临界区,我们要加锁.

bool SelectAll(Json::Value *videos) // 查询所有--输出所有视频信息{
#define SELECTALL_VIDEO "select * from tb_video;"_mutex.lock();bool ret = MysqlQuery(_mysql, SELECTALL_VIDEO);if (ret == false){_mutex.unlock();return false;}MYSQL_RES *res = mysql_store_result(_mysql);if (res == NULL){std::cout << "mysql store结果失败" << 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;}bool SelectOne(int video_id, Json::Value *video) // 查询单个-输入视频id,输出信息{
#define SELECTONE_VIDEO "select * from tb_video where id=%d;"char sql[1024] = {0};sprintf(sql, SELECTONE_VIDEO, video_id);_mutex.lock();bool ret = MysqlQuery(_mysql, sql);if (ret == false){_mutex.unlock();return false;}MYSQL_RES *res = mysql_store_result(_mysql);if (res == NULL){std::cout << "mysql store结果失败" << std::endl;_mutex.unlock();return false;}_mutex.unlock();int num_rows = mysql_num_rows(res);if (num_rows != 1){std::cout << "没有找的数据" << 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) // 模糊匹配-输入名称关键字,输出视频信息{
#define SELECTLIKE_VIDEO "select * from tb_video where name like '%%%s%%';"char sql[1024] = {0};sprintf(sql, SELECTLIKE_VIDEO, key.c_str());_mutex.lock();bool ret = MysqlQuery(_mysql, sql);if (ret == false){_mutex.unlock();return false;}MYSQL_RES *res = mysql_store_result(_mysql);if (res == NULL){std::cout << "mysql store结果失败" << 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;}
};

测试

这里来个测试,这里具体的情况大家自行测试,具体的我就不列举了.

void DataTset()
{aod::TableVideo tb_video;Json::Value video;video["name"] = "白娘子传奇";video["info"] = "这是一条白蛇和青蛇之间的故事,精彩";video["video"] = "/video/snake.mp4";video["image"] = "/img/sanke.jpg";tb_video.Insert(video);video["name"] = "变形金刚";video["info"] = "机器人大战,等你来战";video["video"] = "/video/robot.mp4";video["image"] = "/video/robot.jpg";tb_video.Update(1, video);tb_video.SelectLike("金刚", &video);std::string body;aod::JsonUtil::Serialize(video, &body);std::cout << body << std::endl;// 如何产看结果 序列化tb_video.Delete(1);
}

请求与响应

下面我们开始搭建网络通信模块,这里我们使用restful风格.

  • REST 是 Representational State Transfer 的缩写,一个架构符合REST 原则,就称它为RESTful 架构
  • RESTful 架构可以充分的利用 HTTP 协议的各种功能,是 HTTP 协议的最佳实践,正文通常采用JSON 格式
  • RESTful API 是一种软件架构风格、设计风格,可以让软件更加清晰,更简洁,更有层次,可维护性更好.

restful 使用五种 HTTP 方法,对应 CRUD(增删改查) 操作

  • GET 表示查询获取
  • POST 对应新增
  • PUT 对应修改
  • DELETE 对应删除

image-20230916085630562

下面我们开始构建我们每一个接口的具体的格式.

获取所有视频信息

请求:
GET /video HTTP/1.1
xxxxxxxxxxx
这是一个空行响应:
HTTP/1.1 200 OK
xxxxxxxxxxxxxxx
这是一个空行
[{"info": "好电影","id": 1,"image": "/img/thumbs/mysql.png","name": "Mysql注意事项","video": "/video/movie.mp4",},{"info": "好电影","id": 2,"image": "/img/thumbs/linux.png","name": "Linux注意事项","video": "/video/movie.mp4",}
]

搜索指定关键字名称视频信息

请求:
GET /video?search="Mysql" HTTP/1.1
响应:
HTTP/1.1 200 OK
[{"info": "好电影","id": 1,"image": "/img/thumbs/mysql.png","name": "Mysql注意事项","video": "/video/movie.mp4",}
]

获取指定视频信息

请求:
GET /video/1 HTTP/1.1
响应:
HTTP/1.1 200 OK
[
{
"info": "好电影",
"id": 1,
"image": "/img/thumbs/mysql.png",
"name": "Mysql注意事项",
"video": "/video/movie.mp4",
}
]

删除指定视频信息

请求:
DELETE /video/1 HTTP/1.1
响应:
HTTP/1.1 200 OK

修改指定视频信息

请求:
PUT /video/1 HTTP/1.1
{
"info": "这是一个非常好的教学视频,深入浅出,引人深思",
"id": 1,
"image": "/img/thumbs/mysql.png",
"name": "Mysql注意事项",
"video": "/video/movie.mp4",
}
响应:
HTTP/1.1 200 OK

上传视频信息以及文件
因为上传视频信息的时候,会携带有视频文件和封面图片的文件上传,而这些文件数据都是二进制的,用json 不好
传输,因此在这里使用传统的http 上传文件请求格式,而并没有使用restful 风格。

请求:
POST /video HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarydsrFiETIzKETHWkn
------WebKitFormBoundarydsrFiETIzKETHWkn
Content-Disposition: form-data; name="name"
Xhsell连接事项,也就是视频名称
------WebKitFormBoundarydsrFiETIzKETHWkn
Content-Disposition: form-data; name="info"
一部非常好看的视频的描述信息
------WebKitFormBoundarydsrFiETIzKETHWkn
Content-Disposition: form-data; name="image"; filename="image.jpg"
Content-Type: text/plain
image封面图片数据
------WebKitFormBoundarydsrFiETIzKETHWkn
Content-Disposition: form-data; name="video"; filename="video.mp4"
Content-Type: text/plain
video视频数据
------WebKitFormBoundarydsrFiETIzKETHWkn
Content-Disposition: form-data; name="submit"
------WebKitFormBoundarydsrFiETIzKETHWkn--
响应:
HTTP/1.1 303 See Other
Location: "/"

业务处理

业务处理模块负责与客户端进行网络通信,接收客户端的请求,然后根据请求信息,明确客户端端用户的意图,进行业务处理,并进行对应的结果响应。在视频共享点播系统中,业务处理主要包含两大功能:1、网络通信功能的实现;2、业务功能处理的实现其中网络通信功能的实现咱们借助httplib 库即可方便的搭建http 服务器完成。这也是咱们将网络通信模块与业务处理模块合并在一起完成的原因。

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

  • 客户端的视频数据和信息上传
  • 客户端的视频列表展示(视频信息查询)
  • 客户端的视频观看请求(视频数据的获取)
  • 客户端的视频其他管理(修改,删除)功能
[qkj@localhost source]$ touch server.hpp 
namespace aod {
#define WWWROOT "../http/www"
#define VIDEO_ROOT "/video/"
#define IMAGE_ROOT "/image/"//因为httplib基于多线程,因此数据管理对象需要在多线程中访问,为了便于访问定义全局变量TableVideo *tb_video = nullptr;//这里为了更加功能模块划分清晰一些,不使用lamda表达式完成,否则所有的功能实现集中到一个函数中太过庞大class Server {private:int _port;//服务器的 监听端口httplib::Server _srv;//用于搭建http服务器private://对应的业务处理接口static void Insert(const httplib::Request &req, httplib::Response &rsp);static void Update(const httplib::Request &req, httplib::Response &rsp);static void Delete(const httplib::Request &req, httplib::Response &rsp);static void GetOne(const httplib::Request &req, httplib::Response &rsp);static void GetAll(const httplib::Request &req, httplib::Response &rsp);public:Server(int port):_port(port);bool RunModule();//建立请求与处理函数的映射关系,设置静态资源根目录,启动服务器,};
}

初始化操作

这里我们分为两步

  • 初始化数据库
  • 搭建服务器
bool RunModule()
{tb_video = new TableVideo();// 1. 初始化操作// 创建根目录 wwwFileUtil(WWWROOT).CreateDirectory();std::string root = WWWROOT;std::string video_root_path = root + VIDEO_ROOT;std::string image_root_path = root + IMAGE_ROOT;// 这是文件存放的目录FileUtil(video_root_path).CreateDirectory();FileUtil(image_root_path).CreateDirectory();// 2. 设置静态文件根目录_svr.set_mount_point("/", WWWROOT);// 3. 添加请求_svr.Post("/video", Insert);_svr.Delete("/video/(\\d+)", Delete);_svr.Put("/video/(\\d+)", Update);_svr.Get("/video/(\\d+)", SelectOne);_svr.Get("/video", SelectAll);// 启动服务器_svr.listen("0.0.0.0", _port);return true;
}

新增视频

这里是新增一个视频.

void Server::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"); // 视频图片std::string video_name = name.content; // 这里解释一下content为何是这个,不是文件名称--对于视频而言,这里确实是std::string video_info = info.content;// ../http/www/video/变形金刚ss.mp4std::string root = WWWROOT;std::string video_path = root + VIDEO_ROOT + video_name + video.filename;//../http/www/image/变形金刚1.jpgstd::string image_path = root + IMAGE_ROOT + video_name + image.filename;if (false == FileUtil(video_path).SetContent(video.content)){// std::cerr << "文件存储失败" << std::endl;rsp.status = 500;rsp.body = R"({"result":false, "reason":"视频存储失败"})";rsp.set_header("Content-Type", "application/json");return;}if (false == FileUtil(image_path).SetContent(image.content)){// std::cerr << "文件存储失败" << std::endl;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_json["image"] = IMAGE_ROOT + video_name + image.filename;// 数据库插入if (false == tb_video->Insert(video_json)){// std::cerr << "文件存储失败" << std::endl;rsp.status = 500;rsp.body = R"({"result":false, "reason":"数据库存储失败"})";rsp.set_header("Content-Type", "application/json");return;}rsp.set_redirect("/index.html", 303);
}

修改视频

这是一个修改视频,注意,这里的修改我们注意一下,修改的是文件的名字和简绍,至于数据就不修改了.

static void Update(const httplib::Request &req, httplib::Response &rsp)
{// 需要进行捕捉// 这个是捕捉的数据 /numbers/123// matches[0] = "/numbers/123"  matches[1] = "123"std::string s = req.matches[1];int video_id = atoi(s.c_str()); // 捕捉id// 去数据库里面查找是否存在Json::Value v;if (false == tb_video->SelectOne(video_id, &v)){rsp.status = 400;rsp.body = R"({"result":false, "reason":"视频不存在"})";rsp.set_header("Content-Type", "application/json");return;}// 开始修改Json::Value video;if (false == JsonUtil::UnSerialize(req.body, &video)){rsp.status = 400;rsp.body = R"({"result":false, "reason":"反序列化失败"})";rsp.set_header("Content-Type", "application/json");return;}if (false == tb_video->Update(video_id, video)){rsp.status = 500;rsp.body = R"({"result":false, "reason":"修改数据库失败"})";rsp.set_header("Content-Type", "application/json");return;}
}

查找所有

我们的查询所有和模糊匹配都是一样的,在这里我们判断一下查询的时候是不是模糊匹配.


static void SelectAll(const httplib::Request &req, httplib::Response &rsp)
{// 可能是 模糊匹配bool select_flag = true;std::string search_key;if (true == req.has_param("search")){// 表示是 模糊匹配select_flag = false;search_key = req.get_param_value("search");}Json::Value videos;if (select_flag == true){// 这里是全部if (false == tb_video->SelectAll(&videos)){rsp.status = 500;rsp.body = R"({"result":false, "reason":"数据库信息不存在"})";rsp.set_header("Content-Type", "application/json");return;}}else{if (false == tb_video->SelectLike(search_key, &videos)){rsp.status = 500;rsp.body = R"({"result":false, "reason":"数据库信息不存在"})";rsp.set_header("Content-Type", "application/json");return;}}rsp.status = 200;JsonUtil::Serialize(videos, &rsp.body);rsp.set_header("Content-Type", "application/json");return;
}

查找一个

static void SelectOne(const httplib::Request &req, httplib::Response &rsp)
{// 需要进行捕捉// 这个是捕捉的数据 /numbers/123// matches[0] = "/numbers/123"  matches[1] = "123"std::string s = req.matches[1];int video_id = atoi(s.c_str()); // 捕捉idJson::Value video;if (false == tb_video->SelectOne(video_id, &video)){rsp.status = 500;rsp.body = R"({"result":false, "reason":"视频不存在"})";rsp.set_header("Content-Type", "application/json");return;}JsonUtil::Serialize(video, &rsp.body);rsp.set_header("Content-Type", "application/json");}

删除

static void Delete(const httplib::Request &req, httplib::Response &rsp)
{// 需要进行捕捉// 这个是捕捉的数据 /numbers/123// matches[0] = "/numbers/123"  matches[1] = "123"std::string s = req.matches[1];int video_id = atoi(s.c_str()); // 捕捉id// 去数据库里面查找是否存在Json::Value video;if (false == tb_video->SelectOne(video_id, &video)){rsp.status = 500;rsp.body = R"({"result":false, "reason":"视频不存在"})";rsp.set_header("Content-Type", "application/json");return;}std::string root = WWWROOT;std::string video_path = root + video["video"].asString();std::string image_path = root + video["image"].asString();// 删除文件remove(video_path.c_str());remove(image_path.c_str());// 删除数据库if (false == tb_video->Delete(video_id)){rsp.status = 500;rsp.body = R"({"result":false, "reason":"删除数据库信息失败"})";rsp.set_header("Content-Type", "application/json");return;}
}

测试

下面我们就有一个测试

void ServerTest()
{aod::Server server(8081);server.RunModule();
}

image-20230916123307569

编译好之后,这里我们使用Postman软件尽心测试,注意,这里的视频我放在下方的链接.

https://github.com/qkja/Project/tree/master/video_on_demand/test

前端页面

这里我们不实现,直接给大家代码.这是我们的源码链接.

https://github.com/qkja/Project/tree/master/video_on_demand

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

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

相关文章

如何根据性能需求进行场景设计?

场景设计一 探索 测试环境 客户端: win10 这里可以用linux,但没用,因为想直观查看结果。 被测环境:linux X86 4核CPU16G内存 被测接口:登录接口,没有做数据驱动。 在测试执行前,先使用influxSQL把influxdb的数据清理一下,以防影响结果查看。 有这么一个需求,要求系…

Android 12 源码分析 —— 应用层 五(SystemUI的StatusBar类的启动过程和三个窗口的创建)

Android 12 源码分析 —— 应用层 五&#xff08;SystemUI的StatusBar类的启动过程和三个窗口的创建&#xff09; 在前面的文章中&#xff0c;我们介绍了SystemUI App的基本布局和基本概念。接下来&#xff0c;我们进入SystemUI应用的各个UI是如何被加入屏幕的。那么我们就先从…

SOLIDWORKS Composer反转关键帧实现产品安装过程

SOLIDWORKS Composer 是一款被用来制作交互式产品说明书的工具&#xff0c;可以帮助我们对产品设定精确的机构动画&#xff0c;并能根据材质生成一定细节的渲染图像。 今天我们主要向大家讲解的是&#xff0c;利用SOLIDWORKS Composer关键帧反转实现产品动态的安装。 一般情况下…

微信小程序通过 wxministore 实现类似于vuex的全局装填数据管理

首先 我们打开终端 引入依赖 npm install wxministore --save然后 如果你是新版开发者工具 就 npm i构建一下 如果你是 老版本的 微信开发者工具 就打开右上角详情 选择本地管理 勾选 使用 npm 模块 然后 在根目录下创建一个 store.js 当然建在哪是你自己决定的 反正 后面能…

密集人头检测数据集汇总和格式转换

1、VSCrowd 2022年9月新出的数据集,数据集链接:https://github.com/HopLee6/VSCrowd-Dataset 网盘地址:链接:https://pan.baidu.com/s/17VARxt59y7GnUHIskEGzKw?pwd=m9qo 提取码:m9qo 数据格式: FrameID HeadID x1 y1 x2 y2 p1 p2 HeadID x1 y1 x2 y2 p1 p2 … Fram…

长城网络靶场第三题

关卡描述&#xff1a;1.oa服务器的内网ip是多少&#xff1f; 先进行ip统计&#xff0c;开始逐渐查看前面几个ip 基本上都是b/s&#xff0c;所以大概率是http&#xff0c;过滤一下ip 第一个ip好像和oa没啥关系 第二个ip一点开就是 oa&#xff0c;应该就是他了。 关卡描述&a…

数据仓库模型设计V2.0

一、数仓建模的意义 数据模型就是数据组织和存储方法&#xff0c;它强调从业务、数据存取和使用角度合理存储数据。只有将数据有序的组织和存储起来之后&#xff0c;数据才能得到高性能、低成本、高效率、高质量的使用。 高性能&#xff1a;良好的数据模型能够帮助我们快速查询…

spark6. 如何设置spark 日志

spark yarn日志全解 一.前言二.开启日志聚合是什么样的2.1 开启日志聚合MapReduce history server2.2 如何开启Spark history server 三.不开启日志聚合是什么样的四.正确使用log4j.properties 一.前言 本文只讲解再yarn 模式下的日志配置。 二.开启日志聚合是什么样的 在ya…

基于SSM+Vue的校园教务系统的设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用Vue技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…

LeetCode【27. 移除元素】

为国捐躯赴战场&#xff0c;丹心可并日争光。 给你一个数组 nums 和一个值 val&#xff0c;你需要 原地 移除所有数值等于 val 的元素&#xff0c;并返回移除后数组的新长度。 不要使用额外的数组空间&#xff0c;你必须仅使用 O(1) 额外空间并 原地 修改输入数组。 元素的顺…

docker启动MySQL报错:退出状态码1

docker启动mysql反复重启&#xff0c;通过 使用 docker logs 容器ID chown: cannot read directory /var/lib/mysql/: Permission denied 但是目录权限确认没问题&#xff0c;即使 chmod 777 还是报相同的错误&#xff0c;后来发现是selinux的问题 查看状态 getenforce 临时…

[Rust GUI]eframe(egui框架)代码示例

-2、eframe代替品 你可以使用egui的其他绑定&#xff0c;例如&#xff1a;egui-miniquad&#xff0c;bevy_egui&#xff0c;egui_sdl2_gl 等。 -1、注意 egui库相当于核心库&#xff0c;需要借助eframe框架就可以写界面了。 eframe使用egui_glow渲染&#xff0c;而egui_glow…