项目:图片存储系统(图片服务器)

2 篇文章 0 订阅
2 篇文章 0 订阅

图片存储系统

项目描述:
实现一个 HTTP 服务器,用该服务器来存储图片,针对每个图片提供一个唯一的url, 使用 url 对图片进行访问, 提供对图片的增删改查能力,同时搭配简单的页面辅助完成图片上传/展示

  • 利用 HTTP 服务器来为每个图片提供一个唯一访问的 url
  • 使用 Json 封装 http 请求,响应
  • 提供上传图片,查看图片信息/内容以及删除图片接口
  • 使用 lambda 表达式替换函数

实现环境:Linux MySQL-5.5.60 cpp-httplib 库
涉及技术:HTTP 协议 Json C++11 lambda 表达式

项目流程:
在这里插入图片描述
项目代码维护于github上:https://github.com/luchunabf/item

使用 JSON 作为数据交互格式

json 出自 JavaScript, 是一种非常方便的键值对数据组织格式,主要用途之一就是序列化.
C++ 中可以使用 jsoncpp 这个库来解析和构造 json 数据

yum install jsoncpp-devel 

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

安装 MySQL C API

yum install mysql-devel

代码中使用时需要链接上 MySQL 提供的库

-L /usr/lib64/mysql -lmysqlclient

数据库设计

创建数据库:

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 '图片所在路径')
                         

db.hpp

#pragma once 

#include <cstdlib>
#include <cstring>
#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", "123", "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);
  }


//操作数据库中ImageTable 这个表,
//此处 Insert 等操作, 函数依赖的输入信息较多,
//为了防止参数太多, 可以使用 JSON 来封装参数
class ImageTable
  {
public:
    ImageTable(MYSQL* mysql): mysql_(mysql){}

    //image 就形如以下形式:
    //{
    //   image_name: "test.png",
    //   size: 1024,
    //   upload_time: "2019/08/29",
    //   md5: "abcdef",
    //   type: "png",
    //   path: "test_8_29_image/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["uplode_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 执行失败!%s\n", mysql_error(mysql_));
        return false;
      }
      return true;
    }*/
    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());
         int ret = mysql_query(mysql_, sql);
         if (ret != 0) {
            printf("执行 sql 失败! sql=%s, %s\n", sql, mysql_error(mysql_));
             return false;
              
         }
          return true;      
    }

  //如果是输入型参数,使用 const&
  //如果是输出型参数,使用 *(指针)
  //如果是输入输出型参数, 使用&
    bool SelectAll(Json::Value* images)//image是输出型参数
    {
      char sql[4096] = {0};
      sprintf(sql, "select * from image_table");
      int ret = mysql_query(mysql_, sql);
      if(ret != 0)
      {
        printf("SelectAll 执行失败!%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[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 的结果不是一条记录!实际查到 %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 (namespace)

2. 图片服务器

服务器 API 设计

新增图片

请求:
POST /image HTTP/1.1
Content-Type: application/x-www-form-urlencoded
 
…[内容]…
 
响应:
HTTP/1.1 200 OK
{
  “ok”: true,
}

查看所有图片信息

请求:
GET /image/
HTTP/1.1 200 OK
[
{
 “image_id”: 1,
  “image_name”: “1.png”,
  “type”: “image/png”,
  “md5”: “[md5值]”
  “upload_time”: “2019/8/29”,
  path:"./data/test.png"
}, 
{
  “image_id”: 2,
  “image_name”: “2.png”,
  ……
}
]

查看指定图片信息

请求:
GET /image/:image_id
响应:
HTTP/1.1 200 OK
{
  “image_id”: 1,
  “image_name”: “1.png”,
  “type”: “image/png”,
  “md5”: “[md5值]”
  “upload_time”: “2019/8/29”,
  path:"./data/test.png"
}

查看图片内容

请求:
GET /image/show/:image_id
响应:
HTTP/1.1 200 OK
content-type: image/png
 
[响应 body 中为 图片内容 数据]

删除图片

请求:
DELETE /image/:image_id
响应:
HTTP/1.1 200 OK
{
  “ok”: true
}

构建 HTTP 服务器提供约定的 API 接口

服务器基本框架

使用 cpp-httplib

#include "httplib.h"
int main() {
 using namespace httplib;
 Server server;
 server.Get("/", [](const Request& req, Response& resp) {
 (void)req;
 resp.set_content("<html>hello</html>", "text/html");
 });
 server.set_base_dir("./wwwroot");
 server.listen("0.0.0.0", 9094);
 return 0;
} 
提供约定的 API 接口

这里只说明框架,代码维护于github:https://github.com/luchunabf/item

#include <signal.h>
#include "db.hpp"
#include <stdio.h>
#include <fstream>
#include "httplib.h" 
#include <jsoncpp/json/json.h>
#include <sys/stat.h>

class FileUtil
{
public:
  //写文件
  static bool Write(const std::string& file_name,
      const std::string& content)//将content写入到文件file_name中去
  {
    std::ofstream file(file_name.c_str()); //打开文件
    if(!file.is_open())
    {
      return false;
    }
    file.write(content.c_str(), content.length());//将content写入文件
    file.close();//关闭文件
    return true;
  }

//读文件
static bool Read(const std:: string& file_name, std::string* content)//将file_name文件读入到content中去
{
  std::ifstream file(file_name.c_str());
  if(!file.is_open())
  {
    return false;
  }
  struct stat st;
  stat(file_name.c_str(), &st);计算file_name文件大小
  content->resize(st.st_size);//将content扩容到要读取文件file_name的大小
  //一口气把整个文件读完
  //需要先知道文件的大小
  //char* 缓冲区长度
  //int 读取多长
  file.read((char*)content->c_str(), content->size());
  file.close();
  return true;
}
};


//回调函数
/*void Hello(const httplib::Request& req, httplib::Response& resp)
  //HTTP Content-Type
{
  resp.set_content("<h1>hello</h1>", "text/html");
}*/

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 的重要特性, 捕获变量
  //
  server.Post("/image",[&image_table](const Request& req, Response& resp){
       
        //1.对参数进行校验
        //2.根据文件名称获取到文件数据file对象
        //3.把文件属性信息插入到数据库中
        //4.把图片保存到指定的磁盘目录中
        //5.构造一个响应数据通知客户端上传成功
        
      });



  server.Get("/image", [&image_table](const Request& req, Response& resp)
    {
      //void(req);//没有任何实际的效果
      //1.调用数据库接口来获取数据
      //2.构造响应结果返回给客户端
      });

  server.Get(R"(/image/(\d+))", [&image_table](const Request& req, Response& resp)
    {
      printf("获取单个图片信息\n");
      //1. 先获取图片id
      //2.根据图片信息查询数据库
      //3. 把查询结果返回给客户端
      });

  server.Get(R"(/show/(\d+))", [&image_table](const Request& req, Response& resp){
     printf("获取指定图片内容\n"); //这一步是查看图片,其他的是查信息,用Json封装响应,这里用string接收文件路径,从文件中读取内容
     //1. 先获取图片id
      //2.根据目录找到文件内容,读取文件内容
      //3.把文件内容构造成一个响应
      });
    
  server.Delete(R"(/image/(\d+))", [&image_table](const Request& req, Response& resp){
 
      //1.根据图片id去数据库中查找对应的目录
      //2. 查找到对应文件的路径
      //3.调用数据库进行删除
      //4.删除磁盘上的文件
      //5.构造响应
      });


  server.set_base_dir("./wwwroot");// 设置静态文件目录
  server.listen("0.0.0.0", 9094);
  return 0;
}

3. 使用 Postman 进行测试

在 Postman 中构造请求, 并验证

使用upload.html上传图片:

在这里插入图片描述
响应:
在这里插入图片描述
查看所有图片信息
在这里插入图片描述
在这里插入图片描述
查看指定图片信息
在这里插入图片描述
在这里插入图片描述
查看指定图片内容
在这里插入图片描述
在这里插入图片描述
删除指定图片
在这里插入图片描述
在这里插入图片描述

遇到的问题

1. 在测试合同http服务器接口时,在浏览器上访问该服务器时一直未响应

最后调试发现linux防火墙未关闭,关闭后可以在浏览器中正常访问服务器

 //Linux查看防火墙状态及关闭或者重启的命令(CentOS7或者red hat)
//查看防火墙的状态(是否有开启)
systemctl status firewalld
service iptables status

//暂时关闭防火墙
systemctl stop firewalld
service iptables stop

//暂时关闭后,开启防火墙
systemctl start firewalld
service iptables start

//永久关闭防火墙(开机禁用)
systemctl disable firewalld
chkconfig iptables off

//重启防火墙
service iptables restart

//永久关闭后,开启防火墙(开机自动启用)
systemctl enable firewalld
chkconfig iptables on

//systemctl是CentOS7的服务管理工具中主要的工具,它融合之前service和chkconfig的功能于一体。

2. 使用Json封装响应后,输入格式较复杂

查阅资料,可以叫Json格式化进行输出

Json::FastWriter writer;

resp.set_content(writer.write(resp_json), "application/json");

3. 在代码固定的情况下,不能匹配查询指定id的图片

使用正则表达式可以解决该问题

  // 1. 正则表达式
  // 2. 原始字符串(raw string)
  server.Get(R"(/image/(\d+))", [&image_table](const Request& req, Response& resp){});

4. 使用了正则表达式后,发现代码运行总是报错,如下:

在这里插入图片描述
后来发现时编译器版本问题,g++4.8不支持正则表达式
在这里插入图片描述
升级g++至7.3即可:
使用 devtool 升级 g++ 到 7.3 版本

//以下命令在 root 下使用
yum install centos-release-scl -y
yum install devtoolset-7 -y
//命令
source /opt/rh/devtoolset-7/enable 

在这里插入图片描述
再次运行即可正确启动服务器

扩展

  1. md5 和 上传时间还需完善,目前暂时写死
  2. 存储时合并文件,提高存储效率
  3. 防盗链,权限控制,只让图片被指定用户使用, 借助cookie实现
  4. 对于你内容相同的文件,可以只存一份图片文件,引用计数,使用md5可以判断图片内容是否相同
  • 9
    点赞
  • 55
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值