项目——视频共享系统&&测试

目录

介绍

实现目标

服务器功能

服务器代码划分

环境搭建

认识第三方库

json库

数据库APT

httplib库

服务器

文件工具类

json工具类

视频数据表设计

视频数据类管理

通信接口设计

通信接口类设计

认识前端

HTML

基础标签

标题段落

图片

超链接

表格

顺序/无序

表单

选择

文本框

盒子

CSS

vue

v-cloak

v-bind

v-on

v-show

v-model

v-for

客户端

视频展示页面信息

视频展示页面新增视频按钮

播放页面视频播放

播放页面视频修改

播放页面视频删除

测试报告

项目背景

项目功能

项目计划

功能测试

自动化测试

性能测试


介绍

搭建视频共享点播服务器,可以让所有人通过浏览器访问服务器,实现视频的选择,上传,播放,删除(共享不适合删除,建议隐藏)

实现目标

主要是完成服务器端的程序业务功能的实现以及前端访问界面 html 的编写,能够支持客户端浏览器针对服务器上的所有视频进行操作

服务器功能

  • 针对客户端上传的视频文件以及封面图片进行备份存储;
  • 针对客户端上传的视频完成增删改查功能;
  • 支持客户端浏览器进行视频的观看功能

服务器代码划分

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

环境搭建

默认在gcc/g++版本很低,两个编译器时没有如何依赖共享,都要升级到7.3版本

bash 复制代码
sudo yum install centos-release-scl-rh centos-release-scl 
sudo yum install devtoolset-7-gcc devtoolset-7-gcc-c++
source /opt/rh/devtoolset-7/enable 
echo "source /opt/rh/devtoolset-7/enable" >> ~/.bashrc

安装 Jsoncpp 库

bash 复制代码
sudo yum install epel-release
sudo yum install jsoncpp-devel

下载 httplib 库:在搭建网络通信时就不用花大力气在这上面

bash 复制代码
git clone https://github.com/yhirose/cpp-httplib.git

Mysql 数据库及开发包安装

bash 复制代码
sudo yum install -y mariadb
sudo yum install -y mariadb-server
sudo yum install -y mariadb-devel

修改配置文件

bash 复制代码
sudo vim /etc/my.cnf.d/client.cnf
bash 复制代码
# /etc/my.cnf.d/client.cnf
# These two groups are read by the client library
# Use it for options that affect all clients, but not the server
#
[client]
# 新增下边一行配置,设置客户端默认字符集为utf8
default-character-set = utf8
# This group is not read by mysql client library,
# If you use the same .cnf file for MySQL and MariaDB,
# use it for MariaDB-only client options
[client-mariadb]
bash 复制代码
sudo vim /etc/my.cnf.d/mysql-clients.cnf
bash 复制代码
# /etc/my.cnf.d/mysql-clients.cnf
# These groups are read by MariaDB command-line tools
# Use it for options that affect only one utility
#
[mysql]
# 新增配置
default-character-set = utf8
[mysql_upgrade]
[mysqladmin]
[mysqlbinlog]
[mysqlcheck]
[mysqldump]
[mysqlimport]
[mysqlshow]
[mysqlslap]
bash 复制代码
sudo vim /etc/my.cnf.d/server.cnf
bash 复制代码
# /etc/my.cnf.d/server.cnf
# These groups are read by MariaDB server.
# Use it for options that only the server (but not clients) should see
#
# See the examples of server my.cnf files in /usr/share/mysql/
#
# this is read by the standalone daemon and embedded servers
[server]
# this is only for the mysqld standalone daemon
[mysqld]
# 新增以下配置
collation-server = utf8_general_ci
init-connect = 'SET NAMES utf8'
character-set-server = utf8
sql-mode = TRADITIONAL
# this is only for embedded server
[embedded]
# This group is only read by MariaDB-5.5 servers.
# If you use the same .cnf file for MariaDB of different versions,
# use this group for options that older servers don't understand
[mysqld-5.5]
# These two groups are only read by MariaDB servers, not by MySQL.
# If you use the same .cnf file for MySQL and MariaDB,
# you can put MariaDB-only options here
[mariadb]
[mariadb-5.5]

启动mysql服务

bash 复制代码
systemctl start mariadb

设置开机自启动

bash 复制代码
systemctl enable mariadb

进入数据库

bash 复制代码
mysql -uroot

设置字符集支持中文格式

bash 复制代码
create database demo_db charset utf8mb4;

认识第三方库

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
};

class JSON_API StreamWriter {
	virtual int write(Value const& root, std::ostream* sout) = 0;
}
//使用该设计模式来构造StreamWriter
class JSON_API StreamWriterBuilder : public StreamWriter::Factory {
	virtual StreamWriter* newStreamWriter() const;
}

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;
}

测试代码

cpp 复制代码
#include<iostream>                                                                                                                               
#include<string>
#include<sstream>
#include<memory>
#include<jsoncpp/json/json.h>

int main()
{
  const std::string name="张三";
  int age=22;
  float score[3]={77.5,88,99.5};
  Json::Value v;
  v["名字"]=name;
  v["年龄"]=age;
  v["成绩"].append(score[0]);
  v["成绩"].append(score[1]);
  v["成绩"].append(score[2]);
  //序列化
  Json::StreamWriterBuilder swr;//使用设计模式
  std::unique_ptr<Json::StreamWriter> sw(swr.newStreamWriter());

  std::stringstream ss;
  int ret=sw->write(v,&ss);//ss是输出参数
  if(ret==0)
    std::cout<<ss.str()<<std::endl;
  //反序列化
  const std::string s=ss.str();
  std::string err;
  Json::Value v1;
  Json::CharReaderBuilder crb;
  std::unique_ptr<Json::CharReader> cr(crb.newCharReader());
  bool tmp=cr->parse(s.c_str(),s.c_str()+s.size(),&v1,&err);
  if(tmp)
  {
    std::cout<<v1["名字"].asString()<<' '<<v1["年龄"].asInt()<<std::endl;
    std::cout<<v1["成绩"][0]<<' '<<v1["成绩"][1]<<' '<<v1["成绩"][2]<<std::endl;
  }
  return 0;
}                

运行代码

数据库APT

这里主要介绍 Mysql 的 C 语言 API 接口;Mysql 是 C/S 模式,其实编写代码访问数据库就是实现了一个 Mysql 客户端,实现专有功能

cpp 复制代码
//Mysql操作句柄初始化
MYSQL *mysql_init(MYSQL *mysql);
//参数为空则动态申请句柄空间进行初始化
//失败返回NULL

//连接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---连接的服务器的端口: 默认0是3306端口
//unix_socket---通信管道文件或者socket文件,通常置NULL
//client_flag---客户端标志位,通常置0
//返回值:成功返回句柄,失败返回NULL

//设置当前客户端的字符集
int mysql_set_character_set(MYSQL *mysql, const char *csname)
//mysql--初始化完成的句柄
//csname--字符集名称,通常:"utf8"
//返回值:成功返回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)
//返回值:成功返回结果集的指针, 失败返回NULL;

//获取结果集中的行数
uint64_t mysql_num_rows(MYSQL_RES *result);
//result--保存到本地的结果集地址
//返回值:结果集中数据的列数;

//获取结果集的列数
unsigned int mysql_num_fields(MYSQL_RES *result)
//result--保存到本地的结果集地址
//返回值:结果集中每一条数据的列数

//获取属性集
MYSQL_FIELD * mysql_fetch_fields(MYSQL_RES *result)

//遍历结果集
MYSQL_ROW mysql_fetch_row(MYSQL_RES *result)
//result--保存到本地的结果集地址
//返回值:实际上是一个char **的指针,
//将每一条数据做成了字符串指针数组 row[0]-第0列 row[1]-第1列
//并且这个接口会保存当前读取结果位置,每次获取的都是下一条数据

//释放结果集
void mysql_free_result(MYSQL_RES *result)
//result--保存到本地的结果集地址

//关闭数据库客户端连接,销毁句柄:
void mysql_close(MYSQL *mysql)

//获取mysql接口执行错误原因(一般用不到)
const char *mysql_error(MYSQL *mysql)

首先先准备一个库和创建一个普通用户,给普通用户库的所有权限

测试代码

cpp 复制代码
#include <iostream>
#include <mysql/mysql.h>

const char*host="192.168.109.148";
const char*user="zzj";
const char*password="0628";
const char*db="test_conn";
unsigned int port=3306;


int main()
{
  //创建句柄
  MYSQL* my=mysql_init(nullptr);
  if(my==nullptr) return 1;
  //连接数据库
  MYSQL* mys=mysql_real_connect(my,host,user,password,db,port,nullptr,0);
  if(mys==nullptr) return 2;
  //设置字符集使得操作支持中文
  int ret=mysql_set_character_set(mys,"utf8");
  if(ret!=0) return 3;
  //选择要操作的数据库
  //ret=mysql_select_db(mys,db);
  //if(ret!=0) return 4;
  //mysql指令
  const char* mysql="select *from people";
  ret=mysql_query(mys,mysql);
  if(ret!=0) return 5;
  //获取结果集 行数与列数
  MYSQL_RES* res=mysql_store_result(mys);
  int rows=mysql_num_rows(res);
  int files=mysql_num_fields(res);
  //打印属性
  MYSQL_FIELD* files_at=mysql_fetch_field(res);
  for(int i=0;i<files;i++)
  {
    std::cout<<files_at[i].name<<" ";
  }
  std::cout<<std::endl;
  //打印内容
  for(int i=0;i<rows;i++)
  {
    MYSQL_ROW content=mysql_fetch_row(res);
    for(int j=0;j<files;j++)
    {
      std::cout<<content[j]<<" ";
    }
    std::cout<<std::endl;
  }
  //释放结果集
  mysql_free_result(res);
  //关闭数据库连接
  mysql_close(mys);
  return 0;
}

把查询结果打印出来

httplib库

在httplib库中对应了两个对象:request 和 reponse

cpp 复制代码
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);//设置全套的重定向信息
};

此外还有请求到来时的request对象经过出来后生成reponse对象的各种函数方法

cpp 复制代码
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服务器静态资源根目录
};

一个一个分析:

cpp 复制代码
std::vector<std::pair<std::regex, Handler>>;//存放请求-处理函数映射
cpp 复制代码
std::function<TaskQueue *(void)> new_task_queue;//线程池
bool listen(const char *host, int port, int socket_flags = 0);

客户端发送请求后,tcp服务器通过监听listen接收到请求后,在线程池中找一个线程出来处理请求(这就是下面我们要处理的业务逻辑):先进行请求解析得到Request对象,根据请求方法与资源路径根据上面映射表来找处理函数:有就调用处理,并将空reponse对象传入;处理后就得到填充完毕的reponse对象,(httplib)根据它组成http响应后发送给客户端(不用我们关心),短连接就关闭套接字,长连接就等待超时后关闭,之后等待请求的到来...

cpp 复制代码
bool set_mount_point(const std::string &mount_point, const std::string &dir,
Headers headers = Headers());//设置http服务器静态资源根目录

可能请求的资源是静态资源,这时就不用我们自己处理,对请求路径进行拼接后再指定路径下找该资源,找到后自己返回即可

测试代码

cpp 复制代码
//test_httplib.cc
#include"httplib.h"    
#include<iostream>    
    
using namespace httplib;    
    
int main()    
{    
  Server sr;    
  //设置静态资源    
  sr.set_mount_point("/","./www");    
  //设置方法 正则表达式匹配 ()->捕捉 \d匹配数字 +匹配多个字符     
  sr.Get(R"(/number/(\d+))",[](const Request& req,Response& res)    
      {    
    
        res.set_content(req.matches[1],"text/html");//mathces[0]="/number/12345"    
      });    
  sr.Post("/multipart",[](const Request& req,Response& res)    
      {    
        auto file=req.get_file_value("file1");    
        std::cout<<file.content<<std::endl;                                                                                                                  
        res.status=200;    
      });    
  //监听    
  sr.listen("0.0.0.0",8080);    
  return 0;    
}    
html 复制代码
// www/index.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>  

服务器

文件工具类

编写一个文件工具类,实现:

  • 文件是否存在;
  • 文件大小;
  • 文件内容读取;
  • 文件内容写数据;
  • 创建目录(文件不存在时(目录也是文件))

主要是来熟悉使用系统调用与C++读写文件iofstream的写法

cpp 复制代码
#pragma once                                                                                                                                                 
#include <iostream>
#include <string>
#include <fstream>
#include <unistd.h>

#include <sys/types.h>
#include <sys/stat.h>

namespace util
{
  class FileUtil
  {
    public:
      FileUtil(const std::string&name): _FileName(name){}

      bool FileExit()
      {
        //access(F_OK)判断文件是否存在--存在返回0
        int ret=access(_FileName.c_str(),F_OK);
        if(ret<0)
        {
          std::cout<<"file is no exit"<<std::endl;
          return false;
        }
        return true;
      }
      size_t FileSize()
      {
        //stat获取文件所有属性
        struct stat buf;
        int ret=stat(_FileName.c_str(),&buf);
        if(ret<0)
        {
          std::cout<<"stat fail"<<std::endl;
        }
        return buf.st_size;
      }
      //读文件
      bool GetContent(std::string& content)
      {
        std::ifstream ifs;
        ifs.open(_FileName.c_str(),std::ios::binary);
        if(!ifs.is_open()) 
        {
          std::cout<<"read open file fail"<<std::endl;
          return false;
        }                                                                                                                                                    

        size_t size=FileSize();
        content.resize(size);
        ifs.read(&(content[0]),size);

        ifs.close();
        if(!ifs.good()) return false;
        return true;
      }
      //写文件
      bool SetContent(const std::string& content)
      {
        std::ofstream ofs;
        ofs.open(_FileName.c_str(),std::ios::binary);
        if(!ofs.is_open())
        {
          std::cout<<"write open file fail"<<std::endl;
          return false;
        }

        ofs.write(content.c_str(),content.size());

        ofs.close();
        if(!ofs.good()) return false;
        return true;
      }
   
      bool CreateDir()
      {
        if(FileExit())
        {
          std::cout<<"file exit"<<std::endl;
          return false;
        }
        int ret=mkdir(_FileName.c_str(),0777);
        if(ret<0)
        {
          std::cout<<"CreateDir fail"<<std::endl;
          return false;
        }
        return true;
      }

    private:
      std::string _FileName;
  };
};   

//功能测试
void test_util()    
{    
  util::FileUtil("./www").CreateDir();    
    
  std::string content;    
  util::FileUtil fu("./www/index.html");    
  fu.SetContent("<html></html>");    
  fu.GetContent(content);    
  std::cout<<content.c_str()<<" "<<fu.FileSize()<<std::endl;                                                                                                 
    
} 

json工具类

实现通信时使用json库进行序列化与反序列化(上面已经简单使用过到这里没难度~)

cpp 复制代码
//util.hpp
#include <string>
#include <fstream>
#include <sstream>
#include <memory>
#include <unistd.h>
#include <jsoncpp/json/json.h>

name util
{
    class FileUtil{}...
    class JsonUtil
    {
    public:
      static bool Serialize(Json::Value& root,std::string& content)
      {
        Json::StreamWriterBuilder swr;
        std::unique_ptr<Json::StreamWriter> sw(swr.newStreamWriter());
        std::stringstream ss;
        int ret=sw->write(root,&ss);
        if(ret<0)
        {
          std::cout<<"Serialize Fail"<<std::endl;
          return false;
        }
        content=ss.str();
        return true;
      }
      static bool Unserialize(Json::Value& root,std::string& content)
      {
        Json::CharReaderBuilder crb;
        std::unique_ptr<Json::CharReader> cr(crb.newCharReader());
        std::string err;
        int ret=cr->parse(content.c_str(),content.c_str()+content.size(),&root,&err);
        if(ret<0)
        {
          std::cout<<"Unserialize fail "<<err<<std::endl;
          return false;
        }
        return true;
      }
  };

};

//功能测试
void test_json()    
{    
  Json::Value root;    
  root["姓名"]="小王";    
  root["年龄"]=18;    
  root["成绩"].append(77.5);    
  root["成绩"].append(88.5);    
  root["成绩"].append(99.5);    
  std::string s;    
  util::JsonUtil::Serialize(root,s);    
  std::cout<<s<<std::endl;    
  Json::Value root1;    
  util::JsonUtil::Unserialize(root1,s);    
  std::cout<<root1["姓名"].asString()<<" "<<root1["年龄"].asInt()<<std::endl;    
    
  for(auto& a:root1["成绩"]) std::cout<<a<<" ";    
  std::cout<<std::endl;    
}

视频数据表设计

创建一个数据库aod_system,创建一张表用来保存视频的各种资源数据

视频数据类管理

写一个VideoData类:创建时自动进行mysql句柄初始化,连接数据库的操作;析构时自动关闭连接;其中我们实现各种接口,让外部简单调用实现对数据库的增删查改;

cpp 复制代码
#pragma once
#include <iostream>

#include "util.hpp"
#include "mysql/mysql.h"

namespace data
{
const char*host="192.168.109.148";
const char*user="zzj";
const char*password="0628";
const char*db="aod_system";
unsigned int port=3306; 

  static MYSQL* Mysql_Init()
  {   
    //创建句柄
    MYSQL* my=mysql_init(nullptr);
    if(my==NULL)
    {
      std::cout<<"mysql init fail"<<std::endl;
      return nullptr;
    }
    //连接数据库
    MYSQL* mys=mysql_real_connect(my,host,user,password,db,port,nullptr,0);
    if(mys==nullptr)
    {
      std::cout<<"link mysql fail"<<std::endl;
      return nullptr;
    }
    //设置字符集使得操作支持中文
    int ret=mysql_set_character_set(mys,"utf8");
    if(ret!=0)
    {
      std::cout<<"set character fail"<<std::endl;
      return nullptr;
    }
    return mys;
  }

  static void Mysql_Destory(MYSQL* my)
  {
    mysql_close(my);
  }
  static bool Mysql_Query(MYSQL* my,const std::string& mysql)
  {
    int ret=mysql_query(my,mysql.c_str());
    if(ret!=0)
    {
      std::cout<<mysql.c_str()<<std::endl;
      std::cout<<mysql_error(my)<<std::endl;
      return false;
    }
    return true;
  }
  class VideoData
  {
    public:
      VideoData()
      {
        _mysql=Mysql_Init();
        if(_mysql==nullptr) exit(-1);
      }
      ~VideoData()
      {
        Mysql_Destory(_mysql);
      }

      bool InsertMysql(const Json::Value& root);

      bool UpdateMysql(int video_id,const Json::Value& root);
      
      bool DeleteMysql(int video_id);

      bool SelectOne(int video_id,Json::Value& root);

      bool SelectAll(Json::Value& root);

      bool SelectLike(const std::string& s,Json::Value& root);

    private:
      MYSQL* _mysql;
      std::mutex _mutex;
  };
};

增删改的操作需要让外部传id和json对象进来,我们好通过这些数据拼接sql语句;查的操作就需要传一个空的json对象,把select查询到的数据填充到json对象中以输出型参数的形式返回(注意拼接sql语句的各种符号问题,如果用string来拼接很容易出错,推荐使用宏与sprintf进行格式化输出)

cpp 复制代码
    bool InsertMysql(const Json::Value &root)
    {
      // id name info video image
      std::string sql = "insert tb_video values (null,'" + root["name"].asString() + "','" +
                        root["info"].asString() + "','" + root["video"].asString() + "','" + root["image"].asString() + "')";
      // #define INSERT_MYSQL "insert tb_video values(null,'%s','%s','%s','%s');"
      // char sql[4096+root["info"].size()];
      // sprintf(sql,INSERT_MYSQL,root["name"].asString().c_str(),root["info"].asString().c_str(),root["video"].asString().c_str(),root["image"].asString().c_str());
      return Mysql_Query(_mysql, sql);
    }
    bool UpdateMysql(int video_id, const Json::Value &root)
    {
      std::string sql = "update tb_video set name='" + root["name"].asString() + "',info='" +
                        root["info"].asString() + "',video='" + root["video"].asString() + "',image='" +
                        root["image"].asString() + "'where id=" + std::to_string(video_id);
      return Mysql_Query(_mysql, sql);
    }
    bool DeleteMysql(int video_id)
    {
      std::string sql = "delete from tb_video where id=" + std::to_string(video_id);
      return Mysql_Query(_mysql, sql);
    }

查询结果与创建结果集的操作一定是原子的:查询之后的结果保存在结果集,立刻就要通过结果集函数进行获取,但因为最终实现时是多线程的方式有线程安全,所以要加锁

cpp 复制代码
bool SelectAll(Json::Value& root)
      {
        std::string sql="select *from tb_video";
        //查询sql与结果集之间要是原子的
        _mutex.lock();
        bool ret=Mysql_Query(_mysql,sql);
        if(ret==false) 
        {
          _mutex.unlock();
          return false;
        }
        //保存select内容到root中
        MYSQL_RES* res=mysql_store_result(_mysql);
        if(res==nullptr)
        {
          std::cout<<"gain result fail"<<std::endl;
          _mutex.unlock();
          return false;
        }
        _mutex.unlock();

        int row=mysql_num_rows(res);
        for(int i=0;i<row;i++)
        {
          MYSQL_ROW content=mysql_fetch_row(res);
          Json::Value tmp;
          tmp["id"]=content[0];
          tmp["name"]=content[1];
          tmp["info"]=content[2];
          tmp["video"]=content[3];
          tmp["image"]=content[4];
          root.append(tmp);
        }
        mysql_free_result(res);
        return true;
      }
      bool SelectOne(int video_id,Json::Value& root)
      {
        std::string sql="select *from tb_video where id="+std::to_string(video_id);
        //查询sql与结果集之间要是原子的
        _mutex.lock();
        bool ret=Mysql_Query(_mysql,sql);
        if(ret==false) 
        {
          _mutex.unlock();
          return false;
        }
        //保存select内容到root中
        MYSQL_RES* res=mysql_store_result(_mysql);
        if(res==nullptr)
        {
          std::cout<<"gain result fail"<<std::endl;
          _mutex.unlock();
          return false;
        }
        _mutex.unlock();
        MYSQL_ROW content=mysql_fetch_row(res);
        root["id"]=content[0];
        root["name"]=content[1];
        root["info"]=content[2];
        root["video"]=content[3];
        root["image"]=content[4];
        mysql_free_result(res);
        return true; 
      }
      bool SelectLike(const std::string& s,Json::Value& root)
      {
        std::string sql="select *from tb_video where name like '%%"+s+"%%'";
        //查询sql与结果集之间要是原子的
        _mutex.lock();
        bool ret=Mysql_Query(_mysql,sql);
        if(ret==false) 
        {
          _mutex.unlock();
          return false;
        }
        //保存select内容到root中
        MYSQL_RES* res=mysql_store_result(_mysql);
        if(res==nullptr)
        {
          std::cout<<"gain result fail"<<std::endl;
          _mutex.unlock();
          return false;
        }
        _mutex.unlock();

        int row=mysql_num_rows(res);
        for(int i=0;i<row;i++)
        {
          MYSQL_ROW content=mysql_fetch_row(res);
          Json::Value tmp;
          tmp["id"]=content[0];
          tmp["name"]=content[1];
          tmp["info"]=content[2];
          tmp["video"]=content[3];
          tmp["image"]=content[4];
          root.append(tmp);
        }
        mysql_free_result(res);
        return true;
      }

通信接口设计

所谓的http协议,本质上是tcp协议传输时应用层所规定的一种数据格式,通信接口设计就是要知道什么样的请求对应数据库上增删查改的那个操作,这个过程我们不用自己实现,在restful中就定义好了:GET方法表示查询,POST方法表示新增,PUT方法表示修改,DELETE方法表示删除,正文数据格式通常采用的是json或者xml的数据格式;

通信接口类设计

主要实现的是:面对不同请求时设计出不同的接口来处理;而通信过程中的一系列搭建如创建套接字,监听,连接...都有httplib库提供的Server对象和对应接口来使用,不用我们自己来搭建,这是为了让我们有更多精力放在业务处理逻辑上来

cpp 复制代码
#pragma once
#include "httplib.h"
#include "data.hpp"
namespace aod
{
const std::string wwwroot="./www";
const std::string video_root="/video/";
const std::string image_root="/image/";
const std::string ip="0.0.0.0";
// httplib是基于多线程要设计为全局的
data::VideoData* vd=nullptr;
  class server
  {
    private:
     unsigned int _port;
     httplib::Server _sr;//httplib库为我们搭建通信处理的桥梁
    private:
     //各种不同的请求对应的处理
     static void Insert(const httplib::Request& req,httplib::Response& res);
     static void Delete(const httplib::Request& req,httplib::Response& res);
     static void Select(const httplib::Request& req,httplib::Response& res);
     static void Update(const httplib::Request& req,httplib::Response& res);
    public:
     server(unsigned int port):_port(port){}
     //服务器整体设计
     bool RunServer()
  };
};

RunServer 函数,连接前的准备工作:

  • 初始化视频数据类VideoDate;
  • 创建出资源存放的目录;
  • 建立httplib接口提供的路由表:不同的资源请求(不同的请求方法和资源路径)到来是传入我们自己写的方法来处理
cpp 复制代码
     bool RunServer()
     {
       //初始化视频资源类
       vd=new data::VideoData();
       //设置资源路径
       util::FileUtil(wwwroot).CreateDir();
       util::FileUtil(wwwroot+video_root).CreateDir();
       util::FileUtil(wwwroot+image_root).CreateDir();
       _sr.set_mount_point("/",wwwroot);
       //访问资源对应的处理方法
       _sr.Post("/video",Insert);
       _sr.Delete("/video/(\\d+)",Delete);
       _sr.Put("/video/(\\d+)",Update);
       _sr.Get("/video/(\\d+)",SelectOne);
       _sr.Get("/video/",Select);// 这样路由 /video -> postman 404 Bug?
       //监听
       _sr.listen(ip,_port);
       return true;
     } 

Insert 函数,当资源请求:POST /video 时就调用它来处理:

  • 通过request提供的接口获取客户端提交的视频信息,也就是name,info,video_url(视频信息),image_url(图片信息)
  • 通过上面的信息拼接成视频绝对路径,图片绝对路径保存客户端提交的视频资源,图片资源;
  • 数据库也要新增有关这个客户端提交的视频信息;
cpp 复制代码
static void Insert(const httplib::Request& req,httplib::Response& res)
     {
       if(req.has_file("name")==false||
          req.has_file("info")==false||
          req.has_file("video")==false||
          req.has_file("image")==false)
       {
         res.status=400;
         res.set_content(R"({"result":false,"content":"输入参数缺失"})","application/json");
         return;
       }
       //httplib::MultipartFormData中有{name,content_type,content,filename,}
       //将请求获取到的资源进行保存
       auto name_source=req.get_file_value("name");
       auto info_source=req.get_file_value("info");
       auto video_source=req.get_file_value("video");
       auto image_source=req.get_file_value("image");
       //拼接资源所要保存的绝对路径 ./wwwroot/video/XXXX
       //拼接name避免命名重复
       std::string video_path=wwwroot+video_root+name_source.content+video_source.filename;
       std::string image_path=wwwroot+image_root+name_source.content+image_source.filename;
       if(util::FileUtil(video_path).SetContent(video_source.content)==false)
       {
         res.status=500;
         res.set_content(R"({"result":false,"content":"保存视频失败"})","application/json");
         return;
       }
       if(util::FileUtil(image_path).SetContent(image_source.content)==false)
       {
         res.status=500;
         res.set_content(R"({"result":false,"content":"保存image失败"})","application/json");
         return;
       }
       //把相关信息保存到数据库中
       Json::Value v;
       v["name"]=name_source.content;
       v["info"]=info_source.content;
       //拼接相对路径
       std::string vo=video_root+name_source.content+video_source.filename;
       std::string ie=image_root+name_source.content+image_source.filename;
       v["video"]=vo;
       v["image"]=ie;
       if(vd->InsertMysql(v)==false)
       {
         res.status=500;
         res.set_content(R"({"result":false,"content":"保存数据库失败"})","application/json");
         return;
       }
     }

Delete 函数,当资源请求:DELETE /video/number 时就调用它来处理:

  • 通过req的mathes捕获正则表达式匹配的数字,也就是视频id;
  • 通过视频id在数据库中查询它的相关信息;
  • 通过查询到的信息拼接视频,图片资源所在的路径并进行删除;
  • 使用视频id删除在数据库的相关数据
cpp 复制代码
    static void Delete(const httplib::Request& req,httplib::Response& res)
     {
       // /video/(\\d+)捕获的数字 matches[0]得到完整字符串
       int id=std::stoi(req.matches[1]);
       // 查询数据
       Json::Value v;
       vd->SelectOne(id,v);
       //删除保存的资源文件
       std::string video_file=wwwroot+v["video"].asString();
       std::string image_file=wwwroot+v["image"].asString();
       if(remove(video_file.c_str())!=0)
       {
         res.status=500;
         res.set_content(R"({"result":false,"content":"删除视频资源失败"})","application/json");
         return;
       }
       if(remove(image_file.c_str())!=0)
       {
         res.status=500;
         res.set_content(R"({"result":false,"content":"删除图片资源失败"})","application/json");
         return;
       }

       //删除数据库信息
       if(vd->DeleteMysql(id)==false)
       {
         res.status=500;
         res.set_content(R"({"result":false,"content":"删除数据库数据失败"})","application/json");
         return;
       }
     }

Modify函数,当资源请求:PUT /video/number {修改后的数据} 时就调用它来处理:

  • 获取视频id;
  • 通过request的body函数获取修改后的数据字符串;
  • 进行反序列化处理成Json格式;
  • 通过视频id和Json格式在数据库中进行修改
cpp 复制代码
     static void Update(const httplib::Request& req,httplib::Response& res)
     { 
       int id=std::stoi(req.matches[1]);
       std::string s=req.body;
       //反序列化才能修改
       Json::Value v;
       if(util::JsonUtil::Unserialize(v,s)==false)
       {
         res.status=400;
         res.set_content(R"({"result":false,"content":"反序列化失败"})","application/json");
         return;
       }
       if(vd->UpdateMysql(id,v)==false)
       {
        std::cout<<-1<<std::endl;
         res.status=500;
         res.set_content(R"({"result":false,"content":"更新数据库数据失败"})","application/json");
         return;
       }
     }

SelectOne函数,当资源请求:Get /video/number 时就调用它来处理:

  • 获取视频id;
  • 数据库中查询该id下的相关信息Json;
  • Json反序列化后填写填写响应正文与格式
cpp 复制代码
     static void SelectOne(const httplib::Request& req,httplib::Response& res)
     {
       int id=std::stoi(req.matches[1]);
       Json::Value v;
       if(vd->SelectOne(id,v)==false)
       {
         res.status=500;
         res.set_content(R"({"result":false,"content":"查询数据库数据失败"})","application/json");
         return;
       }
       //组织响应正文给客户端
       std::string result;
       if(util::JsonUtil::Serialize(v,result)==false)
       {
         res.status=500;
         res.set_content(R"({"result":false,"content":"序列化失败"})","application/json");
         return;
       }
       res.set_content(result,"application/json");
     }

SelectALL函数,当资源请求:Get /video/ 时就调用它来处理:

  • 先判断是否为模糊匹配,使用:request的has_param;
  • 是模糊匹配的话要进行标记,还要获取模糊匹配关键字,使用模糊匹配查询接口
  • 不是就使用查询接口
  • Json反序列化后填写填写响应正文与格式
cpp 复制代码
     static void Select(const httplib::Request& req,httplib::Response& res)
     {
       bool tmp=false;
       std::string key;
       if(req.has_param("search"))
       {
         tmp=true;
         key=req.get_param_value("search");// /video?search="key"
       }
       Json::Value v;
       //模糊匹配
       if(tmp)
       {
         if(vd->SelectLike(key,v)==false)
         {
           res.status=500;
           res.set_content(R"({"result":false,"content":"模糊查询数据库数据失败"})","application/json");
           return;
         }
         
       }
       else
       {
         if(vd->SelectAll(v)==false)
         {
           res.status=500;
           res.set_content(R"({"result":false,"content":"更新数据库数据失败"})","application/json");
           return;
         }
       }
       std::string result;
       if(util::JsonUtil::Serialize(v,result)==false)
       {
         res.status=500;
         res.set_content(R"({"result":false,"content":"序列化失败"})","application/json");
         return;
       }
       res.set_content(result,"application/json");
     }

测试使用postman进行各个请求接口测试,要严格按照设计的请求方法与url来进行测试

新增视频(同时检测服务器的指定路径与数据库是否新增客户端提交的视频信息)

获取所有视频信息

通过id获取视频信息

通过name的模糊匹配获取视频信息

通过id与修改后的视频信息(Json格式)更新指定视频的信息(查看数据库是否同步更新了)

通过id删除指定视频信息(查看指定路径与数据库是否没有了指定视频信息)

认识前端

HTML

HTML 代码是由标签构成的。我们可以理解不同的标签代表不同的控件元素,前端浏览器拿到 html 代码之后,根据标签之间的关系进行解析,得到一棵 DOM(Document Object Mode - 文档对象模型的缩写) 树 ,然后它根据 DOM 树渲染出不同的控件元素,得到我们所看到的页面

编写一个简单的HTML代码

html 复制代码
<html>
    <head>
    <title>第一个html页面</title>
    <meta charset="UTF-8">
    </head>
    <body id="唯一标识符">
        hello html
    </body>
</html>

编写HTML代码首先写<html> </html>:表面这是一个HTML代码;通常是一前一后双标签组成,少部分是单标签;head标签通常是页面的信息,如标题,字符集格式等;body标签通常是页面的内容,图片,表格,表单等,通常是head先加载;id表明给该标签一个特殊的标识符

基础标签

标题段落

标题标签:h1~h6,段落标签:p,还有单标签br:br使用把前后内容进行换行;而在p中的内容单独形成段落

html 复制代码
<html>
    </head>
    <body id="唯一标识符">
        <h1>你好</h1>
        <p>段落标签</p>
        第一行<br>第二行
        hello html
    </body>
</html>

图片

html 复制代码
<img src="图片储存路径" title="图片名" >

超链接

html 复制代码
<a href="网址">网站点击内容</a>

表格

  • 表格:table
  • tr:表格的一行
  • th:表头单元格,加粗
  • td:判断单元格
  • colspan:一行n列合并成1列
  • align:(表格)所在区域
  • border:(表格)边框是否有,默认为0
  • celladding:字与表格的间距
  • cellspacing:表格的间距
  • width:(表格)宽度
  • height:(表格)高度
html 复制代码
    <table align="center" border="1" cellpadding="2" cellspacing="1" width="200" height="20">
        <tr>
            <th>菜名</th>
            <th>单价</th>
            <th>折扣</th>
        </tr>
        <tr>
            <td>红烧茄子</td>
            <td>10</td>
            <td>8折</td>
        </tr>
        <!-- colspan 合并列 -->
        <td colspan="3">金额:8¥</td>

    </table>

顺序/无序

无序列表 ul,配合 li 使用;有序列表 ol,配合 li 使用;自定义列表 dl,配合 dt 和 dd,dt 是标题,dd 是标题下的内容

html 复制代码
    <ol>
        <li>order list</li>
    </ol>
    <ul>
        <li>unorder list</li>
    </ul>
    <dl>
        <dt>标题</dt>
        <dd>段落</dd>
    </dl>

表单

  • form:表单标签
  • input:输入标签
  • action:服务器端程序地址
  • method:提交方式
  • enctype:编码类型,其中 multipart/form-data 常用于文件上传
  • type:输入的类型(默认是text类型)
  • name:表单上传时输入类型的字段名
  • placeholder:输入文本框的提示词
  • value:在file类型时无效,在submit中显示按钮提示词;不同的类型有不同的功能
html 复制代码
<form action="server url" method="get" enctype="multipart/form-data">
        <input type="text" name="text_input" placeholder="input text"> <br>
        <input type="file" name="file_input" value="file select"> <br>
        <input type="submit" name="data_input" value="submit data"> <br>
</form>

选择

  • selected:默认选择
html 复制代码
<select name="slect year">
        <option  selected="selected">选择年份</option>
        <option>2019</option>
        <option>2020</option>
</select>

文本框

html 复制代码
<textarea name="文本标签" rows="5" cols="3"></textarea>

盒子

无语义标签:div/span:用来布置页面布局

html 复制代码
<div>占一行</div>
<span>不占一行</span>
<span>真的</span>

div 没有语义. 对于搜索引擎来说没有意义. 为了让搜索引擎能够更好的识别和分析页面(SEO 优化), HTML 引入了更多的 "语义化" 标签. 但是这些标签其实本质上都和 div 一样(对于前端开发来说). 然而对于搜索引擎来说, 见到 header 和 article 这种标签就会重点进行解析

  • header:头部
  • nav:导航
  • article:内容

视频标签:video,音频标签:audio

  • src:资源所在位置;
  • controls:控制视频/音频界面播放控制
html 复制代码
<video src="https://www.runoob.com/try/demo_source/movie.mp4" controls="controls"type="video/mp4"></video>
<audio src="https://www.runoob.com/try/demo_source/horse.mp3" controls="controls"type="audio/mp3"></audio>

CSS

CSS 能够对网页中元素位置的排版进行像素级精确控制, 实现美化页面的效果. 能够做到页面的样式和结构分离

选择器 {n条说明};常见的选择器:通配符*,标签选择器,子类选择器,id选择器

html 复制代码
    <style>
        /*通配选择器*-对所有的标签产生效果,页面布局重置靠左*/
        *{
            margin: 0;
            padding: 0;
        }
        a{
            color:red;
            size: 20px;
        }
        .green
        {
            color:green;
        }
        #blue
        {
            color:blue;
        }    
    </style>
    <a>雷猴啊</a>  
    <a class="green">雷猴啊</a>  
    <a id="blue">雷猴啊</a>  

vue

安装

html 复制代码
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

使用vue

html 复制代码
<body>
<div id="app">{{message}}</div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    var app = new Vue({
    el: '#app',
    data: {
    message: 'hello',
    },
    });
</script>
  • {{XXX}} 里面{}是插值操作
  • 创建了一个app对象,一个d选择器
  • new Vue 后面跟的是json格式的字段
  • el:选择器类型
  • data:数据字段

引用app对象后使用message字段,会自动替换成message在data定义的数据,在页面可以根据需要修改

v-cloak

v-cloak:遮罩,message变成数据需要通过vue加载出来,可能其中会有一小段时间{{message}}显示,我们不想让用户看到就可以使用v-cloak让它数据没出来之前不显示

html 复制代码
<style>
        [v-cloak]
        {
            display: none;
        }
</style>

<div id="app" v-cloak>
    {{str}} {{str1}}
</div>

v-bind

v-bind:使用<a>标签通过data里面的url动态调整网站链接需要使用v-bind绑定

html 复制代码
<div id="app" v-cloak>
<a v-bind:href=url>点击</a>
</div>


<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    var app = new Vue({
    el: '#app',
    data: {
    str: 'hello',
    url:"https://www.baidu.com"
    },
    });
</script>

v-on

v-on:<button>标签点击后要设置app中的方法就要使用v-on进行事件的监听,且app中方法函数统一写在methods方法中

html 复制代码
<div id="app">
<button v-on:click="dialog('点击了')">点击按钮</button>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    var app = new Vue({
    el: '#app', 
    methods:{
        dialog:function(s){
            alert(s);
        }
    },
    });
</script>

v-show

v-show,后面=true是为真,显示下面的内容;为假就不显示

html 复制代码
<div id="app" v-show="!tmp">
<button v-on:click="dialog('点击了')">点击按钮</button>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    var app = new Vue({
    el: '#app',
    data: {
        tmp:false
    },
    methods:{
        dialog:function(s){
            alert(s);
        }
    }
    });
</script>

相应地还有v-if,v-else:它们解析是才是真正的判断,而v-show为假只是把内容隐藏了

html 复制代码
<div id="app">
    <div v-for="hero in heros">{{hero}}</div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    var app = new Vue({
    el: '#app',
    data: {
        heros:['小乔','二乔','大乔']
    },
    });
</script>

v-model

可以将一个 vue 数据与标签数据关联起来,实现一荣俱荣一损俱损的效果

html 复制代码
<div id="app">
    <input type="text" v-model="tmp">
    {{tmp}}
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    var app = new Vue({
    el: '#app',
    data: {
        tmp:'hello world'
    },
    });
</script>

v-for

v-for:循环可以绑定数据到数组来渲染一个标签

html 复制代码
<div id="app">
    <div v-for="hero in heros">{{hero}}</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    let app = new Vue({
    el: '#app',
    data: {
    heros: ['小乔', '曹操', '李白'],
    }
});
</script>

使用ajax异步请求服务器资源

html 复制代码
<script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
	let app = new Vue({
		el: '#app',
		data: {
			numbers: 0,
			videos: []
		},
		methods: {
			myclick: function () {
				$.ajax({
					url: "http://192.168.122.137:9090/video",
					type: "get",
					context: this,//这里是将vue对象传入ajax作为this对象
					success: function (result, status, xhr) {//请求成功后的处理函数
						this.videos = result;
						alert(result);
					}
				})
			}
		}
	});
</script>

客户端

如果从0到1写页面很费时间,可以选择视频页面模板进行修改成我们想要的页面

视频展示页面信息

  • 在vue的methods中:添加GetVideo方法;
  • 实现:ajx发送 GET /video请求所有视频数据后保存在data数组videos中;
  • 视频展示页面以v-for的方式得到每个视频数据并进行填写,包括:视频名,视频图片,以及点击视频跳转后的url中,把视频id给携带上(后面要用到)

视频展示页面新增视频按钮

  • 找到页面的弹窗按钮(点击后会弹出弹窗要我们填写信息,本质上是form);
  • 用户信息填写的内容修改成:视频名,视频内容,视频文件,图片文件;
  • form表单中添加:action = "/video",method = "post",enctype="multipart/form-data";
  • 提交form表单后就给服务器发送 POST /video请求新增视频;
  • 在代码中处理新增视频的业务处理后的响应添加上重定向,跳转到当前页面

播放页面视频播放

  • 跳转到播放页面中的url携带了要播放的视频id,要将id值进行截取;
  • 使用ajx方法发送 Put /video/3 请求视频id为3的视频数据保存在video中;
  • 在播放的src链接给上video.video值(video是视频保存路径),给上下面视频内容video.info

播放页面视频修改

  • 找到页面弹窗按钮,修改弹窗内的信息:视频名,视频内容;
  • 使用ajx方法发送 Put /video/3 请求更新视频id为3的视频数据(原视频数据转成字符串格式一起发送),新视频数据保存在video中;
  • 刷新当前页面:window.location.reload()

播放页面视频删除

  • 新增页面弹窗按钮(拷贝上面按钮代码)
  • 绑定v-on一个ajx方法:发送 Delete /video/3 请求删除视频id为3的视频数据
  • 重定向当前页面到视频展示页面: window.location.href="/index.html"

测试报告

项目背景

视频共享系统采用前端 + 后端实现,使用数据库 + 文件路径的储存用户提交的视频与图片;前端实现了两个页面:视频列表页面与视频播放页面;后端实现了:新增视频,修改视频,查询视频,删除视频的功能;数据库储存了视频,图片的相对路径,前端通过它来找到视频,图片的储存路径;

项目功能

主要实现了:新删查改视频:

新增视频:用户填写完视频相关数据后,后端使用数据库储存的同时,把视频和图片资源放在指定的文件中,下次用户点击视频想要观看时就可以通过路径找到指定的文件;

删除视频:在视频播放页面上面实现了点击视频删除按钮,点击后页面播放的视频相关视频将被删除,同时重定向到视频列表页面;

查找视频:在视频列表页面上可以根据关键词搜索到相关视频页面,用户就可以找到想要的视频点击进行观看;

修改视频:在视频播放页面上实现了视频修改按钮,用户可以修改当前的视频名,视频内容,修改完成后当前视频数据即可生效

项目计划

功能测试

先整理出思维导图


新增视频:填写有效的视频数据与视频,图片文件

结果正确显示在视频列表中

什么都不填写直接提交,此时返回Json格式的报错


查找视频:在搜索框中填写有效的关键词,在当前视频列表能够找到的视频名

结果能够正确返回相关视频

搜索框中什么也不填写直接提交,返回的是全部视频列表


点击视频列表中的任意一个视频播放按钮,可以正常播放

点击没有提交过视频文件的视频办法按钮,则视频播放异常


修该视频:点击播放页面修改视频按钮,提交新的视频名,内容

结果正确显示出修改后的新视频名,内容


删除视频:点击删除视频按钮删除当前播放的视频数据,返回的视频列表中没有显示删除视频的相关数据

自动化测试

以页面为单位,使用selenium库来编写自动化测试脚本进行测试

实现一个类Driver用来初始化driver并生成一个BlogDriver对象 ,每次测试时使用BlogDriver获取driver;在类Driver中设计出屏幕截图方法函数

python 复制代码
//Ultils.py
import datetime
import os
import sys
import time

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.edge.options import Options
from selenium.webdriver.edge.service import Service
from webdriver_manager.microsoft import EdgeChromiumDriverManager
#使用Driver对象进行测试
class Driver:
    driver=""
    def __init__(self):
        option =Options()
        self.driver=webdriver.Edge(service=Service(EdgeChromiumDriverManager().install()),options=option)
        self.driver.implicitly_wait(5)
    def SaveScreem(self):
        #每天截图放在同一个文件夹上
        dirname=datetime.datetime.now().strftime("%Y-%m-%d")
        if not os.path.exists("../image/"+dirname):
            os.mkdir("../image/"+dirname)
        # 测试方法-日期.png
        filename=sys._getframe().f_back.f_code.co_name+"-" +datetime.datetime.now().strftime("%Y-%m-%d-%H.%M.%S")+".png"
        # ../image/日期文件夹/测试方法-日期.png
        self.driver.save_screenshot("../image/"+dirname+"/"+filename)
    def SwitchPage(self):
        BeforeHandle = BlogDriver.driver.current_window_handle
        BlogDriver.driver.find_element(By.CSS_SELECTOR,"#home-main > div > div.col-lg-9.col-md-12.col-sm-12 > div.row.auto-clear > article > div > div.thumbr > a > span > i").click()
        # 切换页面
        Allhandle = BlogDriver.driver.window_handles
        for handle in Allhandle:
            if handle != BeforeHandle:
                BlogDriver.driver.switch_to.window(handle)
#单例模式
BlogDriver=Driver()

视频列表页面设计三个测试:新增视频测试,视频搜索测试,视频播放测试;视频播放测试要进行driver句柄的跳转,这个可以在Driver类中设计成函数,因为后面页面的测试也要用到;视频播放在html设计时放在ifname标签页,也是要先进行ifname切换后,才能定位元素并点击视频播放(注意:很多元素使用隐式等待无法正常截图到需要显示等待才能正确截图)

python 复制代码
from cryptography.hazmat.primitives.asymmetric import ec
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as ec
from selenium.webdriver.support.ui import WebDriverWait
from common.Ultils import BlogDriver
from selenium.webdriver.common.keys import Keys


class VideoLists:
    url=""
    driver=""
    def __init__(self):
        self.url="http://192.168.109.148:8888/"#测试网址
        self.driver=BlogDriver.driver
        self.driver.get(self.url)

    def TestInserVideo(self):
        BlogDriver.SaveScreem()
        BlogDriver.driver.find_element(By.CSS_SELECTOR,"#home1 > div.row.header-top > div.col-lg-3.col-md-6.col-sm-7.hidden-xs > div > button").click()
        #填写视频数据
        BlogDriver.driver.find_element(By.CSS_SELECTOR,"#name").send_keys("抖音视频")
        BlogDriver.driver.find_element(By.CSS_SELECTOR,"#info").send_keys("这时一个抖音视频")
        BlogDriver.driver.find_element(By.CSS_SELECTOR,"#video").send_keys(r"C:\Users\29096\Desktop\1.mp4")
        BlogDriver.driver.find_element(By.CSS_SELECTOR,"#image").send_keys(r"C:\Users\29096\Desktop\1.png")
        BlogDriver.driver.find_element(By.CSS_SELECTOR,"#enquirypopup > div > div > div.modal-body > form > div:nth-child(5) > button").click()
        #等待视频列表刷新新视频数据
        WebDriverWait(self.driver,6).until(ec.visibility_of_element_located((By.CSS_SELECTOR,"#home-main > div > div.col-lg-9.col-md-12.col-sm-12 > div.row.auto-clear > article > div > div.thumbr > a > img")))
        BlogDriver.SaveScreem()
        # BlogDriver.driver.quit()
    def TestSelect(self):
        WebDriverWait(self.driver,6).until(ec.visibility_of_element_located((By.CSS_SELECTOR,"#home-main > div > div.col-lg-9.col-md-12.col-sm-12 > div.row.auto-clear > article > div > div.thumbr > a > img")))
        BlogDriver.SaveScreem()
        input_element=BlogDriver.driver.find_element(By.CSS_SELECTOR,"#search")
        input_element.send_keys("抖音")
        input_element.send_keys(Keys.RETURN)
        #等待弹窗
        WebDriverWait(BlogDriver.driver, 6).until(ec.alert_is_present())
        alert=BlogDriver.driver.switch_to.alert
        alert.accept()
        BlogDriver.SaveScreem()
    def TestVideo(self):
        #句柄切换
        BlogDriver.SwitchPage()
        # 1. 等待 iframe 加载并切换
        iframe = WebDriverWait(BlogDriver.driver, 6).until(
            ec.presence_of_element_located((By.TAG_NAME, "iframe"))
        )
        BlogDriver.driver.switch_to.frame(iframe)  # 切换到 iframe 内部
        # 2. 定位视频元素
        video = WebDriverWait(BlogDriver.driver, 6).until(
            ec.presence_of_element_located((By.TAG_NAME, "video"))
        )
        # 3. 等待视频播放
        WebDriverWait(BlogDriver.driver, 6).until(
            lambda driver: driver.execute_script("return arguments[0].currentTime > 0", video)
        )
        BlogDriver.SaveScreem()
        # 5. 切回主页面
        BlogDriver.driver.switch_to.default_content()
        # BlogDriver.driver.quit()

#调用测试函数
#VideoList=VideoLists()
#VideoList.TestInserVideo()
#VideoList.TestVideo()
#VideoList.TestSelect()

视频播放页面就简单些,实现时一定要先进行句柄切换

python 复制代码
from selenium.webdriver.support import expected_conditions as ec
from selenium.webdriver.common.by import By
from selenium.webdriver.support.wait import WebDriverWait

from common.Ultils import BlogDriver

class VideoWatch:
    url=""
    driver=""
    def __init__(self):
        self.url="http://192.168.109.148:8888"
        self.driver=BlogDriver.driver
        self.driver.get(self.url)

    def TestModifyVideo(self):
        BlogDriver.SwitchPage()
        WebDriverWait(self.driver, 6).until(ec.visibility_of_element_located((By.CSS_SELECTOR, "#single-video-wrapper > div:nth-child(2) > div > article > div.video-content")))
        BlogDriver.SaveScreem()
        BlogDriver.driver.find_element(By.CSS_SELECTOR,"#single-video > div.row.header-top > div.col-lg-3.col-md-6.col-sm-7.hidden-xs > div > button:nth-child(2)").click()
        BlogDriver.driver.find_element(By.CSS_SELECTOR, "#name").clear()
        BlogDriver.driver.find_element(By.CSS_SELECTOR, "#name").send_keys("好看视频")
        BlogDriver.driver.find_element(By.CSS_SELECTOR, "#info").clear()
        BlogDriver.driver.find_element(By.CSS_SELECTOR, "#info").send_keys("这是一个好看视频")
        BlogDriver.driver.find_element(By.CSS_SELECTOR,"#enquirypopup > div > div > div.modal-body > form > div:nth-child(3) > button").click()
        WebDriverWait(self.driver, 6).until(ec.visibility_of_element_located((By.CSS_SELECTOR, "#single-video-wrapper > div:nth-child(2) > div > article > div.video-content")))
        BlogDriver.SaveScreem()

    def TestDeleteVideo(self):
        BlogDriver.SwitchPage()
        BlogDriver.driver.find_element(By.CSS_SELECTOR,"#single-video > div.row.header-top > div.col-lg-3.col-md-6.col-sm-7.hidden-xs > div > button:nth-child(1)").click()
        WebDriverWait(self.driver,6).until(ec.visibility_of_element_located((By.CSS_SELECTOR,"#home-main > h2")))
        BlogDriver.SaveScreem()

#测试函数
#VideoWatch=VideoWatch()
#VideoWatch.TestModifyVideo()
#VideoWatch.TestDeleteVideo()

一个一个测试没问题后一同放在Main中进行测试

python 复制代码
from common.Ultils import BlogDriver
from tests.VideoLists import VideoLists
from tests.VideoWatch import  VideoWatch

if __name__ == "__main__":
    VideoLists().TestInserVideo()
    VideoLists().TestSelect()
    VideoLists().TestVideo()
    VideoWatch().TestModifyVideo()
    VideoWatch().TestDeleteVideo()
    BlogDriver.driver.quit()

Bug:测试是发现相同的视频数据插入时如果一个删除后另一个就删除不掉了;原因是:新增相同视频数据后服务器只保留一份;解决:服务器保存时在文件名后加入时间戳防止只保留一份(也可以在处理新增视频业务模块上进行判断,相同视频数据插入就不做新增)

性能测试

使用jmeter进行按照增删查改的顺序为一个事务,进行梯度压测;单线程时所有请求成功,多线程并发请求新增视频数据,前几秒成功,后面请求全部失败,原因是mysql服务器断开连接,报错数据失败,需要优化数据库面对多连接到来时进行处理

以上测试文件都提交到项目文件中,有需要点击链接查看

相关推荐
程序员鱼皮2 天前
Cursor 网页版来了,这下拉屎时也能工作了
计算机·ai·程序员·开发·项目·编程经验
程序员鱼皮20 天前
知名开源项目Alist被收购!惹程序员众怒,开团炮轰甲方
互联网·github·项目
利刃大大1 个月前
【在线五子棋对战】二、websocket && 服务器搭建
服务器·c++·websocket·网络协议·项目
Kx…………2 个月前
Day3:设置页面全局渐变线性渐变背景色uniapp壁纸实战
前端·学习·uni-app·实战·项目
Kx…………3 个月前
Day3:个人中心页面布局前端项目uniapp壁纸实战
前端·学习·uni-app·实战·项目
Kx…………3 个月前
Day2:前端项目uniapp壁纸实战
前端·学习·uni-app·实战·项目
Kx…………3 个月前
Day2-2:前端项目uniapp壁纸实战
前端·学习·uni-app·html·实战·项目
Zfox_3 个月前
【C++项目】从零实现RPC框架「四」:业务层实现与项目使用
linux·开发语言·c++·rpc·项目
不修×蝙蝠3 个月前
SpringBoot项目实战(初级)
java·spring boot·后端·项目·实训