1、项目背景
类似于在csdn中上传图片一样,我们选择了图片上传后,会进入选择图片文件的界面,然后上传成功后,我们就可以看到上传的图片被展示出来,其实我们是将图片上传到了csdn的后台服务器中,而我们要实现的就是具备这样功能的一个图片服务器,可以提供对图片的增删查改操作。
2、核心功能
该项目的核心就是实现一个HTTP服务器,然后用这个服务器来存储图片,针对每一个图片提供一个唯一的url,有了这个url之后,就可以借助它把图片展示到其它网页上。具体需求包括:
- 上传图片(得到一个url)
- 根据图片的url访问图片,获取图片内容
- 获取某个图片的属性
- 删除图片
3、模块划分
- 数据库存储模块
包括:设计数据库、使用MySQL C API操作数据库
2. 服务器模块
包括:新增图片、查看所有图片元信息、查看指定图片元信息、查看图片内容、删除图片
4、数据库设计
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 '图片所在路径')
其中, image_id为主键自增属性。
什么是md5?
这是一种常见字符串hash算法,具有三个特性:
- 不管源字符串多长,得到的最终md5值都是固定长度
- 源字符串稍微变化一点点内容,md5值会变化很大(降低冲突概率)
- 通过源字符串很容易计算得到md5值,但根据md5推导出原字符串很难(几乎不可能)
md5的作用:用来校验图片内容正确性。
上传图片的时候,服务器就可以计算一个该图片的md5值,后续用户下载图片的时候,也能获取到该图片的md5,用户可以把自己计算的md5和服务器计算的md5进行对比,以此判断下载的图片是否正确。
使用MySQL C API操作数据库
安装MySQL C API
yum install mysql -devel
代码中使用时需要链接上MySQL提供的库
-L /usr/lib64/mysql -lmysqlclient
5、服务器API设计
我们需要实现一个HTTP服务器,HTTP服务器需要接受http请求,返回http响应,此处需要约定不同的请求来表示不同的操作方式,例如:有些请求表示上传图片,有些请求表示查看图片,有些表示删除图片。
此处我们使用Restful风格的设计。
- http method来表示操作的动词:GET查,POST增,PUT改,DELETE删
- http path表示操作的对象
- 补充信息一般使用body来传递,通常情况下body中使用json格式的数据来组织
- 响应数据通常也是用json格式组织
json是一种数据组织格式,最主要的用途之一就是序列化。json源于javascript,用来表示一个对象,类似于map采用key:value的格式。
json优势:方便调试
json劣势:组织格式的效率比较低,更占存储空间和带宽
具体设计:
1、上传图片
请求:
POST /image
Content-Type: application/x-www-form-urlencoded------WebKitFormBoundary5muoelvEmAAVUyQB
Content-Disposition: form-data; name="filename"; filename="图标.jpg"
Content-Type: image/jpeg
......[图片正文].....响应:
HTTP/1.1 200 OK
{
"ok": true,
}
2、查看所有图片元信息
请求:
GET /image/HTTP/1.1 200 OK
[
{
"image_id": 1,
"image_name": "1.png",
"content_type": "image/png",
"md5": "[md5值]"
}
]
3、查看指定图片元信息
请求:
GET /image/:image_id
响应:
HTTP/1.1 200 OK
{
"image_id": 1,
"image_name": "1.png",
"content_type": "image/png",
"md5": "[md5值]"
}
4、查看指定图片内容
请求:
GET /image/show/:image_id
响应:
HTTP/1.1 200 OK
content-type: image/png
[响应 body 中为 图片内容 数据]
5、删除图片
请求:
DELETE /image/:image_id
响应:
HTTP/1.1 200 OK
{
"ok": true
}
6、服务端设计
封装数据库操作
db.hpp
namespace image_system {
static MYSQL* MySQLInit() {}
static void MySQLRelease(MYSQL* mysql) {}
class ImageTable {
ImageTable(MYSQL* mysql) { }
bool Insert(const Json::Value& image);
bool SelectAll(Json::Value* images);
bool SelectOne(int32_t image_id, Json::Value* image);
bool Delete(int image_id);
};
}
具体的各个接口代码实现此处不全部展示了。其中包括新增、查询所以、查询指定图片及删除的接口代码。完成后写一段测试代码测试各个接口的功能是否满足预期。
服务器基本框架
使用cpp-httplib.h
#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;
}
构建HTTP服务器提供约定的API接口
#include <signal.h>
#include <jsoncpp/json/json.h>
#include "util.hpp"
#include "db.hpp"
#include "httplib.h"
#include <openssl/md5.h>
std::string StringMD5(const std::string& str);
const std::string base_path = "./image_data/";
MYSQL* mysql = NULL;
int main() {
using namespace httplib;
using namespace image_system;
Server server;
// 1. 数据库客户端初始化和释放
mysql = MySQLInit();
signal(SIGINT, [](int) { MySQLRelease(mysql); exit(0);});
ImageTable image_table(mysql);
// 2. 先按照 cpp-httplib 的文档演示基本的图片上传处理过程
server.Post("/image_test", [](const Request& req, Response& resp) {
});
// 3. 新增图片.
server.Post("/image", [&image_table](const Request& req, Response& resp) {
});
// 4. 查看所有图片的元信息
server.Get("/image", [&image_table](const Request& req, Response& resp) {
});
// 5. 查看图片元信息
// raw string(c++ 11), 转义字符不生效. 用来表示正则表达式正好合适
// 关于正则表达式, 只介绍最基础概念即可. \d+ 表示匹配一个数字
// http://help.locoy.com/Document/Learn_Regex_For_30_Minutes.htm
server.Get(R"(/image/(\d+))", [&image_table](const Request& req, Response& resp) {
});
// 6. 查看图片内容
server.Get(R"(/image/show/(\d+))", [&image_table](const Request& req, Response& resp) {
});
// 设置静态文件目录
server.set_base_dir("./wwwroot");
server.listen("0.0.0.0", 9094);
return 0;
}
// 需要包含头文件
// #include <openssl/md5.h>
// Makefile 需要 -lcrypto
std::string StringMD5(const std::string& str) {
const int MD5LENTH = 16;
unsigned char MD5result[MD5LENTH];
// 调用 openssl 的函数计算 md5
MD5((const unsigned char*)str.c_str(),str.size(),MD5result);
// 转换成字符串的形式方便存储和观察
char output[1024] = {0};
int offset = 0;
for (int i = 0; i < MD5LENTH; ++i) {
offset += sprintf(output + offset, "%x", MD5result[i]);
}
return std::string(output);
}
测试上传图片,实现一个测试页面upload.html,生成一个按钮,点击之后弹出文件选择框,发送按钮,就会给服务器发送一个特殊的http请求。验证上传的图片是否能成功。
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
</head>
<body>
<form method="POST" enctype="multipart/form-data"
action="http://47.98.116.42:9094/image">
<table>
<tr>
<td>文件上传:</td>
<td><input type="file" name="filename"/></td>
</tr>
<tr>
<td></td>
<td><input type="submit" value="上传"/></td>
</tr>
</table>
</form>
</body>
</html>
完整的上传图片接口、查看所有图片元信息、查看单个图片元信息接口、查看图片内容接口以及删除图片接口的实现代码详见github中。
正则表达式简单了解及使用
raw string(C++ 11),作用是使转义字符不生效,用来表示正则表达式正好合适
正则表达式是一个带有特殊符号的字符串,描述了一个字符串的特征。(字符串应该包含什么信息,某个信息开头,以某个信息结尾,某个信息重复出现多少次等),代码中使用的\d+ 表示匹配一个数字。
7、使用Postman进行测试
http客户端,可以很方便的构造http请求并进行测试。
8、后续扩展点
- 存储时合并文件,上传大量的比较小的文件,把这些逻辑上比较小的文件合并成一个比较大的物理文件,数据库里面除了存这个该文件的路径之外,再存一个偏移量,方便这些小文件的查找。
- 防盗链,其实就是一个权限控制。只让图片能被特定的用户使用。借助cookie,实现相关用户账户验证功能。
- 支持一些简单图片处理功能,例如:请求中带上参数w=50&h=50,得到一个50*50的图片。
- 对内容相同的图片只存一份图片文件。可以通过md5来判定这两个图片是否相同,然后通过引用计数实现同一个图片只存一份。