项目背景
现在很多地方仅仅支持文字发送,而不支持图片发送,,但是在很多特定的场景有需要图片发送等方式,所以我们可以构建一个HTTP服务器来完成这个功能,通过这个服务器为连接服务器的数据库上的每个图片生成一个特定的URL通过这个URL我们就可以完成图片的发送。
项目描述
首先这个项目是一个图片服务器,我们可以上传图片在上面,通过特定的URL来获取这些图片的内容,就可以解决很多需要图片而不支持发送图片的场景,我们就需要对于图片内容和图片信息进行保存,我们会分别采用数据库和磁盘对该信息进行存储
项目设计
我们需要对图片进行存储,图片存储分为了两个部分,首先是就是我们上传的图片信息包括(大小,名字等)这些可以用数据库来进行存储,图片内容部分以二进制方式进行存储,所以图片内容以二进制方式存储在,但是由于图片信息存储如果用类进行存储改动比较麻烦,所以我们引入了JSON格式进行存储,JSON使用键值对方式进行存储,方便修改
模块划分
首先进行数据库创建,然后对于数据库的表格进行创建,我们创建一张表,只需要一张表对于图片信息进行存储,然后在代码模块我们对于数据库的操作进行操作,所以我们需要对于数据库的操作进行封装构建SQL语句,然后再进行服务器的封装需要。
数据库构建
数据库功能封装
#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", "", "image_system2", 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) {}
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;
}
// 如果是输入参数, 使用 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"] = 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
服务器封装
#include <fstream>
#include <signal.h>
#include <sys/stat.h>
#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;
// 客户端请求 /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. 把图片保存到指定的磁盘目录中
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;
}
设计服务器api
1.上传图片
请求
POST(新增)/image HTTP/1.1/
content-type;(application/x-www-form-urlencoded)
【内容】
响应
/HTTP/1.1 200 OK
{
OK : true
}
2.查看所有图片信息
请求
GET/image
响应
/HTTP/1.1 200 OK
【
{
image_id:1,
image_name:“test。png”,
type:“image/png”,
。。。。。。
},
{
。。。。。。。
}
】
3.查看指定图片信息
GET/image/:image_id
响应
/HTTP/1.1 200 OK
{
image_id:1,
image_name:“test。png”,
type:“image/png”,
。。。。。。
}
4.查看指定图片内容
GET/show/:image_id
响应
/HTTP/1.1 200 OK
content-type:image/png
[body 图片内容]
5.删除图片
DELETE/:image_id
响应
/HTTP/1.1 200 OK
{
OK : true
}
HTML文件只需要构造一个界面这里就不做展示了。
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
这样就可以实现图片服务器,保存的图片都有一个URL,我们就可以在很多地方使用这个URL对图片进行引用而不用担心输入。
项目总结
通过这个项目让我更加熟悉Linux操作系统和HTTP协议,让我对于网络有更加深刻的认识,同时熟悉了JSON和我外部库cpp-http让我学到了很多。