🌈C++专栏: 南桥谈C++
🌈C语言专栏: C语言学习系列
🌈Linux学习专栏: 南桥谈Linux
🌈数据结构学习专栏: 数据结构杂谈
🌈数据库学习专栏: 南桥谈MySQL
🌈Qt学习专栏: 南桥谈Qt
🌈菜鸡代码练习: 练习随想记录
🌈git学习: 南桥谈Git
文章目录
- 前言
- 开发环境
- 第三方库
- jsoncpp
- Mysql 数据库 API 认识
- httplib
- 项目实现
- 服务端工具类实现
- 文件实用工具类
- 完整代码
- 功能测试
- Json实用工具类实现
- 功能测试
- 服务端数据管理功能
- 视频数据表的设计
- 数据管理类设计
- 功能测试
- 服务端业务处理模块
- 网络通信接口设计
- 业务处理类模块设计
- 功能测试
- 前端界面实现
前言
项目源代码:代码仓库
开发环境
项目名称:CineShare++(基于C++的视频点播系统)
项目功能:搭建一个共享点播系统,服务器能够支持用户通过前端浏览器进行访问,提供清晰的展示、观看和操作界面。该系统不仅可以实现视频的上传功能,还能满足用户观看视频的需求。同时,系统还具备基本的管理功能,包括视频的增、删、改、查等操作,以便管理员对视频内容进行有效管理和维护。通过此系统,用户可以便捷地上传和浏览视频,管理员则能确保内容的有序管理与更新。
开发环境包括:Ubuntu 22.04 操作系统
,VSCode 编辑器
,g++ 编译器
,gdb 调试工具
,以及 Makefile 构建工具
。
技术特点包括:HTTP 服务器搭建、RESTful 风格接口设计、JSON 序列化处理、线程池管理,以及 HTML、CSS 和 JavaScript 基础开发。这些技术共同支持系统的高效性、可扩展性和用户体验。
项目模块:
- 数据管理模块:基于
Mysql
进行数据管理 - 业务处理模块:基于
httplib
搭建http
,使用RESTful 风格接口设计处理客户端需求 - 前端界面模块:基于基础的
HTML+CSS+JS
完成基于简单模板前端界⾯的修改与功能完成
第三方库
jsoncpp
json
是⼀种数据交换格式,采⽤完全独⽴于编程语⾔的⽂本格式来存储和表⽰数据。
这个第三方库在文章【Linux网络编程】应用层:自定义协议 | 序列化和反序列化 | 系统角度理解read、write、recv、 send 和 tcp 为什么支持全双工 | 实现网络版计算器 | jsoncpp库中介绍过以及了解如何安装jsoncpp
库。
json
数据类型:对象,数组,字符串,数字- 对象:使⽤花括号
{}
括起来的表⽰⼀个对象。 - 数组:使⽤中括号
[]
括起来的表⽰⼀个数组。 - 字符串:使⽤常规双引号
""
括起来的表⽰⼀个字符串
在本项目直接使用jsoncpp
的接口,不需要我们自己去写字符串序列化与反序列化,这样减少了项目的开发周期。
jsoncpp
实现的功能:
- 实现
json
格式的序列化与反序列化 - 完成将多个数据对象组织称为
json
格式字符串 - 完成将
json
格式字符串解析得到多个数据对象
这三个功能主要借助三个类以及对应的成员函数来实现的:
Json::Value
:主要实现中间数据的存储
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_API StreamWriter类
:用于对Json::Value
中数据进行序列化
class JSON_API StreamWriter {
virtual int write(Value const& root, std::ostream* sout) = 0;
}
JSON_API CharReader
:实现反序列化,解析得到的对象存储到Json::Value
中
class JSON_API CharReader {
virtual bool parse(char const* beginDoc, char const* endDoc,
Value* root, std::string* errs) = 0;
}
将Value中存储的数据转换成Json字符串,实现序列化:
#include<iostream>
#include<sstream>
#include<string>
#include<memory>
#include<jsoncpp/json/json.h>
int main()
{
const char *name = "Gavin";
int age = 21;
float score[] = {150, 149, 148};
Json::Value val;
val["Name"] = name;
val["Age"] = age;
val["Score"].append(score[0]);
val["Score"].append(score[1]);
val["Score"].append(score[2]);
Json::StreamWriterBuilder swb;
// 设置 JSON 输出不转义 Unicode 字符
//swb.settings_["indentation"] = ""; // 去掉缩进
swb.settings_["ensureAscii"] = false; // 使中文字符不被转义
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;
}
将Json字符串转换到原始格式,实现反序列化:
void unserialize(const std::string &str)
{
Json::Value val;
Json::CharReaderBuilder crb;
std::unique_ptr<Json::CharReader> cr(crb.newCharReader());
std::string err;
bool ret = cr->parse(str.c_str(),str.c_str()+str.size(),&val,&err);
if(ret==false)
{
std::cout << "parse failed\n";
return ;
}
std::cout << val["Name"] <<std::endl;
std::cout << val["Age"] <<std::endl;
int sz = val["Score"].size();
for(int i=0;i<sz;i++)
std::cout << val["Score"][i] <<std::endl;
}
int main()
{
//serialize();
std::string str = R"({"Name":"Gavin", "Age":"22", "Score":[66, 77, 88]})";
unserialize(str);
return 0;
}
Mysql 数据库 API 认识
MySQL
操作句柄初始化
MYSQL *mysql_init(MYSQL *mysql);
连接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;
//选择操作的数据库
选择操作的数据库
int mysql_select_db(MYSQL *mysql, const char *db)
执行sql语句:
int mysql_query(MYSQL *mysql, const char *stmt_str)
//mysql--初始化完成的句柄
//stmt_str--要执⾏的sql语句
保存查询结果到本地
MYSQL_RES *mysql_store_result(MYSQL *mysql)
获取结果集中的⾏数与列数
uint64_t mysql_num_rows(MYSQL_RES *result);
//result--保存到本地的结果集地址
//返回值:结果集中数据的条数
unsigned int mysql_num_fields(MYSQL_RES *result)
//result--保存到本地的结果集地址
//返回值:结果集中每⼀条数据的列数
遍历结果
MYSQL_ROW mysql_fetch_row(MYSQL_RES *result)
//result--保存到本地的结果集地址
//返回值:实际上是⼀个char **的指针,将每⼀条数据做成了字符串指针数组 row[0]-第0列row[1]-第1列
//并且这个接⼝会保存当前读取结果位置,每次获取的都是下⼀条数据
释放结果集
oid mysql_free_result(MYSQL_RES *result)
//result--保存到本地的结果集地址
//返回值:void
关闭数据库客⼾端连接,销毁句柄
void mysql_close(MYSQL *mysql)
获取mysql接口执⾏错误原因
const char *mysql_error(MYSQL *mysql)
测试样例:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <mysql/mysql.h>
void add(MYSQL *mysql)
{
char *sql = "insert into test_tb values(NULL, 22, '小高', 88);";
int ret = mysql_query(mysql, sql);
if (ret != 0)
{
printf("mysql query failed, err: %d, error: %s\n", mysql_errno(mysql), mysql_error(mysql));
return;
}
}
void mod(MYSQL *mysql)
{
char *sql = "update test_tb set age=25 where name='小高';";
int ret = mysql_query(mysql,sql);
if (ret != 0)
{
printf("mysql query failed, err: %d, error: %s\n", mysql_errno(mysql), mysql_error(mysql));
return;
}
}
void del(MYSQL *mysql)
{
char *sql = "delete from test_tb where name='小高';";
int ret = mysql_query(mysql,sql);
if (ret != 0)
{
printf("mysql query failed, err: %d, error: %s\n", mysql_errno(mysql), mysql_error(mysql));
return;
}
}
void get(MYSQL *mysql)
{
char *sql = "select * from test_tb;";
int ret = mysql_query(mysql, sql);
if (ret != 0)
{
printf("mysql query failed, err: %d, error: %s\n", mysql_errno(mysql), mysql_error(mysql));
return;
}
MYSQL_RES *res = mysql_store_result(mysql);
if (res == NULL)
{
printf("mysql store result error:%s\n", mysql_error(mysql));
return;
}
int row = mysql_num_rows(res);
int col = mysql_num_fields(res);
printf("%10s%10s%10s%10s\n", "ID", "年龄", "姓名", "成绩");
for (int i = 0; i < row; i++)
{
MYSQL_ROW row_data = mysql_fetch_row(res);
for (int i = 0; i < col; i++)
{
printf("%10s", row_data[i]);
}
printf("\n");
}
mysql_free_result(res);
return;
}
int main()
{
MYSQL *mysql = mysql_init(NULL);
if ((mysql_real_connect(mysql, "127.0.0.1", "root", "123456", "test_db", 0, NULL, 0)) == NULL)
{
printf("mysql connect failed: %d, error: %s\n", mysql_errno(mysql), mysql_error(mysql));
return -1;
}
mysql_set_character_set(mysql, "utf8");
//add(mysql);
//add(mysql);
//mod(mysql);
//del(mysql);
get(mysql);
mysql_close(mysql); // 关闭连接
return 0;
}
httplib
httplib
库,⼀个 C++11 单⽂件头的跨平台HTTP/HTTPS
库。
httplib
库实际上是⽤于搭建⼀个简单的 http
服务器或者客户端的库,这种第三⽅⽹络库,可以让我们免去搭建服务器或客⼾端的时间,把更多的精⼒投⼊到具体的业务处理中,提⾼开发效率。
测试用例
#include"./httplib.h"
using namespace httplib;
void HelloGavin(const Request &req, Response &rsp)
{
rsp.body = "Hello Gavin!";
rsp.status = 200;
}
void Numbers(const Request &req, Response &rsp)
{
std::string num = req.matches[1]; // matches存放正则表达式匹配的规则数据,也就是()里面单独捕捉的数据
rsp.set_content(num, "Text/plain");
rsp.status = 200; // 向rsp.body里面添加数据
}
void Multipart(const Request &req, Response &rsp)
{
if(req.has_file("file1") == false)
{
rsp.status = 100;
return;
}
const auto& file = req.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",HelloGavin);
// 正则表达式:\d表示数字,+表示匹配匹配前面的字符一次或者多次,()表示单独捕捉数据
server.Get("/numbers/(\\d+)",Numbers);
server.Post("/multipart",Multipart);
server.listen("0.0.0.0", 8888);
return 0;
}
项目实现
服务端工具类实现
文件实用工具类
在视频点播系统中因为涉及到⽂件上传,需要对上传的⽂件进⾏备份存储,因此⾸先设计封装⽂件操作类,这个类封装完毕之后,则在任意模块中对⽂件进⾏操作时都将变的简单化。
功能:
- 获取文件大小(属性)
- 判断文件是否存在
- 向文件写入数据
- 从文件读取数据
- 针对目录文件多创建一个目录
工具类的框架
namespace aod
{
class FileUtil
{
public:
FileUtil(const std::string name):_name(name){}
bool Exists(); // 判断文件是否存在
size_t Size(); // 获取文件大小
bool ReadContent(std::string *body); // 读取文件数据到body中
bool WriteContent(const std::string &body); //向文件写入数据
bool CreateDirectory(); // 针对目录创建目录
private:
std::string _name;
};
}
判断文件是否存在
调用Linux中access()
函数:
#include <unistd.h>
int access(const char *pathname, int mode);
成功返回0,否则出错返回-1
FileUtil(const std::string name):_name(name){}
// 判断文件是否存在
bool Exists()
{
int ret = access(_name.c_str(), F_OK); // F_OK专门用于检测文件是否存在:存在返回0,不存在返回-1
if(ret != 0) return false;
return true;
}
获取文件大小
Linux中获取文件属性可以调用stat()
函数
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *pathname, struct stat *statbuf);
功能实现:
// 获取文件大小
size_t Size()
{
if(this->Exists() == false) return 0;
struct stat st;
int ret = stat(_name.c_str(), &st);
if(ret != 0) return 0;
return st.st_size;
}
读取文件数据到body中
- 实例化一个文件输入流
- 以二进制形式打开文件
- 获取文件大小
- 重置缓冲区大小
- 将文件依次输入到缓冲区中
- 成功后关闭文件
// 读取文件数据到body中
bool ReadContent(std::string *body)
{
std::ifstream ifs;
ifs.open(_name, std::ios::binary); // 以二进制方式打开
if(ifs.is_open() == false)
{
std::cout << "open failed!\n";
return false;
}
size_t flen = this->Size();
body->resize(flen);
ifs.read(&(*body)[0], flen);
//body[0] 是 std::string 对象的第一个字符,而不是字符串的底层缓冲区地址
// (*body) 获取指针 body 指向的 std::string 对象,
//然后 (*body)[0] 获取该字符串的第一个字符,&(*body)[0] 获取该字符的地址,也就是底层数据的起始位置
if(ifs.good() == false)
{
std::cout << "read file content failed!\n";
ifs.close();
return false;
}
ifs.close();
return true;
}
向文件写入数据
- 实例化一个文件输出流
- 以二进制形式打开
- 写入数据
- 写入成功后关闭文件
//向文件写入数据
bool WriteContent(const std::string &body)
{
std::ofstream ofs;
ofs.open(_name, std::ios::binary); // 以二进制方式打开
if(ofs.is_open() == false)
{
std::cout << "open failed!\n";
return false;
}
ofs.write(body.c_str(), body.size());
if(ofs.good() == false)
{
std::cout << "write file content failed!\n";
ofs.close();
return false;
}
ofs.close();
return true;
}
针对目录创建目录
在Linux中调用mkdir()
函数:
#include <sys/stat.h>
#include <sys/types.h>
int mkdir(const char *pathname, mode_t mode);
功能实现:
// 针对目录创建目录
bool CreateDirectory()
{
if(this->Exists()) return true;
mkdir(_name.c_str(),0777);
return true;
}
完整代码
#ifndef __MY_UTIL__
#define __MY_UTIL__
#include<iostream>
#include<fstream>
#include<string>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
namespace aod
{
class FileUtil
{
public:
FileUtil(const std::string name):_name(name){}
// 判断文件是否存在
bool Exists()
{
int ret = access(_name.c_str(), F_OK); // F_OK专门用于检测文件是否存在:存在返回0,不存在返回-1
if(ret != 0)
{
std::cout << "file is not exists!\n";
return false;
}
return true;
}
// 获取文件大小
size_t Size()
{
if(this->Exists() == false) return 0;
struct stat st;
int ret = stat(_name.c_str(), &st);
if(ret != 0)
{
std::cout << "get file stat failed!\n";
return 0;
}
return st.st_size;
}
// 读取文件数据到body中
bool ReadContent(std::string *body)
{
std::ifstream ifs;
ifs.open(_name, std::ios::binary); // 以二进制方式打开
if(ifs.is_open() == false)
{
std::cout << "open failed!\n";
return false;
}
size_t flen = this->Size();
body->resize(flen);
ifs.read(&(*body)[0], flen);
//body[0] 是 std::string 对象的第一个字符,而不是字符串的底层缓冲区地址
// (*body) 获取指针 body 指向的 std::string 对象,
//然后 (*body)[0] 获取该字符串的第一个字符,&(*body)[0] 获取该字符的地址,也就是底层数据的起始位置
if(ifs.good() == false)
{
std::cout << "read file content failed!\n";
ifs.close();
return false;
}
ifs.close();
return true;
}
//向文件写入数据
bool WriteContent(const std::string &body)
{
std::ofstream ofs;
ofs.open(_name, std::ios::binary); // 以二进制方式打开
if(ofs.is_open() == false)
{
std::cout << "open failed!\n";
return false;
}
ofs.write(body.c_str(), body.size());
if(ofs.good() == false)
{
std::cout << "write file content failed!\n";
ofs.close();
return false;
}
ofs.close();
return true;
}
// 针对目录创建目录
bool CreateDirectory()
{
if(this->Exists()) return true;
mkdir(_name.c_str(),0777);
return true;
}
private:
std::string _name;
};
}
#endif
功能测试
void FileTest1()
{
aod::FileUtil("./www").CreateDirectory();
aod::FileUtil("./www/index.html").WriteContent("<html></html>");
std::string body;
aod::FileUtil("./www/index.html").ReadContent(&body);
std::cout << body << std::endl;
std::cout << aod::FileUtil("./www/index.html").Size() <<std::endl;
}
Json实用工具类实现
主要实现功能:
- 实现序列化
- 实现反序列化
工具框架
// Json工具类
class JsonUtil
{
public:
static bool Serialize(const Json::Value &value ,std::string *body);
static bool Unserialize(const std::string &body, Json::Value *value);
};
序列化
Json::StreamWriterBuilder
:这是JsonCpp
提供的一个构建器,用于配置并生成一个流写入器StreamWriter
。该写入器负责将Json::Value
转换为JSON
格式的字符串。std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter())
:创建一个流写入器对象sw
,它会根据StreamWriterBuilder
中的配置来序列化JSON
数据。std::stringstream ss
:一个字符串流,用于暂时存储序列化后的结果。sw->write(value, &ss)
:执行序列化操作,将value
序列化到字符串流ss
中。write
函数返回值为0
时表示成功,非0
则表示失败。*body = ss.str()
:将字符串流ss
中的内容转换为字符串,并赋值给body
。
static bool Serialize(const Json::Value &value ,std::string *body)
{
Json::StreamWriterBuilder swb;
std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());
std::stringstream ss;
int ret = sw->write(value, &ss);
if(ret != 0)
{
std::cout << "Serialize failed!\n";
return;
}
*body = ss.str();
return true;
}
static bool Unserialize(const std::string &body, Json::Value *value);
};
反序列化
- 定义一个智能指针对象
- 将智能指针指向的对象直接反序列化
- 反序列化成功直接将结果传给了
vlaue
static bool Unserialize(const std::string &body, Json::Value *value)
{
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(), value, &err);
if(ret == false)
{
std::cout << "Unserialize failed!\n";
return false;
}
return true;
}
功能测试
void JsonTest1()
{
Json::Value val;
val["Name"] = "Gavin";
val["Sex"] = "man";
val["Age"] = 21;
val["Score"].append(110.5);
val["Score"].append(110.5);
val["Score"].append(120.5);
std::string body;
aod::JsonUtil::Serialize(val, &body);
std::cout << body <<std::endl;
Json::Value stu;
aod::JsonUtil::Unserialize(body, &stu);
std::cout << stu["Name"].asString() <<std::endl;
std::cout << stu["Sex"].asString() <<std::endl;
std::cout << stu["Age"].asString() <<std::endl;
for(auto &a : stu["Score"])
{
std::cout << a.asFloat() <<std::endl;
}
}
服务端数据管理功能
视频数据表的设计
使用 MySQL数据库管理视频信息,只管理视频及图片的信息,视频以及图片是存储的文件中。
本项目中只是完成一个简单的视频信息表:
- 视频ID
- 视频名称
- 视频描述信息
- 视频文件的
url
路径(只是链接) - 视频封面图片的
url
路径(只是链接)
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,加上静态资源根⽬录就是实际存储路径'
);
数据管理类设计
本模块主要为了实现对数据库的增删查改管理,但是实际的项目中,会存在多个表,每个表要操作的数据不一样,因此对数据进行增删查改时需要为每一个表进行创建一个类。
视频操作:
- 新增
- 修改
- 删除
- 查询所有
- 查询单个
- 模糊匹配
视频信息在接口直接的传递因为字段数量可能很多,因此需要用Json::Value
对象进行存储
基本框架
static MYSQL *MysqlInit();
static void MysqlDestory();
static bool MysqlQuery(MYSQL *mysql, const std::string &sql);
class TableVideo
{
public:
TableVideo();
~TableVideo();
bool Insert(const Json::Value &video);
bool Update(int video_id, const Json::Value &video);
bool Delete(int video_id);
bool SelectAll(Json::Value *videos);
bool SelectOne(int video_id, const Json::Value *video);
bool SelectLike(const std::string &key, Json::Value *videos);
private:
MYSQL *mysql;
std::mutex _mutex;
};
初始化
- 初始化一个句柄
- 建立连接
- 设置字符集
- 将初始化的句柄返回给外部
static MYSQL *MysqlInit()
{
MYSQL *mysql = mysql_init(NULL);
if(mysql == NULL)
{
std::cout << "Init failed!\n";
return NULL;
}
if(mysql_real_connect(mysql, HOST, USER, PASS, NAME, 0, NULL, 0) ==NULL)
{
std::cout << "connect failed!\n";
mysql_close(mysql);
return NULL;
}
mysql_set_character_set(mysql,"utf8");
return mysql;
}
销毁
- 句柄不为空时直接销毁即可
static void MysqlDestory(MYSQL *mysql)
{
if(mysql != NULL)
{
mysql_close(mysql);
}
return;
}
语句执行
static bool MysqlQuery(MYSQL *mysql, const std::string &sql)
{
int ret = mysql_query(mysql, sql.c_str());
if(ret != 0)
{
std::cout << sql << std::endl;
std::cout << mysql_error(mysql) << std::endl;
return false;
}
return true;
}
构造与析构
- 直接调用初始化接口以及销毁接口
TableVideo()
{
_mysql = MysqlInit();
if(_mysql == NULL)
exit(-1);
}
~TableVideo()
{
MysqlDestory(_mysql);
}
新增操作
- 定义一个
sql
表示查询语句 - 调整
sql
的大小 - 设置一个
sql
语句模版 - 格式化字符串填充语句
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"].asString(),
video["info"].asString(),
video["video"].asString(),
video["image"].asString()
);
return MysqlQuery(_mysql,sql);
}
修改操作
bool Update(int video_id, const Json::Value &video)
{
std::string sql;
sql.resize(4096 + video["info"].asString().size()); // 防止简介过长
#define UPDATE_VIDEO "update tb_video set name='%s', info='%s' where id=%d;"
sprintf(&sql[0], UPDATE_VIDEO,
video["name"].asString(),
video["info"].asString(),
video_id
);
return MysqlQuery(_mysql,sql);
}
删除操作
bool Delete(int video_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);
}
查询所有数据
- 类实例化的对象可能在多线程环境下,因此需要保证在查询结果后将数据保存到本地操作时原子的,避免一个线程还没有保存到本地,另一个线程开始查询,此时线程就会
- 处理查新结果:将查询到的结果存储在
MYSQL_RES
结构体中 - 遍历查询结果:
mysql_num_rows(res)
:获取查询结果的行数,即视频记录的数量mysql_fetch_row(res)
:遍历查询结果集,返回当前行的数据(每行是一个MYSQL_ROW
数组)- 在遍历过程中,使用
atoi
函数将row[i]
转换为整数并赋值给video
对象的相应字段
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 result failed!\n";
_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"] = atoi(row[1]);
video["info"] = atoi(row[2]);
video["video"] = atoi(row[3]);
video["image"] = atoi(row[4]);
videos->append(video);
}
mysql_free_result(res);
return true;
}
查询单个数据
bool SelectOne(int video_id, Json::Value *video)
{
#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 result failed!\n";
_mutex.unlock();
return false;
}
_mutex.unlock();
int num_rows = mysql_num_rows(res);
if(num_rows != 1)
{
std::cout << "hace no data!\n";
mysql_free_result(res);
return false;
}
MYSQL_ROW row = mysql_fetch_row(res);
(*video)["id"] = video_id;
(*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[2014] = {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 result failed!\n";
_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"] = atoi(row[1]);
video["info"] = atoi(row[2]);
video["video"] = atoi(row[3]);
video["image"] = atoi(row[4]);
videos->append(video);
}
mysql_free_result(res);
return true;
}
功能测试
void DataTest()
{
aod::TableVideo tb_video;
Json::Value video;
// video["name"] = "Jhon";
// video["info"] = "a man but not a student";
// video["video"] = "/video/man.ma4";
// video["image"] = "/image/man.jpg";
//tb_video.Insert(video);
// tb_video.Update(2, video);
// tb_video.SelectAll(&video);
// tb_video.SelectOne(1, &video);
// tb_video.SelectLike("Gavin", &video);
// std::string body;
// aod::JsonUtil::Serialize(video, &body);
// std::cout << body <<std::endl;
tb_video.Delete(2);
}
服务端业务处理模块
网络通信接口设计
http协议其实是一种数据格式,是一个tcp传输,在应用层采用的一种数据特定格式
网络通信接口设计就是定义好什么样的请求是一个查询请求,什么的请求是一个删除请求。
服务端提供的功能:新增视频、删除视频、修改视频、查询所有视频信息、查询单个视频、模糊匹配视频
接口设计:使用restful
接口设计
RESTful
架构可以充分的利⽤HTTP
协议的各种功能,是 HTTP
协议的最佳实践,正⽂通常采⽤ JSON
格式,使用GET
表示查询,POST
表示新增,PUT
表示修改,DELETE
表示删除,并且正文资源数据采用json
、html
格式。
业务处理类模块设计
本项目中,业务处理主要包含两个功能:
- 网络通信功能的实现
- 业务功能处理的实现
业务处理模块负责与客⼾端进⾏⽹络通信,接收客⼾端的请求,然后根据请求信息,明确客户端端用户的意图,进⾏业务处理,并进⾏对应的结果响应。
网络通信借助httplib
库即可快速搭建,因此完成此项目重点就可以在业务处理中。
业务处理功能包括:
- 客户端的视频数据和信息上传
- 客户端视频列表显示(查询所有视频信息)
- 客户端数据观看请求(视频数据的获取)
- 客户端对视频的其他管理(删除、修改)
业务处理模块框架
#include"data.hpp"
#include"httplib.h"
namespace aod
{
#define WWWROOT "./www"
#define VIDEO_ROOT "./video/" // 实际上是"./www/video/"
#define VIDEO_IMAGE "./image"
TableVideo *tb_video = NULL;
class Server
{
public:
Server(int port): _port(port)
{}
bool RunModule();
private:
int _port;
httplib::Server _ser;
private:
static void Insert(const httplib::Request &req, httplib::Response &res); // 这里需要定义成一个静态成员函数
// 静态成员函数的参数不包括this指针,确保这个函数只有两个参数
static void Update(const httplib::Request &req, httplib::Response &res);
static void Delete(const httplib::Request &req, httplib::Response &res);
static void GetOne(const httplib::Request &req, httplib::Response &res);
static void GetAll(const httplib::Request &req, httplib::Response &res);
};
}
启动服务器
- 首先初始化数据管理模块,创建指定的地址
- 搭建
http
服务器,使用httplib
库
bool RunModule()
{
//1.初始化数据管理模块,创建指定的目录
tb_video = new TableVideo();
FileUtil(WWW_ROOT).CreateDirectory();
std::string video_real_path = WWW_ROOT + VIDEO_ROOT;
FileUtil(video_real_path).CreateDirectory();
std::string image_real_path = WWW_ROOT + IMAGE_ROOT;
//2.搭建http服务器
//a.设置静态资源根目录
_srv.set_mount_point("/", WWW_ROOT);
//b.添加请求与处理函数的映射管理
_srv.Post("/video", Insert);
_srv.Delete("/video/(\\d+)", Delete);
_srv.Put("/video/(\\d+)", Update);
_srv.Get("/video/(\\d+)", GetOne);
_srv.Get("/video", GetAll);
//c.启动
_srv.listen("0.0.0.0", _port);
return true;
}
新增数据
- 从请求中获取文件字段
- 将文件内容写到指定的路径
- 创建一个
Json
对象存储数据到数据库中
static void Insert(const httplib::Request &req, httplib::Response &rsp)
{
if(req.has_file("name") == false ||
req.has_file("info") == false ||
req.has_file("video") == false ||
req.has_file("image") == false
)
{
rsp.status = 400;
rsp.body = R"({"result":false, "reason":"上传的数据信息错误"})";
rsp.set_header("Content-type", "application/json");
}
auto name = req.get_file_value("name");
auto info = req.get_file_value("info");
auto video = req.get_file_value("video");
auto image = req.get_file_value("image");
std::string video_name = name.content;
std::string video_info = info.content;
std::string video_path = WWW_ROOT + VIDEO_ROOT + name.filename + video.filename;
std::string image_path = WWW_ROOT + IMAGE_ROOT + name.filename + image.filename;
if((FileUtil(video_path).WriteContent(video.content)) == NULL)
{
rsp.status = 500;
rsp.body = R"({"result":false, "reason":"视频文件存储失败"})";
rsp.set_header("Content-type", "application/json");
}
if((FileUtil(image_path).WriteContent(image.content)) == NULL)
{
rsp.status = 500;
rsp.body = R"({"result":false, "reason":"图片文件存储失败"})";
rsp.set_header("Content-type", "application/json");
}
Json::Value video_json;
video_json["name"] = video_name;
video_json["info"] = video_info;
video_json["video"] = VIDEO_ROOT + name.filename + video.filename;
video_json["image"] = IMAGE_ROOT + name.filename + image.filename;
if((tb_video->Insert(video_json)) == NULL)
{
rsp.status = 500;
rsp.body = R"({"result":false, "reason":"新增数据失败"})";
rsp.set_header("Content-type", "application/json");
}
return ;
}
修改操作
功能测试
void ServerTest()
{
aod::Server server(8888);
server.RunModule();
}
新增
查询
模糊匹配
修改
删除视频信息
前端界面实现
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="OrcasThemes">
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<title>Home</title>
<!-- Bootstrap core CSS -->
<link href="css/bootstrap.css" rel="stylesheet">
<!-- Custom styles for this template -->
<link rel="stylesheet" href="css/screen.css">
<link rel="stylesheet" href="css/animation.css">
<!--[if IE 7]>
<![endif]-->
<link rel="stylesheet" href="css/font-awesome.css">
<!--[if lt IE 8]>
<link rel="stylesheet" href="css/ie.css" type="text/css" media="screen, projection">
<![endif]-->
<link href="css/lity.css" rel="stylesheet">
<style>
[v-cloak] {
display: none;
}
</style>
</head>
<body>
<div id="myapp">
<!-- HOME 1 -->
<div id="home1" class="container-fluid standard-bg">
<!-- HEADER -->
<div class="row header-top">
<div class="col-lg-3 col-md-6 col-sm-5 col-xs-8">
<a class="gwj_logo" href="#"><img src="img/gwj_logo.png" class="main-logo img-responsive" alt="Muvee Reviews" title="Muvee Reviews"></a>
</div>
<div class="col-lg-6 hidden-md text-center hidden-sm hidden-xs">
</div>
<div class="col-lg-3 col-md-6 col-sm-7 hidden-xs">
<div class="right-box">
<button type="button" class="access-btn" data-toggle="modal" data-target="#enquirypopup">新增视频</button>
</div>
</div>
</div>
<!-- MENU -->
<div class="row home-mega-menu ">
<div class="col-md-12">
<nav class="navbar navbar-default">
<div class="navbar-header">
<button class="navbar-toggle" type="button" data-toggle="collapse" data-target=".js-navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
</div>
<div class="collapse navbar-collapse js-navbar-collapse megabg dropshd ">
<ul class="nav navbar-nav">
<li><a href="index.html">视频点播</a></li>
</ul>
<div class="search-block">
<form>
<input type="search" placeholder="Search">
</form>
</div>
</div>
<!-- /.nav-collapse -->
</nav>
</div>
</div>
<!-- CORE -->
<div class="row">
<!-- SIDEBAR -->
<div class="col-lg-2 col-md-4 hidden-sm hidden-xs">
</div>
<!-- HOME MAIN POSTS -->
<div class="col-lg-10 col-md-8">
<section id="home-main">
<h2 class="icon"><i class="fa fa-television" aria-hidden="true"></i>视频列表</h2>
<div class="row">
<!-- ARTICLES -->
<div class="col-lg-9 col-md-12 col-sm-12">
<div class="row auto-clear">
<article class="col-lg-3 col-md-6 col-sm-4" v-for="video in videos">
<!-- POST L size -->
<div class="post post-medium">
<div class="thumbr">
<a class="afterglow post-thumb" v-bind:href="'/video.html?id='+video.id" target="_blank">
<span class="play-btn-border" title="Play"><i class="fa fa-play-circle headline-round" aria-hidden="true"></i></span>
<div class="cactus-note ct-time font-size-1"><span>02:02</span></div>
<img class="img-responsive" v-bind:src="video.image" alt="#" v-cloak>
</a>
</div>
<div class="infor">
<h4>
<a class="title" href="#" v-cloak>{{video.name}}</a>
</h4>
<span class="posts-txt" title="Posts from Channel"><i class="fa fa-thumbs-up" aria-hidden="true"></i>20.895</span>
<div class="ratings">
<i class="fa fa-star" aria-hidden="true"></i>
<i class="fa fa-star" aria-hidden="true"></i>
<i class="fa fa-star-half-o" aria-hidden="true"></i>
<i class="fa fa-star-o"></i>
<i class="fa fa-star-half"></i>
</div>
</div>
</div>
</article>
</div>
<div class="clearfix spacer"></div>
</div>
<!-- RIGHT ASIDE -->
<div class="col-lg-3 hidden-md col-sm-12 text-center top-sidebar">
</div>
</div>
</section>
</div>
</div>
</div>
<!-- CHANNELS -->
<div id="channels-block" class="container-fluid channels-bg">
</div>
<!-- BOTTOM BANNER -->
</div>
<!-- FOOTER -->
<div id="footer" class="container-fluid footer-background">
<div class="container">
<footer>
<!-- SECTION FOOTER -->
<div class="row">
<!-- SOCIAL -->
<div class="col-lg-3 col-md-3 col-sm-6 col-xs-12">
<div class="row auto-clear">
</div>
</div>
<!-- TAGS -->
<div class="col-lg-3 col-md-3 col-sm-6 col-xs-12">
</div>
<!-- POST -->
<div class="col-lg-3 col-md-3 col-sm-6 col-xs-12">
</div>
<!-- LINKS -->
<div class="col-lg-3 col-md-3 col-sm-6 col-xs-12">
</div>
</div>
<div class="row copyright-bottom text-center">
<div class="col-md-12 text-center">
<a href="" class="footer-logo" title="Video Magazine Bootstrap HTML5 template">
<img src="img/gwj_logo.png" class="img-responsive text-center" alt="Video Magazine Bootstrap HTML5 template">
</a>
<p v-cloak>HAUE © Author by Gaowenjun</p>
</div>
</div>
</footer>
</div>
</div>
<!-- MODAL -->
<div id="enquirypopup" class="modal fade in " role="dialog">
<div class="modal-dialog">
<!-- Modal content-->
<div class="modal-content row">
<div class="modal-header custom-modal-header">
<button type="button" class="close" data-dismiss="modal">×</button>
<h2 class="icon"><i class="fa fa-television" aria-hidden="true"></i>新增视频</h2>
</div>
<div class="modal-body">
<form name="info_form" class="form-inline" action="/add" method="post" enctype="multipart/form-data">
<div class="form-group col-sm-12">
<input type="text" class="form-control" name="name" placeholder="输入视频名称">
</div>
<div class="form-group col-sm-12">
<input type="text" class="form-control" name="info" placeholder="输入视频简介">
</div>
<div class="form-group col-sm-12">
<input type="file" class="form-control" name="video" placeholder="选择视频文件">
</div>
<div class="form-group col-sm-12">
<input type="file" class="form-control" name="image" placeholder="选择封面图片">
</div>
<div class="form-group col-sm-12">
<button class="subscribe-btn pull-right" type="submit" title="Subscribe">上传</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</body>
<!-- JAVA SCRIPT -->
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="js/jquery-1.12.1.min.js"></script>
<script src="js/bootstrap.min.js"></script>
<script src="js/lity.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
$(".nav .dropdown").hover(function() {
$(this).find(".dropdown-toggle").dropdown("toggle");
});
</script>
<script>
let app = new Vue({
el: '#myapp',
data: {
author: "Gaowenjun",
videos:[]
},
methods : {
get_allvideos : function() {
$.ajax({
url: "/video",
type: "get",
context: this,
success : function(result, status, xhr){
this.videos = result;
}
})
}
}
});
app.get_allvideos();
</script>
</html>
video.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="OrcasThemes">
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<title>Home</title>
<!-- Bootstrap core CSS -->
<link href="css/bootstrap.css" rel="stylesheet">
<!-- Custom styles for this template -->
<link rel="stylesheet" href="css/screen.css">
<link rel="stylesheet" href="css/animation.css">
<!--[if IE 7]>
<![endif]-->
<link rel="stylesheet" href="css/font-awesome.css">
<!--[if lt IE 8]>
<link rel="stylesheet" href="css/ie.css" type="text/css" media="screen, projection">
<![endif]-->
<link href="css/lity.css" rel="stylesheet">
<style>
[v-cloak] {
display: none;
}
</style>
</head>
<body>
<div id="myapp">
<!-- HOME 1 -->
<div id="home1" class="container-fluid standard-bg">
<!-- HEADER -->
<div class="row header-top">
<div class="col-lg-3 col-md-6 col-sm-5 col-xs-8">
<a class="gwj_logo" href="#"><img src="img/gwj_logo.png" class="main-logo img-responsive" alt="Muvee Reviews" title="Muvee Reviews"></a>
</div>
<div class="col-lg-6 hidden-md text-center hidden-sm hidden-xs">
</div>
<div class="col-lg-3 col-md-6 col-sm-7 hidden-xs">
<div class="right-box">
<button type="button" class="access-btn" data-toggle="modal" data-target="#enquirypopup">新增视频</button>
</div>
</div>
</div>
<!-- MENU -->
<div class="row home-mega-menu ">
<div class="col-md-12">
<nav class="navbar navbar-default">
<div class="navbar-header">
<button class="navbar-toggle" type="button" data-toggle="collapse" data-target=".js-navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
</div>
<div class="collapse navbar-collapse js-navbar-collapse megabg dropshd ">
<ul class="nav navbar-nav">
<li><a href="index.html">视频点播</a></li>
</ul>
<div class="search-block">
<form>
<input type="search" placeholder="Search">
</form>
</div>
</div>
<!-- /.nav-collapse -->
</nav>
</div>
</div>
<!-- CORE -->
<div class="row">
<!-- SIDEBAR -->
<div class="col-lg-2 col-md-4 hidden-sm hidden-xs">
</div>
<!-- HOME MAIN POSTS -->
<div class="col-lg-10 col-md-8">
<section id="home-main">
<h2 class="icon"><i class="fa fa-television" aria-hidden="true"></i>视频列表</h2>
<div class="row">
<!-- ARTICLES -->
<div class="col-lg-9 col-md-12 col-sm-12">
<div class="row auto-clear">
<article class="col-lg-3 col-md-6 col-sm-4" v-for="video in videos">
<!-- POST L size -->
<div class="post post-medium">
<div class="thumbr">
<a class="afterglow post-thumb" v-bind:href="'/video.html?id='+video.id" target="_blank">
<span class="play-btn-border" title="Play"><i class="fa fa-play-circle headline-round" aria-hidden="true"></i></span>
<div class="cactus-note ct-time font-size-1"><span>02:02</span></div>
<img class="img-responsive" v-bind:src="video.image" alt="#" v-cloak>
</a>
</div>
<div class="infor">
<h4>
<a class="title" href="#" v-cloak>{{video.name}}</a>
</h4>
<span class="posts-txt" title="Posts from Channel"><i class="fa fa-thumbs-up" aria-hidden="true"></i>20.895</span>
<div class="ratings">
<i class="fa fa-star" aria-hidden="true"></i>
<i class="fa fa-star" aria-hidden="true"></i>
<i class="fa fa-star-half-o" aria-hidden="true"></i>
<i class="fa fa-star-o"></i>
<i class="fa fa-star-half"></i>
</div>
</div>
</div>
</article>
</div>
<div class="clearfix spacer"></div>
</div>
<!-- RIGHT ASIDE -->
<div class="col-lg-3 hidden-md col-sm-12 text-center top-sidebar">
</div>
</div>
</section>
</div>
</div>
</div>
<!-- CHANNELS -->
<div id="channels-block" class="container-fluid channels-bg">
</div>
<!-- BOTTOM BANNER -->
</div>
<!-- FOOTER -->
<div id="footer" class="container-fluid footer-background">
<div class="container">
<footer>
<!-- SECTION FOOTER -->
<div class="row">
<!-- SOCIAL -->
<div class="col-lg-3 col-md-3 col-sm-6 col-xs-12">
<div class="row auto-clear">
</div>
</div>
<!-- TAGS -->
<div class="col-lg-3 col-md-3 col-sm-6 col-xs-12">
</div>
<!-- POST -->
<div class="col-lg-3 col-md-3 col-sm-6 col-xs-12">
</div>
<!-- LINKS -->
<div class="col-lg-3 col-md-3 col-sm-6 col-xs-12">
</div>
</div>
<div class="row copyright-bottom text-center">
<div class="col-md-12 text-center">
<a href="" class="footer-logo" title="Video Magazine Bootstrap HTML5 template">
<img src="img/gwj_logo.png" class="img-responsive text-center" alt="Video Magazine Bootstrap HTML5 template">
</a>
<p v-cloak>HAUE © Author by Gaowenjun</p>
</div>
</div>
</footer>
</div>
</div>
<!-- MODAL -->
<div id="enquirypopup" class="modal fade in " role="dialog">
<div class="modal-dialog">
<!-- Modal content-->
<div class="modal-content row">
<div class="modal-header custom-modal-header">
<button type="button" class="close" data-dismiss="modal">×</button>
<h2 class="icon"><i class="fa fa-television" aria-hidden="true"></i>新增视频</h2>
</div>
<div class="modal-body">
<form name="info_form" class="form-inline" action="/add" method="post" enctype="multipart/form-data">
<div class="form-group col-sm-12">
<input type="text" class="form-control" name="name" placeholder="输入视频名称">
</div>
<div class="form-group col-sm-12">
<input type="text" class="form-control" name="info" placeholder="输入视频简介">
</div>
<div class="form-group col-sm-12">
<input type="file" class="form-control" name="video" placeholder="选择视频文件">
</div>
<div class="form-group col-sm-12">
<input type="file" class="form-control" name="image" placeholder="选择封面图片">
</div>
<div class="form-group col-sm-12">
<button class="subscribe-btn pull-right" type="submit" title="Subscribe">上传</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</body>
<!-- JAVA SCRIPT -->
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="js/jquery-1.12.1.min.js"></script>
<script src="js/bootstrap.min.js"></script>
<script src="js/lity.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
$(".nav .dropdown").hover(function() {
$(this).find(".dropdown-toggle").dropdown("toggle");
});
</script>
<script>
let app = new Vue({
el: '#myapp',
data: {
author: "Gaowenjun",
videos:[]
},
methods : {
get_allvideos : function() {
$.ajax({
url: "/video",
type: "get",
context: this,
success : function(result, status, xhr){
this.videos = result;
}
})
}
}
});
app.get_allvideos();
</script>
</html>