目录

一:图片分享的优化

二:图片浏览的优化

三:文件的共享(不是分享)

四:共享文件的排行榜


 C/C++精品项目之图床共享云存储(1):基础组件-CSDN博客

 C/C++精品项目之图床共享云存储(2):MySql连接池_mysql 连接池c++ ping-CSDN博客

 C/C++精品项目之图床共享云存储(3):网络缓冲区类和main-CSDN博客

 C/C++精品项目之图床共享云存储(4):注册,登录,token,排序文件-CSDN博客

 C/C++精品项目之图床共享云存储(5):FastDFS存储原理,文件的秒传-CSDN博客

 C/C++精品项目之图床共享云存储(6):图片的共享,浏览,获取,以及短链的生成-CSDN博客

一:图片分享的优化

        我们今天首先把分享和共享给搞清楚。我之前的文件好像将共享和分享经常说在一起,到今天我突然发现分享和共享用在的地方是不同的,下面让我带你们好好屡屡。

        我们上次讲解了图片的分享,我们这个图片的分享,在前端就是一个按钮,我们点击这个分享按钮,我们就进入了分享的接口,然后在最后将我们生成的urlmd5进行返回。这是分享,分享给别人一个urlmd5,它使用这个urlmd5,就可以访问我们分享出来的文件。

        而共享并不需要我们进行分享出来一个urlmd5出来,我们将文件共享之后,其他所有人都可以看到,而分享,只有有分享的urlmd5才能看到。

        这里我们开始讲解我们优化后的图片分享功能,我们在之前的代码中添加了redis的服务,我们将一些数据存放到redis中去,我们之前使用的share表,其中存放了md5和urlmd5,我们的查询操作是在这里面进行的,后来我们经过修改,将这俩数据给分开了,md5仍然在info表中,而share表中只有urlmd5了,但是添加了这个文件的完整url下载地址了,也就是说我们不需要通过urlmd5去查找md5然后再找url了,我们直接可以在urlmd5这个表中找到url。

        当然我们在这里加入了redis,我们不仅要在mysql中添加一组数据,而且还要在redis中也要添加一样的数据。当我们去查询的时候,直接去查询redis去不仅提高速度,还降低mysql的压力。

//现在是分享链接:生成urlmd5,然后找到info表里的完整url下载地址,然后将urlmd5,url,用户名等插入到新的分享表中去,这里面没有对应的md5了。 \
然后将这一份相同的信息,插入到redis中去,一样是没有对应的md5.
//分享图片
static void handleSharePicture(const char *user, const char *file_md5,
                       const char *file_name, string &str_json) {
    char sql_cmd[SQL_MAX_LEN] = {0};
    string file_url;
    int file_size = 0;
    CResultSet *result_set = NULL;
    //数据库操作
    // 获取数据库连接
    CDBManager *db_manager = CDBManager::getInstance();
    CDBConn *db_conn = db_manager->GetDBConn("tuchuang_master");
    AUTO_REL_DBCONN(db_manager, db_conn);
    CacheManager *cache_manager = CacheManager::getInstance();
    CacheConn *cache_conn = cache_manager->GetCacheConn("token");
    AUTO_REL_CACHECONN(cache_manager, cache_conn);

    //查询file_info url
    //   通过文件的MD5查找对应的url地址
    sprintf(sql_cmd, "select url,size from file_info where md5 ='%s'", file_md5);
    LogInfo("执行: {}", sql_cmd);
    result_set = db_conn->ExecuteQuery(sql_cmd);
    if (result_set && result_set->Next()) {
        file_url = result_set->GetString("url");
        file_size = result_set->GetInt("size");
        delete result_set;
    } else {
        if (result_set)
            delete result_set;
        LogError("{} 操作失败", sql_cmd);
        encodeSharePictureJson(HTTP_RESP_FAIL, "", str_json);
        return;
    }
    LogInfo("url: {}, size: {}", file_url, file_size);  

    //操作数据库
    string urlmd5 = RandomString(32);  // uuid生成唯一值,再转成base64
    //分享时间
    //获取当前时间
    time_t now = time(NULL);
    char create_time[TIME_STRING_LEN];
    strftime(create_time, TIME_STRING_LEN - 1, "%Y-%m-%d %H:%M:%S", localtime(&now));

    LogInfo("urlmd5:{}", urlmd5);
    char key[5] = {0}; //提取码目前没有给到前端
    sprintf(sql_cmd,
            "insert into share_picture_list2 (user, file_url, file_name, urlmd5, "
            "`key`, pv, size, create_time) values ('%s', '%s', '%s', '%s', '%s', %d, %d, "
            "'%s')",   user, file_url.c_str(), file_name, urlmd5.c_str(), key, 0,
            file_size, create_time);
    LogInfo("执行: {}", sql_cmd);
    if (!db_conn->ExecuteCreate(sql_cmd)) {
        LogError("{} 操作失败", sql_cmd);
        encodeSharePictureJson(HTTP_RESP_FAIL, urlmd5, str_json);
    } else {
        encodeSharePictureJson(HTTP_RESP_OK, urlmd5, str_json);
        //成功则加载到redis
        cache_conn->Hset(urlmd5, "user", user);
        cache_conn->Hset(urlmd5, "file_url", file_url);
        cache_conn->Hset(urlmd5, "file_name", file_name);
        cache_conn->Hset(urlmd5, "pv", FormatString("%d", 0));
        cache_conn->Hset(urlmd5, "size", FormatString("%d", file_size));
        cache_conn->Hset(urlmd5, "create_time", create_time);
    }        
}

二:图片浏览的优化

        我们在上面不是说了我们可以直接通过urlmd5找到完整的下载地址吗,于是我们下面先从redis中查找这个urlmd5的信息,如果找到,直接返回,找不到,我们再从mysql中进行查询,如果找不到说明我们已经取消文件的分享,如果在mysql中找到,我们直接包装返回即可。

        当然下面我们还增加了一个浏览量的优化,因为访问Mysql是比较慢的,而访问redis是很快的,因此,我们可以每次先增加redis中的浏览量,每五次在mysql中进行一次更新操作,这样就减少了mysql的很多操作。

//分享图片时 通过hash存储 urlmd5对应的字段
//浏览图片时 先通过redis读取,如果没有再从mysql读取,如果mysql也没有则说明这个分享失效了
//这个浏览发来urlmd5,我们先从redis中读取这个数据,如果有,直接返回对应的url,如果没有,那么从mysql中查询:share_picture2,返回url \
没有也报错,然后更新阅读计数,我们这里的更新是先更新redis的引用计数,因为redis快,而mysql慢,我们每增加五个,我们就在mysql中更新一次,减少频率。
static void handleBrowsePicture(const char *urlmd5, string &str_json) {
    int ret = 0;
    char sql_cmd[SQL_MAX_LEN] = {0};
    string file_name;
    string user;
    string file_url;
    string create_time;
    int pv = 0;
    int file_size = 0;

    //查询 share_picture_list2, file_info的url
    CDBManager *db_manager = CDBManager::getInstance();
    CDBConn *db_conn = db_manager->GetDBConn("tuchuang_slave");
    AUTO_REL_DBCONN(db_manager, db_conn);
    CacheManager *cache_manager = CacheManager::getInstance();
    CacheConn *cache_conn = cache_manager->GetCacheConn("token");
    AUTO_REL_CACHECONN(cache_manager, cache_conn);

    CResultSet *result_set = NULL;
     LogInfo("urlmd5: {}", urlmd5);

    //先从redis获取
    std::map<string, string> values;
    bool ret_b = cache_conn->HgetAll(urlmd5, values);
    if(ret_b && values.size() != 0) {  //说明redis是有缓存的
        //c从redis获取到缓存
        user = values["user"];
        file_url = values["file_url"];
        file_name = values["file_name"];
        pv = stoi(values["pv"]);
        file_size = stoi(values["size"]);
        create_time = values["create_time"];
    } else {
        //  从分享图片列表查询到文件信息
        sprintf(sql_cmd,
            "select user, file_url, file_name, pv, size, create_time from "
            "share_picture_list2 where urlmd5 = '%s'", urlmd5);
        LogDebug("执行: {}", sql_cmd);
        result_set = db_conn->ExecuteQuery(sql_cmd);
        if (result_set && result_set->Next()) {
            user = result_set->GetString("user");
            file_url = result_set->GetString("file_url");
            file_name = result_set->GetString("file_name");
            pv = result_set->GetInt("pv");
            file_size = result_set->GetInt("size");
            create_time = result_set->GetString("create_time");
            delete result_set;
        } else {
            if (result_set)
                delete result_set;
            ret = -1;
            goto END;
        }     
        //更新缓存到redis
         //成功则加载到redis
        cache_conn->Hset(urlmd5, "user", user);
        cache_conn->Hset(urlmd5, "file_url", file_url);
        cache_conn->Hset(urlmd5, "file_name", file_name);
        cache_conn->Hset(urlmd5, "pv", FormatString("%d", pv));
        cache_conn->Hset(urlmd5, "size", FormatString("%d", file_size));
        cache_conn->Hset(urlmd5, "create_time", create_time);
    }
    LogInfo("url: {}", file_url);  
    //更新浏览计数
    pv += 1;
    if(cache_conn->Hset(urlmd5, "pv", FormatString("%d", pv)) < 0) {
        ret = -1;
        goto END;
    }

    if(pv %5 == 0) {
        // 3. 更新浏览次数, 可以考虑保存到redis,减少数据库查询的压力 修改、创建是比较花时间
         //浏览计数增加  redis实时更新,mysql 每隔5个计数再更新到MySQL
        sprintf(sql_cmd, "update share_picture_list2 set pv = %d where urlmd5 = '%s'", pv, urlmd5);
        LogDebug("执行: {}", sql_cmd);
        if (!db_conn->ExecuteUpdate(sql_cmd)) {
            LogError("{} 操作失败", sql_cmd);
            ret = -1;
            goto END;
        }
    }

    ret = 0;
END:
     // 4. 返回urlmd5 和提取码key
    if (ret == 0) {
        encodeBrowselPictureJson(HTTP_RESP_OK, pv, file_url, user,
                                 create_time, str_json);
    } else {
        encodeBrowselPictureJson(HTTP_RESP_FAIL, pv, file_url, user,
                                 create_time, str_json);
    }
}

而对于自己分享的图片和取消操作这俩方法基本没什么改变。

三:文件的共享(不是分享)

        我们对本文件来说,我们是对文件进行共享操作,首先就是通过redis在共享列表中查找是否已经有这个文件了,万一别人存放了或者自己存放了。如果存放了那么直接返回,如果没存放,我们就查找一遍MySQL,如果MySQL中有,那我们先将数据存放到redis中,然后直接返回。如果没有,那就说明,这个文件还没有人进行共享。我们先将用户表中的这个文件标记为共享,然后在mysql中进行插入到共享文件中去,然后就是同步到redis中去。

//分享文件
int handleShareFile(string &user, string &md5, string &filename) {
    /*
    a)先判断此文件是否已经分享,判断集合有没有这个文件,如果有,说明别人已经分享此文件,中断操作(redis操作)
    b)如果集合没有此元素,可能因为redis中没有记录,再从mysql中查询,如果mysql也没有,说明真没有(mysql操作)
    c)如果mysql有记录,而redis没有记录,说明redis没有保存此文件,redis保存此文件信息后,再中断操作(redis操作)
    d)如果此文件没有被分享,mysql保存一份持久化操作(mysql操作)
    e)redis集合中增加一个元素(redis操作)
    f)redis对应的hash也需要变化 (redis操作)
    */
    ShareState share_state = ShareFail;
    int ret = 0;
    char sql_cmd[SQL_MAX_LEN] = {0};
    char fileid[1024] = {0};
    int ret2 = 0;
    CDBManager *db_manager = CDBManager::getInstance();
    CDBConn *db_conn = db_manager->GetDBConn("tuchuang_slave");
    AUTO_REL_DBCONN(db_manager, db_conn);

    CacheManager *cache_manager = CacheManager::getInstance();
    CacheConn *cache_conn = cache_manager->GetCacheConn("token");
    AUTO_REL_CACHECONN(cache_manager, cache_conn);
    //文件标示,md5+文件名
    sprintf(fileid, "%s%s", md5.c_str(), filename.c_str());

    if (cache_conn) {
        ret2 = cache_conn->ZsetExit(FILE_PUBLIC_ZSET, fileid);
    } else {
        ret2 = 0;
    }
    LogInfo("fileid: {}, ZsetExit: {}", fileid, ret2);
    //===1、先判断此文件是否已经分享,判断集合有没有这个文件,如果有,说明别人已经分享此文件,中断操作(redis操作)
    if (ret2 == 1) //存在
    {
        LogWarn("别人已经分享此文件");
        share_state = ShareHad;
        goto END;
    } else if (ret2 == 0) //不存在
    {
        //===2、如果集合没有此元素,可能因为redis中没有记录,再从mysql中查询,如果mysql也没有,说明真没有(mysql操作)
        //===3、如果mysql有记录,而redis没有记录,说明redis没有保存此文件,redis保存此文件信息后,再中断操作(redis操作)
        //查看此文件别人是否已经分享了
        sprintf(sql_cmd,
                "select * from share_file_list where md5 = '%s' and file_name "
                "= '%s'",
                md5.c_str(), filename.c_str());
        //返回值:1有记录
        ret2 = CheckwhetherHaveRecord(
            db_conn,
            sql_cmd); //执行sql语句, 最后一个参数为NULL
                      //,如果有则说明没有及时保持到redis,这里需要保存到redis
        if (ret2 == 1) //说明有结果,别人已经分享此文件
        {
            // redis保存此文件信息
            cache_conn->ZsetAdd(FILE_PUBLIC_ZSET, 0, fileid);
            cache_conn->Hset(FILE_NAME_HASH, fileid, filename);
            LogWarn("别人已经分享此文件");
            share_state = ShareHad;
            goto END;
        }
    } else //出错
    {
        ret = -1;
        goto END;
    }

    //===4、如果此文件没有被分享,mysql保存一份持久化操作(mysql操作)

    // sql语句, 更新共享标志字段
    sprintf(sql_cmd,
            "update user_file_list set shared_status = 1 where user = '%s' and "
            "md5 = '%s' and file_name = '%s'",
            user.c_str(), md5.c_str(), filename.c_str());

    if (!db_conn->ExecuteUpdate(sql_cmd, false)) {
        LogError("{} 操作失败", sql_cmd);
        ret = -1;
        goto END;
    }

    time_t now;
    ;
    char create_time[TIME_STRING_LEN];
    //获取当前时间
    now = time(NULL);
    strftime(create_time, TIME_STRING_LEN - 1, "%Y-%m-%d %H:%M:%S",
             localtime(&now));

    //分享文件的信息,额外保存在share_file_list保存列表
    /*
        -- user	文件所属用户
        -- md5 文件md5
        -- create_time 文件共享时间
        -- file_name 文件名字
        -- pv 文件下载量,默认值为1,下载一次加1
    */
    sprintf(sql_cmd,
            "insert into share_file_list (user, md5, create_time, file_name, "
            "pv) values ('%s', '%s', '%s', '%s', %d)",
            user.c_str(), md5.c_str(), create_time, filename.c_str(), 0);
    if (!db_conn->ExecuteCreate(sql_cmd)) {
        LogError("{} 操作失败", sql_cmd);
        ret = -1;
        goto END;
    }

    // 共享文件数量+1
    ret = CacheIncrCount(cache_conn, FILE_PUBLIC_COUNT);
    if (ret < 0) {
        LogError("CacheIncrCount failed");
        ret = -1;
        goto END;
    }

    //===5、redis集合中增加一个元素(redis操作)
    cache_conn->ZsetAdd(FILE_PUBLIC_ZSET, 0,
                        fileid); // 如果失败是需要撤销mysql数据库的操作的

    //===6、redis对应的hash也需要变化 (redis操作)
    //     fileid ------>  filename
    LogInfo("Hset FILE_NAME_HASH {}-{}", fileid, filename);
    ret = cache_conn->Hset(FILE_NAME_HASH, fileid, filename);
    if (ret < 0) {
        LogWarn("Hset FILE_NAME_HASH failed");
    }
    share_state = ShareOk;
END:
    return (int)share_state;
}

四:共享文件的排行榜

        这里就到了我们一开始提到的分享和共享的问题了,我们这个共享文件和查找用户自己的文件是比较相似的。都是先解析命令。在这里对于获取总文件数量以及获取总文件信息,都没啥可讲的,都是正常思路,查找即可返回。

//这个函数是处理排行榜的。这个函数和上面的myfiles文件很像,那个文件是处理本用户的文件排序,而这个文件是处理在共享文件中的文件排序。
int ApiSharefiles(string &url, string &post_data, string &str_json) {
    // 解析url有没有命令

    // count 获取用户文件个数
    // display 获取用户文件信息,展示到前端
    char cmd[20];
    string user_name;
    string token;
    int start = 0; //文件起点
    int count = 0; //文件个数

    LogInfo("post_data: {}", post_data);

    //解析命令 解析url获取自定义参数
    QueryParseKeyValue(url.c_str(), "cmd", cmd, NULL);
    
    LogInfo("cmd = {}", cmd);
    if (strcmp(cmd, "count") == 0){ // count 获取用户文件个数
        // 解析json
        if (handleGetSharefilesCount(count) < 0){ //获取共享文件个数
            encodeSharefilesJson(1, 0, str_json);
        } else {
            encodeSharefilesJson(0, count, str_json);
        }
        return 0;
    } else {
        //获取共享文件信息 127.0.0.1:80/sharefiles&cmd=normal
        //按下载量升序 127.0.0.1:80/sharefiles?cmd=pvasc
        //按下载量降序127.0.0.1:80/sharefiles?cmd=pvdesc
        if (decodeShareFileslistJson(post_data, start, count) < 0){ //通过json包获取信息
            encodeSharefilesJson(1, 0, str_json);
            return 0;
        }
        if (strcmp(cmd, "normal") == 0) {
            handleGetShareFilelist(start, count, str_json); // 获取共享文件
        } else if (strcmp(cmd, "pvdesc") == 0) {
            handleGetRankingFilelist(start, count, str_json); ////获取共享文件排行版
        } else {
            encodeSharefilesJson(1, 0, str_json);
        }
    }
    return 0;
}

        下面这个函数才是比较重要的,就是获取共享文件中的排行榜。我们在redis中,对排行榜的操作还是很简单的,只需要按着浏览量进行排序即可了。但是我们在这之前先考虑一个问题,我们在共享文件列表中,如果传入两个file_name一样的文件,那么我们会怎么样?所以这个就是我们需要考虑的。

        我们可以使用一种方法,因为md5是唯一的,如果名称一样,md5一样,那不就是一个文件吗,所以没必要上传,因此我们在redis中会将这个文件的MD5+file_name这俩放到一起作为名称存储,这样就不会出现上面的情况。我们会将文件的信息存储到一个FILE_NAME_HASH结构中和一个FILE_PUBLIC_ZSET结构中,一个存name,一个存浏览量。

        首先我们要判断的就是查看redis中的共享文件和mysql中的共享文件数量是否一致,如果不一致,那我们需要将redis中的数据全部删除,从MySQL中重新同步,但是!数据太多怎么办?会十分的耗时阻塞。我们可以后台开一个线程让他去同步。当我们同步完成之后,我们还从redis中获取数据,通过redis的排序命令,将浏览量按着顺序返回,然后封装json返回即可。

//获取共享文件排行版
//按下载量降序127.0.0.1:80/api/sharefiles?cmd=pvdesc
void handleGetRankingFilelist(int start, int count, string &str_json) {
    /*
    a) mysql共享文件数量和redis共享文件数量对比,判断是否相等
    b) 如果不相等,清空redis数据,从mysql中导入数据到redis (mysql和redis交互)
    c) 从redis读取数据,给前端反馈相应信息
    */

    int ret = 0;
    char sql_cmd[SQL_MAX_LEN] = {0};
    int total = 0;
    char filename[1024] = {0};
    int sql_num;
    int redis_num;
    int score;
    int end;
    RVALUES value = NULL;
    Json::Value root;
    Json::Value files;
    int file_count = 0;

    CDBManager *db_manager = CDBManager::getInstance();
    CDBConn *db_conn = db_manager->GetDBConn("tuchuang_slave");
    AUTO_REL_DBCONN(db_manager, db_conn);
    CResultSet *pCResultSet = NULL;

    CacheManager *cache_manager = CacheManager::getInstance();
    CacheConn *cache_conn = cache_manager->GetCacheConn("token");
    AUTO_REL_CACHECONN(cache_manager, cache_conn);

    // 获取共享文件的总数量
    ret = getShareFilesCount(db_conn, cache_conn, total);
    if (ret != 0) {
        LogError("{} 操作失败", sql_cmd);
        ret = -1;
        goto END;
    }
    //===1、mysql共享文件数量
    sql_num = total;

    //===2、redis共享文件数量
    redis_num = cache_conn->ZsetZcard(
        FILE_PUBLIC_ZSET); // Zcard 命令用于计算集合中元素的数量。
    if (redis_num == -1) {
        LogError("ZsetZcard  操作失败");
        ret = -1;
        goto END;
    }

    LogInfo("sql_num: {}, redis_num: {}", sql_num, redis_num);

    //===3、mysql共享文件数量和redis共享文件数量对比,判断是否相等
    if (redis_num != sql_num) // 如果数量太多会导致阻塞, redis mysql数据不一致怎么处理?
    { //===4、如果不相等,清空redis数据,重新从mysql中导入数据到redis
      //(mysql和redis交互)

        // a) 清空redis有序数据
        cache_conn->Del(FILE_PUBLIC_ZSET); // 删除集合
        cache_conn->Del(FILE_NAME_HASH); // 删除hash, 理解 这里hash和集合的关系

        // b) 从mysql中导入数据到redis
        // sql语句
        strcpy( sql_cmd, "select md5, file_name, pv from share_file_list order by pv desc");
        LogInfo("执行: {}", sql_cmd);

        pCResultSet = db_conn->ExecuteQuery(sql_cmd);
        if (!pCResultSet) {
            LogError("{} 操作失败", sql_cmd);
            ret = -1;
            goto END;
        }

        // mysql_fetch_row从使用mysql_store_result得到的结果结构中提取一行,并把它放到一个行结构中。
        // 当数据用完或发生错误时返回NULL.
        while (pCResultSet->Next()) // 这里如果文件数量特别多,导致耗时严重,
                          // 可以这么去改进当
                          // mysql的记录和redis不一致的时候,开启一个后台线程去做同步
        {
            char field[1024] = {0};
            string md5 = pCResultSet->GetString("md5"); // 文件的MD5
            string file_name = pCResultSet->GetString("file_name"); // 文件名
            int pv = pCResultSet->GetInt("pv");
            sprintf(field, "%s%s", md5.c_str(),
                    file_name.c_str()); //文件标示,md5+文件名

            //增加有序集合成员
            cache_conn->ZsetAdd(FILE_PUBLIC_ZSET, pv, field);

            //增加hash记录
            cache_conn->Hset(FILE_NAME_HASH, field, file_name);
        }
    }

    //===5、从redis读取数据,给前端反馈相应信息
    // char value[count][1024];
    value = (RVALUES)calloc(count, VALUES_ID_SIZE); //堆区请求空间
    if (value == NULL) {
        ret = -1;
        goto END;
    }

    file_count = 0;
    end = start + count - 1; //加载资源的结束位置
    //降序获取有序集合的元素   file_count获取实际返回的个数
    ret = cache_conn->ZsetZrevrange(FILE_PUBLIC_ZSET, start, end, value,
                                    file_count);
    if (ret != 0) {
        LogError("ZsetZrevrange 操作失败");
        ret = -1;
        goto END;
    }

    //遍历元素个数
    for (int i = 0; i < file_count; ++i) {
        // files[i]:
        Json::Value file;
        /*
        {
            "filename": "test.mp4",
            "pv": 0
        }
        */
        ret = cache_conn->Hget(FILE_NAME_HASH, value[i],
                               filename); // 通过 文件md5+文件名 -> 获取文件名
        if (ret != 0) {
            LogError("hget  操作失败");
            ret = -1;
            goto END;
        }
        file["filename"] = filename; // 返回文件名

        //-- pv 文件下载量
        score = cache_conn->ZsetGetScore(FILE_PUBLIC_ZSET, value[i]);
        if (score == -1) {
            LogError("ZsetGetScore  操作失败");
            ret = -1;
            goto END;
        }
        file["pv"] = score; // 返回下载量
        files[i] = file;
    }
    if (file_count > 0)
        root["files"] = files;

END:
    if (ret == 0) {
        root["code"] = 0;
        root["total"] = total;
        root["count"] = file_count;
    } else {
        root["code"] = 1;
    }
    str_json = root.toStyledString();
}

本期讲解,主要是将分享和共享给区别开来,以及对文件分享,浏览的优化,以及对共享文件排行榜问题的解决。0voice · GitHub

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐