当前位置: 首页 > news >正文

西安网站建设 翼驰小网站下载渠道有哪些

西安网站建设 翼驰,小网站下载渠道有哪些,江门网站制作报价,新乡seo推广目录 一、项目的相关背景 二、所用技术栈和开发环境 三、项目的宏观结构 四、compile_server模块设计 1. 编译服务#xff08;compiler模块#xff09; 2. 运行服务#xff08;runner模块#xff09; 3. 编译并运行服务#xff08;compile_run模块#xff09; 4… 目录 一、项目的相关背景 二、所用技术栈和开发环境 三、项目的宏观结构 四、compile_server模块设计 1. 编译服务compiler模块  2. 运行服务runner模块 3. 编译并运行服务compile_run模块 4. 打包成网络服务compile_server模块 五、基于MVC结构的oj_server模块设计 1. 什么是MVC结构 2. oj_model模块 3. oj_view模块 4. oj_control模块 5. 打包成网络服务oj_server 六、前端页面的设计 1. indx.html 2. all_questions.html 3. one_question.html 七、项目总结  项目源码https://gitee.com/lu-code-xiaomiao/load-balancing-online---oj 一、项目的相关背景 学习编程的小伙伴大家对力扣、牛客或其他在线编程的网站一定都不陌生这些编程网站除了提供了在线编程还有其他的一些功能。我们这个项目只是做出能够在线编程的功能。 二、所用技术栈和开发环境 技术栈 C STL 标准库 Boost 准标准库(字符串切割) cpp-httplib 第三方开源网络库 ctemplate 第三方开源前端网页渲染库 jsoncpp 第三方开源序列化、反序列化库 负载均衡设计 多进程、多线程 MySQL C connect Ace前端在线编辑器(了解) html/css/js/jquery/ajax (了解) 开发环境 Centos 7 云服务器 vscode 三、项目的宏观结构 我们的项目核心是三个模块 comm : 公共模块主要包含httplib网络服务、log日志信息、util项目中都需要使用到的工具类的集合compile_server : 编译与运行模块主要包含编译服务、运行服务、编译和运行服务oj_server : 获取题目列表查看题目编写题目界面负载均衡客户端向服务器的oj_server发起请求有可能是请求题目的列表、请求特定题目的编写、请求代码提交对于请求题目列表和编写只需要向文件或MySQL获取数据并显示成网页即可但是提交代码的时候我们就要考虑多用户提交的情况所以oj_server在收到不同客户端发来的提交代码的请求时就要需要负载均衡式的选择我们后端的complie_server进行编译并运行然后反馈最终结果。 四、compile_server模块设计 compile_server模块主要包括编译服务、运行服务和编译运行服务最后打包成网络服务。 1. 编译服务compiler模块  compiler模块只负责代码的编译要对代码进行编译那么我们就需要有file_name文件名如1234.cpp         对代码进行编译有可能成功形成.exe文件后续可以直接运行。也有可能失败对于编译失败了的原因我们也需要保存起来用于反馈给用户否则客户怎么知道错误在哪里。 对于客户提交过来的文件如1234我们需要对文件进行路径拼接拼接出1234.cpp、1234.exe、1234.compiler_error 所以我们将这个功能编写到我们的comm模块中 #pragma once#include iostream #include string #include vector #include unistd.hnamespace ns_util {const std::string temp_path ./temp/;//temp目录用来存放这些文件class PathUtil {public:static std::string AddSuffix(const std::string file_name, const std::string suffix) {// 拼接路径名文件名后缀名std::string path_name temp_path;//路径名path_name file_name;//文件名path_name suffix;//后缀名return path_name;}// 编译时需要的临时文件// 构建源文件路径 后缀的完整文件名// 1234 - ./temp/1234.cppstatic std::string Src(const std::string file_name) {return AddSuffix(file_name, .cpp);}// 构建可执行程序的完整路径 后缀名static std::string Exe(const std::string file_name) {return AddSuffix(file_name, .exe);}static std::string CompilerError(const std::string file_name) {return AddSuffix(file_name, .compile_error);}};}其中./temp是对用户提交过来的文件名进行路径的拼接形成三个文件的存放位置这是编译时需要的三个临时文件有了这三个临时文件后我们就可以对用户的代码进行编译的操作了。 #pragma once#include iostream #include unistd.h #include sys/wait.h #include sys/types.h #include sys/stat.h #include fcntl.h#include ../comm/util.hpp #include ../comm/log.hpp// 只负责进行代码的编译namespace ns_compiler {// 引入路径拼接功能using namespace ns_util;using namespace ns_log;class Compiler {public:Compiler() {}~Compiler() {}//返回值编译成功true否则false//输入参数编译的文件名//file_name: 1234//1234 - ./temp/1234.cpp//1234 - ./temp/1234.exe//1234 - ./temp/1234.compiler_errorstatic bool Compile(const std::string file_name) {pid_t pid fork();//创建子进程成功就给子进程返回0给父进程返回pidif (pid 0) {LOG(ERROR) 内部错误创建子进程失败 \n;return false;} else if (pid 0) {umask(0);//将umask设置为0以防系统修改我们设置的权限//_stderr文件用来保存编译出错时产生的信息int _stderr open(PathUtil::CompilerError(file_name).c_str(), O_CREAT | O_WRONLY, 0644);if (_stderr 0) {LOG(WARNING) 没有成功形成stderr文件 \n;exit(1);}//我们不需要显示到显示器上所以重定向标准错误到_stderrdup2(_stderr, 2);//程序替换并不影响进程的文件描述符表//子进程: 调用编译器完成对代码的编译工作//g -o target src -stdc11execlp(g, g, -o, PathUtil::Exe(file_name).c_str(), PathUtil::Src(file_name).c_str(), -stdc11, nullptr);//这个函数替换的时候不要忘记最后给个nullptrLOG(ERROR) 启动编译器g失败可能是参数错误 \n;exit(2);} else {waitpid(pid, nullptr, 0);//编译是否成功,就看有没有形成对应的可执行程序if (FileUtil::IsFileExists(PathUtil::Exe(file_name))) {LOG(INFO) PathUtil::Src(file_name) 编译成功! \n;return true;}}LOG(ERROR) 编译失败没有形成可执行程序 \n;return false;}}; }其中关于LOG你暂时可以理解为std::cout 后面我会给出完整的代码 2. 运行服务runner模块 我们已经完成的编译服务相应的会在temp目录下形成三个临时文件当然编译成功会形成.exe文件失败会形成compiler_error文件不会形成.exe文件相应的错误信息回报存在这个文件中。有了.exe文件后我们接下来的工作就是对可执行程序进行运行了。 用户提交的代码虽然经过编译器编译后形成了可执行程序但是对于代码的运行也需要三个临时文件1234.stdin、1234.stdout、1234.stderr 这三个文件分别表示1234.stdin用户外部自测输入的参数但是我们不考虑直接使我们提供参数1234.stdout代表运行成功后的结果我们不需要显示到显示器上用文件保存起来用于反馈给客户1234.stderr代表运行失败后的结果我们不需要显示到显示器上用文件保存起来用于反馈给客户所以我们在util中再添加三个运行时需要的临时文件 #pragma once#include iostream #include string #include vector #include unistd.hnamespace ns_util {const std::string temp_path ./temp/;//temp目录用来存放这些文件class PathUtil {public:// 拼接路径名文件名后缀名static std::string AddSuffix(const std::string file_name, const std::string suffix) {std::string path_name temp_path;//路径名path_name file_name;//文件名path_name suffix;//后缀名return path_name;}// 编译时需要的临时文件// 构建源文件路径 后缀的完整文件名// 1234 - ./temp/1234.cppstatic std::string Src(const std::string file_name) {return AddSuffix(file_name, .cpp);}// 构建可执行程序的完整路径 后缀名static std::string Exe(const std::string file_name) {return AddSuffix(file_name, .exe);}static std::string CompilerError(const std::string file_name) {return AddSuffix(file_name, .compile_error);}// 运行时需要的临时文件static std::string Stdin(const std::string file_name) {return AddSuffix(file_name, .stdin);}static std::string Stdout(const std::string file_name) {return AddSuffix(file_name, .stdout);}// 构建该程序对应的标准错误完整路径 后缀名static std::string Stderr(const std::string file_name) {return AddSuffix(file_name, .stderr);}};}运行时需要的三个临时文件我们已经可以进行路径拼接了接下来我们来完成运行模块 #pragma once#include iostream #include string #include unistd.h #include sys/types.h #include sys/stat.h #include fcntl.h #include sys/wait.h #include ../comm/util.hpp #include ../comm/log.hppnamespace ns_runner {using namespace ns_log;using namespace ns_util;class Runner {public:Runner() {}~Runner() {}public:// 指明文件名即可不需要代路径不需要带后缀/** 返回值 0程序异常了退出时收到了信号返回值就是对应的信号编号* 返回值 0正常运行完毕了结果保存到了对应的临时文件中* 返回值 0内部错误* */static int Run(const std::string file_name, int cpu_limit, int mem_limit) {/** 程序运行* 1. 代码跑完结果正确* 2. 代码跑完结果不正确* 3. 代码没跑完异常了* Run需要考虑代码跑完结果正确是否吗不考虑* 结果正确是否是由我们的测试用例决定的* 我们只考虑是否正确运行完毕** 我们必须知道可执行程序是谁* 一个程序在默认启动时* 标准输入--我们不处理--* 标准输出程序运行完成输出结果是什么* 标准错误运行时错误信息*/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;return -1; // 打开文件失败}pid_t pid fork();if (pid 0) {LOG(ERROR) 运行时创建子进程失败 \n;close(_stdin_fd);close(_stdout_fd);close(_stderr_fd);return -2;// 代表创建子进程失败} else if (pid 0) {//重定向dup2(_stdin_fd, 0);dup2(_stdout_fd, 1);dup2(_stderr_fd, 2);/*我要执行谁 我想在命令行上如何执行该程序*/execl(_execute.c_str(), _execute.c_str(), nullptr);exit(1);} else {close(_stdin_fd);close(_stdout_fd);close(_stderr_fd);int status 0;waitpid(pid, status, 0);// 程序运行异常一定是因为收到了信号!LOG(INFO) 运行完毕info (status 0x7F) \n;return status 0x7F;}}}; }虽然上面已经基本完成了运行模块但是还是有缺陷的我们常常在力扣或牛客上刷题时明确标注了时间限制和内存限制或者栈的限制。所以我们对资源的限制也需要做一些处理我们这里只处理时间和内存上的限制。在运行模块中添加如下的函数是一个系统调用 #include sys/resource.h//系统调用接口// 设置进程占用资源大小的接口 static void SetProcLimit(int _cpu_limit, int _mem_limit) {// 设置cpu时长struct rlimit cpu_limit;//调用setrlimit所需的结构体cpu_limit.rlim_max RLIM_INFINITY;//硬约束——无穷INFINITYcpu_limit.rlim_cur _cpu_limit; //软约束——当前cpu能跑的时长setrlimit(RLIMIT_CPU, cpu_limit);//系统调用接口// 设置内存大小struct rlimit mem_limit;//调用setrlimit所需的结构体mem_limit.rlim_max RLIM_INFINITY;//硬约束——无穷INFINITYmem_limit.rlim_cur _mem_limit * 1024; //单位是字节 转化为KB //软约束——当前内存最大上限setrlimit(RLIMIT_AS, mem_limit);//系统调用接口 } 3. 编译并运行服务compile_run模块 编译模块和运行模块有了之后我们将其整合到一起编译并运行服务 在编译模块中我们是根据用户传过来的文件名先形成三个临时文件1234.cpp、1234.exe、1234.compiler_error然后对1234.cpp进行编译形成1234.exe。在运行模块中我们是对1234.exe进行运行形成三个临时文件1234.stdin、1234.stdout、1234.stderr在编译并运行的模块中才是真正的接收用户传过来的数据信息通过编译和运行模块的分别处理完成用户的请求编译运行工作这些数据信息是通过网络传输过来的我们知道通过网络接收用户传过来json串的其原因可以看上一篇项目博客中的介绍其中json串中应该包含如下in_json {code: “#include iostream ....int main(){...}”,input: 用户的输入像牛客哪些,cpu_limit: 1024,mem_limit: 30 } 我们提供一个start函数用于解析这个in_json串将数据解析出来 然后将提取出来的代码写入到特定的文件中但是存在多个用户提交代码我们就需要保证每个文件的唯一性。 如何保证每个文件的唯一性呢我们采用毫秒级时间戳原子递增的唯一值来实现 #pragma once#include iostream #include string #include vector #include atomic #include fstream #include sys/types.h #include sys/stat.h #include unistd.h #include sys/time.hnamespace ns_util {class TimeUtil {public:/*struct timeval{_time.tv_sec//秒_time.tv_usec//微秒};*/// 获取秒级别时间戳这是为日志模块提供的函数static std::string GetTimeStamp() {struct timeval _time;gettimeofday(_time, nullptr);return std::to_string(_time.tv_sec);}// 获取毫秒级别时间戳static std::string GetTimeMs() {struct timeval _time;gettimeofday(_time, nullptr);return std::to_string(_time.tv_sec * 1000 _time.tv_usec / 1000);}};const std::string temp_path ./temp/;class PathUtil {public:static std::string AddSuffix(const std::string file_name, const std::string suffix) {// 拼接路径名文件名后缀名std::string path_name temp_path;//路径名path_name file_name;//文件名path_name suffix;//后缀名return path_name;}/// 编译时需要的临时文件// 构建源文件路径 后缀的完整文件名// 1234 - ./temp/1234.cppstatic std::string Src(const std::string file_name) {return AddSuffix(file_name, .cpp);}// 构建可执行程序的完整路径 后缀名static std::string Exe(const std::string file_name) {return AddSuffix(file_name, .exe);}static std::string CompilerError(const std::string file_name) {return AddSuffix(file_name, .compile_error);}/// 运行时需要的临时文件static std::string Stdin(const std::string file_name) {return AddSuffix(file_name, .stdin);}static std::string Stdout(const std::string file_name) {return AddSuffix(file_name, .stdout);}// 构建该程序对应的标准错误完整路径 后缀名static std::string Stderr(const std::string file_name) {return AddSuffix(file_name, .stderr);}};class FileUtil {public://判断文件是否存在static bool IsFileExists(const std::string path_name) {// 获取文件属性的函数stat成功返回0struct stat st;if (stat(path_name.c_str(), st) 0) {// 获取属性成功文件已经存在return true;}return false;}//形成唯一的临时文件static std::string UniqFileName() {static std::atomic_uint id(0);id;// 毫秒级时间戳原子递增唯一值来保证唯一性std::string ms TimeUtil::GetTimeMs();std::string uniq_id std::to_string(id);return ms _ uniq_id;}//文件的写入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;}//文件的读取static bool ReadFile(const std::string target, std::string *content, bool keep false) {(*content).clear();std::ifstream in(target);if (!in.is_open()) {return false;}std::string line;// getline:不保存分隔符,但有些时候需要保留\n// getline:内部重载了强制类型转换while (std::getline(in, line)) {(*content) line;(*content) (keep ? \n : );}in.close();return true;}};}我们可以获取到唯一的文件后我们将获取到的in_json串进行解析 提供路径拼接函数形成唯一的源文件将in_json中的代码写入到文件中它保存在我们的temp目录下然后进行编译工作编译是通过创建子进程执行函数替换其中所需的源文件和可执行程序文件都可以通过路径拼接来完成最终线程可执行程序紧接着就是去调用runner模块进行程序的运行也是通过路径拼接的方式找到文件它的返回值是int大于0程序异常退出时收到了信号返回值就是对应的信号小于0内部错误子进程创建失败等于0正常运行完毕结果保存到对应的临时文件中。我们可以通过这个返回值来进行判断程序运行的结果并自行设置状态码将状态码对应到不同的信息我们可以通过实现一个CodeToDesc函数。         当然在temp目录下会不断的形成临时文件我们还需要做个处理就是清理工作。 #pragma once#include compiler.hpp #include runner.hpp #include ../comm/log.hpp #include ../comm/util.hpp #include signal.h #include jsoncpp/json/json.hnamespace ns_compile_and_run {using namespace ns_log;using namespace ns_util;using namespace ns_runner;using namespace ns_compiler;class CompileAndRun {public:// code 0进程收到了信号导致异常崩溃// code 0整个过程非运行报错代码为空编译报错等// code 0整个过程全部完成// 将错误代码转为描述CodeToDesc()static std::string CodeToDesc(int code, const std::string file_name) {std::string desc;switch (code) {case 0:desc 编译运行成功;break;case -1:desc 提交的代码为空;break;case -2:desc 未知错误;break;case -3:FileUtil::ReadFile(PathUtil::CompilerError(file_name), desc, true);break;case SIGABRT: //6号信号desc 内存超过了范围;break;case SIGXCPU: //24号信号desc CPU使用超时;break;case SIGFPE: //8号信号desc 浮点数溢出;break;default :desc 未知 std::to_string(code);break;}return desc;}//清理temp目录下的临时文件static void RemoveTempFile(const std::string file_name) {/*清理文件的个数是不确定的但是有哪些我们是知道的*///unlink函数是Linux下删除特定文件的一个函数参数是字符串形式//清理源文件std::string _src PathUtil::Src(file_name);if (FileUtil::IsFileExists(_src)) unlink(_src.c_str());//清理编译出错文件std::string _compiler_error PathUtil::CompilerError(file_name);if (FileUtil::IsFileExists(_compiler_error)) unlink(_compiler_error.c_str());//清理可执行程序文件std::string _execute PathUtil::Exe(file_name);if (FileUtil::IsFileExists(_execute)) unlink(_execute.c_str());//清理标准输入文件std::string _stdin PathUtil::Stdin(file_name);if (FileUtil::IsFileExists(_stdin)) unlink(_stdin.c_str());//清理标准输出文件std::string _stdout PathUtil::Stdout(file_name);if (FileUtil::IsFileExists(_stdout)) unlink(_stdout.c_str());//清理标准错误文件std::string _stderr PathUtil::Stderr(file_name);if (FileUtil::IsFileExists(_stderr)) unlink(_stderr.c_str());}/** 输入* code用户提交的代码* input用户给自己提交代码对应的输入不做处理* cpu_limit时间要求* mem_limit空间要求** 输出* 必填字段* status状态码* reason请求结果* 选填字段* stdout我的程序运行完的结果* stderr我的程序运行完的错误结果* *//** start函数功能:* 通过网络接收用户传过来的json串in_json其中in_json包含如下* in_json* {* code: “#include iostream ....int main(){...}”,* input: 用户的输入像牛客哪些,* cpu_limit: 1024,* mem_limit: 30* }* 我们start函数就需要去解析这个in_json串将数据取出来* 然后将提取出来的代码写入到特定的文件中因为存在多个用户提交代码我们就需要保证每个文件的唯一性** */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);//in_json——你要解析是谁in_value——解析的结果放到哪里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;//状态码int run_result 0;// 在goto之间定义的变量是不允许的我们提前定义std::string file_name;// 需要内部形成的唯一文件名为后续编译和运行提供好文件名Json::Value out_value;if (code.size() 0) {status_code -1;// 表示代码为空goto END;}// 形成的文件名只具有唯一性没有目录没有后缀// 毫秒级时间戳原子性递增的唯一值来保证唯一性file_name FileUtil::UniqFileName();if (!FileUtil::WriteFile(PathUtil::Src(file_name), code)) // 形成临时源文件{status_code -2; // 表示未知错误goto END;}if (!Compiler::Compile(file_name)) { //编译文件// 编译失败status_code -3; // 表示代码编译时发生了错误goto END;}run_result Runner::Run(file_name, cpu_limit, mem_limit);//运行可执行程序文件if (run_result 0) {status_code -2; // 表示未知错误goto END;} else if (run_result 0) {// 程序运行崩溃了(源于某种信号)status_code run_result;} else {status_code 0;// 表示运行成功}END:out_value[status] status_code;out_value[reason] CodeToDesc(status_code, file_name);if (status_code 0) {// 整个过程全部成功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);}}; } 4. 打包成网络服务compile_server模块 #include compile_run.hpp #include ../comm/httplib.h using namespace ns_compile_and_run; using namespace httplib; // 编译服务随时可能被多个人请求必须保证传递上来的code形成源文件名称的时候要具有 // 唯一性要不然多个用户之间会相互影响void Usage(std::string proc){std::cerr Usage: \n\t proc std::endl; }// ./compile_server port 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){// 用户请求的服务正文是我们想要的json stringstd::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;charsetutf-8);}});svr.listen(0.0.0.0, atoi(argv[1])); // 启动http服务/** 这里是测试代码in_json:{code: #include..., input: , cpu_limit:1, mem_limit:10240}out_json:{status:0, reason:, stdout:, stderr:}通过http 让client 给我们上传一个json string下面的工作充当客户端请求的json串std::string in_json;Json::Value in_value;in_value[code] R(#include iostreamint main(){std::cout 你可以看见我了 std::endl;return 0;});in_value[input] ;in_value[cpu_limit] 1;in_value[mem_limit] 10240 * 3;Json::FastWriter writer;in_json writer.write(in_value);//std::cout in_json std::endl;std::string out_json; // 这个是将来给客户返回的json串CompileAndRun::Start(in_json, out_json);std::cout out_json std::endl;*/return 0; }五、基于MVC结构的oj_server模块设计 1. 什么是MVC结构 经典MVC模式中M是指业务模型V是指用户界面视图C则是控制器使用MVC的目的是将M和V的实现代码分离从而使同一个程序可以使用不同的表现形式。其中View的定义比较清晰就是用户界面。  Mmodel表示的是模型代表业务规则。在MVC的三个部件中模型拥有最多的处理任务。被模型返回的数据时中立的模型与数据格式无关这样一个模型就能够为多个视图提供数据由于应用于模型的代码只需要写一次就可以被多个视图重用所以减少了代码的重复性Vview表示的视图代表用户看到并与之交互的界面。在视图中没有真正的处理发生它只是作为一种输出数据并允许用户操作的方式。Ccontroller表示的是控制器控制器接收用户的输入并调用模型M和视图V去完成用户需求。控制器本身不输出任何东西和任何处理。它只接收请求并决定调用那个模型构建去处理请求然后再确定用那个视图来显示返回的数据。2. oj_model模块 oj_model模块主要是和数据交互的这里的数据就是我们后端文件或者数据库当中的题目信息题目应该包含如下的信息 题目的编号1题目的标题求最大值题目的难度简单、中等、困难题目的时间要求1s题目的空间要求30000KB题目的描述给定一个数组求最大值题目预设给用户在线编辑的代码#includeiostream...题目的测试用例        到这里我们剧需要有自己对应的题库啦我们这个模块当中新增一个目录questions用来存放我们的题库这个questions目录下包含题目列表文件形式和每个题目的文件夹其中又包含题目的描述、题目预设给用户在线编辑的代码header和题目的测试用例tail   #pragma once#include ../comm/log.hpp #include ../comm/util.hpp #include iostream #include string #include vector #include cassert #include fstream #include cstdlib #include unordered_map // 根据题目list文件加载所有的题目信息到内存中 // model主要用来和数据进行交互对外提供访问数据接口namespace ns_model {using namespace std;using namespace ns_log;using namespace ns_util;//题目的相关信息节点struct Question {string number; // 题目的编号string title; // 题目的标题string star; // 难度简单/中等/困难int cpu_limit; // 题目的时间要求int mem_limit; // 题目的空间要求string desc; // 题目的描述string header; // 题目预设给用户在线编辑的代码string tail; // 题目的测试用例需要和header拼接形成完整代码};const string questions_list ./questions/questions.list; //题目列表的路径const string question_path ./questions/; //题库路径class Model {private:// kv --- k:题号 v:题目细节unordered_mapstring, Question questions;public:Model() {assert(LoadQuestionList(questions_list));}// 加载配置文件questions/questions.list 题目编号文件bool LoadQuestionList(const string question_list) {ifstream in(question_list);//打开配置文件if (!in.is_open()) {LOG(FATAL) 加载题库失败请检查是否存在题库文件 \n;return false;}string line;//按行读取题目列表的路径中的内容while (getline(in, line)) {//题目列表的路径的内容如// 1 判断回文数 简单 1 30000// 2 找出最大值 简单 1 30000vectorstring tokens;//保存切分的子串如vector{1判断回文数简单130000}StringUtil::SplitString(line, tokens, );//按空格进行切分if (tokens.size() ! 5) {LOG(WARNING) 加载部分题目失败请检查文件格式 \n;continue;}//填充QuestionQuestion q;q.number tokens[0]; //填写题目编号1q.title tokens[1]; //填写题目标题判断回文数q.star tokens[2]; //填写题目难度简单q.cpu_limit atoi(tokens[3].c_str()); //填写cpu限制1q.mem_limit atoi(tokens[4].c_str()); //填写内存限制30000//拼接题目路径//题库路径在 questions/ 如// questions/// 1/// 2/string _path question_path; //_path:题目路径_path q.number;_path /;FileUtil::ReadFile(_path desc.txt, (q.desc), true); //读取题目的描述路径true表示需要换行FileUtil::ReadFile(_path header.cpp, (q.header), true);//读取题目预设给用户在线编辑的代码的路径FileUtil::ReadFile(_path tail.cpp, (q.tail), true); //读取题目的测试用例路径questions.insert({q.number, q});//插入到unordered_map中}LOG(INFO) 加载题库......成功 \n;in.close();return true;}//获取所有题目bool GetAllQuestions(vectorQuestion *out) {//如果没有题目if (questions.size() 0) {LOG(ERROR) 用户获取题库失败 \n;return false;}//遍历questions题目和题目细节的映射放到vector中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) 用户获取题目失败题目编号 number \n;return false;}(*q) iter-second;return true;}~Model() {}}; }3. oj_view模块 oj_view模块是将model中的数据进行渲染构建出网页所以我们需要引入一个第三方库ctemplate功能如下 #pragma once#include iostream #include string #include ctemplate/template.h #include oj_model.hppnamespace ns_view {using namespace ns_model;const std::string template_path ./template_html/;class View {public:View() {}~View() {}public://所有题目的网页void AllExpandHtmml(const std::vectorQuestion questions, std::string *html) {// 题目的编号 题目的标题 题目的难度// 推荐使用表格显示//1.形成路径std::string src_html template_path all_questions.html;//被渲染的网页//2.形成数据字典ctemplate::TemplateDictionary root(all_questions.html);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.获取被渲染的网页ctemplate::Template *tpl ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP);//4.开始完成渲染功能tpl-Expand(html, root);}//一道题目的网页void OneExpandHtmml(const Question q, std::string *html) {//1.形成路径std::string src_html template_path one_question.html;//被渲染的网页//2.形成数据字典ctemplate::TemplateDictionary root(one_question.html);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.获取被渲染的网页ctemplate::Template *tpl ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP);//4.开始完成渲染功能tpl-Expand(html, root);}}; }4. oj_control模块 oj_control模块主要是控制通过获取用户的输入调用不同的模型构建view。但是我们还需要完成一个负载均衡的概念因为在后端进行编译服务的时候如果只提供一台主机当用户请求比较多或主机挂了则会影响用户体验。 #pragma once#include iostream #include string #include vector #include mutex #include cassert #include fstream #include algorithm #include jsoncpp/json/json.h#include ../comm/httplib.h #include ../comm/util.hpp #include ../comm/log.hpp #include oj_model.hpp #include oj_view.hppnamespace ns_control {using namespace std;using namespace ns_log;using namespace ns_util;using namespace ns_model;using namespace ns_view;using namespace httplib;//提供服务的主机class Machine {public:std::string ip; //编译服务的ipint port; //编译服务的portuint64_t load; //编译服务的负载数量std::mutex *mtx;//C中mutex是禁止拷贝的所有使用指针来完成public:Machine() : ip(), port(0), load(0), mtx(nullptr) {}~Machine() {}public://递增负载void IncLoad() {if (mtx) mtx-lock();load;if (mtx) mtx-unlock();}//递减负载void DecLoad() {if (mtx) mtx-lock();--load;if (mtx) mtx-unlock();}// 清除负载void ResetLoad(){if (mtx) mtx-lock();load 0;if (mtx) mtx-unlock();}//获取主机负载uint64_t Load() {uint64_t _load 0;if (mtx) mtx-lock();_load load;if (mtx) mtx-unlock();return _load;}};const std::string service_machine ./conf/service_machine.conf;//负载均衡模块class LoadBlance {private:// 可以提供给我们服务编译的所有主机每一台主机都有自己的下标充当当前主机的idstd::vectorMachine machines;// 所有在线主机idstd::vectorint online;// 所有离线主机idstd::vectorint offline;//保证LoadBlance它的数据安全std::mutex mtx;public:LoadBlance() {assert(LoadConf(service_machine));LOG(INFO) 加载 service_machine 成功 \n;}~LoadBlance() {}public:// 加载主机bool LoadConf(const std::string machine_conf) {std::ifstream in(machine_conf);if (!in.is_open()) {LOG(FATAL) 加载: machine_conf 失败 \n;return false;}std::string line;while (std::getline(in, line)) {std::vectorstd::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;}// 智能选择bool SmartChoice(int *id, Machine **m) {//1.使用选择好的主机更新该主机的负载//2.我们需要可能离线该主机mtx.lock();//负载均衡的算法//1.随机数算法2.轮询随机算法int online_num online.size();if (online_num 0) {mtx.unlock();LOG(WARNING) 所有的后端编译主机已经全部离线请运维的同事尽快查看 \n;return false;}//通过遍历的方式找到所有负载最小的机器*id online[0];*m machines[online[0]];uint64_t min_load machines[online[0]].Load();for (int i 1; i online_num; i) {uint64_t curr_load machines[online[i]].Load();if (min_load curr_load) {min_load curr_load;*id online[i];*m machines[online[i]];}}mtx.unlock();return true;}// 离线主机void OfflineMachine(int which) {mtx.lock();for (auto iter online.begin(); iter ! online.end(); iter) {if (*iter which) {machines[which].ResetLoad();//负载清0//要离线的主机找到了online.erase(iter);offline.push_back(which);break;//因为break的存在所以我们暂时不考虑迭代器失效的问题}}mtx.unlock();}// 在线主机void OnlineMachine() {//当所有主机都离线的时候我们统一上线mtx.lock();online.insert(online.end(), offline.begin(), offline.end());offline.erase(offline.begin(), offline.end());mtx.unlock();LOG(INFO) 所有的离线主机已上线...... \n;}//for testvoid ShowMachine() {mtx.lock();std::cout 当前在线主机列表;for (auto id: online) {std::cout id ;}std::cout std::endl;std::cout 当前离线主机列表;for (auto id: offline) {std::cout id ;}std::cout std::endl;mtx.unlock();}};//这是我么核心业务逻辑的控制器class Control {private:Model model_; //model_主要用来和数据进行交互对外提供访问数据接口View view_; //提供html渲染功能LoadBlance load_blance_; //核心负载均衡器public:Control() {}~Control() {}public:// 恢复离线主机上线void RecoveryMachine(){load_blance_.OnlineMachine();}// 根据全部题目数据构建网页bool AllQuestions(string *html) {bool ret true;vectorQuestion all;if (model_.GetAllQuestions(all)) {// 1. 先对题号进行排序sort(all.begin(), all.end(), [](const Question q1, const Question q2){return atoi(q1.number.c_str()) atoi(q2.number.c_str());//升序排序});// 2. 获取题目信息成功将所有的题目数据构建成网页view_.AllExpandHtmml(all, html);} else {*html 获取题目失败 形成题目列表失败;ret false;}return ret;}// 根据一道题目数据构建网页bool OneQuestion(const string number, string *html) {bool ret true;Question q;if (model_.GetOneQuestion(number, q)) {// 获取指定题目信息成功将指定题目数据构建成网页view_.OneExpandHtmml(q, html);} else {*html 指定题目 number 不存在;ret false;}return ret;}//in_json包含//code#include...//input“”void Judge(const std::string number, const std::string in_json, std::string *out_json) {//LOG(DEBUG) in_json \nnumnber: number \n;//1.根据题目编号拿到题目细节Question q;model_.GetOneQuestion(number, q);//2.对in_json进行反序列化得到题目id和用户提交的源代码Json::Reader reader;Json::Value in_value;reader.parse(in_json, in_value);std::string code in_value[code].asString();//3.重新拼接用户代码测试用例代码形成新的代码Json::Value compile_value;compile_value[input] in_value[input].asString();compile_value[code] code \n q.tail;//这里加\n是为了展示的代码和测试用例代码在编译的时候不发生错误能够去掉那个#ifdefinecompile_value[cpu_limit] q.cpu_limit;compile_value[mem_limit] q.mem_limit;Json::FastWriter writer;std::string compile_string writer.write(compile_value);//4.选择负载最低的主机//规则一直选择直到主机可用否则就是全部挂掉while (true) {int id 0;Machine *m nullptr;if (!load_blance_.SmartChoice(id, m)) {break;}//5.然后发起http请求得到结果Client cli(m-ip, m-port);m-IncLoad();LOG(INFO) 选择主机成功主机id id 详情 m-ip : m-port 当前主机的负载是 m-Load() \n;if (auto res cli.Post(/compile_and_run, compile_string, application/json;charsetutf-8)) {//6.将结果赋值给out_jsonif (res-status 200) {*out_json res-body;m-DecLoad();LOG(INFO) 请求编译和运行服务成功...... \n;break;}m-DecLoad();} else {//请求失败LOG(ERROR) 选择当前请求的主机的id id 详情 m-ip : m-port 可能已经离线 \n;load_blance_.OfflineMachine(id);load_blance_.ShowMachine();//仅仅是为了调式}}}}; } 5. 打包成网络服务oj_server #include iostream #include signal.h #include ../comm/httplib.h #include oj_control.hppusing namespace httplib; using namespace ns_control;static Control *ctrl_ptr nullptr; void Recovery(int signo){ctrl_ptr-RecoveryMachine(); } int main() {signal(SIGQUIT, Recovery);// 用户请求的服务路由功能Server svr;Control ctrl;ctrl_ptr ctrl;// 获取所有的题目列表svr.Get(/all_questions, [ctrl](const Request req, Response resp){// 返回一张包含所有题目的html网页std::string html;ctrl.AllQuestions(html);// 用户看到的是什么网页数据拼上了题目相关的数据resp.set_content(html, text/html; charsetutf-8);});// 用户要根据题目编号获取题目内容// /question/100 - 正则匹配// R() 原始字符串raw string保持字符串内容的原貌不用做相关的转义svr.Get(R(/question/(\d)), [ctrl](const Request req, Response resp){std::string number req.matches[1];//获取题目编号std::string html;ctrl.OneQuestion(number, html);resp.set_content(html, text/html; charsetutf-8);});// 用户提交代码使用我们的判题功能(1.每道题的测试用例 2.执行compile_and_run)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;charsetutf-8);});svr.set_base_dir(./wwwroot);svr.listen(0.0.0.0, 8080);return 0; }六、前端页面的设计 1. indx.html 当用户访问根目录时显示的网页 !DOCTYPE html html langenheadmeta charsetUTF-8meta http-equivX-UA-Compatible contentIEedgemeta nameviewport contentwidthdevice-width, initial-scale1.0title这是我的个人oj系统/titlestyle/*起手式100%保证我们的样式设置可以不受默认影响*/* {margin: 0px;/*消除网页的默认外边距*/padding: 0px;/*消除网页的默认内边距*/}html,body {width: 100%;height: 100%;}.container .navbar{width: 100%;height: 50px;background-color:black;/* 给父级标签overflow取消后续float带来的影响 */overflow: hidden;}.container .navbar a{/* 设置a标签是行内块元素允许你设置宽度*/display: inline-block;/* 设置a标签的宽度默认a标签是行内元素无法设置宽度*/width: 80px;/* 设置字体的颜色 */color: white;/* 设置字体的大小 */font-size: large;/* 设置文字的高度和导航栏一样的高度 */line-height: 50px;/* 去掉a标签的下划线 */text-decoration: none;/* 设置a标签的文字居中 */text-align: center;}/* 设置鼠标事件 */.container .navbar a:hover{background-color:green;}/* 设置浮动 */.container .navbar .login{float:right;}.container .content {/* 设置标签的宽度 */width: 800px;/* background-color: #ccc; *//* 整体居中 */margin: 0px auto;/* 设置文字居中 */text-align: center;/* 设置上外边距 */margin-top: 200px;}.container .content .front_ {/* 设置标签为块级元素独占一行可以设置高度宽度等属性 */display: block;/* 设置每个文字的上外边距 */margin-top: 20px;/* 去掉a标签的下划线 */text-decoration: none;}/style /head!-- body backgroundC:\Users\MLG\Desktop\壁纸.jpg --body background./壁纸.jpg div classcontainer!--导航栏--div classnavbara href/首页/aa href/all_questions题库/aa href#竞赛/aa href#讨论/aa href#求职/aa classlogin href#登录/a/div!--网页的内容--div classcontenth1 classfront_欢迎来到我的Online_Judge平台/h1a classfront_ href/all_questions点击我开始编程啦/a/div /div /body/html2. all_questions.html 当用户获取题目列表的时候显示的网页  !DOCTYPE html html langenheadmeta charsetUTF-8meta nameviewportcontentwidthdevice-width, user-scalableno, initial-scale1.0, maximum-scale1.0, minimum-scale1.0meta http-equivX-UA-Compatible contentieedgetitle在线OJ-题目列表/titlestyle/*起手式100%保证我们的样式设置可以不受默认影响*/* {margin: 0px;/*消除网页的默认外边距*/padding: 0px;/*消除网页的默认内边距*/}html,body {width: 100%;height: 100%;}.container .navbar {width: 100%;height: 50px;background-color: black;/* 给父级标签overflow取消后续float带来的影响 */overflow: hidden;}.container .navbar a {/* 设置a标签是行内块元素允许你设置宽度*/display: inline-block;/* 设置a标签的宽度默认a标签是行内元素无法设置宽度*/width: 80px;/* 设置字体的颜色 */color: white;/* 设置字体的大小 */font-size: large;/* 设置文字的高度和导航栏一样的高度 */line-height: 50px;/* 去掉a标签的下划线 */text-decoration: none;/* 设置a标签的文字居中 */text-align: center;}/* 设置鼠标事件 */.container .navbar a:hover {background-color: green;}.container .navbar .login{float: right;}.container .question_list {padding-top: 50px;width: 800px;height: 600px;margin: 0px auto;/* background-color: #ccc; */text-align: center;}.container .question_list table {width: 100%;font-size: large;font-family:Lucida Sans, Lucida Sans Regular, Lucida Grande, Lucida Sans Unicode, Geneva, Verdana, sans-serif;margin-top: 50px;background-color: #c6cbcc;}.container .question_list h1{color: green;}.container .question_list table .item{width: 100px;height: 40px;font-size: large;font-family:Times New Roman, Times, serif;}.container .question_list table .item a{text-decoration: none;color:black;}.container .question_list table .item a:hover{color: blue;text-decoration: underline;}/style /headbody div classcontainerdiv classnavbar!--导航栏--div classnavbara href/首页/aa href/all_questions题库/aa href#竞赛/aa href#讨论/aa href#求职/aa classlogin href#登录/a/div/divdiv classquestion_listh1Online_Judge题目列表/h1tabletrth classitem编号/thth classitem标题/thth classitem难度/th/tr{{#question_list}}trtd classitem{{number}}/tdtd classitema href/question/{{number}}{{title}}/a/tdtd classitem{{star}}/td/tr{{/question_list}}/table/div/div/body/html 3. one_question.html 当用户获取单道题目所显示的网页 !DOCTYPE html html langenheadmeta charsetUTF-8meta nameviewportcontentwidthdevice-width, user-scalableno, initial-scale1.0, maximum-scale1.0, minimum-scale1.0meta http-equivX-UA-Compatible contentieedgetitle{{number}}.{{title}}/title!-- 引入ACE CDN --script srchttps://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ace.js typetext/javascriptcharsetutf-8/script!-- 引入语法 --script srchttps://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ext-language_tools.js typetext/javascriptcharsetutf-8/scriptscript srchttp://code.jquery.com/jquery-2.1.1.min.js/scriptstyle* {margin: 0;padding: 0;}html,body {width: 100%;height: 100%;}.container .navbar {width: 100%;height: 50px;background-color: black;/* 给父级标签overflow取消后续float带来的影响 */overflow: hidden;}.container .navbar a {/* 设置a标签是行内块元素允许你设置宽度*/display: inline-block;/* 设置a标签的宽度默认a标签是行内元素无法设置宽度*/width: 80px;/* 设置字体的颜色 */color: white;/* 设置字体的大小 */font-size: large;/* 设置文字的高度和导航栏一样的高度 */line-height: 50px;/* 去掉a标签的下划线 */text-decoration: none;/* 设置a标签的文字居中 */text-align: center;}/* 设置鼠标事件 */.container .navbar a:hover {background-color: green;}.container .navbar .login {float: right;}.container .part1 {width: 100%;height: 600px;overflow: hidden;}.container .part1 .left_desc {width: 50%;height: 600px;float: left;overflow: scroll;/* 添加滚动条*/}.container .part1 .left_desc h3 {padding-top: 10px;padding-left: 10px;}.container .part1 .left_desc pre {padding-top: 10px;padding-left: 10px;font-size: medium;font-family: Franklin Gothic Medium, Arial Narrow, Arial, sans-serif;}.container .part1 .right_code {width: 50%;float: right;}.container .part1 .right_code .ace_editor {height: 600px;}.container .part2 {width: 100%;overflow: hidden;}.container .part2 .result {width: 300px;float: left;}.container .part2 .btn-submit {width: 100px;height: 30px;margin-top: 1px;margin-right: 1px;font-size: large;float: right;background-color: #26bb9c;color: #FFF;border-radius: 1ch;/* 给按钮带圆角*/border: 0px;}.container .part2 button:hover {color: green;}.container .part2 .result{margin-top: 15px;margin-left: 15px;}.container .part2 .result pre{font-size: larger;}/style /headbody div classcontainerdiv classnavbara href/首页/aa href/all_questions题库/aa href#竞赛/aa href#讨论/aa href#求职/aa classlogin href#登录/a/div!-- 左右呈现题目描述和预设代码 --div classpart1div classleft_desch3span idnumber{{number}}/span.{{title}}.{{star}}/h3pre{{desc}}/pre/divdiv classright_codepre idcode classace_editortextarea classace_text-input{{pre_code}}/textarea/pre/div/div!-- 提交结果并显示 --div classpart2div classresult/divbutton classbtn-submit onclicksubmit()提交代码/button/div /divscript//初始化对象editor ace.edit(code);//设置风格和语言更多风格和语言请到github上相应目录查看// 主题大全http://www.manongjc.com/detail/25-cfpdrwkkivkikmk.htmleditor.setTheme(ace/theme/monokai);editor.session.setMode(ace/mode/c_cpp);// 字体大小editor.setFontSize(16);// 设置默认制表符的大小:editor.getSession().setTabSize(4);// 设置只读true时只读用于展示代码editor.setReadOnly(false);// 启用提示菜单ace.require(ace/ext/language_tools);editor.setOptions({enableBasicAutocompletion: true,enableSnippets: true,enableLiveAutocompletion: true});function submit() {// 1. 收集当前页面的有关数据1.题号 2.代码我们采用JQuery// console.log(哈哈);var code editor.getSession().getValue();//console.log(code);var number $(.container .part1 .left_desc h3 #number).text();//console.log(number);var judge_url /judge/ number;console.log(judge_url);// 2. 构建json并向后台发起基于http的json请求$.ajax({method: Post, //向后端发起请求的方式post、geturl: judge_url, //向后端指定的url发起请求dataType: json, //告知server我们需要什么格式contentType: application/json;charsetutf-8, //告知server我给你的是什么格式data: JSON.stringify({code: code,input: }),success: function (data) {//成功得到结果//console.log(data);show_result(data);}});// 3. 得到结果解析并显示到result中function show_result(data) {// console.log(data.status);// console.log(data.reason);// 拿到result结果标签var result_div $(.container .part2 .result);// 清空上一次的运行结果result_div.empty();// 首先拿到结果的状态码和原因结果var _status data.status;var _reason data.reason;var reson_lable $(p,{text: _reason});reson_lable.appendTo(result_div);if (status 0) {// 请求是成功的编译运行没出问题但是结果是否通过看测试用例的结果var _stdout data.stdout;var _stderr data.stderr;var reson_lable $(p,{text: _reason});var stdout_lable $(pre,{text: _stdout});var stderr_lable $(pre,{text: _stderr});stdout_lable.appendTo(result_div);stderr_lable.appendTo(result_div);} else {}}} /script /body/html 七、项目总结  基于注册和登陆的录题功能 业务扩展自己写一个论坛接入到在线OJ中 即便是编译服务在其他机器上也其实是不太安全的可以将编译服务部署在docker 4. 目前后端compiler的服务我们使用的是http方式请求(仅仅是因为简单)但是也可以将我们的compiler服务设计成为远程过程调用推荐:rest_rpc,替换我们的httplib功能上更完善一下判断一道题目正确之后自动下一道题目 navbar中的功能可以一个一个的都实现一下
http://www.hkea.cn/news/14269842/

相关文章:

  • 中国采购与招标网官方网站WordPress恶意扫描
  • 13572074638网站建设程序员都需要学什么
  • seo优化网站二维码制作网站有哪些
  • 上海网站建设方案服务百度信息流开户多少钱
  • 做h5的网站页面学技术网站
  • 抓取网站访客数据原理张家港百度网站制作
  • 做网站怎么优化网站推广任务 ip点击
  • 网站错误列表刷seo排名
  • 外贸建站推广哪家好网站首页分类怎么做的
  • 开源网站有哪些网站建设对接流程图
  • 3d云打印网站开发深圳住房建设局官方网站
  • 网站网站制作400多少钱城乡建设部注册建筑师网站
  • 男孩子怎么做网站推广wordpress上传图片x
  • 成都个人网站做网站思想
  • 城市建设模拟游戏登陆网站视频制作哪里可以学
  • 贵阳白云区城乡建设局网站土特产网站模板 织梦
  • 做网站一定要备案吗如何建设网站和app
  • 建个人网站要多少钱个人网站建设方案书用备案的
  • 黄页网站推广app咋做广告thinkphp做网站教程
  • WordPress下如何用页面做侧边栏seo服务公司深圳
  • 昆明网络公司开发网站seo三要素
  • 网站运营培训机构网站建设综合实训
  • 基于jquery做的网站抖音产品推广方案
  • 门户网站微信服务号建设方案如何利用互联网挣钱
  • 微信引流神器手机电影网站怎么做合肥市建设工程信息网官网
  • 网站后台主流网站开发语言福州建设工程造价信息网
  • 产品营销类网站织梦修改网站标题
  • 免费咨询合肥网站优化哪家好
  • 重庆信息网官网系统优化app
  • 类似于拼多多的网站怎么做高清视频网络服务器免费