一个人拥有此生此世是不够的,他还应该拥有诗意的世界!
图片服务器
1. 项目背景
实现一个HTTP服务器, 然后利用这个服务器来存储图片, 针对每一张图片都有一张唯一的URL, 可以借助这个URL将图片展示到其他网页上.
存储图片 (写博客, 上传图片的时候最终显示到终端上), HTML构建网页所必须的东西,
https://csdnimg.cn/cdn/content-toolbar/csdn-logo_.png?v=20190924.1 (CSDN网络路径, 通过这个路径可以访问到网页上的图片)
需求诞生
-
针对有些软件不支持上传图片功能, 需要上传图片的地址, 例如:GitHub中的issue,项目中有bug需要改进什么的, 有时候需要截张图片, 如何向作者反馈, 就需要放一张链接, 对应图片的链接放到服务器上;
-
自己搭建的静态网页博客上传图片是需要上传图片的地址;
-
实际中所遇到的问题;
核心需求
-
上传图片;
-
根据图片的url访问图片,获取图片内容(即下载);
-
获取某个图片的属性 (大小,格式,上传时间,路径等等);
-
删除图片;
服务器针对的是网络通信, 谈到服务器就会涉及到客户端(主动发送请求)和服务端 (被动接受请求), 两者之间的角色有可能发替换, 同一个进程有可能使客户端也可能是服务端(例如: 手机上点外卖, 手机上的APP显示的是客户端, 饿了吗服务器是服务器, 点击付款的时候, 饿了吗服务器调用支付宝功能, 在支付场景下, 饿了吗服务器又相当于是客户端).
2. 项目模块
2.1 数据存储模块
数据存储模块主要是使用MySQL数据库对我们的文件进行存储和管理, 另一方面通过磁盘(外存)来保存我们上传的图片, 因为内存不能持久化存储.
2.1.1 数据库设计
我们在数据库中设计一张表用来存储图片的属性也就是表的结构.
创建数据库(图片内容即字符串)
create database if not exists image_system;
use image_system;
创建图片表
drop table if exists `image_table`
create table `image_table`(
image_id int not null primary key auto_increment,
image_name varchar(256),
size bigint,
upload_time varchar(50), //图片的上传时间
content_type varchar(50) comment '图片类型',
path varchar(1024) comment '图片所在路径',
md5 varchar(128) //校验和,用来校验图片内容的正确性
);
创建数据库,表都统一写到一个db.sql文件中
这样做的目的是例如数据库被重新重建, 自己将程序写到服务器上, 后边又因为服务器的磁盘空间不够用, 就需要将服务器迁移到其他服务器上, 数据库里面的内容也需要迁移, 建议可以将数据库中的建表语句都写到一个文件中, 让mysql客户端批量处理文件中的内容.
mysql -uroot < db.sql
重定向本质: 就是将文件描述符表中的数组下标映射到不同的文件结构上.
> 重定向, 本来是输出到标准输出上, 输出到文件中;
< 重定向, 把标准输入重定向到文件上;
2> 重定向, 把标准错误重定向到文件中.
md5
其中对于表单的创建我们使用到了md5对数据的完整性的保护,那么md5又是什么?
首先md5是一种字符串hash算法,md5有两种版本, 64位版本得到的是8个字节的数字, 128位版本得到的是16个字节的数字, 并且他有三个特性可以保证数据的安全性:
-
不管是什么样的字符串,最终得到的md5值都是一个固定的长度。
-
如果一个字符串,内容稍有变化,得到的md5值的差异还是很大的。
-
通过源字符串计算md5很容易,但是拿到md5还原源字符串理论上是不可能的。
(其中md5这个字段是用来进行校验图片内容正确性的, 上传图片之后, 服务器就可以计算一个该图片的md5值, 后序用户下载图片的时候, 也能获取到该图片的md5, 用户可以把自己计算的md5和服务器计算的md5对比, 就知道自己的图片是否下载正确了.)
crc校验
循环校验, 直接把我们需要的数据的每个字节循环相加 不考虑溢出, 最终得到的结果就是.
2.1.2 插入语句(mysql_insert)
#include<cstdio>
#include<cstdlib>
#include<mysql/mysql.h> //相对路径的目录
int main()
{
//使用mysql API来操作数据库了
//1.创建mysql操作句柄(遥控器)结构体指针
MYSQL * mysql = mysql_init(NULL);
//2.拿着句柄和数据库建立连接
//"127.0.0.1"环回IP
if(mysql_real_connect(mysql,"127.0.0.1","root","940822","image_system",3306,NULL,0) == NULL)
{
//数据库连接失败
printf("连接失败!%s\n",mysql_error(mysql));
return 1;
}
//3.设置编码格式(查看服务器编码方式与客户端编码方式一致)
mysql_set_character_set(mysql,"utf8");
//4.拼接sql语句
char sql [4096]={0};
// printf是将格式化字符串输出到标准输出上, sprintf字符串的格式化操作,输出到字符串里面
sprintf(sql,"insert into image_table values(null,'test.png',1024,'2019/08/26','abcdef','png','data/test.png')");
// 5.执行SQL语句,负责了客户端给服务器发送数据的过程
int ret= mysql_query(mysql,sql);
if(ret != 0)
{
printf("执行 sql 失败! %s\n",mysql_error(mysql));
return 1; //进程退出码
}
//6.关闭句柄
mysql_close(mysql);
return 0;
}
2.1.3 查询语句
#include<cstdio>
#include<cstdlib>
#include<mysql/mysql.h>
int main()
{
//使用mysql API来操作数据库了
//1.创建mysql操作句柄
MYSQL * mysql =mysql_init(NULL);
//2.拿着句柄和数据库建立连接
if(mysql_real_connect(mysql,"127.0.0.1","root","940822","image_system",3306,NULL,0)==NULL)
{
//数据库连接失败
printf("连接失败!%s\n",mysql_error(mysql));
return 1;
}
//3.设置编码格式
mysql_set_character_set(mysql,"utf8");
//4.拼接sql语句
char sql [4096]={0};
sprintf(sql,"select * from image_table");
// 5.执行SQL语句,负责了客户端给服务器发送数据的过程
int ret=mysql_query(mysql,sql);
if(ret != 0)
{
printf("执行 SQL 失败! %s\n",mysql_error(mysql));
return 1; //进程退出码
}
//6.获取结果集合(遍历结果)将得到的结果看成一个二维数组, 先循环获取每一行, 再循环获取每一列;
MYSQL_RES* result=mysql_store_result(mysql);
int rows=mysql_num_rows(result);
int cols=mysql_num_fields(result);
for(int i=0;i<rows;++i){
MYSQL_ROW row=mysql_fetch_row(result);
for(int j=0;j<cols;++j) {
printf("%s\t",row[j]);
}
printf("\n");
}
//7. 释放结果集合(防止内存泄漏)
mysql_free_result(result);
//8.关闭句柄
mysql_close(mysql);
return 0;
}
2.1.4 实现一个数据库的客户端, 使用MySQL的API来实现
#pragma once
#include<cstdlib>
#include<cstdio>
#include<mysql/mysql.h>
#include<jsoncpp/json/json.h>
namespace image_system{
static MYSQL* MySQLInit(){
//使用mysql API来操作数据库
//1、先创建一个mysql的句柄
MYSQL* mysql = mysql_init(NULL);
//2、拿着句柄和数据库建立连接
if( mysql_real_connect(mysql,"127.0.0.1","root","940822","image_system",3306,NULL,0) == NULL){
//数据库连接失败
printf("连接失败!!!%s\n",mysql_error(mysql));
return NULL;
}
//3、设置编码格式
mysql_set_character_set(mysql,"utf8");
return mysql;
}
static void MySQLRelease(MYSQL* mysql){
mysql_close(mysql);
}
class ImageTable{
public:
ImageTable(MYSQL* mysql):mysql_(mysql){}
//image就形如以下形式
//{
// image_name: "test.png",
// size: 1024,
// upload_time: "2019/08/28",
// md5: "abcdef",
// type: "png",
// path: "data/test.png"
//}
bool Insert(const Json::Value& image){
char sql[4*1024] = {0};
sprintf(sql,"insert into image_table values(null,'%s',%d,'%s','%s','%s','%s')",image["image_name"].asCString(),
image["size"].asInt(),image["upload_time"].asCString(),image["md5"].asCString(),
image["type"].asCString(),image["path"].asCString());
printf("[Insert sql] %s\n",sql);
int ret = mysql_query(mysql_,sql);
if(ret!=0){
printf("Insert 执行 SQL失败!%s\n",mysql_error(mysql_));
return false;
}
return true;
}
bool SelectAll(Json::Value* images){
char sql[1024*4] = {0};
sprintf(sql,"select* from image_table");
int ret = mysql_query(mysql_,sql);
if(ret!=0){
printf("SelectAll 执行 SQL 失败!%s\n",mysql_error(mysql_));
return false;
}
//遍历结果集合,并把结果集写到images参数之中
MYSQL_RES* result = mysql_store_result(mysql_);
int rows = mysql_num_rows(result);
for(int i = 0; i<rows; ++i){
MYSQL_ROW row = mysql_fetch_row(result);
//数据库查出的每条记录都相当于是一个图片信息
//需要把这个信息转成JSON格式
Json::Value image;
image["image_id"] = atoi(row[0]);
image["image_name"] = row[1];
image["size"] = atoi(row[2]);
image["upload_time"] = row[3];
image["md5"] = row[4];
image["type"] = row[5];
image["path"] = row[6];
images->append(image);
}
//忘了就会导致内存泄露
mysql_free_result(result);
return true;
}
bool SelectOne(int image_id,Json::Value* image_ptr){
char sql[1024*4] = {0};
sprintf(sql,"select* from image_table where image_id = %d",image_id);
int ret = mysql_query(mysql_,sql);
if(ret!=0){
printf("SelectOne 执行 SQL 失败!%s\n",mysql_error(mysql_));
return false;
}
//遍历结果集合
MYSQL_RES* result = mysql_store_result(mysql_);
int rows = mysql_num_rows(result);
if(rows!=1){
printf("SelectOne 查询结果不是1条记录!实际查到 %d 条!\n",rows);
return false;
}
MYSQL_ROW row = mysql_fetch_row(result);
Json::Value image;
image["image_id"] = atoi(row[0]);
image["image_name"] = row[1];
image["size"] = atoi(row[2]);
image["upload_time"] = row[3];
image["md5"] = row[4];
image["type"] = row[5];
*image_ptr = image;
//释放结果集合
mysql_free_result(result);
return true;
}
bool Delete(int image_id){
char sql[1024*4] = {0};
sprintf(sql,"delete from image_table where image_id = %d",image_id);
int ret = mysql_query(mysql_,sql);
if(ret!=0){
printf("Delete执行 SQL 失败!%s\n",mysql_error(mysql_));
return false;
}
return true;
}
private:
MYSQL* mysql_;
};
}//end image_system
**json**
json是一种数据组织格式, 最主要的用途之一就是序列化, json源于JavaScript用来表示一个"对象",使用键值对来表示:
{
"hero_name": "锤石",
"skill 1": 死亡之钩,
"skill 2": 灯笼,
"skill 3 " 阻挡,
"skill 4" 牢笼,
}
json优势: 方便调试;
json劣势: 组织格式的效率比较低, 更占用存储空间和带宽.
protobuf: 谷歌出品的一种二进制序列化协议.
2.2 服务器模块 (给前端提供一些接口, 和前端网页进行交互)
HTTP服务器需要接受http请求,返回http响应。此处需要约定不同的请求来表示不同的操作方式。例如有些请求表示上传图片,有些请求表示查看图片,有些表示删除图片,我们需要实现一个HTTP服务器此处使用Restful风格的设计,那么什么是Restful风格的设计?
- http method来表示操作的动词:GET查,POST增,PUT改,DELETE删;
- http path表示操作的对象;
- 补充信息一般使用body来传递,通常情况下使用json格式的数据来组织
- 响应数据通常也是用json格式组织;
(我们在实现HTTP服务器的时候用到了GitHub上的cpp-httplib这个开源库)
我们在使用的时候将其放到库的链接搜索路径下,然后再代码中包含库的头文件就可以用这个库了;开始设计服务器的一些API了,服务器API设计有五部分组成。
- 上传图片
- 查看所有图片信息
- 查看指定图片信息
- 查看指定图片内容
- 删除图片
创建db.hpp (.hpp头文件即包含声明又包含实现)
#pragma once
#include <cstdio>
#include <cstdlib>
#include <mysql/mysql.h>
#include <jsoncpp/json/json.h>
namespace image_system {
//初始化
static MYSQL* MySQLInit() {
// 使用 mysql API 来操作数据库了
// 1. 先创建一个 mysql 的句柄
MYSQL* mysql = mysql_init(NULL);
// 2. 拿着句柄和数据库建立链接
if (mysql_real_connect(mysql, "127.0.0.1", "root", "940822", "image_system", 3306, NULL, 0) == NULL) {
// 数据库链接失败
printf("连接失败! %s\n", mysql_error(mysql));
return NULL;
}
// 3. 设置编码格式
mysql_set_character_set(mysql, "utf8");
return mysql;
}
// 释放
static void MySQLRelease(MYSQL* mysql) {
mysql_close(mysql);
}
// 操作数据库中的 image_table 这个表.
// 此处 Insert 等操作, 函数依赖的输入信息比较多.
// 为了防止参数太多, 可以使用 JSON 来封装参数
// 创建一个ImageTable这个类,和我们的数据库表进行交互
class ImageTable {
public:
ImageTable(MYSQL* mysql) : mysql_(mysql) {}
// image 就形如以下形式:
// {
// image_name: "test.png",
// size: 1024,
// upload_time: "2019/08/28",
// md5: "abcdef",
// type: "png",
// path: "data/test.png"
// }
// 使用 Json 的原因: 1. 扩展更方便; 2. 方便和服务器接受到的数据打通
// asCString 转换为C风格字符串
bool Insert(const Json::Value& image) {
char sql[4 * 1024] = {0};
sprintf(sql, "insert into image_table values(null, '%s', %d, '%s', '%s', '%s', '%s')", image["image_name"].asCString(),
image["size"].asInt(), image["upload_time"].asCString(),
image["md5"].asCString(), image["type"].asCString(),
image["path"].asCString());
// 可以更好地确定拼装的sql语句是否正确
printf("[Insert sql] %s\n", sql);
// 执行sql语句
int ret = mysql_query(mysql_, sql);
if (ret != 0) {
printf("Insert 执行 SQL 失败! %s\n", mysql_error(mysql_));
return false;
}
return true;
}
// 个人习惯.
// 如果是输入参数, 使用 const&
// 如果是输出参数, 使用 *
// 如果是输入输出参数, 使用 &
bool SelectAll(Json::Value* images) {
char sql[1024 * 4] = {0};
sprintf(sql, "select * from image_table");
int ret = mysql_query(mysql_, sql);
if (ret != 0) {
printf("SelectAll 执行 SQL 失败! %s\n", mysql_error(mysql_));
return false;
}
// 遍历结果集合, 并把结果集写到 images 参数之中
MYSQL_RES* result = mysql_store_result(mysql_);
int rows = mysql_num_rows(result);
for (int i = 0; i < rows; ++i) {
MYSQL_ROW row = mysql_fetch_row(result);
// 数据库查出的每条记录都相当于是一个图片的信息
// 需要把这个信息转成 JSON 格式
Json::Value image; // image 对象
// 第零列就是image_id,image_id通过方括号的形式插入一个键值对,row[0]得到的类型为char型
image["image_id"] = atoi(row[0]);
image["image_name"] = row[1];
image["size"] = atoi(row[2]);
image["upload_time"] = row[3];
image["md5"] = row[4];
image["type"] = row[5];
image["path"] = row[6];
// 将image对象添加到参数之中, 若干个对象添加到参数之中
images->append(image);
}
// 忘了就会导致内存泄露
mysql_free_result(result);
return true;
}
bool SelectOne(int image_id, Json::Value* image_ptr) {
char sql[1024 * 4] = {0};
sprintf(sql, "select * from image_table where image_id = %d",
image_id);
int ret = mysql_query(mysql_, sql);
if (ret != 0) {
printf("SelectOne 执行 SQL 失败! %s\n", mysql_error(mysql_));
return false;
}
// 遍历结果集合
MYSQL_RES* result = mysql_store_result(mysql_);
int rows = mysql_num_rows(result);
if (rows != 1) {
printf("SelectOne 查询结果不是 1 条记录! 实际查到 %d 条!\n", rows);
return false;
}
MYSQL_ROW row = mysql_fetch_row(result);
Json::Value image;
image["image_id"] = atoi(row[0]);
image["image_name"] = row[1];
image["size"] = atoi(row[2]);
image["upload_time"] = row[3];
image["md5"] = row[4];
image["type"] = row[5];
image["path"] = row[6];
*image_ptr = image;
// 释放结果集合
mysql_free_result(result);
return true;
}
bool Delete(int image_id) {
char sql[1024 * 4] = {0};
sprintf(sql, "delete from image_table where image_id = %d",
image_id);
int ret = mysql_query(mysql_, sql);
if (ret != 0) {
printf("Delete 执行 SQL 失败! %s\n", mysql_error(mysql_));
return false;
}
return true;
}
private:
MYSQL* mysql_;
};
} // end image_system 命名空间结束
上传图片
为了让服务器在网页上提供一个最简单的图片上传功能,需要一个html文件;
upload.html
<html>
<head></head>
<bady>
<form id="upload-form" action="http://192.168.11.130:9094/image" method="post"
enctype="multipart/form-data">
<input type="file" id="upload" name="upload" /> <br />
<input type="submit" value="Upload" />
</form>
</body>
</html>
然后我们再设计服务器接收图片的API
image_server.cc
#include <fstream> //C++版本的读写操作
#include <signal.h>
#include <sys/stat.h>
#include "httplib.h"
#include "db.hpp"
// C++读写文件
class FileUtil {
public:
static bool Write(const std::string& file_name,
const std::string& content) {
std::ofstream file(file_name.c_str());
if (!file.is_open()) {
return false;
}
file.write(content.c_str(), content.length());
file.close();
return true;
}
static bool Read(const std::string& file_name,
std::string* content) {
std::ifstream file(file_name.c_str());
if (!file.is_open()) {
return false;
}
// 包含#include <sys/stat.h> 调用stat函数,获取文件长度更新为resize和文件长度一样长
struct stat st;
stat(file_name.c_str(), &st);
content->resize(st.st_size);
// 一口气把整个文件都读完
// 需要先知道该文件的大小
// char* 缓冲区长度
// int 读取多长
file.read((char*)content->c_str(), content->size());
file.close();
return true;
}
};
MYSQL* mysql = NULL;
int main() {
//作用域不同(放在下面只有函数内部生效,放在上面整个文件都生效)
//命名空间是为了防止名字冲突
using namespace httplib;
mysql = image_system::MySQLInit();
image_system::ImageTable image_table(mysql);
// 通过捕捉二维信号,在二维信号的回调函数中调用realease
signal(SIGINT, [](int) {
image_system::MySQLRelease(mysql);
exit(0);
});
Server server;
// 客户端请求 /hello 路径的时候, 执行一个特定的函数
// 指定不同的路径对应到不同的函数上, 这个过程
// 称为 "设置路由"
// 服务器中有两个重要的概念:
// 1. 请求(Request)
// 2. 响应(Response)
// [&image_table] 这是 lambda 的重要特性, 捕获变量
// 本来 lambda 内部是不能直接访问 image_table 的.
// 捕捉之后就可以访问了. 其中 & 的含义是相当于按引用捕获
server.Post("/image", [&image_table](const Request& req, Response& resp) {
Json::FastWriter writer;
Json::Value resp_json;
printf("上传图片\n");
// 1. 对参数进行校验
auto ret = req.has_file("upload");
if (!ret) {
printf("文件上传出错!\n");
resp.status = 404;
// 可以使用 json 格式组织一个返回结果
resp_json["ok"] = false;
resp_json["reason"] = "上传文件出错, 没有需要的upload 字段";
resp.set_content(writer.write(resp_json), "application/json");
return;
}
// 2. 根据文件名获取到文件数据 file 对象
const auto& file = req.get_file_value("upload");
// file.filename;
// file.content_type;
// 3. 把图片属性信息插入到数据库中
Json::Value image;
image["image_name"] = file.filename;
image["size"] = (int)file.length;
image["upload_time"] = "2018/08/29"; // TODO
image["md5"] = "aaaaaaa"; // TODO
image["type"] = file.content_type;
image["path"] = "./data/" + file.filename;
ret = image_table.Insert(image);
if (!ret) {
printf("image_table Insert failed!\n");
resp_json["ok"] = false;
resp_json["reason"] = "数据库插入失败!";
resp.status = 500;
resp.set_content(writer.write(resp_json), "application/json");
return;
}
// 4. 把图片保存到指定的磁盘目录中,需要创建data目录
auto body = req.body.substr(file.offset, file.length);
// asString() 得到std Stringfenge分隔的字符串
FileUtil::Write(image["path"].asString(), body);
// 5. 构造一个响应数据通知客户端上传成功
resp_json["ok"] = true;
resp.status = 200;
resp.set_content(writer.write(resp_json), "application/json");
return;
});
// Lambda表达式
// 后续有客户端请求path路径为/image,即执行[&image_table](const Request& req,Response& resp)这样的函数
// server.Get("/image", [&image_table](const Request& req,Response& resp))
// [&]表示请求
server.Get("/image", [&image_table](const Request& req,
Response& resp) {
(void) req; // 没有任何实际的效果
printf("获取所有图片信息\n");
Json::Value resp_json;
Json::FastWriter writer;
// 1. 调用数据库接口来获取数据
bool ret = image_table.SelectAll(&resp_json);
if (!ret) {
printf("查询数据库失败!\n");
resp_json["ok"] = false;
resp_json["reason"] = "查询数据库失败!";
resp.status = 500;
resp.set_content(writer.write(resp_json), "application/json");
return;
}
// 2. 构造响应结果返回给客户端
resp.status = 200;
resp.set_content(writer.write(resp_json), "application/json");
});
// 1. 正则表达式
// 2. 原始字符串(raw string),原始字符串中无转义字符
// (\d+)只匹配一个或多个数字
server.Get(R"(/image/(\d+))", [&image_table](const Request& req, Response& resp) {
Json::FastWriter writer;
Json::Value resp_json;
// 1. 先获取到图片 id
// matches返回的是字符串,stoi将返回的字符串
int image_id = std::stoi(req.matches[1]);
printf("获取 id 为 %d 的图片信息!\n", image_id);
// 2. 再根据图片 id 查询数据库
bool ret = image_table.SelectOne(image_id, &resp_json);
if (!ret) {
printf("数据库查询出错!\n");
resp_json["ok"] = false;
resp_json["reason"] = "数据库查询出错";
resp.status = 404;
resp.set_content(writer.write(resp_json), "application/json");
return;
}
// 3. 把查询结果返回给客户端
resp_json["ok"] = true;
resp.set_content(writer.write(resp_json), "application/json");
return;
});
server.Get(R"(/show/(\d+))", [&image_table](const Request& req,
Response& resp) {
Json::FastWriter writer;
Json::Value resp_json;
// 1. 根据图片 id 去数据库中查到对应的目录
int image_id = std::stoi(req.matches[1]);
printf("获取 id 为 %d 的图片内容!\n", image_id);
Json::Value image;
bool ret = image_table.SelectOne(image_id, &image);
if (!ret) {
printf("读取数据库失败!\n");
resp_json["ok"] = false;
resp_json["reason"] = "数据库查询出错";
resp.status = 404;
resp.set_content(writer.write(resp_json), "application/json");
return;
}
// 2. 根据目录找到文件内容, 读取文件内容
std::string image_body;
printf("%s\n", image["path"].asCString());
ret = FileUtil::Read(image["path"].asString(), &image_body);
if (!ret) {
printf("读取图片文件失败!\n");
resp_json["ok"] = false;
resp_json["reason"] = "读取图片文件失败";
resp.status = 500;
resp.set_content(writer.write(resp_json), "application/json");
return;
}
// 3. 把文件内容构造成一个响应
resp.status = 200;
// 不同的图片, 设置的 content type 是不一样的.
// 如果是 png 应该设为 image/png
// 如果是 jpg 应该设为 image/jpg
resp.set_content(image_body, image["type"].asCString());
});
server.Delete(R"(/image/(\d+))", [&image_table](const Request& req, Response& resp) {
Json::FastWriter writer;
Json::Value resp_json;
// 1. 根据图片 id 去数据库中查到对应的目录
int image_id = std::stoi(req.matches[1]);
printf("删除 id 为 %d 的图片!\n", image_id);
// 2. 查找到对应文件的路径
Json::Value image;
bool ret = image_table.SelectOne(image_id, &image);
if (!ret) {
printf("删除图片文件失败!\n");
resp_json["ok"] = false;
resp_json["reason"] = "删除图片文件失败";
resp.status = 404;
resp.set_content(writer.write(resp_json), "application/json");
return;
}
// 3. 调用数据库操作进行删除
ret = image_table.Delete(image_id);
if (!ret) {
printf("删除图片文件失败!\n");
resp_json["ok"] = false;
resp_json["reason"] = "删除图片文件失败";
resp.status = 404;
resp.set_content(writer.write(resp_json), "application/json");
return;
}
// 4. 删除磁盘上的文件
// C++ 标准库中, 没有删除文件的方法
// C++ 17 标准里是有的.
// 此处只能使用操作系统提供的函数了
unlink(image["path"].asCString());
// 5. 构造响应
resp_json["ok"] = true;
resp.status = 200;
resp.set_content(writer.write(resp_json), "application/json");
});
server.set_base_dir("./wwwroot");
server.listen("0.0.0.0", 9094);
return 0;
}
jsoncpp操作和解析json:
一个核心类, 两个重要方法.
json::value非常类似于std::map
Reader::parse把一个json字符串转换成json::value对象
Writer::write把一个json::value对象转换成字符串.
3. 项目测试
#include "db.hpp"
// 单元测试
// C++ 的单元测试框架都有哪些?
void TestImageTable() {
// 创建一个 ImageTable 类, 去调用其中的方法, 验证结果
// 使格式化出来后的json具有一定的序列化
// 将一个json::value对象转换成字符串
Json::StyledWriter writer;
MYSQL* mysql = image_system::MySQLInit();
image_system::ImageTable image_table(mysql);
bool ret = false;
// 1. 插入数据
// Json::Value image;
// image["image_name"] = "test.png";
// image["size"] = 1024;
// image["upload_time"] = "2019/08/28";
// image["md5"] = "abcdef";
// image["type"] = "png";
// image["path"] = "data/test.png";
// ret = image_table.Insert(image);
// printf("ret = %d\n", ret);
// 2. 查找所有图片信息
//Json::Value images;
//ret = image_table.SelectAll(&images);
//printf("ret = %d\n", ret);
//将一个std::string 转换为c风格的字符串
//printf("%s\n", writer.write(images).c_str());
// 3. 查找指定图片信息
// Json::Value image;
// ret = image_table.SelectOne(1, &image);
// printf("ret = %d\n", ret);
// printf("%s\n", writer.write(image).c_str());
// 4. 删除指定图片
ret = image_table.Delete(1);
printf("ret = %d\n", ret);
image_system::MySQLRelease(mysql);
}
int main() {
TestImageTable();
return 0;
}
4. 项目的扩展
4.1 存储时合并文件
如果上传大量的比较小的文件时,在磁盘空间不太充裕时可能会产生磁盘碎片,把这些逻辑上比较小的文件合并成一个比较大的物理文件,在读取文件时,数据库中除了存该文件的路径之外,再存一个偏移量,在已知路径的相对偏移量开始读起,就可以正常读取文件;
磁盘碎片应该称为文件碎片,是因为文件被分散保存到整个磁盘的不同地方,而不是连续地保存在磁盘连续的簇中形成的。硬盘在使用一段时间后,由于反复写入和删除文件,磁盘中的空闲扇区会分散到整个磁盘中不连续的物理位置上,从而使文件不能存在连续的扇区里。这样,再读写文件时就需要到不同的地方去读取,增加了磁头的来回移动,降低了磁盘的访问速度。
4.2 防盗链
只要其他人拿到了我的url就可以使用我的图床, 所以可以增加权限控制,只让图片能被特定的用户使用;
防盗链的方法:使用登录验证,判断引用地址,使用cookie,使用POST下载,使用图形验证码,打包下载等其中使用cookie是通过实现用户账户功能,登录之后就得到了cookie,有了cookie就可以正常使用;
4.3 支持图片处理功能
比如在qq和手机相册中常见的缩略图功能,这样的好处是如果原图片比较大(像是2k和4k的图片),相同带宽下缩略图加载更快,可以在用户请求时选择添加一个参数,比如width=100&length=120,借助C++图片处理库:计算机视觉库OpenCV,开源图形库FreeImage等
4.4 相同图片只保留一份
节省服务器资源,用md5实现对文件内容是否相同的判断,在实现时还需要进行引用计数,比如一张图片上传了两次,但是删除一次,但是在另一路径下的相同的没被删除的图片文件也被删了.
5. 项目总结
本项目就是实现一个HTTP服务器,然后用这个服务器来存储图片,针对每个图片提供一个唯一的url,有了这个url之后就可以借助它把图片展示到其他网页上。项目主要分成两个模块:数据库存储模块和服务器模块。数据存储模块我们主要是通过MySQL的API来操作数据库,用JSON对数据库中的image_table这个表进行操作。服务器模块我们主要是通过cpp-httplib这个库来为服务器提供一些向外的接口。当项目代码完成之后我们又使用postman这个软件对我们所完成的操作进行了测试。
通过这个项目,我觉得自己对于知识的掌握还是不太牢固,自己的知识面还是比较窄,需要学习的东西还比较多, 希望自己今后不断加油!