中文亚洲精品无码_熟女乱子伦免费_人人超碰人人爱国产_亚洲熟妇女综合网

當(dāng)前位置: 首頁(yè) > news >正文

各種類型網(wǎng)站建設(shè)售后完善長(zhǎng)沙網(wǎng)站建站模板

各種類型網(wǎng)站建設(shè)售后完善,長(zhǎng)沙網(wǎng)站建站模板,成全視頻在線觀看免費(fèi)看,360安全網(wǎng)址文章目錄 項(xiàng)目介紹所用技術(shù)與開(kāi)發(fā)環(huán)境所用技術(shù)開(kāi)發(fā)環(huán)境 項(xiàng)目框架compiler_server模塊compiler編譯功能comm/util.hpp 編譯時(shí)的臨時(shí)文件comm/log.hpp 日志comm/util.hpp 時(shí)間戳comm/util.hpp 檢查文件是否存在compile_server/compiler.hpp 編譯功能總體編寫 runner運(yùn)行功能資源設(shè)…

文章目錄

  • 項(xiàng)目介紹
  • 所用技術(shù)與開(kāi)發(fā)環(huán)境
    • 所用技術(shù)
    • 開(kāi)發(fā)環(huán)境
  • 項(xiàng)目框架
  • compiler_server模塊
    • compiler編譯功能
      • comm/util.hpp 編譯時(shí)的臨時(shí)文件
      • comm/log.hpp 日志
      • comm/util.hpp 時(shí)間戳
      • comm/util.hpp 檢查文件是否存在
      • compile_server/compiler.hpp 編譯功能總體編寫
    • runner運(yùn)行功能
      • 資源設(shè)置
      • comm/util.hpp 運(yùn)行時(shí)的臨時(shí)文件
      • compile_server/runner.hpp 運(yùn)行功能編寫
    • compile_server/compile_run.hpp 編譯且運(yùn)行
      • comm/util.hpp 生成唯一文件名
      • comm/uti.hpp 寫入文件/讀出文件
      • 清理臨時(shí)文件
      • compiler_run模塊的整體代碼
      • 本地進(jìn)行編譯運(yùn)行模塊的整體測(cè)試
    • compiler_server模塊(打包網(wǎng)絡(luò)服務(wù))
      • compiler_server/compile_server.cc
  • oj_server模塊
    • oj_server.cc 路由框架
    • oj_model.hpp/oj_model2.hpp
      • 文件版本
      • 數(shù)據(jù)庫(kù)版本:
    • oj_view.hpp
    • oj_control.cpp

項(xiàng)目介紹

項(xiàng)目是基于負(fù)載均衡的一個(gè)在線判題系統(tǒng),用戶自己編寫代碼,提交給后臺(tái),后臺(tái)再根據(jù)負(fù)載情況選擇合適的主機(jī)提供服務(wù)編譯運(yùn)行服務(wù)。

所用技術(shù)與開(kāi)發(fā)環(huán)境

所用技術(shù)

  • C++ STL 標(biāo)準(zhǔn)庫(kù)
  • Boost 準(zhǔn)標(biāo)準(zhǔn)庫(kù)(字符串切割)
  • cpp-httplib 第三方開(kāi)源網(wǎng)絡(luò)庫(kù)
  • ctemplate 第三方開(kāi)源前端網(wǎng)頁(yè)渲染庫(kù)
  • jsoncpp 第三方開(kāi)源序列化、反序列化庫(kù)
  • 負(fù)載均衡設(shè)計(jì)
  • 多進(jìn)程、多線程
  • MySQL C connect
  • Ace前端在線編輯器
  • html/css/js/jquery/ajax

開(kāi)發(fā)環(huán)境

  • Centos 7 云服務(wù)器
  • vscode

項(xiàng)目框架

在這里插入圖片描述

compiler_server模塊

模塊結(jié)構(gòu)
在這里插入圖片描述

總體流程圖
在這里插入圖片描述

compiler編譯功能

  • 在運(yùn)行編譯服務(wù)的時(shí)候,compiler收到來(lái)自oj_server傳來(lái)的代碼;我們對(duì)其進(jìn)行編譯
  • 在編譯前,我們需要一個(gè)code.cpp形式的文件;
  • 在編譯后我們會(huì)形成code.exe可執(zhí)行程序,若編譯失敗還會(huì)形成code.error來(lái)保存錯(cuò)誤信息;
  • 因此,我們需要對(duì)這些文件的后綴進(jìn)行添加,所以我們創(chuàng)建temp文件夾,該文件夾用來(lái)保存code代碼的各種后綴;
  • 所以在傳給編譯服務(wù)的時(shí)候只需要傳文件名即可,拼接路徑由comm公共模塊下的util.hpp提供路徑拼接

comm/util.hpp 編譯時(shí)的臨時(shí)文件

#pragma once#include <iostream>#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/time.h>namespace ns_util
{const std::string path = "./temp/";// 合并路徑類class PathUtil{public:static std::string splic(const std::string &str1, const std::string &str2){return path + str1 + str2;}// cpp文件 + 后綴名// file_name -> ./temp/xxx.cppstatic std::string Src(const std::string &file_name){return splic(file_name, ".cpp");}// exe文件 + 后綴名static std::string Exe(const std::string &file_name){return splic(file_name, ".exe");}static std::string CompilerError(const std::string &file_name){return splic(file_name, ".compile_error");}};   
}

comm/log.hpp 日志

日志需要輸出:等級(jí)、文件名、行數(shù)、信息、時(shí)間

#pragma once#include <string>
#include "util.hpp"namespace ns_log
{using namespace ns_util;// 日志等級(jí)enum{INFO,DEBUG,WARNING,ERROR,FATAL,};inline std::ostream& Log(const std::string& level, const std::string& file_name, int line){std::string log = "[";log += level;log += "]";log += "[";log += file_name;log += "]";log += "[";log += std::to_string(line);log += "]";  log += "[";log += TimeUtil::GetTimeStamp();log += "]";  std::cout << log;return std::cout;}#define LOG(level) Log(#level, __FILE__, __LINE__)
} 

獲取時(shí)間利用的是時(shí)間戳,在util工具類中編寫獲取時(shí)間戳的代碼。利用操作系統(tǒng)接口:gettimeofday

comm/util.hpp 時(shí)間戳

class TimeUtil
{
public:static std::string GetTimeStamp(){struct timeval _t;gettimeofday(&_t, nullptr);return std::to_string(_t.tv_sec);}
};    

進(jìn)行編譯服務(wù)的編寫,根據(jù)傳入的源程序文件名,子進(jìn)程對(duì)stderr進(jìn)行重定向到文件compile_error中,使用execlp進(jìn)行程序替換,父進(jìn)程在外面等待子進(jìn)程結(jié)果,等待成功后根據(jù)是否生成可執(zhí)行程序決定是否編譯成功;

判斷可執(zhí)行程序是否生成,我們利用系統(tǒng)調(diào)用stat來(lái)查看文件屬性,如果有,則說(shuō)明生成,否則失敗;

comm/util.hpp 檢查文件是否存在

class FileUtil
{
public:static bool IsFileExists(const std::string path_name){// 系統(tǒng)調(diào)用 stat 查看文件屬性// 獲取屬性成功返回 0struct stat st;if (stat(path_name.c_str(), &st) == 0){return true;}return false;}
};

compile_server/compiler.hpp 編譯功能總體編寫

#pragma once#include <iostream>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <wait.h>
#include <fcntl.h>#include "../comm/util.hpp"
#include "../comm/log.hpp"// 只負(fù)責(zé)代碼的編譯
namespace ns_compiler
{// 引入路徑拼接using namespace ns_util;using namespace ns_log;class Compiler{Compiler() {}~Compiler() {}public:// 返回值:是否編譯成功// file_name :  xxx// file_name -> ./temp/xxx.cpp// file_name -> ./temp/xxx.exe// file_name -> ./temp/xxx.errorstatic bool Compile(const std::string &file_name){pid_t id = fork();if (id < 0){LOG(ERROR) << "內(nèi)部錯(cuò)誤,當(dāng)前子進(jìn)程無(wú)法創(chuàng)建" << "\n";return false;}else if (id == 0) // 子進(jìn)程 編譯程序{int _error = open(PathUtil::Error(file_name).c_str(), O_CREAT | O_WRONLY, 0644);if (_error < 0){LOG(WARNING) << "沒(méi)有成功形成 error 文件" << "\n";exit(1);}// 重定向標(biāo)準(zhǔn)錯(cuò)誤到 _errordup2(_error, 2);// g++ -o target src -std=c++11execlp("g++", "g++", "-o", PathUtil::Exe(file_name).c_str(),PathUtil::Src(file_name).c_str(), "-std=c++11", nullptr);LOG(ERROR) << "g++執(zhí)行失敗,檢查參數(shù)是否傳遞正確" << "\n";exit(2);}else // 父進(jìn)程 判斷編譯是否成功{waitpid(id, nullptr, 0);if (FileUtil::IsFileExists(PathUtil::Exe(file_name))){LOG(INFO) << PathUtil::Exe(file_name) << "編譯成功!" << "\n";return true;}LOG(ERROR) << "編譯失敗!" << "\n";return false;}}};}

runner運(yùn)行功能

編譯完成后,我們就可以執(zhí)行可執(zhí)行程序了,執(zhí)行前,首先打開(kāi)三個(gè)文件xxx.stdin,xxx.stdout,xxx.stderr并將標(biāo)準(zhǔn)輸入、標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯(cuò)誤分別重定向到三個(gè)文件中。創(chuàng)建子進(jìn)程來(lái)進(jìn)行程序替換執(zhí)行程序;每道題的代碼運(yùn)行時(shí)間和內(nèi)存大小都有限制,所以在執(zhí)行可執(zhí)行程序之前我們對(duì)內(nèi)存和時(shí)間進(jìn)行限制。

資源設(shè)置

利用setrlimit系統(tǒng)調(diào)用來(lái)實(shí)現(xiàn)

int setrlimit(int resource, const struct rlimit *rlim);
        static void SetProcLimit(int cpu_limit, int mem_limit){struct rlimit cpu_rlimit;cpu_rlimit.rlim_cur = cpu_limit;cpu_rlimit.rlim_max = RLIM_INFINITY;setrlimit(RLIMIT_CPU, &cpu_rlimit);struct rlimit mem_rlimit;mem_rlimit.rlim_cur = mem_limit * 1024;mem_rlimit.rlim_max = RLIM_INFINITY;setrlimit(RLIMIT_AS, &mem_rlimit);}

comm/util.hpp 運(yùn)行時(shí)的臨時(shí)文件

        static std::string Stdin(const std::string &file_name){return splic(file_name, ".stdin");}static std::string Stdout(const std::string &file_name){return splic(file_name, ".stdout");}// error文件 + 后綴名static std::string Stderr(const std::string &file_name){return splic(file_name, ".stderr");}

compile_server/runner.hpp 運(yùn)行功能編寫

#pragma once
#include <iostream>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <wait.h>
#include <fcntl.h>
#include <sys/resource.h>#include "../comm/util.hpp"
#include "../comm/log.hpp"namespace ns_runner
{using namespace ns_log;using namespace ns_util;class Runner{public:Runner() {}~Runner() {}static void SetProcLimit(int cpu_limit, int mem_limit){struct rlimit cpu_rlimit;cpu_rlimit.rlim_cur = cpu_limit;cpu_rlimit.rlim_max = RLIM_INFINITY;setrlimit(RLIMIT_CPU, &cpu_rlimit);struct rlimit mem_rlimit;mem_rlimit.rlim_cur = mem_limit * 1024;mem_rlimit.rlim_max = RLIM_INFINITY;setrlimit(RLIMIT_AS, &mem_rlimit);}// 指明文件名即可,無(wú)后綴、無(wú)路徑// 返回值: // < 0 內(nèi)部錯(cuò)誤 // = 0運(yùn)行成功,成功寫入stdout等文件 // > 0運(yùn)行中斷,用戶代碼存在問(wèn)題static int Run(const std::string& file_name, int cpu_limit, int mem_limit){// 運(yùn)行程序會(huì)有三種結(jié)果:/*  1. 代碼跑完,結(jié)果正確2. 代碼跑完,結(jié)果錯(cuò)誤3. 代碼異常Run 不考慮結(jié)果正確與否,只在意是否運(yùn)行完畢;結(jié)果正確與否是有測(cè)試用例決定程序在啟動(dòng)的時(shí)候默認(rèn)生成以下三個(gè)文件標(biāo)準(zhǔn)輸入:標(biāo)準(zhǔn)輸出:標(biāo)準(zhǔn)錯(cuò)誤:*/std::string _execute = PathUtil::Exe(file_name);std::string _stdin   = PathUtil::Stdin(file_name); std::string _stdout  = PathUtil::Stdout(file_name);std::string _stderr  = PathUtil::Stderr(file_name);umask(0);int _stdin_fd = open(_stdin.c_str(), O_CREAT | O_RDONLY, 0644);int _stdout_fd = open(_stdout.c_str(), O_CREAT | O_WRONLY, 0644);int _stderr_fd = open(_stderr.c_str(), O_CREAT | O_WRONLY, 0644);if(_stdin_fd < 0 || _stdout_fd < 0 || _stderr_fd < 0){LOG(ERROR) << "內(nèi)部錯(cuò)誤, 標(biāo)準(zhǔn)文件打開(kāi)/創(chuàng)建失敗" << "\n";// 文件打開(kāi)失敗return -1;}pid_t id =  fork();if(id < 0){ close(_stdin_fd);close(_stdout_fd);close(_stderr_fd);LOG(ERROR) << "內(nèi)部錯(cuò)誤, 創(chuàng)建子進(jìn)程失敗" << "\n";return -2;}else if(id == 0) // 子進(jìn)程{dup2(_stdin_fd, 0);dup2(_stdout_fd, 1);dup2(_stderr_fd, 2); SetProcLimit(cpu_limit, mem_limit);execl(_execute.c_str(), /*要執(zhí)行誰(shuí)*/ _execute.c_str(), /*命令行如何執(zhí)行*/ nullptr);exit(1);}else // 父進(jìn)程{close(_stdin_fd);close(_stdout_fd);close(_stderr_fd);int status = 0;waitpid(id, &status, 0);LOG(INFO) << "運(yùn)行完畢!退出碼為: " << (status & 0x7F) << "\n";return status & 0x7f;}}};
}

compile_server/compile_run.hpp 編譯且運(yùn)行

  • 用戶的代碼會(huì)以json串的方式傳給該模塊
  • 給每一份代碼創(chuàng)建一個(gè)文件名具有唯一性的源文件
  • 調(diào)用上面的編譯和運(yùn)行執(zhí)行該源文件
  • 再把結(jié)果構(gòu)建成json串返回給上層

json串的結(jié)構(gòu)

在這里插入圖片描述

comm/util.hpp 生成唯一文件名

當(dāng)一份用戶提交代碼后,我們?yōu)槠渖傻脑次募枰哂形ㄒ恍?。名字生成唯一性我們可以利用毫秒?jí)時(shí)間戳加上原子性的增長(zhǎng)計(jì)數(shù)實(shí)現(xiàn)

獲取毫秒時(shí)間戳在TimeUtil工具類中,生成唯一文件名在FileUtil工具類中

        static std::string GetTimeMs(){struct timeval _time;gettimeofday(&_time, nullptr);return std::to_string(_time.tv_sec * 1000 + _time.tv_usec / 1000);}

comm/uti.hpp 寫入文件/讀出文件

因?yàn)樾枰顚戇\(yùn)行成功結(jié)果和運(yùn)行時(shí)報(bào)錯(cuò)的結(jié)果,所以我們寫一個(gè)寫入文件和讀出文件,放在FileUtil

static bool WriteFile(const std::string &target, const std::string &content){std::ofstream out(target);if (!out.is_open()){return false;}out.write(content.c_str(), content.size());out.close();return true;}// 根據(jù)路徑文件進(jìn)行讀出// 注意,默認(rèn)每行的\\n是不進(jìn)行保存的,需要保存請(qǐng)?jiān)O(shè)置參數(shù)static bool ReadFile(const std::string &path_file, std::string *content, bool keep = false){// 利用C++的文件流進(jìn)行簡(jiǎn)單的操作std::string line;std::ifstream in(path_file);if (!in.is_open())return "";while (std::getline(in, line)){(*content) += line;if (keep)(*content) += "\n";}in.close();return true;}

清理臨時(shí)文件

編譯還是運(yùn)行都會(huì)生成臨時(shí)文件,所以可以在編譯運(yùn)行的最后清理一下這一次服務(wù)生成的臨時(shí)文件

static void RemoveTempFile(const std::string &file_name){// 因?yàn)榕R時(shí)文件的存在情況存在多種,刪除文件采用系統(tǒng)接口unlink,但是需要判斷std::string src_path = PathUtil::Src(file_name);if (FileUtil::IsFileExists(src_path))unlink(src_path.c_str());std::string stdout_path = PathUtil::Stdout(file_name);if (FileUtil::IsFileExists(stdout_path))unlink(stdout_path.c_str());std::string stdin_path = PathUtil::Stdin(file_name);if (FileUtil::IsFileExists(stdin_path))unlink(stdin_path.c_str());std::string stderr_path = PathUtil::Stderr(file_name);if (FileUtil::IsFileExists(stderr_path))unlink(stderr_path.c_str());std::string compilererr_path = PathUtil::CompilerError(file_name);if (FileUtil::IsFileExists(compilererr_path))unlink(compilererr_path.c_str());std::string exe_path = PathUtil::Exe(file_name);if (FileUtil::IsFileExists(exe_path))unlink(exe_path.c_str());}

提供一個(gè)Start方法讓上層調(diào)用編譯運(yùn)行模塊,參數(shù)是一個(gè)輸入形式的json串和一個(gè)要給上層返回的json

使用jsoncpp反序列化,解析輸入的json串。調(diào)用形成唯一文件名的方法生成一個(gè)唯一的文件名,然后使用解析出來(lái)的代碼部分創(chuàng)建出一個(gè)源文件,把文件名交給編譯模塊進(jìn)行編譯,再把文件名和時(shí)間限制,內(nèi)存限制傳給運(yùn)行模塊運(yùn)行,記錄這個(gè)過(guò)程中的狀態(tài)碼。再最后還要序列化一個(gè)json串返還給用戶,更具獲得狀態(tài)碼含義的接口填寫狀態(tài)碼含義,根據(jù)狀態(tài)碼判斷是否需要填寫運(yùn)行成功結(jié)果和運(yùn)行時(shí)報(bào)錯(cuò)的結(jié)果,然后把填好的結(jié)果返還給上層。

最終調(diào)用一次清理臨時(shí)文件接口把這一次服務(wù)生成的所有臨時(shí)文件清空即可。

兩個(gè)json的具體內(nèi)容
在這里插入圖片描述

compiler_run模塊的整體代碼

#pragma once
#include <jsoncpp/json/json.h>#include "compiler.hpp"
#include "runner.hpp"
#include "../comm/util.hpp"
#include "../comm/log.hpp"namespace ns_complie_and_run
{using namespace ns_log;using namespace ns_util;using namespace ns_compiler;using namespace ns_runner;class ComplieAndRun{public:static void RemoveTempFile(const std::string &file_name){// 因?yàn)榕R時(shí)文件的存在情況存在多種,刪除文件采用系統(tǒng)接口unlink,但是需要判斷std::string src_path = PathUtil::Src(file_name);if (FileUtil::IsFileExists(src_path))unlink(src_path.c_str());std::string stdout_path = PathUtil::Stdout(file_name);if (FileUtil::IsFileExists(stdout_path))unlink(stdout_path.c_str());std::string stdin_path = PathUtil::Stdin(file_name);if (FileUtil::IsFileExists(stdin_path))unlink(stdin_path.c_str());std::string stderr_path = PathUtil::Stderr(file_name);if (FileUtil::IsFileExists(stderr_path))unlink(stderr_path.c_str());std::string compilererr_path = PathUtil::CompilerError(file_name);if (FileUtil::IsFileExists(compilererr_path))unlink(compilererr_path.c_str());std::string exe_path = PathUtil::Exe(file_name);if (FileUtil::IsFileExists(exe_path))unlink(exe_path.c_str());}// > 0:進(jìn)程收到信號(hào)導(dǎo)致異常崩潰// < 0:整個(gè)過(guò)程非運(yùn)行報(bào)錯(cuò)// = 0:整個(gè)過(guò)程全部完成static std::string CodeToDesc(int status, const std::string &file_name){std::string desc;switch (status){case 0:desc = "運(yùn)行成功!";break;case -1:desc = "代碼為空";break;case -2:desc = "未知錯(cuò)誤";break;case -3:desc = "編譯報(bào)錯(cuò)\n";FileUtil::ReadFile(PathUtil::CompilerError(file_name), &desc, true);break;case 6:desc = "內(nèi)存超過(guò)范圍";break;case 24:desc = "時(shí)間超時(shí)";break;case 8:desc = "浮點(diǎn)數(shù)溢出";break;case 11:desc = "野指針錯(cuò)誤";break;default:desc = "未處理的報(bào)錯(cuò)-status為:" + std::to_string(status);break;}return desc;}/*輸入:code: 用戶提交的代碼input: 用戶提交的代碼對(duì)應(yīng)的輸入cpu_limit:mem_limit:輸出:必有,status: 狀態(tài)碼reason: 請(qǐng)求結(jié)果可能有,stdout: 運(yùn)行完的結(jié)果stderr: 運(yùn)行完的錯(cuò)誤*/static void Start(const std::string &in_json, std::string *out_json){Json::Value in_value;Json::Reader reader;reader.parse(in_json, in_value, 1);std::string code = in_value["code"].asString();std::string input = in_value["input"].asString();int cpu_limit = in_value["cpu_limit"].asInt();int mem_limit = in_value["mem_limit"].asInt();int status_code = 0;Json::Value out_value;int run_result = 0;std::string file_name;if (!code.size()){status_code = -1; // 代碼為空goto END;}// 毫秒級(jí)時(shí)間戳 + 原子性遞增唯一值:來(lái)保證唯一性file_name = FileUtil::UniqFileName();if (!FileUtil::WriteFile(PathUtil::Src(file_name), code)){status_code = -2; // 未知錯(cuò)誤goto END;}if (!Compiler::Compile(file_name)){status_code = -3; // 未知錯(cuò)誤goto END;}run_result = Runner::Run(file_name, cpu_limit, mem_limit);if (run_result < 0)status_code = -2; // 未知錯(cuò)誤else if (run_result > 0)status_code = run_result; // 崩潰elsestatus_code = 0;END:out_value["status"] = status_code;out_value["reason"] = CodeToDesc(status_code, file_name);if (status_code == 0){// 整個(gè)過(guò)程全部成功std::string _stdout;FileUtil::ReadFile(PathUtil::Stdout(file_name), &_stdout, true);out_value["stdout"] = _stdout;std::string _stderr;FileUtil::ReadFile(PathUtil::Stderr(file_name), &_stderr, true);out_value["stderr"] = _stderr;}Json::StyledWriter writer;*out_json = writer.write(out_value);RemoveTempFile(file_name);}};
}

本地進(jìn)行編譯運(yùn)行模塊的整體測(cè)試

自己手動(dòng)構(gòu)造一個(gè)json串,編譯、運(yùn)行、返回結(jié)果json串

#include "compile_run.hpp"using namespace ns_complie_and_run;// 編譯服務(wù)會(huì)被同時(shí)請(qǐng)求,保證代碼的唯一性
int main()
{// 客戶端請(qǐng)求jsonstd::string in_json;Json::Value in_value;in_value["code"] = R"(  #include<iostream>int main() {std::cout << "Hello, world!" << std::endl;int *p = new int[1024 * 1024 * 20 ];return 0;})";in_value["input"] = "";       in_value["cpu_limit"] = 1;  in_value["mem_limit"] = 10240;   Json::FastWriter writer;in_json = writer.write(in_value);std::cout << "in_json: " << std::endl << in_json << std::endl;std::string out_json;ComplieAndRun::Start(in_json, &out_json);std::cout << "out_json: " << std::endl << out_json << std::endl;return 0;
}

compiler_server模塊(打包網(wǎng)絡(luò)服務(wù))

編譯運(yùn)行服務(wù)已經(jīng)整合在一起了,接下來(lái)將其打包成網(wǎng)絡(luò)服務(wù)即可
我們利用httplib庫(kù)將compile_run打包為一個(gè)網(wǎng)絡(luò)編譯運(yùn)行服務(wù)

compiler_server/compile_server.cc

  • 使用了 httplib 庫(kù)來(lái)提供 HTTP 服務(wù)
  • 實(shí)現(xiàn)了一個(gè)編譯運(yùn)行服務(wù)器
  • 通過(guò)命令行參數(shù)接收端口號(hào)
  • 一個(gè)POST /compile_and_run主要的編譯運(yùn)行接口
  • 接收JSON格式的請(qǐng)求體,包含:代碼內(nèi)容、輸入數(shù)據(jù)、CPU 限制、內(nèi)存限制
#include "compile_run.hpp"
#include "../comm/httplib.h"using namespace ns_compile_and_run;
using namespace httplib;void Usage(std::string proc)
{std::cerr << "Usage : " << "\n\t" << proc << "prot" << std::endl;
}int main(int argc, char* argv[])
{if(argc != 2){Usage(argv[0]);return 1;}Server svr;svr.Post("/compile_and_run", [](const Request &req, Response &resp){std::string in_json = req.body;std::string out_json;if(!in_json.empty()){CompileAndRun::Start(in_json, &out_json);resp.set_content(out_json, "application/json;charset=utf-8");}});svr.listen("0.0.0.0", atoi(argv[1])); // 啟動(dòng) http 服務(wù)return 0;
}

oj_server模塊

oj_server.cc 路由框架

步驟:

  • 服務(wù)器初始化:
  1. 創(chuàng)建 HTTP 服務(wù)器實(shí)例
  2. 初始化控制器
  3. 設(shè)置信號(hào)處理函數(shù)
  • 請(qǐng)求處理:
  1. 接收 HTTP 請(qǐng)求
  2. 根據(jù) URL 路由到對(duì)應(yīng)處理函數(shù)
  3. 調(diào)用控制器相應(yīng)方法
  4. 返回處理結(jié)果
  • 判題流程:
  1. 接收用戶提交的代碼
  2. 通過(guò)控制器進(jìn)行判題
  3. 返回判題結(jié)果

  • 創(chuàng)建一個(gè)服務(wù)器對(duì)象
int main()
{Server svr;  // 服務(wù)器對(duì)象
}
  • 獲取所有題目列表
    返回所有題目的HTML頁(yè)面
svr.Get("/all_questions", [&ctrl](const Request &req, Response &resp){std::string html;ctrl.AllQuestions(&html);resp.set_content(html, "text/html; charset=utf-8");
});
  • 獲取單個(gè)題目
    返回單個(gè)題目的詳細(xì)信息頁(yè)面
svr.Get(R"(/question/(\d+))", [&ctrl](const Request &req, Response &resp){std::string number = req.matches[1];std::string html;ctrl.Question(number, &html);resp.set_content(html, "text/html; charset=utf-8");
});
  • 提交代碼判題
    處理用戶提交的代碼
    返回 JSON 格式的判題結(jié)果
svr.Post(R"(/judge/(\d+))", [&ctrl](const Request &req, Response &resp){std::string number = req.matches[1];std::string result_json;ctrl.Judge(number, req.body, &result_json);resp.set_content(result_json, "application/json;charset=utf-8");
});
  • 服務(wù)器配置和啟動(dòng)
svr.set_base_dir("./wwwroot");
svr.listen("0.0.0.0", 8080);
  • 維護(hù)一個(gè)全局控制器指針
    Recovery 函數(shù)處理 SIGQUIT 信號(hào),用于服務(wù)器恢復(fù)
static Control *ctrl_ptr = nullptr;void Recovery(int signo)
{ctrl_ptr->RecoveryMachine();
}

oj_model.hpp/oj_model2.hpp

整體架構(gòu)為MVC模式

Model層 由oj_model.hpp文件版本和 oj_model2.hpp數(shù)據(jù)庫(kù)版本構(gòu)成;

負(fù)責(zé)數(shù)據(jù)的存儲(chǔ)和訪問(wèn),提供了兩種實(shí)現(xiàn)方式

  1. 基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)
struct Question {string number;     // 題目編號(hào)string title;      // 題目標(biāo)題string star;       // 難度等級(jí)int cpu_limit;     // CPU時(shí)間限制(秒)int mem_limit;     // 內(nèi)存限制(KB)string desc;       // 題目描述string header;     // 用戶代碼模板string tail;       // 測(cè)試用例代碼
};
  1. 存儲(chǔ)方案設(shè)計(jì)

文件版本

優(yōu)勢(shì):
簡(jiǎn)單直觀,易于管理
適合小規(guī)模題庫(kù)
方便備份和版本控制
劣勢(shì):
并發(fā)性能較差
擴(kuò)展性有限
數(shù)據(jù)一致性難保證

目錄結(jié)構(gòu)

./questions/├── questions.list    # 題目基本信息└── 1/                # 每個(gè)題目獨(dú)立目錄├── desc.txt      # 題目描述├── header.cpp    # 代碼模板└── tail.cpp      # 測(cè)試用例

具體代碼

#pragma once#include "../comm/log.hpp"
#include "../comm/util.hpp"#include <iostream>
#include <string>
#include <unordered_map>
#include <cassert>
#include <vector>
#include <fstream>
#include <cstdlib>
#include <boost/algorithm/string.hpp>// 根據(jù)題目 list 文件,加載所有題目的信息到內(nèi)存中
// model:主要用來(lái)和數(shù)據(jù)進(jìn)行交互,對(duì)外提供訪問(wèn)數(shù)據(jù)的接口namespace ns_model
{using namespace std;using namespace ns_log;using namespace ns_util;struct Question{string number;        // 題目編號(hào)string title;         // 題目的標(biāo)題string star;          // 題目的難度int cpu_limit;        // 題目的時(shí)間要求(s)int mem_limit;        // 題目的空間要求(KB)string desc;     // 題目的描述string header;   // 題目給用戶的部分代碼string tail;     // 題目的測(cè)試用例,和 header 形成完整代碼提交給后端編譯};const string questions_list = "./questions/questions.list" ;const string question_path = "./questions/" ;class Model{private:// 【題號(hào) < - > 題目細(xì)節(jié)】unordered_map<string, Question> questions;public:Model(){assert(LoadQuestionList(questions_list));}bool LoadQuestionList(const string &question_list){// 加載配置文件 : questions/questions.list + 題目編號(hào)文件ifstream in(question_list);if(!in.is_open()) {LOG(FATAL) << "題目加載失敗!請(qǐng)檢查是否存在題庫(kù)文件" << std::endl;return false;}std::string line;while(getline(in, line)){   vector<string> tokens;StringUtil::SplitString(line, &tokens, " ");if(tokens.size() != 5){LOG(WARNING) << "加載部分題目失敗!請(qǐng)檢查文件格式" << std::endl;continue;}Question q;q.number = tokens[0];q.title = tokens[1];q.star = tokens[2];q.cpu_limit = atoi(tokens[3].c_str());q.mem_limit = atoi(tokens[4].c_str());string path = question_path;path += q.number;path += "/";FileUtil::ReadFile(path+"desc.txt", &(q.desc), true);FileUtil::ReadFile(path+"header.cpp", &(q.header), true);FileUtil::ReadFile(path+"tail.cpp", &(q.tail), true);questions.insert({q.number, q});} LOG(INFO) << "加載題目成功!" << std::endl;in.close();return true;}bool GetAllQuestions(vector<Question> *out){if(questions.size() == 0) {LOG(ERROR) << "用戶獲取題庫(kù)失敗!" << std::endl;return false;}for(const auto &q : questions)out->push_back(q.second);return true;}bool GetOneQuestion(const string &number, Question *q){const auto& iter = questions.find(number);if(iter == questions.end()) {LOG(ERROR) << "用戶獲取題庫(kù)失敗!題目編號(hào)為:" << number << std::endl;return false;}(*q) = iter->second;return true;}~Model(){}};
}

數(shù)據(jù)庫(kù)版本:

優(yōu)勢(shì):
更好的并發(fā)性能
事務(wù)支持,保證數(shù)據(jù)一致性

表設(shè)計(jì)

CREATE TABLE oj_questions (number VARCHAR(20) PRIMARY KEY,title VARCHAR(255) NOT NULL,star VARCHAR(20) NOT NULL,description TEXT,header TEXT,tail TEXT,cpu_limit INT,mem_limit INT
);
#pragma once#include "../comm/log.hpp"
#include "../comm/util.hpp"#include <iostream>
#include <string>
#include <unordered_map>
#include <cassert>
#include <vector>
#include <fstream>
#include <cstdlib>
#include <boost/algorithm/string.hpp>
#include "include/mysql.h"// 根據(jù)題目 list 文件,加載所有題目的信息到內(nèi)存中
// model:主要用來(lái)和數(shù)據(jù)進(jìn)行交互,對(duì)外提供訪問(wèn)數(shù)據(jù)的接口namespace ns_model
{using namespace std;using namespace ns_log;using namespace ns_util;struct Question{string number; // 題目編號(hào)string title;  // 題目的標(biāo)題string star;   // 題目的難度string desc;   // 題目的描述string header; // 題目給用戶的部分代碼string tail;   // 題目的測(cè)試用例,和 header 形成完整代碼提交給后端編譯int cpu_limit; // 題目的時(shí)間要求(s)int mem_limit; // 題目的空間要求(K)};const std::string oj_question = "***";const std::string host = "***";const std::string user = "***";const std::string passwd = "***";const std::string db = "***";const int port = 3306;class Model{public:Model(){}bool QueryMysql(const std::string &sql, vector<Question> *out){       // 創(chuàng)建MySQL句柄MYSQL* my = mysql_init(nullptr);// 連接數(shù)據(jù)庫(kù)//if(nullptr == mysql_real_connect(&my, host.c_str(), user.c_str(), db.c_str(), passwd.c_str(), port, nullptr, 0))if(nullptr == mysql_real_connect(my, host.c_str(), user.c_str(),  passwd.c_str(),db.c_str(), port, nullptr, 0)){std::cout << mysql_error(my) << std::endl;LOG(FATAL) << "連接數(shù)據(jù)庫(kù)失敗!!!" << "\n";return false;}LOG(INFO) << "連接數(shù)據(jù)庫(kù)成功!!!" << "\n";// 設(shè)置鏈接的編碼格式,默認(rèn)是拉丁的mysql_set_character_set(my, "utf8");// 執(zhí)行sql語(yǔ)句//if(0 != mysql_query(&my, sql.c_str()))if(0 != mysql_query(my, sql.c_str())){LOG(WARNING) << sql << " execute error!" << "\n";return false;} // 提取結(jié)果//MYSQL_RES *res = mysql_store_result(&my);MYSQL_RES *res = mysql_store_result(my);// 分析結(jié)果int rows = mysql_num_rows(res);// 獲得行數(shù)int cols = mysql_num_fields(res);// 獲得列數(shù)struct Question q;for(int i = 0; i < rows; i++){MYSQL_ROW row = mysql_fetch_row(res);q.number = row[0];q.title = row[1];q.star = row[2];q.desc = row[3];q.header = row[4];q.tail = row[5];q.cpu_limit = atoi(row[6]);q.mem_limit = atoi(row[7]);out->push_back(q);}// 釋放結(jié)果空間free(res);// 關(guān)閉MySQL連接//mysql_close(&my);mysql_close(my);return true;}bool GetAllQuestions(vector<Question> *out){std::string sql = "select * from oj.";sql += oj_question;return QueryMysql(sql, out);}bool GetOneQuestion(const string &number, Question *q){bool res = false;std::string sql = "select * from oj.";sql += oj_question;sql += " where number = ";sql += number;vector<Question> result;if (QueryMysql(sql, &result)){if (result.size() == 1){*q = result[0];res = true;}}return res;}~Model(){}};
}
  1. 接口設(shè)計(jì)
    返回bool表示操作成功與否
class Model {
public:// 獲取所有題目bool GetAllQuestions(vector<Question> *out);// 獲取單個(gè)題目bool GetOneQuestion(const string &number, Question *q);
};

oj_view.hpp

View層 由oj_view.hpp構(gòu)成;
使用 ctemplate庫(kù)來(lái)進(jìn)行 HTML模板渲染

  • 使用 TemplateDictionary存儲(chǔ)渲染數(shù)據(jù)
  • 使用 Template::GetTemplate加載模板
  • 使用 Expand方法進(jìn)行渲染
  • 獲取所有題目的渲染;
void AllExpandHtml(const vector<struct Question> &questions, std::string *html)
  1. 設(shè)置模板文件路徑 (all_questions.html)
  2. 創(chuàng)建模板字典
  3. 遍歷所有題目,為每個(gè)題目添加:
  • 題號(hào) (number)
  • 標(biāo)題 (title)
  • 難度等級(jí) (star)
  1. 渲染模板
  • 獲取單個(gè)題目的渲染;
void OneExpandHtml(const struct Question &q, std::string *html)
  1. 設(shè)置模板文件路徑 (one_question.html)
  2. 創(chuàng)建模板字典并設(shè)置值:
  • 題號(hào) (number)
  • 標(biāo)題 (title)
  • 難度等級(jí) (star)
  • 題目描述 (desc)
  • 預(yù)設(shè)代碼 (header)
  1. 渲染模板
#pragma once#include <iostream>
#include <string>
#include <ctemplate/template.h>// #include "oj_model.hpp"
#include "oj_model2.hpp"namespace ns_view
{using namespace ns_model;const std::string template_path = "./template_html/";class View{public:View() {};~View() {};public:void AllExpandHtml(const vector<struct Question> &questions, std::string *html){// 題目的編號(hào) 題目的標(biāo)題 題目的難度// 推薦使用表格顯示// 1. 形成路徑std::string src_html = template_path + "all_questions.html";// 2. 形成數(shù)據(jù)字典ctemplate::TemplateDictionary root("all_questions");for (const auto &q : questions){ctemplate::TemplateDictionary *sub = root.AddSectionDictionary("question_list");sub->SetValue("number", q.number);sub->SetValue("title", q.title);sub->SetValue("star", q.star);}// 3. 獲取被渲染的htmlctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP);// 4. 開(kāi)始完成渲染功能tpl->Expand(html, &root);}void OneExpandHtml(const struct Question &q, std::string *html){// 1. 形成路徑std::string src_html = template_path + "one_question.html";// 2. 形成數(shù)字典ctemplate::TemplateDictionary root("one_question");root.SetValue("number", q.number);root.SetValue("title", q.title);root.SetValue("star", q.star);root.SetValue("desc", q.desc);root.SetValue("pre_code", q.header);//3. 獲取被渲染的htmlctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP);//4. 開(kāi)始完成渲染功能tpl->Expand(html, &root);}};
}

oj_control.cpp

Controller層 由oj_control.hpp構(gòu)成;

  • 提供服務(wù)的主機(jī) Machine 類
    表示提供編譯服務(wù)的主機(jī)
    包含 IP、端口、負(fù)載信息
    提供負(fù)載管理方法(增加、減少、重置、獲取負(fù)載)
    // 提供服務(wù)的主機(jī)class Machine{public:std::string ip;int port;uint64_t load;std::mutex *mtx;public:Machine() : ip(""), port(0), load(0), mtx(nullptr){}~Machine(){}public:// 提升主機(jī)負(fù)載void IncLoad(){if (mtx)mtx->lock();load++;if (mtx)mtx->unlock();}// 減少主機(jī)負(fù)載void DecLoad(){if (mtx)mtx->lock();load--;if (mtx)mtx->unlock();}void ResetLoad(){ if (mtx)mtx->lock();load = 0;if (mtx)mtx->unlock();}// 獲取主機(jī)負(fù)載uint64_t Load(){uint64_t _load = 0;if (mtx)mtx->lock();_load = load;if (mtx)mtx->unlock();return _load;}};
  • LoadBlance 類 (負(fù)載均衡模塊)
    管理多臺(tái)編譯服務(wù)器
    維護(hù)在線/離線主機(jī)列表
    主要功能:
    從配置文件加載主機(jī)信息
    智能選擇負(fù)載最低的主機(jī)
    處理主機(jī)上線/離線
// 負(fù)載均衡模塊class LoadBlance{private:// 提供編譯的主機(jī)// 每一臺(tái)都有自己下標(biāo)std::vector<Machine> machines;// 所有在線的主機(jī) idstd::vector<int> online;// 所有離線的主機(jī) idstd::vector<int> offline;// 保證 LoadBlance 的數(shù)據(jù)安全std::mutex mtx;public:LoadBlance(){assert(LoadConf(machine_path));LOG(INFO) << "加載" << machine_path << "成功" << "\n";}~LoadBlance(){}public:bool LoadConf(const std::string &machine_list){std::ifstream in(machine_list);if (!in.is_open()){LOG(FATAL) << "主機(jī)加載失敗" << "\n";return false;}std::string line;while (std::getline(in, line)){std::vector<std::string> tokens;StringUtil::SplitString(line, &tokens, " ");if (tokens.size() != 2){LOG(WARNING) << "切分 " << line << "失敗" << "\n";continue;}Machine m;m.ip = tokens[0];m.port = atoi(tokens[1].c_str());m.load = 0;m.mtx = new std::mutex();online.push_back(machines.size());machines.push_back(m);}in.close();return true;}// id: 輸出型參數(shù)// m:  輸出型參數(shù)bool SmartChoice(int *id, Machine **m){// 1. 使用選擇好的主機(jī)(更新負(fù)載)// 2. 我們可能需要離線該主機(jī)mtx.lock();// 負(fù)載均衡的算法// 1. 隨機(jī)數(shù)法// 2. 輪詢 + hashint online_num = online.size();if (online_num == 0){LOG(FATAL) << "所有的主機(jī)掛掉!在線主機(jī)數(shù)量: " << online_num << ", 離線主機(jī)數(shù)量: " << offline.size() << "\n";mtx.unlock();return false;}// 找負(fù)載最小的主機(jī)*id = online[0];*m = &machines[online[0]];uint64_t min_load = machines[online[0]].Load();for (int i = 0; i < online_num; i++){uint64_t cur_load = machines[online[i]].Load();if (cur_load < min_load){min_load = cur_load;*id = online[i];*m = &machines[online[i]];}}mtx.unlock();return true;}void OfflineMachine(int id){mtx.lock();for(auto it = online.begin(); it != online.end(); it++){if(*it == id){machines[id].ResetLoad();// 離線主機(jī)已經(jīng)找到online.erase(it);offline.push_back(*it);break;}}mtx.unlock();}void OnlineMachine(){// 當(dāng)所有主機(jī)離線后,統(tǒng)一上線mtx.lock();online.insert(online.end(), offline.begin(), offline.end()); offline.erase(offline.begin(), offline.end());mtx.unlock();LOG(INFO) << "所有的主機(jī)已上線" << "\n";}void ShowMachine(){mtx.lock();std::cout << "在線主機(jī)列表: " << "\n";for(auto &it : online){std::cout << it << " ";}std::cout << std::endl;std::cout << "離線主機(jī)列表: " << "\n";for(auto &it : offline){std::cout << it << " ";}std::cout << std::endl;mtx.unlock();}};
  • Control 類 (核心控制器)
    整合 Model(數(shù)據(jù)層)和 View(視圖層)
    判題
// 控制器class Control{private:Model _model; // 提供后臺(tái)數(shù)據(jù)View _view;   // 提供網(wǎng)頁(yè)渲染LoadBlance _load_blance;public:Control(){}~Control(){}public:void RecoveryMachine(){_load_blance.OnlineMachine();}// 根據(jù)題目數(shù)據(jù)構(gòu)建網(wǎng)頁(yè)// html 輸出型參數(shù)bool AllQuestions(string *html){bool ret = true;vector<struct Question> all;if (_model.GetAllQuestions(&all)){sort(all.begin(), all.end(), [](const struct Question &q1, const struct Question &q2){return q1.number < q2.number;});_view.AllExpandHtml(all, html);}else{*html = "獲取題目失敗,形成題目列表失敗";ret = false;}return ret;}bool Question(const std::string number, string *html){bool ret = true;struct Question q;if (_model.GetOneQuestion(number, &q)){_view.OneExpandHtml(q, html);}else{*html = "指定題目:" + number + "不存在";ret = false;}return ret;}void Judge(const std::string &number, const std::string in_json, std::string *out_json){// 0.根據(jù)題目編號(hào)拿到題目細(xì)節(jié)struct Question q;_model.GetOneQuestion(number, &q);// 1.in_json 進(jìn)行反序列話,得到題目的 id ,得到用戶提交的源代碼 inputJson::Reader reader;Json::Value in_value;reader.parse(in_json, in_value);std::string code = in_value["code"].asString();// 2.重新拼接用戶的代碼 + 測(cè)試用例,形成新的代碼Json::Value compile_value;compile_value["input"] = in_value["input"].asString();compile_value["code"] = code + "\n" + q.tail;compile_value["cpu_limit"] = q.cpu_limit;compile_value["mem_limit"] = q.mem_limit;Json::FastWriter writer;std::string compile_string = writer.write(compile_value);// 3.選擇負(fù)載最低的主機(jī)while (true){Machine *m = nullptr;int id = 0;if (!_load_blance.SmartChoice(&id, &m)){break;}LOG(INFO) << "選擇主機(jī)成功, id = " << id << "詳情:" << m->ip << ":" << m->port << "\n";// 4.發(fā)起 http 請(qǐng)求,得到結(jié)果Client cli(m->ip, m->port);m->IncLoad();if(auto res = cli.Post("/compile_and_run", compile_string, "application/json;charset=utf-8")){// 5.將結(jié)果賦值給 out_jsonif(res->status == 200){*out_json = res->body;m->DecLoad();LOG(INFO) << "請(qǐng)求編譯、運(yùn)行成功" << "\n";break;}m->DecLoad();}else{// 請(qǐng)求失敗LOG(ERROR) << "當(dāng)前請(qǐng)求主機(jī)id = " << id << "詳情:" << m->ip << ":" << m->port << " 該主機(jī)可能已經(jīng)離線" << "\n";_load_blance.OfflineMachine(id);_load_blance.ShowMachine(); // for test}}}};

首先

  • AllQuestions(): 獲取并展示所有題目列表
  • Question(): 獲取并展示單個(gè)題目詳情

其次Judge

1. 獲取題目信息
2. 解析用戶提交的代碼
3. 組裝完整的測(cè)試代碼
4. 選擇負(fù)載最低的編譯主機(jī)
5. 發(fā)送HTTP請(qǐng)求到編譯主機(jī)
6. 處理編譯運(yùn)行結(jié)果

然后負(fù)載均衡處理使用最小負(fù)載優(yōu)先算法
在這里插入圖片描述

基本編譯運(yùn)行提交代碼已經(jīng)實(shí)現(xiàn),后續(xù)還會(huì)增加其他功能

http://www.risenshineclean.com/news/1066.html

相關(guān)文章:

  • 網(wǎng)站如何添加認(rèn)證聯(lián)盟南京seo優(yōu)化培訓(xùn)
  • 任何做網(wǎng)站如何進(jìn)行網(wǎng)站性能優(yōu)化?
  • 網(wǎng)站 拉新近期時(shí)事新聞10條
  • 湖南網(wǎng)站建設(shè)小公司排名黃岡seo顧問(wèn)
  • 網(wǎng)站建設(shè)服務(wù)聯(lián)享科技信息流廣告
  • 有沒(méi)有個(gè)人網(wǎng)站百度站長(zhǎng)平臺(tái)鏈接提交
  • web開(kāi)發(fā)基礎(chǔ)期末自測(cè)題答案代哥seo
  • 百度網(wǎng)盟網(wǎng)站有哪些企業(yè)營(yíng)銷型網(wǎng)站
  • php成品網(wǎng)站推廣網(wǎng)站排名
  • 個(gè)人備案域名可以做哪些網(wǎng)站嗎學(xué)軟件開(kāi)發(fā)學(xué)費(fèi)多少錢
  • 臺(tái)州椒江網(wǎng)站建設(shè)公司騰訊企點(diǎn)官網(wǎng)下載
  • 放網(wǎng)站的圖片做多大分辨率seo內(nèi)部?jī)?yōu)化方案
  • 網(wǎng)站建設(shè)的目前背景西安網(wǎng)絡(luò)優(yōu)化哪家好
  • 商城網(wǎng)站建設(shè)是 什么seo關(guān)鍵字優(yōu)化價(jià)格
  • 開(kāi)發(fā)一套網(wǎng)站價(jià)格株洲seo排名
  • 淘寶導(dǎo)購(gòu)網(wǎng)站怎么做it培訓(xùn)機(jī)構(gòu)怎么樣
  • 專業(yè)的營(yíng)銷型網(wǎng)站建設(shè)競(jìng)價(jià)廣告代運(yùn)營(yíng)
  • 福州搜索優(yōu)化實(shí)力江蘇seo哪家好
  • 一個(gè)網(wǎng)站 多個(gè)域名新站seo優(yōu)化快速上排名
  • 微網(wǎng)站開(kāi)發(fā)平臺(tái)免費(fèi)網(wǎng)絡(luò)推廣公司介紹
  • 大連電子商務(wù)網(wǎng)站建設(shè)網(wǎng)絡(luò)營(yíng)銷的真實(shí)案例分析
  • wordpress更換網(wǎng)站域名seo技術(shù)培訓(xùn)
  • 對(duì)網(wǎng)站備案的認(rèn)識(shí)賬號(hào)seo是什么
  • 北京軟件公司有哪些seo任務(wù)
  • 做視頻網(wǎng)站 買帶寬谷歌廣告聯(lián)盟一個(gè)月能賺多少
  • wordpress站內(nèi)優(yōu)化網(wǎng)絡(luò)營(yíng)銷評(píng)價(jià)的名詞解釋
  • 嗶哩嗶哩b站肉片免費(fèi)入口在哪里自己可以創(chuàng)建網(wǎng)站嗎
  • 友情網(wǎng)站制作藝人百度指數(shù)排行榜
  • 烏魯木齊住房和城鄉(xiāng)建設(shè)廳網(wǎng)站百度上首頁(yè)
  • 在農(nóng)村做相親網(wǎng)站怎么樣百度域名提交收錄網(wǎng)址