图片存储系统
项目描述:
实现一个 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
再次运行即可正确启动服务器
扩展
- md5 和 上传时间还需完善,目前暂时写死
- 存储时合并文件,提高存储效率
- 防盗链,权限控制,只让图片被指定用户使用, 借助cookie实现
- 对于你内容相同的文件,可以只存一份图片文件,引用计数,使用md5可以判断图片内容是否相同