实现一个HTTP服务器,用这个服务器存储图片,针对每个图片提供一个唯一的url,借助这个url就可以将图片展示到其他网页上
github中的issue如果需要上传图片时,是不支持本地直接上传的,
还有自己搭建的静态网页博客上传图片时也需要填图片的地址,而不能直接上传
1.上传图片
2.根据图片的url访问图片,获取图片内容(即下载)
3.获取某个图片的属性
4.删除
1.数据存储模块(文件+数据库)
2.服务器模块(给前端提供一些接口)
只使用一张表
分别是图片id,图片名,图片大小,上传时间,图片文件类型,操作对象,md5校验和
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字段进行文件正确性校验,在传输之前算一次,在传输之后算一次,两个值如果相等的话说明传输没问题
md5是一种hash算法
无论是怎样的字符串,最终得到的md5值都是固定长度
如果有一个字符发生变化,得到的md5值差异也很大
通过原字符串计算md5很容易但是通过md5还原却很难
实现一个数据库的客户端程序的方法
mysql已经提供了一系列的API
需要先安装mysql的API:
yum install mysql++-devel.x86_64
在使用mysql的API时包含头文件就好了,即
#include
//数据库的初始化及释放
static MYSQL* MySQLInit(){}
static void MySQLRelease(MYSQL* mysql){}
//创建一个类,通过这个类来操作数据库表
class ImageTable{
public:
ImageTable(){}//构造函数
bool Insert(){}//插入数据
bool SelectALL(){}//查找所有数据
bool SelectOne(){}//查找某一个数据
bool Delete(){}//删除数据
private:
MYSQL* mysql_;
};
1.插入Insert
bool Insert(const Json::Value& image){
char sql[4 * 1024] = {0};
//直接拼装SQL语句,存在缺陷
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;
}
分析:
在用json时,需要包含使用第三方库jsoncpp,直接yum install就可以了
jsoncpp有一个核心类和两个重要方法
核心类Json::Value ->类似于std::map,所以也可以通过[]取到对应的数据
两个方法
Reader::parse把一个json字符串转成Json::Value对象-->序列化
Writer::write把一个Json::Value对象转成字符串-->反序列化
用json封装参数优于直接定义类来封装参数的原因有两点:
直接拼装SQL的方式有一个缺陷,容易受到SQL注入攻击
例如
如果本来是这样的 insert into image_table values(null,'%s'....)
但是如果有人这样输入 test.png');drop database
拼接以后就变成了这样 insert into image_table values(null,'test.png');drop database'....)
数据库就凉凉了
2.查找所有SelectAll
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;iappend(image);
}
//释放结果集合,防止内存泄漏
mysql_free_result(result);
return true;
}
3.查找某一个SelectOne
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;
}
相对于SelectAll加一个校验,判断是否取出了所要的唯一一个
4.删除Delete
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;
}
http服务器接收http请求,返回http响应,此处需要约定面对不同的请求,服务器给予不同的响应,比如上传,下载,删除等等
HTTP底层协议也就是TCP协议,面向连接,保证可靠性
HTTP请求方法:
GET:向指定的资源发出“显示”请求,使用 GET 方法应该只用在读取数据上,而不应该用于产生“副作用”的操作中
POST:指定资源提交数据,请求服务器进行处理(例如提交表单或者上传文件)。数据被包含在请求文本中。这个请求可能会创建新的资源或者修改现有资源,或两者皆有。
PUT:向指定资源位置上传其最新内容
DELETE:请求服务器删除 Request-URI 所标识的资源
实现HTTP服务器时使用Restful风格的设计
json是一种数据组织格式,最主要的用途之一就是序列化,源于javacript用来表示一个对象
json优点:便于观察,方便调试
json缺点:组织格式的效率较低,占用内存和带宽较大
例如
{
“name”:“盖伦”,
“skill-q”:“跑得快,沉默”,
“skill-w”:“护盾”,
“skill-e”:“大陀螺–爱地魔力转圈圈”,
“skill-r”:“大宝剑”
}
此外除了json以外还有protobuf,二进制序列化协议,效率较高,但是不方便调试
借助第三方库实现http服务器
cpp-httplib官方文档
https://github.com/yhirose/cpp-httplib
class FileUtil {
public:
static bool Write() {}//写文件
static bool Read() {} //读文件
};
int main(){
using namespace httplib;
mysql=image_system::MySQLInit();//初始化数据库
image_system::ImageTable image_table(mysql);//借助image_table这个对象来操作数据库
image_system::MySQLRelease(mysql);//关闭数据库
Server server;//设置一个server对象
//插入图片
server.Post(){
//1.对参数进行校验
//2.根据文件名获取到文件的数据file对象
//3.把图片的属性信息插入到数据库中
//4.把图片保存到指定的磁盘目录
//5.构造一个响应数据通知客户端上传成功
});
//查看所有图片信息
server.Get(){
//1.调用数据库接口来获取数据
//2.构造响应结果返回给客户端
});
//查看指定图片的信息
server.Get(){
//1.先获取到图片 id
// 2. 再根据图片 id 查询数据库
// 3. 把查询结果返回给客户端
});
//查看指定图片的内容
server.Get(){
// 1. 根据图片 id 去数据库中查到对应的目录
// 2. 根据目录找到文件内容, 读取文件内容
//3. 把文件内容构造成一个响应
});
//删除图片
server.Delete(){
// 1. 根据图片 id 去数据库中查到对应的目录
// 2. 查找到对应文件的路径
// 3. 调用数据库操作进行删除
// 4. 删除磁盘上的文件
// 5. 构造响应
});
return 0;
}
1.读写文件函数
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;
}
//要读取文件需要先知道文件的大小,把string* content当成是一个缓冲区
//用stat获取文件大小,需要先创建一个stat struct
struct stat st;
stat(file_name.c_str(), &st);//读指定目录的文件转成字符串
content->resize(st.st_size);//把字符串content的长度设置成跟文件一样
//把文件一次性都读取完
//file.read可以按照指定长度来读取
//char*为缓冲区大小,int为读取的长度
file.read((char*)content->c_str(), content->size());
file.close();
return true;
}
2.数据库的初始化及关闭
using namespace httplib;
//在程序刚运行时就连接数据库
mysql=image_system::MySQLInit();
image_system::ImageTable image_table(mysql);
signal(SIGINT,[](int){
image_system::MySQLRelease(mysql);
exit(0);
注意数据库需要关闭,关闭的时机就是服务端主动关闭时
服务端关闭是通过ctrl+c,所以可以通过捕捉2号信号来确定关闭数据库的时机
3.插入图片
//使用的是lambda表达式
//Request:请求,可读不可写
//Response:响应,可读可写
//[&image_table]这是lambda的重要特性,捕获变量
//lambda内部是不能直接访问image_table的,但是通过捕获就可以了,&相当于引用
server.Post("/image",[&image_table](const Request& req,Response& resp){
Json::Value resp_json;
Json::FastWriter writer;
printf("上传图片\n");
//1.对参数进行校验
//auto size = req.files.size();//size为图片的个数
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("name1");
// file.filename;
// file.content_type;
//上传图片的内容
//3.把图片的属性信息插入到数据库中
Json::Value image;
image["image_name"] = file.filename;
image["size"] = (int)file.length;
time_t tt;
time(&tt);
tt = tt + (8*3600);
tm* t = gmtime(&tt);
char res[1024] = {0};
image["upload_time"] = res;
std::string md5value;
auto body = req.body.substr(file.offset, file.length);
md5(body,md5value);
image["md5"] = md5value;
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;
});
以下的代码来自cpp-httplib文档,表示http服务器是如何处理上传文件的请求的,我加了一些注释
svr.Post("/multipart", [&](const auto& req, auto& res) {
auto size = req.files.size();//请求获取文件大小
auto ret = req.has_file("name1"));//判定是否有指定名字的文件
const auto& file = req.get_file_value("name1");//借助文件名找到文件内容
// file.filename;
// file.content_type;
auto body = req.body.substr(file.offset, file.length));//offset, length这是文件的具体内容
})
上传图片时如何实现的
点击发送按钮,就会给服务器发送一个特殊的http请求
html文件上传参考
https://www.jianshu.com/p/7636d5c60a8d
以下是我的html文件的代码
其中body既包含图片的属性信息又包含图片的内容
4.查看所有图片信息
server.Get("/image",[&image_table](const Request& req,Response& resp){
printf("获取所有图片信息\n");
Json::Value resp_json;
Json::FastWriter writer;
//调用数据库接口来获取数据
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");
});
5.查看指定图片的信息
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;
});
下方代码是httplib官方文档示例的一部分
svr.Get(R"(/numbers/(\d+))", [&](const Request& req, Response& res) {
auto numbers = req.matches[1];
res.set_content(numbers, "text/plain");
});
其中/numbers/(\d+) 为正则表达式,/numbers表示必须包含这个字符串,\d表示必须包含一个0~9的数字,+表示这个数字应该出现一次或多次,匹配的结果例如:/numbers/121
正则表达式:是一个带有特殊符号的字符串,描述了字符串的特征,(字符串应该包含什么信息)
但是正则表达式在低版本(g++4.8)的编译器中不支持,需要升级g++版本
在C和C++中要想表示,必须用\,这里可以使用C++11中的原始字符串(raw string),
示例R"(/image/(\d+))"
6.查看指定图片的内容
server.Get(R"(/show/(\d+))",[&image_table](const Request& req,Response& resp){
Json::FastWriter writer;
Json::Value resp_json;
Json::Value image;
// 1. 根据图片 id 去数据库中查到对应的目录
int image_id = std::stoi(req.matches[1]);
printf("获取 id 为 %d 的图片内容!\n", image_id);
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;//状态码
//正文应该是图片信息,图片的类型与数据库中的图片type一致
resp.set_content(image_body, image["type"].asCString());
});
7.删除图片
server.Delete(R"(/image/(\d+))",[&image_table](const Request& req,Response& resp){
// 1. 根据图片 id 去数据库中查到对应的目录
int image_id = std::stoi(req.matches[1]);
printf("删除 id 为 %d 的图片!\n", image_id);
// 2. 查找到对应文件的路径
Json::Value image;
Json::FastWriter writer;
Json::Value resp_json;
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++ 标准库中不能删除文件,只能使用操作系统提供的函数
unlink(image["path"].asCString());
// 5. 构造响应
resp_json["ok"] = true;
resp.status = 200;
resp.set_content(writer.write(resp_json), "application/json");
});
POST /image HTTP/1.1
Content-Type:application/x-www-form-urlencoded 与提交表单密切相关
提交表单是指把客户端输入的数据提交到服务器上进行处理,是HTML中客户端给服务器上传数据的一种常见方法
响应
HTTP/1.1 200 OK
{
ok:true
}
2.查看图片信息
测试的代码
void TestImageTable(){
//用Json转化成字符串,使用StyledWriter的目的是让转化出的字符串具有一定的格式,便于查看
Json::StyledWriter writer;
//创建一个ImageTable类,调用其中的方法,验证结
MYSQL* mysql = image_system::MySQLInit();
image_system::ImageTable image_table(mysql);
bool ret=false;
//插入数据
// Json::Value image;
// image["image_name"]="test.png";
// image["size"]=1024;
// image["upload_time"]="2019/09/01";
// image["md5"]="123456";
// image["type"]="png";
// image["path"]="data/test.png";
// ret=image_table.Insert(image);
// printf("ret=%d\n",ret);
//查找所有图片信息
//Json::Value images;
//ret=image_table.SelectALL(&images);
// printf("ret=%d\n",ret);
// printf("%s\n",writer.write(images).c_str());//转化成c风格的字符串
//查找指定图片信息
// Json::Value image;
//ret=image_table.SelectOne(1,&image);
//printf("ret=%d\n",ret);
//printf("%s\n",writer.write(image).c_str());
//删除指定图片
ret=image_table.Delete(1);
printf("ret=%d\n",ret);
image_system::MySQLRelease(mysql);
}
int main(){
TestImageTable();
return 0;
}
1.存储时合并文件
如果上传大量的比较小的文件时,在磁盘空间不太充裕时可能会产生磁盘碎片,把这些逻辑上比较小的文件合并成一个比较大的物理文件,在读取文件时,数据库中除了存该文件的路径之外,再存一个偏移量,在已知路径的相对偏移量开始读起,就可以正常读取文件
磁盘碎片应该称为文件碎片,是因为文件被分散保存到整个磁盘的不同地方,而不是连续地保存在磁盘连续的簇中形成的。硬盘在使用一段时间后,由于反复写入和删除文件,磁盘中的空闲扇区会分散到整个磁盘中不连续的物理位置上,从而使文件不能存在连续的扇区里。这样,再读写文件时就需要到不同的地方去读取,增加了磁头的来回移动,降低了磁盘的访问速度。
2.防盗链
只要其他人拿到了我的url就可以使用我的图床
所以可以增加权限控制,只让图片能被特定的用户使用
防盗链的方法:使用登录验证,判断引用地址,使用cookie,使用POST下载,使用图形验证码,打包下载等
其中使用cookie是通过实现用户账户功能,登录之后就得到了cookie,有了cookie就可以正常使用
https://www.cnblogs.com/wangyongsong/p/8204698.html
3.支持图片处理功能
比如在qq和手机相册中常见的缩略图功能,这样的好处是如果原图片比较大(像是2k和4k的图片),相同带宽下缩略图加载更快,可以在用户请求时选择添加一个参数,比如width=100&length=120
借助C++图片处理库:计算机视觉库OpenCV,开源图形库FreeImage等
4.相同图片只保留一份
节省服务器资源,用md5实现对文件内容是否相同的判断,在实现时还需要进行引用计数,比如一张图片上传了两次,但是删除一次,但是在另一路径下的相同的没被删除的图片文件也被删了,
问题1:
在读取文件是先是按行循环读取,但是出现黑屏问题,读取错误
原因是我的文件不是文本文件,而是图片文件,如果按行读取可能会遇到这种错误按行读取不合适,所以又采用file.read,按照指定长度读取,read的参数两个:一个是char*缓冲区长度,一个是int读取的长度,但是缓冲区的长度怎么确定呢,我又通过stat确定文件的长度,根据此来确定缓冲区的长度
问题2:
在进行删除操作时,数据库可以直接调用删除,但是磁盘上由于C++98和C++11中都没有删除的功能,我用的是系统调用接口unlink
研究一下httplib和json
做这个项目遇到的问题太多了,感觉自己的知识面太窄了,还是要多多学习,而且由于之前也没有项目经历,在做这个项目之前没有明确的框架,不过最终还是做成功了,虽然在大佬们面前这可能是很简单的项目,但是对于我来讲,却是不平凡的,