图片服务器

一、项目背景

  • 如果大家能看到这篇文章,那么相信大家对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这个软件对我们所完成的操作进行了测试。
  通过这个项目,我觉得自己对于知识的掌握还是不太牢固,自己的知识面还是比较窄,需要学习的东西还比较多。同时这个项目还有很多可以扩展的地方一个是:防盗链、另一个是存储时合并文件,鉴于目前自己还比较菜,之后等自己修炼完成之后会将其完成的。
       对于此项目有做的不对的地方还请大佬们批评指正!!!

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值