项目:图片服务器

 

图片服务器

项目意图:在写博客过程中,上传图片时,看到下面这个窗口有了想法自己能否做出类似的图片服务器?

项目:图片服务器_第1张图片项目:图片服务器_第2张图片

存储图片的服务器(图床)

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

实现环境:Centos7.2

核心需求

  • 上传一个图片(得到一个url)
  • 根据图片的url访问图片(获取图片内容)
  • 获取某个图片的属性(png/jpg/user)
  • 删除等操作

模块划分

 

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:校验和

1,不管啥样的字符串,最终的到的md5值都是固定长度

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

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

  • md5这个字段用来进行校验图片内容正确性
  • 上传图片之后,服务器就可以计算一个该图片的md5值
  • 后续用户下载图片的时候,也能获取到该图片的md5,用户可以吧自己计算的md5和服务器计算的md5对比,就知道自己的图片是否下载正确了。

针对图片内容,可以直接存在磁盘上。

以下是数据库表的代码:

db.sql

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 int,
  upload_time varchar(50),
  md5 varchar(128),
  type varchar(128),
  path varchar(1024)
);

insert into image_table values(null, 'test.png', 1024, '2019/08/16', 'aaaabbbbbccccc', 'png', 'data/test.png')

 使用MySQL C API操作数据库

 

安装MySQL API:

yum install mysql++-devel.x86_64

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

-L /usr/lib64/mysql -lmysqlclient

数据库插入数据

mysql_insert.cc

#include 
#include 
#include 

int main() {
  // 使用 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 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/16', '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;
}

数据库查找数据

mysql_select.cc

#include 
#include 
#include 

int main() {
  // 使用 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 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;
}

 makefile

.PHONY:all
all:mysql_select mysql_insert

mysql_insert:mysql_insert.cc
	g++ $^ -o $@ -L/usr/lib64/mysql -lmysqlclient

mysql_select:mysql_select.cc
	g++ $^ -o $@ -L/usr/lib64/mysql -lmysqlclient

.PHONY:clean
clean:
	rm mysql_insert mysql_select

2,服务器模块
(给前端提供一个接口)

需要一个http服务器,能接受http请求,回http响应,需要约定不同的请求方式来表示不通的操作方式,例如有些请求表示上传图片,一些请求表示查看图片,一些表示删除图片。

使用Restfull风格的设计。

Restfull:

1、http method 来表示操作的动词:GET查,POST增,PUT改,DELETE删

2、http path 表示操作的对象。

3、补充信息一般使用 body 来传递,通常情况下 body 中使用 json 格式来组织
json是一种数据组织格式,最主要的用途之一就是序列化。
json是源于JavaScript
json优势:方便调试,劣势:效率低,更占用存储空间和带宽
yum install jsoncpp-devel

4、响应数据一般也用 body

API具体设计

新增图片


请求:
POST /image
Content-Type: application/x-www-form-urlencoded
------WebKitFormBoundary5muoelvEmAAVUyQB
Content-Disposition: form-data; name="filename"; filename="图标.jpg"
Content-Type: image/jpeg
......[图片正文].....

查看指定图片元信息

请求:
GET /image/:image_id
响应:
HTTP/1.1 200 OK
{
 "image_id": 1,
 "image_name": "1.png",
 "content_type": "image/png",
 "md5": "[md5值]"
}


查看图片内容

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

删除图片

响应:
HTTP/1.1 200 OK
{
 "ok": true,
}

封装数据库

接口设计:
db.hpp
 

#pragma once

#include 
#include 
#include 
#include 

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

// 操作数据库中的 image_table 这个表. 
// 此处 Insert 等操作, 函数依赖的输入信息比较多. 
// 为了防止参数太多, 可以使用 JSON 来封装参数
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["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

  使用 Json 的原因: 1. 扩展更方便; 2. 方便和服务器接受到的数据打通

可以使用 jsoncpp 这个库来解析和构造 json 数据

测试数据库操作

单元测试:
db_test.cc

 

#include "db.hpp"

// 单元测试

void TestImageTable() {
  // 创建一个 ImageTable 类, 去调用其中的方法, 验证结果
  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);
  // 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;
}

服务器基本框架
使用cpp-httplib

gitclonehttps://github.com/yhirose/cpp-httplib
框架:

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

编译选项:g++ main.cc -lpthread -std=c++11

在这里注意:若 g++版本编译不通过需将g++版本升到7.3版本。

以下命令在root下使用:

yum install centos-release-scl -y

yum install devtoolset-7 -y

以下命令添加在 bashrc 中:

source /opt/rh/devtoolset-7/enable

使用g++ -v来验证g++版本

 

 

image_server.cc
 

#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) {
      image_system::MySQLRelease(mysql);
      exit(0);
    });

  Server server;

  // [&image_table] 是 lambda 的重要特性, 捕获变量
  
  // 捕捉之后就可以访问其中 & 的含义是相当于按引用捕获
  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");
    });


  
  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++ 标准库中, 没有删除文件的方法
     
      // 此处只能使用操作系统提供的函数
      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;
}

makefile

FLAG=-L /usr/lib64/mysql -lmysqlclient -ljsoncpp -g

all:image_server db_test

image_server:image_server.cc
	g++ $^ -o $@ -std=c++11 -lpthread $(FLAG)

db_test:db_test.cc db.hpp
	g++ db_test.cc -o $@  $(FLAG)

.PHONY:clean
clean:
	rm db_test image_server

 

3,客户端模块

实现一个客户端页面upload.html,放到wwwroot中

upload.html
 




        

上传图片终端返回:

 

4,测试模块

使用postman构造http请求完成测试
构造GET请求实现获取所有图片属性:

项目:图片服务器_第3张图片

终端返回:


GET获取单个图片信息:


项目:图片服务器_第4张图片

终端返回:


项目:图片服务器_第5张图片

GET获取一个不存在的图片信息

项目:图片服务器_第6张图片

终端返回:

项目:图片服务器_第7张图片

GET获取指定图片内容:


项目:图片服务器_第8张图片

终端返回:

项目:图片服务器_第9张图片

DELETE删除指定图片

项目:图片服务器_第10张图片

终端返回:

可以在数据库中看到ID为8的图片已经被删除:

项目:图片服务器_第11张图片

项目总结

核心就是一个HTTP服务器,提供对图片的增删改查能力。同时搭配简单的页面辅助完成图片的上传/展示。

重要知识点:

  1. 简单的Web服务器设计能力。
  2. C/C++操作 MySQL 数据库
  3. 数据库设计
  4. Restful 风格 的API
  5. json 和 jsoncpp 的使用
  6. 强化 HTTP 协议的理解
  7. cpp-httplib 的使用和原理
  8. 正则表达式
  9. 基于 md5 进行校验
  10. postman 工具的使用
  11. 软件测试的基本思想和方法

本项目扩展点:

权限控制:只让图片能被特定用户使用,需用到防盗链相关方面知识,借助cookie可实现相关的用户账户功能。

防盗链:https://www.cnblogs.com/wangyongsong/p/8204698.html

支持一些简单的图片处理功能,就类似于CSDN里面图片高度,宽度,以及像素大小等功能,和其对齐方式等。

对相同内容的图片可以只存一份图片文件。这个需要添加引用计数。

 

 

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