图床的实现

  • Post author:
  • Post category:其他




项目简述



图床

实现一个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<mysql/mysql.h>



对数据库相关接口进行封装




基本框架

//数据库的初始化及释放
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封装参数优于直接定义类来封装参数的原因有两点:

  • 如果数据库需要扩展,用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;i<rows;++i){
          MYSQL_ROW row =mysql_fetch_row(result);
          //数据库查出来的每一条记录都相当于是一个图片的信息
          //需要把这个信息转换成JSON格式
          Json::Value image;
          image["image_id"]=atoi(row[0]);//将字符创char*转换成整数
          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对象添加到images中
          images->append(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;
      }



服务器API设计:

http服务器接收http请求,返回http响应,此处需要约定面对不同的请求,服务器给予不同的响应,比如上传,下载,删除等等

HTTP底层协议也就是TCP协议,面向连接,保证可靠性

HTTP请求方法:

GET:向指定的资源发出“显示”请求,使用 GET 方法应该只用在读取数据上,而不应该用于产生“副作用”的操作中

POST:指定资源提交数据,请求服务器进行处理(例如提交表单或者上传文件)。数据被包含在请求文本中。这个请求可能会创建新的资源或者修改现有资源,或两者皆有。

PUT:向指定资源位置上传其最新内容

DELETE:请求服务器删除 Request-URI 所标识的资源

实现HTTP服务器时使用Restful风格的设计

  • 用http method来表示操作的动词,通常用GET表示查,POST增,PUT改,DELETE删
  • 用http path表示操作的对象
  • body中保存其他信息,比如图片属性,文件名等等
  • body中的信息可以用json来组织,包含第三方库jsoncpp
  • 响应数据通常也是用json格式组织

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这是文件的具体内容
})

上传图片时如何实现的

  • 客户端(网页)包含一段特殊的html代码,生成一个按钮,点击之后弹出文件选择框

点击发送按钮,就会给服务器发送一个特殊的http请求

html文件上传参考

https://www.jianshu.com/p/7636d5c60a8d

以下是我的html文件的代码

<html>
<head></head>
<body>
<form id="upload-form" action="http://129.204.158.213:9094/image" method="post" enctype="multipart/form-data" >
   <input type="file" id="upload" name="upload" /> <br />
   <input type="submit" value="Upload" />
</form>
</body>
</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




总结:

做这个项目遇到的问题太多了,感觉自己的知识面太窄了,还是要多多学习,而且由于之前也没有项目经历,在做这个项目之前没有明确的框架,不过最终还是做成功了,虽然在大佬们面前这可能是很简单的项目,但是对于我来讲,却是不平凡的,



版权声明:本文为weixin_44769612原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。