图床项目

项目设计思路:

  • 项目背景
  • 项目核心需求
  • 项目模块划分
  • 设计API
  • 项目总结

一. 项目背景

一个网页上的图片是怎样展示的???

  • 有一个url来表示图片的位置
  • 有一个image标签,里面引用这个位置

例如我们随便打开一个网页,查看源代码就可以看到这个image,以及该图片的网络路径
在这里插入图片描述
URL:URL 全名叫统一资源定位符,uniform resource Locator,俗称网页或网址,字面上来理解,它就是用来定位资源的。相当于图书上面的标签,有了这些标签,管理员可以很快的找到相应的图书。
一个完整的URL包括以下信息:协议,IP地址,路径,端口号

二. 项目的核心需求:

实现一个HTTP服务器,然后用这个服务器来存储图片,针对每个图片提供一个唯一的url,有了这个url后就可以借助它把图片展示到其他网页上

  • 上传图片(得到一个url);
  • 根据图片的url来访问图片,获取图片内容;
  • 获取某个图片的属性;
  • 删除

三. 项目模块划分:

  • 数据存储模块(上传的图片需要存储到服务器);
  • 服务器模块(给前端提供一些接口);

1.数据存储模块

磁盘文件和数据库(MySQL)
数据库设计:只需要一张表,如下表
针对图片内容,可以直接存在磁盘上。

create table image_table(
        image_id int,
        image_name varchar(256),
        size int,
        upload_time varchar(50),
        type varchar(50),
        path varchar(1024),
        md5 varchar(50)
        )

注意:::
**md5:**是一种字符串hash算法

  • 不管是哪种字符串,最终得到的md5值都是固定的
  • 如果一个字符串内容稍微有变化,得到的md5值差异很大
  • 通过原字符串计算md5很容易,但是拿到的md5还原原串理论上是不可能的

md5作用:
md5这个字段用来校验图片内容正确性,上传图片后,服务器就可以计算一个该图片的md5值,后续用户下载图片的时候,也能获取到该图片的md5,用户可以把自己计算的md5和服务器计算的md5对比,即可直到自己的图片是否下载成功

MySQL客户端代码:

#include 
#include 
#include 
#include 

namespace image_system{

  static MYSQL* MySQLInit(){
    //1.先创建一个mysql的句柄
    MYSQL* mysql = mysql_init(NULL);
    //2.用句柄和数据库建立连接
    if(mysql_real_connect(mysql,"127.0.0.1","root","1","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或者类来封装参数 JSON改动更为灵活
  class ImageTable{
    public:
      ImageTable(MYSQL* mysql) : _mysql(mysql){}
      //image形如以下形式
      //{
      //  image_name: "test.png",
      //  size: 200,
      //  upload_time: "2010/08/28",
      //  md5: "aaaaaaa",
      //  yupe: "png",
      //  path: "data/test.png"
      //}
      //使用JSON原因:1.扩展更为方便  2.和服务器接收到的数据更方便打通
      bool Insert(const Json::Value& image){
        char sql[4096] = {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;
      }

      //函数参数设计:
      //1.输入型参数,const&
      //2.输出型参数,使用*
      //3.输入输出型参数,使用&
      bool SelectAll(Json::Value* images){
        char sql[4096] = {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);
        int i;
        for(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[4096] = {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[4096] = {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

2.服务器模块

如何设计一个HTTP服务器???

需要借助第三方库 cpp-httplib
我们可以在Github上查找该库
图床项目_第1张图片
只需要在项目中包含这个头文件即可使用,利用该库生成一个简单的服务器,如下:

#include "httplib.h"

//回调函数,一个函数,调用时机由代码框架和操作系统来决定
void Hello(const httplib::Request& req,httplib::Response& resp){
	resp.set_content("

hello

"
,"text/html");//“text/html”是HTTP Content-Type } int main(){ using namespace httplib;//引用命名空间,在函数内部生效,避免名称冲突 Server server; //客户端请求 / 路径的时候,执行一个特定的函数 //指定不同的路径对应到不同的函数上,这个过程称为”设置路由“ //第一个参数是路径,第二个参数是函数指针,当客户端请求path //为/的请求时,就会执行函数指针所指向的函数,相当于把函数注册到了代码框架中,什么时候 //调用不确定,由代码框架和操作系统来决定,此为回调函数 server.Get("/",Hello); server.listen("0.0.0.0",9094);//此表示了服务器启动的全部过程,ip地址和端口号 return 0; }

然后启动服务器,在网页上输入你的地址47.98.116.42:9094/hello,(此处用的是阿里云的IP)即可查看

注意:此时不能用局域网ip地址访问,需要用外网Ip访问

图床项目_第2张图片
查看网页源码
图床项目_第3张图片

HTTP服务器需要接受http请求,返回http响应,此处需要约定不同的请求来表示不同的操作方式,例如有些请求表示上传图片,有些请求表示查看图片或者删除图片
此处使用Restful风格的设计:

  • http method来表示操作动词
    ----例如:POST(增) DELETE(删) PUT(改) GET(查)
  • http path表示操作的对象
  • body来传递补充信息,通常情况下body使用json格式的数据来组织数据
  • 响应数据通常也是用json格式组织
注意:::

(1)json源于javascript用来表示一个“对象”;是一种数据组织格式,最主要的用途之一就是序列化
(2)json格式的数据都是用**{}括起来的,用键值对的形式**表示的
(3)json优势:方便调试,方便和服务器接受到的数据打通
(4)json劣势:组织格式的效率比较低,更占用存储空间和带宽
(5) protobuf是谷歌出品的一种二进制序列化协议

四. 设计服务器API:

为了让数据库和服务器有更方便的交互,根据MySQL官网提供的一些API(应用程序接口)来实现一个自主客户端
这里我们在ImageTable类中封装了sql语句包括以下一些操作:
1.上传图片
-----POST/image HTTP/1.1(版本号)
Content-Type(和提交表单密切相关;提交表单:即HTML中客户端给服务器上传数据的一种常见方法
2.查看所有图片信息
------GET/image
3.查看指定图片信息
-----GET/image/:image_id
4.查看指定图片内容(保存在磁盘文件上,其他的保存在数据库中)
-----GET/show/:image_id
5.删除图片
-----DELETE/image/:image_id

此处我们需要用到第三方库jsoncpp
注意:该库包含一个核心类,两个重要方法

  • 一个核心类:Jsoncpp::Value(类似于std::Map)
  • 两个重要方法:1.Reader::parse,Reader类中有一个parse方法,把一个Json字符串中转换为Json::value对象(反序列化过程)
    2.Writer::Write,Write类中有一个write方法,把Json::value对象转化为Json字符串(序列化过程)
  • 序列化:将数据对象按照指定的协议组织成为能够后进行持久化存储/数据传输的二进制数据串
  • 反序列化:将二进制数据串按照指定的协议解析得到各个数据对象
安装方式:
yum install jsoncpp-devel.x86_64
是否安装成功,查看ls -l usr/include/jsoncpp/json 中是否有我们需要的一些头文件等

例如html上传图片

<html>
<head></head>
<body>
<form id="upload-form" action="http://47.98.116.42:9094/image" method="post" enctype="multipart/form-data" >
   <input type="file" id="upload" name="upload" /> <br />
   <input type="submit" value="Upload" />
</form>
</body>
</html>

form标签表示这是一个form表单,form表单是一种传统的浏览器、网页和服务器交互的方式,其功能就是提供一些选项框,借助这些选项框将数据提交给服务器。注意需要修改form表单中的action,action表示将表单提交给指定服务器。

API接口源码:
#include 
#include 
#include 
#include "httplib.h"
#include "db.hpp"

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;
    }
    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);
  signal(SIGINT, [](int) {//中断服务器ctrl + c
      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. 把图片保存到指定的磁盘目录中
			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;
    });

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

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

五. 总结:

该项目借助httplib库简单实现了一个图片服务器,提供了四个接口,分别是上传图片,获取图片信息,查看图片内容,删除图片;
其中利用md5来校验上传图片内容的正确性,图片内容保存在磁盘文件中,图片信息保存在数据库中,服务器和客户端之间的数据是采用Json格式字符串组织传输的。

你可能感兴趣的:(项目)