项目背景
大家应该在电脑上刷过视频吧,这个项目就是模拟一下我们刷视频的整个流程,我们要做的是一个类似B站的网页,这里面包含视频的上传修改和观看以及删除,注意我这个是一个简易版本的,在后面我会做一个升级,增加其他的功能.
基本原理
下面我们说一下我们项目的基本原理.我们这里做的是服务器客户端类型的项目.当客户端发起请求之后,我们服务端分析请求,看他是做什么的,例如请求资源,还是推送资源,我们对每一个请求都做一个处理让后把响应发送给客户端.
服务端程序负责功能
- 针对客户端上传的视频文件以及封面进行存储
- 针对客户端上传的视频完成增删查改内容
- 支持浏览器进行视频观看内容
服务端功能模块划分
- 数据管理模块: 负责针对客户端上传的视频信息进行管理
- 网络通信模块: 搭建网络服务器,与客户端进行通信
- 业务处理模块: 针对客户端的请求处理各项业务并进行相应
- 前端界面模块: 完成前端浏览器上的各个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版本
shell
[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库
shell
[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库
shell
[qkj@Qkj ~]$ git clone https://github.com/yhirose/cpp-httplib.git
MySQL数据库
安装与卸载中,用户全部切换成为root,一旦 安装,普通用户能使用的 初期练习,mysql不进行用户管理,全部使用root进行,尽快适应mysql语句,后面学了用户管理,在考虑新建普通用户
卸载旧环境
首先检测我们系统是否存在mariadb在运行存在,这个也是一个数据库,是MySQL的分支.如果存在这个软件,那么按照下面的步骤停止服务.
shell
[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是否存在.
shell
[root@Qkj ~]# rpm -qa | grep mariadb # 或者使用这个指令rpm -qa | grep mysql
....
如果出现了这样的结果.
shell
[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
那么此时我们需要卸载这些安装包.下面我们说两个方式.
可以一个一个删除.
shell
#卸载显示出来的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/
我们进入之后,然后我们发现他的页面实在是太简单了.
这里我们右键,选择查看网页源代码,此时我们这里详细一些.
我们这里会发现,此时这里有太多的版本了,此时我们需要关注我们红色的框里面的内容.
让后我们查看一下我们系统的版本.
shell
[root@Qkj ~]# cat /etc/redhat-release
CentOS Linux release 7.9.2009 (Core)
[root@Qkj ~]#
我们发现自己的版本是CentOS 7版本,这里我们选择这一类的.
这里需要注意的,我们是7.9版本的,我们最好选择7.9以上的,例如7.10.如果没有7.10,可以去下载这个el7.rpm文件.我们把他下载到Windows,让拖到我们服务器上就可以了.
shell
[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源.我的应该和大家的不同.
shell
[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]#
下面我们开始安装.
shell
[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源.
cpp
[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源是不是可以使用.
shell
[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的服务端,直接执行下面的指令.
shell
[root@Qkj mysql]# sudo yum install -y mysql-community-server
但是如果我们出现了安装错误,我们例如下面的情况.
shell
#安装遇到秘钥过期的问题:
#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
安装客户端
注意,如果我们执行了上面的指令,我们的客户端已经安装了.
查看配置文件
安装完成之后,我们去查看一下我们的配置文件.需要保证这两个文件是存在的.
shell
[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之后,这里我们就可以启动服务了.
shell
[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 mysqld
1 1579 1578 1578 ? -1 Sl 27 0:01 /usr/sbin/mysqld --daemonize --pid-file=/var/run/mysqld/mysqld.pid
1513 1608 1607 1481 pts/0 1607 S+ 0 0:00 grep --color=auto mysqld
[root@Qkj mysql]#
登陆MySQL
注意,MySQL支持密码登陆,不过这里我们先暂时不说.我们这里使用两个方式登陆,总有一个方法可行的.
直接使用MySQL客户端登陆,如果不行,那么就使用下一个方法.
shell
如果你安装的最新的mysql,没有所谓的临时密码,root默认没有密码
# 试着直接client登陆一下
我们这里修改配置文件,然后重启服务,注意,一定要重启,在最后一行
shell
[root@Qkj mysql]# vim /etc/my.cnf
shell
[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服务开机自启动.
shell
[root@Qkj mysql]# systemctl enable mysqld
[root@Qkj mysql]# systemctl daemon-reload
配置文件
我们配置文件的修改主要分为两个方面.
修改编码格式
这里我们要修改编码格式.
shell
[root@Qkj mysql]# vim /etc/my.cnf
修改存储引擎
shell
[root@Qkj mysql]# vim /etc/my.cnf
数据库管理系统如何存储数据,如何为存储的数据建立索引和如何更新,查询数据等技术的实现方法,就是我们的存储引擎,这里我们使用innodb.
常见问题
如果我们已经配置好了编码,但是我们还是不支持中文,我们确保你在终端命令行中可以输入中文
shell
[root@Qkj mysql]# env |grep LANG
LANG=en_US.UTF-8
[root@Qkj mysql]#
第三方库认识
在正式开始之前,我们需要一些前置的知识,这里我们要学习下面几个东西
- jsoncpp库认识和学习
- http-lib库的认识
jsoncpp库认识和学习
json 是一种数据交换格式,采用完全独立于编程语言的文本格式来存储和表示数据。例如:小明同学的学生信息.
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格式字符串解析得到多个数据对象的功能。
cpp
//Json数据对象类
class Json::Value
{
Value &operator=(const Value &other); //Value重载了[]和=,因此所有的赋值和获取数据都可以通过
Value& operator[](const std::string& key);//简单的方式完成 val["姓名"] = "小明";
Value& operator[](const char* key);
Value removeMember(const char* key);//移除元素
const Value& operator[](ArrayIndex index) const; //val["成绩"][0]
Value& append(const Value& value);//添加数组元素val["成绩"].append(88);
ArrayIndex size() const;//获取数组元素个数 val["成绩"].size();
std::string asString() const;//转string string name = val["name"].asString();
const char* asCString() const;//转char* char *name = val["name"].asCString();
Int asInt() const;//转int int age = val["age"].asInt();
float asFloat() const;//转float
bool asBool() const;//转 bool
};
Json数据对象类,这里可以认为他是一个KV类型的结构,其中V可以可以理解为一个数组.
序列化
这个是我们序列化的类,里面最重要的就是write函数,我们发现FastWriter和StyledWriter都继承了抽象类Writer并且都重写了write纯虚函数.
cpp
//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
只是一个简单的宏.
下面我们使用一下,我们的逻辑应该是实例化一个Value对象,然后把这个对象给序列化.
cpp
#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;
}
StyledWriter
这个类打印的结果比较美观一些.
cpp
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;
}
上面使用比较简单,不过它是低版本的,如果用低版本的接口可能会有警告,那么此时我们需要使用高版本的.
cpp
//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;
}
cpp
int main()
{
// 序列化 -- 高版本
Json::StreamWriterBuilder swb; // 他的作用就是new出来一个对象,可以实现多态
std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());
return 0;
}
下面看一下我们的序列化是具体如何使用的.
cpp
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;
}
反序列化
这里是低版本的.
cpp
//json反序列化类,低版本用起来更简单
class JSON_API Reader {
bool parse(const std::string& document, Value& root, bool collectComments = true);
}
cpp
#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;
}
同样的,这个是低版本的,我们也是需要学习一下高版本的.
cpp
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;
}
MySQL API认识
我们这里使用MySQL的API,直接通过代码链接我们的MySQL.下面我们用一下.
cpp
#include <iostream>
#include <mysql/mysql.h>
using namespace std;
int main()
{
cout << "版本: " << mysql_get_client_info() << endl;
return 0;
这里直接演示.
shell
[qkj@localhost example]$ g++ mysql_test.cc -L/lib64/mysql -lmysqlclient
[qkj@localhost example]$ ./a.out
版本: 5.7.43
[qkj@localhost example]$
访问数据库
访问数据库的第一步,我们首先要创建一个句柄.
cpp
int main()
{
// cout << "版本: " << mysql_get_client_info() << endl;
MYSQL *msql = mysql_init(nullptr);
if (nullptr == msql)
{
return 0;
}
mysql_close(msql);
return 0;
}
当我们创建好句柄之后,这里我们就可以链接我们的MySQL了,这是一个函数.
cpp
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
下面我们链接一下,注意要设置字符集.
cpp
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;
}
当我们链接上数据库的时候,此时我们就可以使用sql语句了,这里非常简答.
cpp
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而言,我们所有的结果都被保存好了,此时我们手动拿出来就可以了.
cpp
#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;
}
httplib库认识
httplib 库,一个C++11 单文件头的跨平台HTTP/HTTPS 库。安装起来非常容易。只需包含httplib.h 在你的代码中即可。httplib 库实际上是用于搭建一个简单的http 服务器或者客户端的库,这种第三方网络库,可以让我们免去搭建服务器或客户端的时间,把更多的精力投入到具体的业务处理中,提高开发效率。
cpp
namespace httplib
{
struct MultipartFormData
{
std::string name;
std::string content;
std::string filename;
std::string content_type;
};
using MultipartFormDataItems = std::vector<MultipartFormData>;
struct Request
{
std::string method; // 存放请求方法
std::string path; // 存放请求资源路径
Headers headers; // 存放头部字段的键值对map
std::string body; // 存放请求正文
// for server
std::string version; // 存放协议版本
Params params; // 存放url中查询字符串 key=val&key=val的 键值对map
MultipartFormDataMap files; // 存放文件上传时,正文中的文件信息
Ranges ranges;
bool has_header(const char *key) const; // 判断是否有某个头部字段
std::string get_header_value(const char *key, size_t id = 0) const; // 获取头部字段值
void set_header(const char *key, const char *val); // 设置头部字段
bool has_file(const char *key) const; // 文件上传中判断是否有某个文件的信息
MultipartFormData get_file_value(const char *key) const; // 获取指定的文件信息
};
struct Response
{
std::string version; // 存放协议版本
int status = -1; // 存放响应状态码
std::string reason;
Headers headers; // 存放响应头部字段键值对的map
std::string body; // 存放响应正文
std::string location; // Redirect location重定向位置
void set_header(const char *key, const char *val); // 添加头部字段到headers中
void set_content(const std::string &s, const char *content_type); // 添加正文到body中
void set_redirect(const std::string &url, int status = 302); // 设置全套的重定向信息
};
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
<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>
cpp
#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 表示数字,
// + 表示一次或者多次
// ()--表示捕捉数据s
server.Get("/numbers/(\\d+)", Numbers);
server.Post("/multipart", Multipart);
server.listen("0.0.0.0", 8081);
return 0;
}
这是请求动态的.
这是请求静态的.
这里我一点上串,这是因为我们的这里是二进制文件,所有的乱码,不过我们不用担心.
项目开始
下面我们正式开始编写我们的项目代码.
文件工具类
在视频点播系统中因为涉及到文件上传,需要对上传的文件进行备份存储,因此首先设计封装文件操作类,这个类封装完毕之后,则在任意模块中对文件进行操作时都将变的简单化
- 获取文件大小(属性)
- 判断文件是否存在
- 向文件写入数据
- 从文件读取数据
- 针对目录文件多一个创建目录
shell
[qkj@localhost source]$ touch util.hpp
这里我们看框架.
cpp
namespace aod
{
class FileUtil
{
public:
FileUtil(const std::string &name)
: _name(name)
{
}
public:
/// @brief 针对目录是创建目录
/// @return
bool CreateDirectory()
{}
/// @brief 向文件中写入数据
/// @param body
/// @return
bool SetContent(const std::string &body)
{
}
/// @brief 获取文件数据到body中
/// @param body
/// @return
bool GetContent(std::string *body)
{
}
/// @brief 获取文件大小
/// @return
std::size_t Size()
{
}
/// @brief 判断文件是否存在
/// @return
bool Exists()
{
}
private:
std::string _name; // 文件路径名称
};
}
实现
这里我们一个一个的实现.
如何判断文件存在,这里存在一个接口.
cpp
int access(const char *pathname, int mode);
那么这里就可以使用它了.
cpp
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;
}
拿到文件的大小
cpp
int stat(const char *restrict path, struct stat *restrict buf);
cpp
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;
}
读取数据
cpp
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() 这是const
if (ifs.good() == false)
{
std::cerr << "读取文件失败" << std::endl;
ifs.close();
return false;
}
ifs.close();
return true;
}
写文件
cpp
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;
}
创建一个文件.
cpp
bool CreateDirectory()
{
if (Exists() == true)
return true;
mkdir(_name.c_str(), 0777);
return true;
}
测试
下面我们开始测试.
shell
[qkj@localhost source]$ touch aod.cpp
这是测试
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
makefile
aod:aod.cpp
g++ -std=c++11 -o $@ $^
.PHONY:clean
clean:
rm -f aod
shell
[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的,主要有两个功能
- 序列化
- 反序列
实现
cpp
class JsonUtil
{
public:
/// @brief 序列化
/// @param val
/// @param body
/// @return
static 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
/// @return
static 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;
}
};
测试
下面是测试
cpp
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
shell
[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数据库下设计.
mysql
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,加上静态资源根目录就是实际存储路径'
);
数据管理类设计
数据管理模块负责统一对于数据库中数据的增删改查管理,其他所有模块要进行数据的操作都通过数据管理模块完成。然而,数据库中有可能存在很多张表,每张表中数据又有不同,要进行的数据操也各不相同,因此咱们将数据的操作分摊到每一张表上,为每一张表中的数据操作都设计一个类,通过类实例化的对象来访问这张数据库表中的数据,这样的话当我们要访问哪张表的时候,使用哪个类实例化的对象即可。那么对于我们的数据,我们就要管理一下我们的数据表,也就是SQL语句,这里包含:
- 新增
- 修改
- 删除
- 查询所有
- 查询单个
- 模糊匹配
注意, 视频信息在接口之间的 传递因为字段数量可能很多,因此使用Json::Value 对象进行传递
shell
[qkj@localhost source]$ touch data.hpp
cpp
#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);
}
下面我们开始编写每一个功能编写.
数据库的初始化
cpp
static MYSQL *MysqlInit()
{
#define HOST "127.0.0.1"
#define USER "root"
#define PASSWARD ""
#define DB "aod_system"
#define PORT 3306
MYSQL *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;
}
销毁
cpp
static void MysqlDestroy(MYSQL *mysql)
{
if (nullptr == mysql)
return;
mysql_close(mysql);
}
语句执行
cpp
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语句,这里我们是这样做了的.
cpp
class TableVideo
{
private:
MYSQL *_mysql; // 一个对象就是一个客户端,管理一张表
std::mutex _mutex; // 防备操作对象在多线程中使用存在的线程安全 问题
public:
TableVideo(); // 完成mysql句柄初始化
~TableVideo(); // 释放msyql操作句柄
bool Insert(const Json::Value &video); // 新增-传入视频信息
bool Update(int video_id, const Json::Value &video); // 修改-传入视频id,和信息
bool Delete(const int video_id); // 删除-传入视频ID
bool SelectAll(Json::Value *videos); // 查询所有--输出所有视频信息
bool SelectOne(int video_id, Json::Value *video); // 查询单个-输入视频id,输出信息
bool SelectLike(const std::string &key, Json::Value *videos); // 模糊匹配-输入名称关键字,输出视频信息
};
这里先完成初始化.
cpp
TableVideo() // 完成mysql句柄初始化
{
_mysql = MysqlInit();
if (_mysql == NULL)
exit(-1);
}
~TableVideo()
{
MysqlDestroy(_mysql);
}
新增、修改、删除
这里编写这三个功能的语句.
cpp
// 释放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);
}
全列查找、查找一个、模糊查找
这里我们需要说一下,我们的查找语句很好的执行,保存查找结果到本地也是可以的,但是这里存在一个线程不安全的问题,注意,我们的访问数据库可以理解是可以多个进程访问的,此时这里就是一个临界区,我们要加锁.
cpp
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;
}
};
测试
这里来个测试,这里具体的情况大家自行测试,具体的我就不列举了.
cpp
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 对应删除
下面我们开始构建我们每一个接口的具体的格式.
获取所有视频信息
cpp
请求:
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",
}
]
搜索指定关键字名称视频信息
cpp
请求:
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",
}
]
获取指定视频信息
cpp
请求:
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",
}
]
删除指定视频信息
cpp
请求:
DELETE /video/1 HTTP/1.1
响应:
HTTP/1.1 200 OK
修改指定视频信息
cpp
请求:
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 风格。
cpp
请求:
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 服务器完成。这也是咱们将网络通信模块与业务处理模块合并在一起完成的原因。
而业务处理模块所要完成的业务功能主要有:
- 客户端的视频数据和信息上传
- 客户端的视频列表展示(视频信息查询)
- 客户端的视频观看请求(视频数据的获取)
- 客户端的视频其他管理(修改,删除)功能
shell
[qkj@localhost source]$ touch server.hpp
cpp
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();//建立请求与处理函数的映射关系,设置静态资源根目录,启动服务器,
};
}
初始化操作
这里我们分为两步
- 初始化数据库
- 搭建服务器
cpp
bool RunModule()
{
tb_video = new TableVideo();
// 1. 初始化操作
// 创建根目录 www
FileUtil(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;
}
新增视频
这里是新增一个视频.
cpp
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.mp4
std::string root = WWWROOT;
std::string video_path = root + VIDEO_ROOT + video_name + video.filename;
//../http/www/image/变形金刚1.jpg
std::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);
}
修改视频
这是一个修改视频,注意,这里的修改我们注意一下,修改的是文件的名字和简绍,至于数据就不修改了.
cpp
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;
}
}
查找所有
我们的查询所有和模糊匹配都是一样的,在这里我们判断一下查询的时候是不是模糊匹配.
cpp
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;
}
查找一个
cpp
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()); // 捕捉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;
}
JsonUtil::Serialize(video, &rsp.body);
rsp.set_header("Content-Type", "application/json");
}
删除
cpp
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;
}
}
测试
下面我们就有一个测试
cpp
void ServerTest()
{
aod::Server server(8081);
server.RunModule();
}
编译好之后,这里我们使用Postman软件尽心测试,注意,这里的视频我放在下方的链接.
https://github.com/qkja/Project/tree/master/video_on_demand/test
前端页面
这里我们不实现,直接给大家代码.这是我们的源码链接.