项目意图:在写博客过程中,上传图片时,看到下面这个窗口有了想法自己能否做出类似的图片服务器?
存储图片的服务器(图床)
项目描述:实现一个HTTP服务器,然后用这个服务器来存储图片,针对每个图片提供一个唯一的url,有了这个url之后就可以借助它把图片展示到其他网页上。
实现环境:Centos7.2
数据库(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还原原串理论上不可能
针对图片内容,可以直接存在磁盘上。
以下是数据库表的代码:
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
需要一个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
新增图片
请求:
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;
}
#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
实现一个客户端页面upload.html,放到wwwroot中
upload.html
上传图片终端返回:
使用postman构造http请求完成测试
构造GET请求实现获取所有图片属性:
终端返回:
GET获取单个图片信息:
终端返回:
GET获取一个不存在的图片信息
终端返回:
GET获取指定图片内容:
终端返回:
DELETE删除指定图片
终端返回:
可以在数据库中看到ID为8的图片已经被删除:
核心就是一个HTTP服务器,提供对图片的增删改查能力。同时搭配简单的页面辅助完成图片的上传/展示。
本项目扩展点:
权限控制:只让图片能被特定用户使用,需用到防盗链相关方面知识,借助cookie可实现相关的用户账户功能。
防盗链:https://www.cnblogs.com/wangyongsong/p/8204698.html
支持一些简单的图片处理功能,就类似于CSDN里面图片高度,宽度,以及像素大小等功能,和其对齐方式等。
对相同内容的图片可以只存一份图片文件。这个需要添加引用计数。