一、项目背景
- 如果大家能看到这篇文章,那么相信大家对csdn已经有了一定的了解;以我们写博客为例,我们可能都会在自己写的博客上上传图片,而这张图片最终就会展示在网页上,那么一张图片是怎么展示在网页上的呢
我们以csdn首页的图标为例看一下csdn的主页Logo图片是怎么在其主页存储的
我们右键复制一下 csdn 的 Logo地址。然后在新标签页打开图片:
我们可以看到这一张图片也有其图片的地址,我们通过这个地址就能找到这张图片,那么这个图片是在网页中是怎么存放的呢?
从图上我们看到csdn网页中的源代码有这张Logo的地址,我们通过这个地址就可以找到我们要的这张图片,从而就可以展示在网页上了。
这个项目就是实现一个HTTP服务器,然后用这个服务器来存储图片,针对每个图片提供一个唯一的url,有了这个url之后就可以借助它把图片展示到其他网页上了
项目主要有一下几步组成
1、上传图片(得到一个url)
2、根据图片的url访问图片 获取图片内容
3、获取某个图片的属性
4、删除
二、项目的模块
项目主要有两个模块组成,数据存储模块和服务器模块
设计思路:
- 数据存储模块:使用MySQL存储图片的描述信息,并且将获取到的图片内容保存到数据库中和磁盘上。
- 服务器模块:将我们所存的图片与前端进行交互,给前端提供一些接口,使得客户端能够访问到我们存储的图片。
1、数据存储模块
数据库设计:我们在数据库中设计一张表用来存储图片的属性也就是表的结构。
create table image_table(
image_id int,//图片的id号
image_name varchar(256),//图片的名字
size int,//图片的大小
upload_time varchar(50),//图片的上传时间
type varchar(50),//图片的类型(png,jpg等格式)
path varchar(1024),//图片的路径
md5 varchar(50)//校验和,用来校验图片内容的正确性
);
对于上面最后一个字段md5,我们在这在对它进行一个简单的介绍
md5是一种字符串hash算法
1、不管啥样的字符串,最终得到的md5值都是固定长度
2、如果一个字符串,内容稍有变化,得到的md5值差异很大
3、通过原字符串计算md5很容易,但是拿到的md5还原原串理论上不可能
其中md5这个字段用来进行校验图片内容正确性的;上传图片之后,服务器就可以计算一个该图片的md5值,后续用户下载图片的时候,
也能获取到该图片的md5,用户可以把自己计算的md5和服务器计算的md5对比,就知道自己的图片是否下载正确了
现在数据库已经设计完成了,这时候就应该使用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","","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来封装参数呢?
1、使用 json 扩展更方便
2、方便和服务器接收到的数据打通
3、操作数据库中的image_table这个表的Insert等操作,函数依赖的输入信息比较多为了防止参数太 多,可以使用JSON来封装参数
2、服务器模块
HTTP服务器需要接受http请求,返回http响应。此处需要约定不同的请求来表示不同的操作方式。
例如有些请求表示上传图片,有些请求表示查看图片,有些表示删除图片
我们需要实现一个HTTP服务器此处使用Restful风格的设计,那么什么是Restful风格的设计?
1、http method来表示操作的动词:GET查,POST增,PUT改,DELETE删
2、http path表示操作的对象
3、补充信息一般使用body来传递,通常情况下使用json格式的数据来组织
4、响应数据通常也是用json格式组织
我们在实现HTTP服务器的时候用到了GitHub上的cpp-httplib这个开源库
我们在使用的时候将其放到库的链接搜索路径下,然后再代码中包含库的头文件就可以用这个库了
我们首先先尝试着使用一下这个库,先用一个简单的hello程序演示一下
我们在浏览器上输入ip地址和端口号之后,hello就成功的显示到了网页上了
接下来我们就开始设计服务器的一些API了,服务器API设计有五部分组成。
- 上传图片
- 查看所有图片信息
- 查看指定图片信息
- 查看指定图片内容
- 删除图片
2-1、上传图片
为了让服务器在网页上提供一个最简单的图片上传功能,需要一个html文件
然后我们打开一下看一下什么效果
然后我们再设计服务器接收图片的API
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;
time_t tt;
time(&tt);
tt = tt+(8*3600);//transfrom the time zone
tm* t = gmtime(&tt);
char res[1024] = {0};
sprintf(res,"%d-%02d-%02d %02d:%02d:%02d",t->tm_year+1900,t->tm_mon+1,t->tm_mday,t->tm_hour,t->tm_min,t->tm_sec);
image["upload_time"] = res;
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、把图片保存到指定的磁盘目录中
auto body = req.body.substr(file.offset, file.length);
FileUtil::Write(image["path"].asString(),body);
//5、构造一个响应数据通知客户端上传成功
resp_json["ok"] = true;
resp.status = 200;
resp.set_content(writer.write(resp_json),"application/json");
return;
});
2-2、获取所有图片信息
server.Get("/image",[&image_table](const Request& req,Response & resp){
(void)req;//没有任何实际的效果
printf("获取所有图片信息\n");
Json::FastWriter writer;
Json::Value resp_json;
//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" );
});
2-3、获取指定图片信息
server.Get(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、再根据图片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 ;
});
2-4获取指定图片内容
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;
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());
});
2-5、删除图片
server.Delete(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);
//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;
}
三、项目的测试
1、数据存储模块的测试
1-1、插入数据的测试
#include"db.hpp"
void TestImageTable(){
//创建一个ImageTable类,去调用其中的方法,验证结果
Json::StyledWriter writer;
MYSQL* mysql = image_system::MySQLInit();
image_system::ImageTable image_table(mysql);
bool ret = false;
//插入数据
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);
image_system::MySQLRelease(mysql);
}
int main(){
TestImageTable();
return 0;
}
-
没插入数据之前,我们可以看到是一张空表
-
,接下来我们对代码进行编译,看一下能否成功的将其插入到数据库中
服务端成功的接收到了插入的图片数据
数据库中成功的插入了一条数据
1-2、查找所有图片信息的测试
//2、查找所有图片信息 Json::Value images; ret = image_table.SelectAll(&images); printf("ret = %d\n",ret); printf("%s\n",writer.write(images).c_str());
接下来运行代码,看能否查找到图片信息
查找所有图片信息成功
1-3、查找指定图片信息的测试
//3、查找指定图片信息
Json::Value image;
ret = image_table.SelectOne(1,&image);
printf("ret = %d\n",ret);
printf("%s\n",writer.write(image).c_str());
1-4、删除图片
//4、删除指定图片
ret = image_table.Delete(2);
printf("ret = %d\n",ret);
我们发现执行完这个操作之后,表中image_id=1的这条数据就被删除了
2、服务器模块的测试
2-1、上传图片的测试
POST /image HTTP/1.1
Content-Type:application /x-www-form-urlencoded
[内容]
响应
HTTP/1.1 200 OK
{
ok:true
}
1、客户端上传成功
2、服务端显示成功
3、查看数据库是否有这张图片的信息
2-2、获取所有图片信息的测试
请求
GET /image
响应
HTTP/1.1 200 ok
[
{
image_id:1,
image_name:"test.png"
type:"image/png"
md5:"abcdef",
upload_time:"2019/08/26",
path:"data/test.png"
},
{
image_id
image_name
......
}
]
这里使用Postman来检测一下,Postman相当于一个http客户端,可以很方便的构造http请求并进行测试。并且可以存储请求,便于下次测试。
1、Postman成功获取所有图片信息
2、服务端正确响应
2-3、获取指定图片信息的测试
GET/image/:image_id
HTTP 200 ok
{
image_id: 1,
image_name:"test.png",
size:1024,
upload_time:"2019/08/26",
.....
}
1、Postman成功获取指定图片信息
2、服务端正确响应
2-4获取指定图片内容的测试
GET/show/:image_id
HTTP/1.1 200 OK
content-type:image/png
[body 图片内容数据]
1、Postman成功获取指定图片信息内容
2、服务端正确响应
2-5删除图片的测试
DELETE/image/:image_id
HTTP/1.1 200 ok
{
ok:true
}
1、Postman成功删除指定图片信息
2、服务端正确响应
3、数据库中成功删除指定图片
项目总结
本项目就是实现一个HTTP服务器,然后用这个服务器来存储图片,针对每个图片提供一个唯一的url,有了这个url之后就可以借助它把图片展示到其他网页上。项目主要分成两个模块:数据库存储模块和服务器模块。数据存储模块我们主要是通过MySQL的API来操作数据库,用JSON对数据库中的image_table这个表进行操作。服务器模块我们主要是通过cpp-httplib这个库来为服务器提供一些向外的接口。当项目代码完成之后我们又使用postman这个软件对我们所完成的操作进行了测试。
通过这个项目,我觉得自己对于知识的掌握还是不太牢固,自己的知识面还是比较窄,需要学习的东西还比较多。同时这个项目还有很多可以扩展的地方一个是:防盗链、另一个是存储时合并文件,鉴于目前自己还比较菜,之后等自己修炼完成之后会将其完成的。
对于此项目有做的不对的地方还请大佬们批评指正!!!