图片服务器

一.目录

  1.项目产生的背景

  2.项目模块的实现

  3.项目的基本测试

  4.项目的扩展

  5.项目中的问题

  

 

1.项目产生的背景

      有时候需要向网页上上传一张图片,但是这个网页却不支持基本的图片上传方式。因此我就想设计一个可以存储图片的服务器,通过使用这个服务器来保存我们可能会使用到的图片,并且这个服务器会对每一张图片产生唯一的一个url,用户可以通过这个url,将图片展示到其他地方。

2.项目模块的实现

(1)数据存储模块

数据存储模块主要就是使用MySQL数据库对我们的文件进行存储和管理,另一方面就是通过我们的磁盘来保存我们上传的图片。

数据库设计:

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(50), 
                         size bigint, 
                         upload_time varchar(50),
                         md5 varchar(128),
                         content_type varchar(50) comment '图片类型', 
                         path varchar(1024) comment '图片所在路径')

其中对于表单的创建我们使用到了md5对数据的完整性的保护,那么md5又是什么?

首先md5是一种字符串哈市算法,并且他有三个特性可以保证数据的安全性。

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

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

3.通过源字符串计算md5很容易,但是拿到md5还原源字符串理论上是不可能的。

2.使用MySQL C API操作数据库

通过使用这个API函数,可以让我们通过代码进行数据库的操作,

接下来实现的就是对数据库插入数据的代码:

 #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", "110", "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, "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;

数据库的插入实现完成之后,我们就可以使用查询函数对我们的插入进行验证:

 #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", "110", "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)服务器模块

HTTP服务器需要接受http请求,返回http响应,此处我们需要约定不同的请求来表示不同的操作方式。

如有的请求是上传图片,有的请求是查看图片,有些则是表示删除的请求。

服务端代码如下:

#include<fstream>                                                                                                     #include<openssl/md5.h>
 #include<iostream>
 #include<time.h>
 #include <signal.h>
 #include <sys/stat.h>
 #include "httplib.h"
 #include "db.hpp"
 
 using namespace std;
 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;

string get_strtime()
{
  time_t tt;
  time(&tt);
  tm* t= localtime(&tt);
  char buf[128] = {0};
  int ret = strftime(buf, 128, "%F", t);

  string tmp;
  tmp.assign(buf, ret);
  cout << tt << endl;
  
  return tmp;
}


int main() {                                                                                                                                                                          
  using namespace httplib;

  mysql = image_system::MySQLInit();
  image_system::ImageTable image_table(mysql);
  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.c
     ontent_type;
// 3. 把图片属性信息插入到数据库中
     Json::Value image;
     image["image_name"] = file.filename;
     image["size"] = (int)file.length;
     image["upload_time"] = get_strtime();
     image["md5"] = "aaaaaaaa"; // 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;
}       

3.项目的基本测试

测试效果如下:

查看指定图片

4.项目的扩展

存储时合并文件,上传大量的比较小的文件,把这些逻辑上比较小的文件合并成一个比较大的物理文件,数据库中除了存这个该文件路径外,再存一个偏移量。

5.项目中遇到的问题

首先就是对库文件的使用方法不熟悉,需要通过查阅网上相关内容才能继续进行,下来就是对有些协议的使用,对其概念有点生疏,因此还需要我再继续加强各种基础协议栈的掌握。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

      

  

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值