目录
项目背景:
项目描述:
核心需求:
相关技术:
md5算法:
GitHub上的cpp-httplib开源库
json 和 jsoncpp 的使用:
正则表达式:
模块划分:
项目流程:
数据库设计:
服务器API设计:
服务端实现:
当我们在一些软件进行写作或者数据分享的时候,有时候需要上传一些图片来进行辅助说明,当我们上传一张本地图片后,网页就会显示我们所上传的图片,比如博客,简书,知乎等;
一般来讲这些文档主要有三种格式:HTML、CSS、JS。HTML相当于网页的骨架,CSS相当于网页的衣服,用来规定网页的样式,比如字体大小以及排版等等,而Javascript主要负责一些动态的逻辑,比如在网页上按下一个按键后会显示什么等等。
一张图片能在网页上显示的原因是网页的HTML文档中嵌入了这张图片的URL,我们可以知道是有一个专门的服务器存储了这张图片,向外提供了一个URL链接让其他端口来访问这张图片。这个专门存储图片的服务器叫做图片服务器也叫做图床。
本项目就是实现一个简单的HTTP图片服务器,用这个服务器来存储图片,针对每张图片提供一个唯一的URL,有了这个URL之后其他网页就可以借助它把图片展示出来。
使用cpp-httplib构建 HTTP 服务器提供约定的 API 接口;
需要一个http服务器,能接受http请求,回http响应,需要约定不同的请求方式来表示不通的操作方式,例如有些请求表示上传图片,一些请求表示查看图片,一些表示删除图片
HTTP/1.1协议中共定义了八种方法(也叫“动作”)来以不同方式操作指定的资源;
使用 JSON 作为数据交互格式
json 出自 JavaScript, 是一种非常方便的键值对数据组织格式,主要用途之一就是序列化.
C++ 中可以使用 jsoncpp 这个库来解析和构造 json 数据
yum install jsoncpp-devel
数据存储模块:
服务器模块:
客户端模块:
测试模块
1.安装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(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/26',
'aaaabbbbcccc','png','./data/test.png');
2.数据库插入数据:
mysql_insert.cc
// 1. 初始化句柄
// 2. 建立连接
// 3. 设置编码格式
// 4. 拼装 SQL 语句
// 5. 执行 SQL 语句
// 6. 关闭句柄
#include
#include
#include
#include
int main() {
// 1. 初始化句柄
MYSQL* connect_fd = mysql_init(NULL);
// 2. 建立链接
// mysql_init 返回的指针
// 主机地址
// 用户名
// 密码
// 数据库名
// 端口号
// unix_socket
// client_flag
if (mysql_real_connect(connect_fd, "127.0.0.1", "root", "",
"image_system", 3306, NULL, 0) == NULL) {
printf("连接失败! %s\n", mysql_error(connect_fd));
return 1;
}
// 3. 设置编码格式
mysql_set_character_set(connect_fd, "utf8");
// 4. 拼装 SQL 语句
char sql[4096] = {0};
char image_name[] = "滑稽.jpg";
//数据库查找数据
mysql_select.cc
int size = 16 * 1024;
char upload_time[] = "2019/05/14";
char md5[] = "123456";
char content_type[] = "jpg";
char path[] = "./滑稽.jpg";
sprintf(sql, "insert into image_table values(null, '%s', %d, '%s', '%s', '%s', '%s')",
image_name, size, upload_time, md5, content_type, path);
// 5. 执行 SQL 语句
int ret = mysql_query(connect_fd, sql);
if (ret != 0) {
printf("执行 sql 失败! %s\n", mysql_error(connect_fd));
return 1;
}
// 6. 关闭句柄
mysql_close(connect_fd);
printf("执行成功!\n");
return 0;
}
3.数据库查找数据;
mysql_select.cc
// 1. 初始化句柄
// 2. 建立连接
// 3. 设置编码格式
// 4. 拼装 SQL 语句
// 5. 执行 SQL 语句
// 6. 遍历查询结果
// 7. 释放结果集
// 8. 关闭句柄
#include
#include
#include
int main() {
// 1. 初始化句柄
MYSQL* connect_fd = mysql_init(NULL);
// 2. 建立链接
// mysql_init 返回的指针
// 主机地址
// 用户名
// 密码
// 数据库名
// 端口号
// unix_socket
// client_flag
if (mysql_real_connect(connect_fd, "127.0.0.1", "root", "",
"image_system", 3306, NULL, 0) == NULL) {
printf("连接失败! %s\n", mysql_error(connect_fd));
return 1;
}
// 3. 设置编码格式
服务器 API 设计
新增图片
mysql_set_character_set(connect_fd, "utf8");
// 4. 拼装 SQL 语句
char sql[1024 * 4] = {0};
sprintf(sql, "select * from image_table");
// 5. 执行 SQL 语句
int ret = mysql_query(connect_fd, sql);
if (ret < 0) {
printf("执行 sql 失败! %s\n", mysql_error(connect_fd));
return 1;
}
// 6. 遍历查询结果
MYSQL_RES* result = mysql_store_result(connect_fd);
if (result == NULL) {
printf("获取结果失败! %s\n", mysql_error(connect_fd));
return 1;
}
// a) 获取行数和列数
int rows = mysql_num_rows(result);
int fields = mysql_num_fields(result);
printf("rows: %d, fields: %d\n", rows, fields);
// b) 打印结果
for (int i = 0; i < rows; ++i) {
MYSQL_ROW row = mysql_fetch_row(result);
for (int j = 0; j < fields; ++j) {
printf("%s\t", row[j]);
}
printf("\n");
}
// 7. 释放结果集
mysql_free_result(result);
// 8. 关闭句柄
mysql_close(connect_fd);
printf("执行成功!\n");
return 0;
}
请求:
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,
}
请求:
GET /image/
HTTP/1.1 200 OK
[
{
"image_id": 1,
"image_name": "1.png",
"content_type": "image/png",
"md5": "[md5值]"
}
]
请求:
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 中为 图片内容 数据]
删除图片:
请求:
DELETE /image/:image_id
响应:
HTTP/1.1 200 OK
{
"ok": true
}
接口设计
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);
};
}
json 出自 JavaScript, 是一种非常方便的键值对数据组织格式, 目前被业界广泛使用.
C++ 中可以使用 jsoncpp 这个库来解析和构造 json 数据
// 实现一个数据库接口测试程序, 用来验证前面的数据库接口是否正确
#include
#include "db.hpp"
using namespace image_system;
void TestImageTable() {
bool ret = false;
// 更友好的格式化显示 Json
Json::StyledWriter writer;
MYSQL* mysql = MySQLInit();
Json::Value image;
image["name"] = "滑稽.jpg";
image["size"] = 16 * 1024;
image["upload_time"] = "2019/01/01";
image["md5"] = "987654321";
image["content_type"] = "image/jpg";
image["path"] = "./滑稽.jpg";
std::cout << "==============测试插入=====================" << std::endl;
ImageTable image_table(mysql);
ret = image_table.Insert(image);
std::cout << "Insert: " << ret << std::endl;
std::cout << "==============测试查找=====================" << std::endl;
Json::Value images;
ret = image_table.SelectAll(&images);
std::cout << "SelectAll: " << ret << std::endl
<< writer.write(images) << std::endl;
Json::Value image_out;
ret = image_table.SelectOne(1, &image_out);
std::cout << "SelectOne: " << ret << std::endl
<< writer.write(image_out) << std::endl;
std::cout << "==============测试删除=====================" << std::endl;
int image_id = 2;
ret = image_table.Delete(image_id);
std::cout << "Delete: " << ret << std::endl;
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 ///////////////////////////////////////////////////////
// 构建 HTTP 服务器提供约定的 API 接口
///////////////////////////////////////////////////////
#include
#include
#include "util.hpp"
#include "db.hpp"
#include "httplib.h"
#include
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
// 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);
}
server.Post("/image_test", [](const Request& req, Response& resp) {
auto size = req.files.size();
bool ret = req.has_file("filename");
const auto& file = req.get_file_value("filename");
// file.filename
// file.content_type
auto body = req.body.substr(file.offset, file.length);
LOG(INFO) << "size: " << size << ", ret: " << ret << ", " << file.filename << ", "
<< file.content_type << ", " << file.offset << ", " << file.length <<
std::endl;
FileUtil::WriteFile(file.filename, req.body.substr(file.offset, file.length));
resp.set_content("ok", "text/html");
});
一个HTTP服务器的作用是接收到http请求,并根据请求返回相应的http响应。
各个接口代码实现
db.hpp
// 这个文件相当于 model 层.
// 只进行数据的基本 CURD , 不涉及到更复杂的数据加工
#pragma once
#include
#include
#include
#include
#include
#include
#include
namespace image_system {
static MYSQL* MySQLInit() {
MYSQL* connect_fd = mysql_init(NULL);
if (mysql_real_connect(connect_fd, "127.0.0.1", "root", "",
"image_system", 3306, NULL, 0) == NULL) {
printf("连接失败! %s\n", mysql_error(connect_fd));
return NULL;
}
mysql_set_character_set(connect_fd, "utf8");
return connect_fd;
}
static void MySQLRelease(MYSQL* mysql) {
mysql_close(mysql);
}
class ImageTable {
public:
ImageTable(MYSQL* mysql) : mysql_(mysql) { }
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["name"].asCString(), image["size"].asInt(), image["upload_time"].asCString(),
image["md5"].asCString(), image["content_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;
}
bool SelectAll(Json::Value* images) {
char sql[1024 * 4] = {0};
// 可以根据 tag_id 来筛选结果
sprintf(sql, "select * from image_table");
int ret = mysql_query(mysql_, sql);
if (ret != 0) {
printf("执行 sql 失败! %s\n", mysql_error(mysql_));
return false;
}
MYSQL_RES* result = mysql_store_result(mysql_);
if (result == NULL) {
printf("获取结果失败! %s\n", mysql_error(mysql_));
return false;
}
int rows = mysql_num_rows(result);
for (int i = 0; i < rows; ++i) {
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["content_type"] = row[5];
image["path"] = row[6];
images->append(image);
}
return true;
}
bool SelectOne(int32_t image_id, Json::Value* image) {
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("执行 sql 失败! %s\n", mysql_error(mysql_));
return false;
}
MYSQL_RES* result = mysql_store_result(mysql_);
if (result == NULL) {
printf("获取结果失败! %s\n", mysql_error(mysql_));
return false;
}
int rows = mysql_num_rows(result);
if (rows != 1) {
printf("查找结果不为 1 条. rows = %d!\n", rows);
return false;
}
MYSQL_ROW row = mysql_fetch_row(result);
(*image)["image_id"] = atoi(row[0]);
(*image)["name"] = row[1];
(*image)["size"] = atoi(row[2]);
(*image)["upload_time"] = row[3];
(*image)["md5"] = row[4];
(*image)["content_type"] = row[5];
(*image)["path"] = row[6];
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("执行 sql 失败! sql=%s, %s\n", sql, mysql_error(mysql_));
return false;
}
return true;
}
private:
MYSQL* mysql_;
};
} // end blog_system
实现完整上传图片接口 // 3. 新增图片.
server.Post("/image", [&image_table](const Request& req, Response& resp) {
Json::FastWriter writer;
Json::Value resp_json;
LOG(INFO) << "新增图片" << std::endl;
// 1. 进行参数校验
bool ret = req.has_file("filename");
if (!ret) {
resp_json["ok"] = false;
resp_json["reason"] = "req has no filename field!";
resp.status = 400;
resp.set_content(writer.write(resp_json), "application/json");
return;
}
// 2. 构造 Json 格式数据, 并调用数据层接口插入数据
const auto& file = req.get_file_value("filename");
const std::string& image_body = req.body.substr(file.offset, file.length);
Json::Value image;
image["name"] = file.filename;
image["size"] = (int)file.length;
image["upload_time"] = TimeUtil::FormatTime();
image["md5"] = StringMD5(image_body);
image["content_type"] = file.content_type;
// 为了防止重复, 用 md5 作为文件名更稳妥
image["path"] = base_path + file.filename;
ret = image_table.Insert(image);
if (!ret) {
resp_json["ok"] = false;
resp_json["reason"] = "insert db failed!";
resp.status = 500;
resp.set_content(writer.write(resp_json), "application/json");
return;
}
// 3. 保存文件实体
FileUtil::WriteFile(image["path"].asString(), image_body);
// 4. 构造响应
resp_json["ok"] = true;
resp.set_content(writer.write(resp_json), "text/html");
});
实现查看所有图片元信息接口 // 4. 查看所有图片的元信息
server.Get("/image", [&image_table](const Request& req, Response& resp) {
(void) req;
Json::Reader reader;
Json::FastWriter writer;
Json::Value resp_json;
LOG(INFO) << "查看所有图片信息: " << std::endl;
// 1. 调用数据库接口查询数据
Json::Value images;
bool ret = image_table.SelectAll(&images);
if (!ret) {
resp_json["ok"] = false;
resp_json["reason"] = "SelectAll failed!\n";
resp.status = 500;
resp.set_content(writer.write(resp_json), "application/json");
return;
}
// 2. 构造响应结果
resp.set_content(writer.write(images), "application/json");
return;
});
实现查看单个图片元信息接口 // 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) {
Json::Reader reader;
Json::FastWriter writer;
Json::Value resp_json;
// 1. 获取到图片 id
int image_id = std::stoi(req.matches[1]);
LOG(INFO) << "查看图片信息: " << image_id << std::endl;
// 2. 调用数据库接口查询数据
Json::Value image;
bool ret = image_table.SelectOne(image_id, &image);
if (!ret) {
resp_json["ok"] = false;
resp_json["reason"] = "SelectOne failed!\n";
resp.status = 500;
resp.set_content(writer.write(resp_json), "application/json");
return;
}
// 3. 构造响应结果
resp.set_content(writer.write(image), "application/json");
return;
});
实现查看图片内容接口
// 6. 查看图片内容
server.Get(R"(/image/show/(\d+))", [&image_table](const Request& req, Response& resp) {
Json::Reader reader;
Json::FastWriter writer;
Json::Value resp_json;
// 1. 获取到图片 id
int image_id = std::stoi(req.matches[1]);
LOG(INFO) << "查看图片信息: " << image_id << std::endl;
// 2. 调用数据库接口查询数据
Json::Value image;
bool ret = image_table.SelectOne(image_id, &image);
if (!ret) {
resp_json["ok"] = false;
resp_json["reason"] = "SelectOne failed!\n";
resp.status = 500;
resp.set_content(writer.write(resp_json), "application/json");
return;
}
std::string image_body;
ret = FileUtil::ReadFile(image["path"].asString(), &image_body);
if (!ret) {
resp_json["ok"] = false;
resp_json["reason"] = "path open failed\n";
resp.status = 500;
resp.set_content(writer.write(resp_json), "application/json");
return;
}
// 3. 构造响应结果
resp.set_content(image_body, image["content_type"].asCString());
return;
});
实现删除图片接口
// 7. 删除图片
server.Delete(R"(/image/(\d+))", [&image_table](const Request& req, Response& resp) {
Json::Value resp_json;
Json::FastWriter writer;
// 1. 解析获取 blog_id
// 使用 matches[1] 就能获取到 blog_id
// LOG(INFO) << req.matches[0] << "," << req.matches[1] << "\n";
int image_id = std::stoi(req.matches[1]);
LOG(INFO) << "删除指定图片: " << image_id << std::endl;
// 2. 调用数据库接口删除博客
bool ret = image_table.Delete(image_id);
if (!ret) {
resp_json["ok"] = false;
resp_json["reason"] = "Delete failed!\n";
resp.status = 500;
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;
});