Linux_图片搜索器

一个人拥有此生此世是不够的,他还应该拥有诗意的世界!

图片服务器

1. 项目背景

实现一个HTTP服务器, 然后利用这个服务器来存储图片, 针对每一张图片都有一张唯一的URL, 可以借助这个URL将图片展示到其他网页上.

存储图片 (写博客, 上传图片的时候最终显示到终端上), HTML构建网页所必须的东西,
https://csdnimg.cn/cdn/content-toolbar/csdn-logo_.png?v=20190924.1 (CSDN网络路径, 通过这个路径可以访问到网页上的图片)

需求诞生

  1. 针对有些软件不支持上传图片功能, 需要上传图片的地址, 例如:GitHub中的issue,项目中有bug需要改进什么的, 有时候需要截张图片, 如何向作者反馈, 就需要放一张链接, 对应图片的链接放到服务器上;

  2. 自己搭建的静态网页博客上传图片是需要上传图片的地址;

  3. 实际中所遇到的问题;

核心需求

  1. 上传图片;

  2. 根据图片的url访问图片,获取图片内容(即下载);

  3. 获取某个图片的属性 (大小,格式,上传时间,路径等等);

  4. 删除图片;

服务器针对的是网络通信, 谈到服务器就会涉及到客户端(主动发送请求)和服务端 (被动接受请求), 两者之间的角色有可能发替换, 同一个进程有可能使客户端也可能是服务端(例如: 手机上点外卖, 手机上的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个字节的数字, 并且他有三个特性可以保证数据的安全性:

  1. 不管是什么样的字符串,最终得到的md5值都是一个固定的长度。

  2. 如果一个字符串,内容稍有变化,得到的md5值的差异还是很大的。

  3. 通过源字符串计算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风格的设计?

  1. http method来表示操作的动词:GET查,POST增,PUT改,DELETE删;
  2. http path表示操作的对象;
  3. 补充信息一般使用body来传递,通常情况下使用json格式的数据来组织
  4. 响应数据通常也是用json格式组织;
    (我们在实现HTTP服务器的时候用到了GitHub上的cpp-httplib这个开源库)

我们在使用的时候将其放到库的链接搜索路径下,然后再代码中包含库的头文件就可以用这个库了;开始设计服务器的一些API了,服务器API设计有五部分组成。

  1. 上传图片
  2. 查看所有图片信息
  3. 查看指定图片信息
  4. 查看指定图片内容
  5. 删除图片

创建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这个软件对我们所完成的操作进行了测试。
  通过这个项目,我觉得自己对于知识的掌握还是不太牢固,自己的知识面还是比较窄,需要学习的东西还比较多, 希望自己今后不断加油!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值