例如我们随便打开一个网页,查看源代码就可以看到这个image,以及该图片的网络路径
URL:URL 全名叫统一资源定位符,uniform resource Locator,俗称网页或网址,字面上来理解,它就是用来定位资源的。相当于图书上面的标签,有了这些标签,管理员可以很快的找到相应的图书。
一个完整的URL包括以下信息:协议,IP地址,路径,端口号
实现一个HTTP服务器,然后用这个服务器来存储图片,针对每个图片提供一个唯一的url,有了这个url后就可以借助它把图片展示到其他网页上
磁盘文件和数据库(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:**是一种字符串hash算法
md5作用:
md5这个字段用来校验图片内容正确性,上传图片后,服务器就可以计算一个该图片的md5值,后续用户下载图片的时候,也能获取到该图片的md5,用户可以把自己计算的md5和服务器计算的md5对比,即可直到自己的图片是否下载成功
MySQL客户端代码:
#include
#include
#include
#include
namespace image_system{
static MYSQL* MySQLInit(){
//1.先创建一个mysql的句柄
MYSQL* mysql = mysql_init(NULL);
//2.用句柄和数据库建立连接
if(mysql_real_connect(mysql,"127.0.0.1","root","1","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或者类来封装参数 JSON改动更为灵活
class ImageTable{
public:
ImageTable(MYSQL* mysql) : _mysql(mysql){}
//image形如以下形式
//{
// image_name: "test.png",
// size: 200,
// upload_time: "2010/08/28",
// md5: "aaaaaaa",
// yupe: "png",
// path: "data/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["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;
}
//函数参数设计:
//1.输入型参数,const&
//2.输出型参数,使用*
//3.输入输出型参数,使用&
bool SelectAll(Json::Value* images){
char sql[4096] = {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);
int i;
for(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 查询结果不是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[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
需要借助第三方库 cpp-httplib
我们可以在Github上查找该库
只需要在项目中包含这个头文件即可使用,利用该库生成一个简单的服务器,如下:
#include "httplib.h"
//回调函数,一个函数,调用时机由代码框架和操作系统来决定
void Hello(const httplib::Request& req,httplib::Response& resp){
resp.set_content("hello
","text/html");//“text/html”是HTTP Content-Type
}
int main(){
using namespace httplib;//引用命名空间,在函数内部生效,避免名称冲突
Server server;
//客户端请求 / 路径的时候,执行一个特定的函数
//指定不同的路径对应到不同的函数上,这个过程称为”设置路由“
//第一个参数是路径,第二个参数是函数指针,当客户端请求path
//为/的请求时,就会执行函数指针所指向的函数,相当于把函数注册到了代码框架中,什么时候
//调用不确定,由代码框架和操作系统来决定,此为回调函数
server.Get("/",Hello);
server.listen("0.0.0.0",9094);//此表示了服务器启动的全部过程,ip地址和端口号
return 0;
}
然后启动服务器,在网页上输入你的地址47.98.116.42:9094/hello,(此处用的是阿里云的IP)即可查看
HTTP服务器需要接受http请求,返回http响应,此处需要约定不同的请求来表示不同的操作方式,例如有些请求表示上传图片,有些请求表示查看图片或者删除图片
此处使用Restful风格的设计:
(1)json源于javascript用来表示一个“对象”;是一种数据组织格式,最主要的用途之一就是序列化
(2)json格式的数据都是用**{}括起来的,用键值对的形式**表示的
(3)json优势:方便调试,方便和服务器接受到的数据打通
(4)json劣势:组织格式的效率比较低,更占用存储空间和带宽
(5) protobuf是谷歌出品的一种二进制序列化协议
为了让数据库和服务器有更方便的交互,根据MySQL官网提供的一些API(应用程序接口)来实现一个自主客户端
这里我们在ImageTable类中封装了sql语句包括以下一些操作:
1.上传图片
-----POST/image HTTP/1.1(版本号)
Content-Type(和提交表单密切相关;提交表单:即HTML中客户端给服务器上传数据的一种常见方法
2.查看所有图片信息
------GET/image
3.查看指定图片信息
-----GET/image/:image_id
4.查看指定图片内容(保存在磁盘文件上,其他的保存在数据库中)
-----GET/show/:image_id
5.删除图片
-----DELETE/image/:image_id
此处我们需要用到第三方库jsoncpp
注意:该库包含一个核心类,两个重要方法
yum install jsoncpp-devel.x86_64
是否安装成功,查看ls -l usr/include/jsoncpp/json 中是否有我们需要的一些头文件等
例如html上传图片:
<html>
<head></head>
<body>
<form id="upload-form" action="http://47.98.116.42:9094/image" method="post" enctype="multipart/form-data" >
<input type="file" id="upload" name="upload" /> <br />
<input type="submit" value="Upload" />
</form>
</body>
</html>
form标签表示这是一个form表单,form表单是一种传统的浏览器、网页和服务器交互的方式,其功能就是提供一些选项框,借助这些选项框将数据提交给服务器。注意需要修改form表单中的action,action表示将表单提交给指定服务器。
#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) {//中断服务器ctrl + c
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;
}
该项目借助httplib库简单实现了一个图片服务器,提供了四个接口,分别是上传图片,获取图片信息,查看图片内容,删除图片;
其中利用md5来校验上传图片内容的正确性,图片内容保存在磁盘文件中,图片信息保存在数据库中,服务器和客户端之间的数据是采用Json格式字符串组织传输的。