🐶博主主页:@ᰔᩚ. 一怀明月ꦿ
❤️🔥专栏系列:线性代数,C初学者入门训练,题解C,C的使用文章,「初学」C++,linux
🔥座右铭:“不要等到什么都没有了,才下定决心去做”
🚀🚀🚀大家觉不错的话,就恳求大家点点关注,点点小爱心,指点指点🚀🚀🚀
目录
一、项目介绍
二、项目技术与开发环境
1.项目技术
2.开发环境
三、项目的宏观结构
1.项⽬核⼼是三个模块
2.项目的框架
3.项目实现流程
四、comm公共模块设计
1.日志模块开发
2.UTIL工具模块开发
1)TimeUtil模块
2)PathUtil模块
3)FileUtil模块
4)StringUtil模块
3.httplib.h模块
五、compile_server模块
1.compile编译模块
2.runneryu运行模块
3.compile_run模块
4.compile_server.cc
5.temp缓冲区
6.Makefile
六、oj_server模块
1.oj_model数据交互模块
2.oj_view视图模块
3.oj_control控制模块
4.oj_server.cc
5.questions文件版题库
1)1
[1]desc
[2]header.cpp
[3]tail.cpp
2)questions.list
6.wwwroot主页
7.template_html
1)all_questions.html
2)one_question.html
8.conf
9.Makefile
七、MySQL版本的实现
1.oj_model_m
2.include
3.lib
4.linux下载mysql服务
5.在Ubuntu上设置MySQL可以远程登录用户
6.创建表结构
八、综合
1.安装 jsoncpp
2.安装 cpp-httplib
3.安装boost库
4.安装ctemplate
5.结果展示
一、项目介绍
随着计算机技术的快速发展和互联网的普及,在线评测系统(Online Judge,简称 OJ 系统)在教育和竞赛领域中得到了广泛应用。OJ 系统是一个用于批量评测编程作业和竞赛题目的平台,通过自动化评测和实时反馈,提供了高效、公正和具有即时性的编程评测服务。
该项目旨在构建一个功能强大的 OJ 系统,为学生、教师和竞赛参与者提供一个便捷的在线学习和评测环境。下面将介绍该项目的历史、项目实例、市场需求和未来发展。
1)项目历史: 过去几年中,计算机编程的重要性逐渐被广大学生和教育机构所认识到。传统的教学方法已经不再能够满足快速发展的计算机科学领域的需求。为了更好地培养学生的编程能力和解决实际问题的能力,开发一个功能全面的 OJ 系统成为了迫切的需求。
2)项目实例:
LeetCode(https://leetcode.com/):LeetCode 是一个非常受欢迎的面向算法和数据结构练习的在线评测平台。它提供了一系列编程题目,并为用户提供了在线编码环境和自动评测功能。
Codeforces(https://codeforces.com/):Codeforces 是一个面向竞赛编程的 OJ 系统,其主要目标是提供高质量的编程竞赛题目和评测服务。它支持多种编程语言,并为用户提供实时排名和评测结果等功能。
HackerRank(https://www.hackerrank.com/):HackerRank 是一个面向编程技能测试和面试准备的 OJ 系统。它提供了丰富的编程题目和技术挑战,并为用户提供实时评测、学习资源和招聘服务等功能。
UVa Online Judge(https://onlinejudge.org/):UVa Online Judge 是一个老牌的 OJ 系统,拥有大量的编程题目,并为用户提供了在线编程环境和自动评测功能。它旨在提供一个开放的学习和竞赛平台。
3)市场需求: 在当今的教育和竞赛环境中,对于计算机编程的需求越来越高。越来越多的教育机构和竞赛组织需要一个可靠的 OJ 系统来支持他们的教学和竞赛活动。正因如此,开发一个功能完善且易于使用的 OJ 系统成为了市场的迫切需求。该系统不仅能够满足学习者的自主学习需求,还能够提供教师管理和评估的功能,能够适应不同层次和不同类型的编程题目。
4)未来发展: 随着人工智能、大数据、云计算等技术的不断发展,未来 OJ 系统还可以进一步提升和完善。例如,可以引入自动评测算法的优化和智能化,增加更多的编程题库和实例,支持多种编程语言的评测,提供个性化学习推荐等功能。此外,该系统还可以扩展到更广泛的应用领域,如软件工程测评、面试准备等。与此同时,与其他教育平台和在线学习资源的集成将是未来 OJ 系统的重要发展方向。通过不断创新和拓展,该项目将为学生和教师提供更加优质的编程学习和评测体验,并为教育领域的发展做出积极的贡献。
二、项目技术与开发环境
1.项目技术
- C++ STL 标准库
- Boost 准标准库(字符串切割)
- cpp-httplib 第三⽅开源⽹络库
- ctemplate 第三⽅开源前端⽹⻚渲染库
- jsoncpp 第三⽅开源序列化、反序列化库
- 负载均衡设计
- 多进程、多线程
- MySQL
- Ace前端在线编辑器
- html/css/js/jquery/ajax
2.开发环境
- Ubuntu 22.04 64位 服务器
- vscode
- Navicat Premium
三、项目的宏观结构
1.项⽬核⼼是三个模块
- comm :公共模块
- compile_server:编译与运⾏模块
- oj_server:获取题⽬列表,查看题⽬编写题⽬界⾯,负载均衡,其他功能
2.项目的框架
图1
3.项目实现流程
- 第一步:compile_server
- 第二步:oj_server
- 第三步:⽂件版的在线OJ
- 第四步:⽂件版的在线OJ
- 第五步:MySQL 版的在线OJ
四、comm公共模块设计
文件处理、日志、网络服务
1.日志模块开发
- 日志等级
- 打印日志的文件名称
- 报错行
- 添加日志的时间
- 开放性输出
开放性输出就是说我们可以在后面输出自己想输出的东西,比如
LOG(INFO)<<"功能正常运行"<<endl;
#pragma once #include <iostream> #include <string> #include "util.hpp" using namespace std; namespace ns_log { using namespace ns_util; //日志等级 enum { INFO = 0, //正常运行 DEBUG, //调试信息 WARNING, //警告信息,但不影响程序运行 ERROR, //错误信息,程序运行错误 FATAL //严重错误,系统崩溃 }; //我们通过Log函数,其返回值是标准输出流对象,从而实现开放性日志 inline std::ostream &Log(const string &level, const string &file_name, int line) { // 添加日志等级 string message = "["; message += level; message += "]"; // 添加报错文件名称 message += "["; message += file_name; message += "]"; // 添加报错行号 message += "["; message += to_string(line); message += "]"; // 添加报错时间 message += "["; message += TimeUtil::GetTimeStamp();//TimeUtil::GetTimeStamp(),将日志转化为字符串,XXX(秒) message += "]"; // cout本质 内部是包含缓冲区 cout << message; // 不要endl进行刷新 return cout; } // LOG(INFO)<<"messge"<<"\n"; // 开放式接口 #define LOG(level) Log(#level, __FILE__, __LINE__)//LOG是函数log的宏替换函数,这样提供了,两个默认的参数,__FILE__(自动获取文件名称), __LINE__(自动获取文件行数) }
2.UTIL工具模块开发
1)TimeUtil模块
将时间戳转化为字符串形式
- 秒
- 毫秒
class TimeUtil { public: static string GetTimeStamp() { struct timeval _time; gettimeofday(&_time, nullptr); return to_string(_time.tv_sec);//秒 } static string GetTimeMs() { struct timeval _time; gettimeofday(&_time, nullptr); return to_string(_time.tv_sec * 1000 + _time.tv_usec / 1000);//毫秒 } };
- to_string:是C++中用于将数字或其他可转换为字符串的类型转换为字符串
gettimeofday
是一个用于获取当前时间和日期的系统调用函数。它以微秒精度返回当前的UTC时间和日期。2)PathUtil模块
形成一些文件,源文件,可执行文件,编译错误文件。
const string temp_path = "./temp/"; class PathUtil { public: //添加后缀 static string AddSuffix(const string &file_name, const string &Suffix)//需要提供文件名和文件名后缀 { string path_name = temp_path; //./temp/ path_name += file_name; //./temp/code path_name += Suffix; //./temp/code.stderr return path_name; } // 编译时需要有的临时文件 // 构建一个源文件+路径+后缀的完整文件名 // 1234 -> ./temp/1234.cpp static string Src(const string &file_name) { return AddSuffix(file_name, ".cpp"); } // 构建一个可执行程序的源文件+路径+后缀的完整文件名 static string Exe(const string &file_name) { return AddSuffix(file_name, ".exe"); } // 构建一个可执行程序的标准错误完整的路径+后缀名 static string Err(const string &file_name) { return AddSuffix(file_name, ".stderr"); } // 构建一个可执行程序的标准错误完整的路径+后缀名-编译该源文件时运行的错误 static string CompilerError(const string &file_name) { return AddSuffix(file_name, ".compile_error"); } // 运行时所需要的临时文件 //输入缓冲区文件 static string Stdin(const string &file_name) { return AddSuffix(file_name, ".stdin"); } //输出缓冲区文件 static string Stdout(const string &file_name) { return AddSuffix(file_name, ".stdout"); } //错误缓冲区文件 static string Stderr(const string &file_name) { return AddSuffix(file_name, ".stderr"); } };
3)FileUtil模块
判断文件是否存在、生成唯一的文件名、读写文件
class FileUtil { public: static bool IsFileExists(const string &path_name) { struct stat st; if (stat(path_name.c_str(), &st) == 0) { // 获取属性成功,文件已经存在 return true; } return false; } // 毫秒级时间戳+源子性递增唯一值:来保证唯一性 static string UniqFileName() { static atomic_uint id(0); string ms = TimeUtil::GetTimeMs(); string uniq_id = to_string(id++); return ms + "_" + uniq_id; } static bool WriteFile(const string &target, const string &code) { ofstream out(target, ios::out); if (!out.is_open()) { return false; } out.write(code.c_str(), code.size()); out.close(); return true; } static bool ReadFile(const string &target, string *content, bool keep = false) { (*content).clear(); ifstream in(target, ios::in); if (!in.is_open()) { return false; } string line; // getline:不保存行分隔符 // getline内部重载了强制类型转化 while (getline(in, line)) { (*content) += line; (*content) += (keep ? "\n" : ""); } in.close(); return true; } };
std::atomic_uint
是C++标准库中的一个原子类型,用于支持并发编程中的原子操作。它提供了原子无锁访问的能力,用于保证多个线程之间对数据的操作不会发生竞争条件4)StringUtil模块
class StringUtil { public: /*********************************************************************** * str:输入型,目标要切分的字符串 * target:输出型,保存切分完毕的结果 * sep:输入型,切分符 ***********************************************************************/ static void SplitString(const string &str, vector<string> *target, string sep) { // boost库 boost::split(*target, str, boost::is_any_of(sep), boost::token_compress_on); //用于将字符串str按照指定的分隔符sep进行分割,并将分割后的子字符串存储在target指针所指向的容器中 } };
boost::split
是 Boost 库中的一个函数,用于将一个字符串拆分成多个子串,并将结果存储在目标容器中split函数的第一个参数是一个迭代器,指向目标容器的起始位置。第二个参数是要分割的字符串str。第三个参数是分隔符sep,可以是一个字符串,也可以是一个字符数组。第四个参数是一个标志,用于指定是否压缩分割后的子字符串,默认为boost::token_compress_on,表示压缩分割后的子字符串
3.httplib.h模块
cpp-httplib第三方网络库,我们自己写网络套接字来进行通信也是可以的,不过太麻烦了,我们直接使用开源第三方库cpp-httplib
进行cpp-httplib的安装后,cpp-httplib是header独立分开的,只需要将它里面的httplib.h文件拷贝到项目中。如果你想的话,也可以拷贝到系统目录下/usr/include/,但是不推荐
需要注意的是
cpp-httplib的使用需要使用高版本的gcc/g++
cpp-httplib是阻塞式的多线程http网络库,因为里面使用了原生线程库,所以在编译的时候,需要带上选项-lpthread
五、compile_server模块
1.compile编译模块
负责将用户提交的源代码转换为目标代码的过程。其主要功能包括词法分析、语法分析(构建语法树)、语义分析(检查语义正确性)、代码优化(提高程序性能)、目标代码生成(生成目标机器代码或中间代码)、符号表管理(记录变量、函数等信息)以及错误处理(收集和报告错误信息
#pragma once #include <iostream> #include <string> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <cstdio> #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 我们需要处理成./temp/1234.cpp (temp专门用来存储生成的临时文件) // 我们还需要形成 ./temp/1234.exe文件 // 我们还需要形成 ./temp/1234.stderr文件 static bool Compile(string &file_name) { pid_t pid = fork(); if (pid < 0) { LOG(ERROR) << "内部错误,创建子进程失败" << endl; return false; } else if (pid == 0) { umask(0); int _stderr = open(PathUtil::CompilerError(file_name).c_str(), O_WRONLY | O_CREAT, 0644); //./temp/code.stderr if (_stderr < 0) { LOG(WARNING) << "没有成功形成stderr文件" << endl; exit(1); } // 重定向:标准错误到_stderr中 dup2(_stderr, 2); // 子进程:调用编译器,完成对代码的编译工作 // g++ -o target src -std=c++11 execlp("g++", "g++", "-o", PathUtil::Exe(file_name).c_str(), PathUtil::Src(file_name).c_str(), "-D", "COMPILER_ONLINE", "-std=c++11", nullptr); LOG(ERROR) << "启动编译器g++失败,可能是参数错误" << endl; exit(2); } else { waitpid(pid, nullptr, 0); // 编译是否成功,就看有没有形成对应的可执行程序 if (FileUtil::IsFileExists(PathUtil::Exe(file_name))) { LOG(INFO) << PathUtil::Src(file_name) << "编译成功" << endl; return true; } } //如果程序(g++)替换失败,才会输出下面的日志信息 LOG(ERROR) << "编译失败,没有形成可执行程序" << endl; return false; // } private: }; }
2.runneryu运行模块
编译成功,我们需要运行可执行文件,其中会成功,也会失败,成功我们需要向用户返回运行结果,运行错误,我们需要向用户提供运行错误的信息
#pragma once #include <iostream> #include <string> #include <sys/types.h> #include <unistd.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/wait.h> #include <sys/resource.h> #include <sys/time.h> #include "../comm/util.hpp" #include "../comm/log.hpp" using namespace std; namespace ns_runner { using namespace ns_util; using namespace ns_log; class Runner { public: Runner() {} ~Runner() {} // 设置进程占用资源大小的接口 static void SetProcLimit(int _cpu_limit, int _mem_limit) { struct rlimit cpu_limit, mem_limit; cpu_limit.rlim_cur = _cpu_limit; cpu_limit.rlim_max = RLIM_INFINITY; mem_limit.rlim_cur = _mem_limit * 1024; // 设置最小内存资源占用的空间为以kb为单位 mem_limit.rlim_max = RLIM_INFINITY; setrlimit(RLIMIT_CPU, &cpu_limit); setrlimit(RLIMIT_AS, &mem_limit); } public: // 指明路径即可,不需要代码路径,不需要带后缀 /*********************************************************************** * 返回值>0:程序异常了,退出时收到信号,返回值就是信号编号 * 返回值<0:程序异常了,fork失败(内部失败) * 返回值=0:程序正常运行完毕,结果保存在对应的临时文件中 * * cpu_limit:cpu时间限制,单位是秒 * mem_limit:内存限制,单位是kb ************************************************************************/ static int Run(const string &file_name, int cpu_limit, int mem_limit) { // 程序运行: // 1.代码跑完,结果正确 // 2.代码跑完,结果不正确 // 3.代码没跑完,异常了 // Run需要考虑代码跑完,结果正确与否??不考虑 // 结果正确与否,是由我们测试用例决定的! // 我们只考虑:是否正确运行完毕 // 我们必须知道可执行程序是谁? // 一个程序在默认启动的时候 // 标准输入:暂时不处理 // 标准输出:程序运行完成,输出结果是什么 // 标准错误:运行时错误的信息 string _execute = PathUtil::Exe(file_name); string _stdin = PathUtil::Stdin(file_name); string _stdout = PathUtil::Stdout(file_name); string _stderr = PathUtil::Stderr(file_name); 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) << "运行时打开标准文件失败" << endl; return -1; // 代表打开文件失败 } pid_t pid = fork(); if (pid < 0) { LOG(ERROR) << "创建子进程失败" << endl; close(_stdin_fd); close(_stdout_fd); close(_stderr_fd); return -2; // 代表fork失败 } else if (pid == 0) { // 子进程 dup2(_stdin_fd, 0); dup2(_stdout_fd, 1); dup2(_stderr_fd, 2); // 限制资源—— SetProcLimit(cpu_limit, mem_limit); // 执行程序 execl(_execute.c_str() /*我要执行谁*/, _execute.c_str() /*我想要在命令行上如何执行*/, NULL); exit(1); } else { close(_stdin_fd); close(_stdout_fd); close(_stderr_fd); int status; waitpid(pid, &status, 0); // 程序运行异常,一定是因为收到了信号 LOG(INFO) << "运行完毕,info:" << (status & 0x7f) << endl; return status & 0x7f; } } }; }
3.compile_run模块
compile_run模块主要完成compile模块和runner模块的调度,首先编译程序,然后运行程序,最后返回运行后的结果。编译错误和运行错误都会将错误原因返回用户
#pragma once // 适配用户请求 #include "compiler.hpp" #include "runner.hpp" #include "../comm/util.hpp" #include "../comm/log.hpp" #include <signal.h> #include <unistd.h> #include <jsoncpp/json/json.h> using namespace std; namespace ns_compile_and_run { using namespace ns_compiler; using namespace ns_runner; using namespace ns_util; using namespace ns_log; class CompileAndRun { public: // code>0:进程收到了信号导致导致崩溃 // code<0:整个过程非运行出错(代码为空,编译报错) // code=0:整个过程全部完成 static string CodeToDesc(int code, const string &file_name) { 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 = "time out"; break; case SIGFPE: // 8 desc = "浮点数溢出"; break; default: desc = "未知" + to_string(code); break; } return desc; } // 删除临时文件 static void RemoveTempFile(const string &file_name) { // 清理文件的个数是不确定的 // 删除源文件 string _src = PathUtil::Src(file_name); if (FileUtil::IsFileExists(_src)) unlink(_src.c_str()); // 删除编译报错文件 string _compiler_error = PathUtil::CompilerError(file_name); if (FileUtil::IsFileExists(_compiler_error)) unlink(_compiler_error.c_str()); // 删除可执行文件 string _execute = PathUtil::Exe(file_name); if (FileUtil::IsFileExists(_execute)) unlink(_execute.c_str()); // 删除标准输入文件 string _stdin = PathUtil::Stdin(file_name); if (FileUtil::IsFileExists(_stdin)) unlink(_stdin.c_str()); // 删除标准输出文件 string _stdout = PathUtil::Stdout(file_name); if (FileUtil::IsFileExists(_stdout)) unlink(_stdout.c_str()); // 删除标准错误文件 string _stderr = PathUtil::Stderr(file_name); if (FileUtil::IsFileExists(_stderr)) unlink(_stderr.c_str()); } /**************************************************************** * 输入: * code:用户提交的代码 * input:用户给自己提交的代码对应的输入 * cpu_limit:时间要求 * mem_limit:空间要求 * 选填: * time_limit: * **************************************************************** * 输出: * status:状态码 * reason:请求的原因 * 选填: * stdout:我的程序运行完的结果 * stderr:我的程序运行完的错误结果 * in_json:{"code","#include...","cpu_limit:1","mem_limit:1024"} * out_json:{"status":0,"reason":"ok","stdout":"","stderr":"error"} *****************************************************************/ static void Start(const string &in_json, string *out_json) { Json::Value in_value; Json::Reader reader; // 反序列化 reader.parse(in_json, in_value); // 最后处理差错问题 string code = in_value["code"].asString(); 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; string file_name; // 需要内部形成的临时文件名 if (code.size() == 0) { status_code = -1; // 代码为空 goto END; } // 形成一个唯一的文件名 // 形成的文件名只具有唯一性,没有目录和后缀 // 毫秒级时间戳+源子性递增唯一值:来保证唯一性 file_name = FileUtil::UniqFileName(); // 形成临时src源文件 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); cout << out_value["reason"] << endl; if (status_code == 0) { // 整个过程全部成功 string _stdout; FileUtil::ReadFile(PathUtil::Stdout(file_name), &_stdout, true); out_value["stdout"] = _stdout; 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); return; } }; }
在Linux系统中,JsonCpp 是一个流行的 C++ 库,用于处理 JSON(JavaScript Object Notation)数据格式。JsonCpp 提供了一组易于使用的 API,使得在 C++ 程序中解析、构建和操作 JSON 数据变得简单。
4.compile_server.cc
形成编译运行网络服务
#include "compile_run.hpp" #include"../comm/httplib.h" using namespace std; using namespace ns_compile_and_run; using namespace httplib; void Usage(string proc) { cerr<<"Usage: "<<"\n\t"<<proc<<" <port>"<<endl; } // 编译服务随时可能被多个人请求,必须保证传递上来的code,形成源文件名称的时候,要具有唯一性 int main(int argc,char* argv[]) { if(argc!=2) { Usage(argv[0]); return -1; } //提供的编译服务,打包形成一个网络服务 //cpp-httplib Server svr; svr.Get("/hello",[](const Request &req,Response &resp){ resp.set_content("hello world,hello 你好", "text/plain;charset=utf-8"); }); svr.Post("/compile_and_run",[](const Request &req,Response &resp){ //用户请求的服务正文是我们想要的json string string in_json=req.body; string out_json; if(!in_json.empty()) { CompileAndRun::Start(in_json,&out_json); resp.set_content(out_json,"application/json;charset=utf-8"); } }); //svr.set_base_dir("./wwwroot");//指明了根目录 svr.listen("0.0.0.0",atoi(argv[1]));//启动http服务 return 0; }
5.temp缓冲区
主要用存放编译运行产生的临时文件
- 编译错误文件
- 编译源文件
- 可执行文件
- 运行错误文件
- 交互输入文件
- 输出运行结果文件
这些都是临时文件,在向用户响应之后,这些文件将会自动被清理,可以手动设置保留这些临时文件,在compile_run模块中取消RemoveTempFile方法的调用
6.Makefile
compile_server:compile_server.cc g++ -o $@ $^ -std=c++11 -ljsoncpp -lpthread .PHONY:clean clean: rm -f compile_server
因为使用了jsconcpp库需要在编译的时候进行链接,还有使用了多线程需要链接pthread库
六、oj_server模块
oj_server主要是用于面向客户交互的部分,编译运行是compile_sever负责
oj_server的功能
- 获取首页
- 获取题目列表
- 获取指定题目,并提供编辑功能
- 提交判题功能(编译功能)
MVC模式
- 这种模式可以减少各个模块的耦合性,保证整个系统运行的稳定性
- M: model,通常是用和数据交互的模块,比如,对题库进行增删改查
- V: view,通常是拿到数据之后,要构建网页,渲染网页内容,展示给用户(浏览器)
- C: control,控制器,就是我们业务核心
1.oj_model数据交互模块
给用户提供题目题库,让用户可以自主决定在线解答指定的题
#pragma once // 文件版 #include "../comm/log.hpp" #include <iostream> #include <string> #include <unordered_map> #include <cassert> #include <vector> #include <fstream> #include "../comm/util.hpp" #include <cstdlib> // 根据题目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 questlines_list = "./questions/questions.list"; const string question_path = "./questions/"; class Model { private: // 题号: 题目细节 unordered_map<string, Question> questions; public: Model() { assert(LoadquestionList(questlines_list)); } bool LoadquestionList(const string &question_list) { // 加载配置文件:questions/questions.list+题目编号文件 ifstream in(question_list); if (!in.is_open()) { LOG(FATAL) << "加载题库失败,请检查是否存在题库文件" << endl; return false; // 打开文件失败 } string line; while (getline(in, line)) { vector<string> tokens; // 保留好切分的字符串 StringUtil::SplitString(line, &tokens, " "); // 1 判断回文数 简单 1 30000 if (tokens.size() != 5) { LOG(WARNING) << "加载部分题库失败,请检查题库文件格式" << endl; continue; } struct 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(make_pair(q.number, q)); } LOG(INFO) << "加载题库...成功" << endl; in.close(); return true; } bool GetAllQuestions(vector<struct Question> *out) { if (questions.size() == 0) { 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) << "用户获取题目失败" << endl; return false; // 题目不存在 } (*q) = iter->second; return true; } ~Model() { } }; }
Boost 是一个广泛使用的 C++ 库集合,为 C++ 开发人员提供了丰富的工具和功能增强。Boost 库通过提供高质量、可移植、可重用的开源组件,扩展了 C++ 标准库,涵盖了各个领域,包括算法、容器、字符串处理、多线程、网络编程、数学和并发编程等。
以下是 Boost 库的一些核心模块和功能:
- 智能指针(Smart Pointers):包括
shared_ptr
、unique_ptr
和weak_ptr
,提供了更安全和方便的动态内存管理。- 容器(Containers):包括
vector
、list
、map
、set
等常用容器的增强版本,提供了更多功能和效率。- 字符串处理(String Handling):包括字符串分割、替代、查找、格式化等操作,提供了正则表达式库和较强大的字符串处理工具。
- 算法(Algorithms):提供了很多算法模板,例如排序、查找、合并、计数等,扩展了 C++ 标准库中的算法。
- 文件系统(Filesystem):提供了用于文件和目录操作的跨平台接口,比标准库的
std::filesystem
更早支持。- 多线程(Multithreading):提供了线程和互斥量、条件变量等同步和并发编程工具。
- 正则表达式(Regex):提供了正则表达式库,支持 Perl 风格的正则表达式,方便进行文本处理。
- 数学(Math):提供了数值计算、随机数生成、矩阵操作等数学相关的库。
- 网络编程(Networking):提供了 TCP/IP 网络编程和网络通信的库,如套接字、HTTP 请求等。
- 序列化(Serialization):提供了对象序列化和反序列化的支持,使对象可以在不同平台和语言间进行通信和持久化。
我们在这里使用boost库字符串处理功能中的字符串分割
2.oj_view视图模块
主要是完成网页的渲染功能,负责对题库和单个题目的渲染
#pragma onece #include <iostream> #include <string> #include <ctemplate/template.h> //#include "oj_model.hpp" #include "oj_model_m.hpp" namespace ns_view { using namespace std; using namespace ns_model; // string number;//题号 // string title;//题目标题 // string star;//难度 // int cpu_limit;//时间限制 // int mem_limit;//内存限制 // string desc;//题目描述 // string header;//题目预设给用户在线编辑器的代码 // string tail;//题目的测试用例,需要和header拼接,形成完整的代码 const string template_path = "./template_html/"; class View { public: View() {} ~View() {} public: void AllExpandHtml(const vector<struct Question> &questions, string *html) { // 题目编号 题目的难度 题目的难度 // 推荐使用表格显示 // 1.形成路径 string src_html = template_path + "all_questions.html"; // 2.形成字典 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.获取被渲染的html ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP); // 4.开始完成渲染功能 tpl->Expand(html, &root); } void OneExpandHtml(struct Question &q, string *html) { // 1.形成路径 string src_html = template_path + "one_question.html"; // 2.形成字典 ctemplate::TemplateDictionary root("one_question"); root.SetValue("number", q.number); root.SetValue("title", q.title); root.SetValue("star", q.star); // root.SetValue("cpu_limit",to_string(q.cpu_limit)); // root.SetValue("mem_limit",to_string(q.mem_limit)); root.SetValue("desc", q.desc); root.SetValue("pre_code", q.header); // 3.获取被渲染的html ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP); // 4.开始完成渲染功能 tpl->Expand(html, &root); } }; }
ctemplate 是一个基于 C++ 的模板库,用于生成动态内容或静态文本。它提供了一种简单且强大的方式来将数据与模板进行结合,生成输出结果。ctemplate 的设计目标是快速且易于使用,尤其适用于 Web 开发中的动态页面生成。
3.oj_control控制模块
完成对oj_model和oj_view模块调度控制
#pragma once #include <iostream> #include <string> #include <vector> #include <mutex> #include <cassert> #include <fstream> #include <algorithm> #include <jsoncpp/json/json.h> #include "../comm/log.hpp" #include "../comm/util.hpp" //#include "oj_model.hpp" #include "oj_model_m.hpp" #include "oj_view.hpp" #include "../comm/httplib.h" namespace ns_control { using namespace ns_model; using namespace ns_view; using namespace ns_log; using namespace ns_util; using namespace std; using namespace httplib; const string service_machine = "./conf/service_machine.conf"; // 负载均衡模块 class Machine { public: string ip; // 编译服务的ip int port; // 编译服务的port uint64_t load; // 编译服务的负载 mutex *mtx; // 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 GetLoad() { if (mtx) mtx->lock(); uint64_t _load = load; if (mtx) mtx->unlock(); return _load; } }; class LoadBlance { private: vector<Machine> _machines; // 可以给我们提供编译服务的所有的主机 // 每一台主机都有自己的下标,充当当前主机的id // 所有在线的主机id vector<int> online; // 所有离线的主机id vector<int> offline; // 保证LoadBlance的线程安全 mutex mtx; public: LoadBlance() { assert(LoadConf(service_machine)); LOG(INFO) << "加载" << service_machine << "成功" << endl; } ~LoadBlance() {} public: bool LoadConf(const string &machine_conf) { ifstream in(machine_conf, ios::in); if (!in.is_open()) { LOG(FATAL) << "加载:" << machine_conf << "失败" << endl; return false; } string line; while (getline(in, line)) { vector<string> tokens; StringUtil::SplitString(line, &tokens, ":"); if (tokens.size() != 2) { LOG(WARNING) << "切分" << line << "失败" << endl; continue; } Machine m; m.ip = tokens[0]; m.port = atoi(tokens[1].c_str()); m.load = 0; m.mtx = new mutex(); online.push_back(_machines.size()); // 默认初始化主机的时候都在online _machines.push_back(m); } in.close(); return true; } // id:输出型参数 // m:输出型参数 bool SmartChoice(int *id, Machine **m) { // 1。使用选择好的主机(更新该主机的负载) // 2.我们需要可能离线该主机 mtx.lock(); // 负载均衡的方法 // 1.随机数+hash // 2.轮询+hash int online_num = online.size(); if (online_num == 0) { mtx.unlock(); LOG(FATAL) << "所有的后端编译主机已经离线,请运维的同事查看" << endl; return false; } // 通过遍历的方式,找到所有负载最小的机器 *id = online[0]; *m = &_machines[online[0]]; // 拿到负载最小主机的地址 uint64_t min_load = _machines[online[0]].GetLoad(); for (int i = 0; i < online_num; ++i) { uint64_t curload = _machines[online[i]].GetLoad(); if (min_load > curload) { min_load = curload; *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(); // 要离线的主机已经找到啦 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) << "所有的主机上线啦" << endl; } // for test void ShowMachinees() { mtx.lock(); cout << "当前主机列表: "; for (auto &id : online) { cout << id << " "; } cout << endl; cout << "当前离线主机列表:"; for (auto &id : offline) { cout << id << " "; } cout << endl; mtx.unlock(); } }; // 这是我们的核心业务逻辑的控制器 class Control { private: Model _model; View _view; LoadBlance _load_balance; // 核心负载均衡 public: Control() {} ~Control() {} public: void Recovery() { _load_balance.OnlineMachine(); } // 根据题目数据构建网页 // html:输出型参数 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 atoi(q1.number.c_str()) < atoi(q2.number.c_str()); }); // 获取题目信息成功,将所有的题目数据构建成网页 _view.AllExpandHtml(all, html); } else { ret = false; *html = "获取题目失败,形成题目列表失败"; } return ret; } bool Question(const string &number, string *html) { bool ret = true; struct Question q; if (_model.GetOneQuestion(number, &q)) { // 获取指定题目信息成功,将所有的题目数据构建成网页 _view.OneExpandHtml(q, html); } else { ret = false; *html = "指定题目" + number + "不存在"; } return ret; } // code:"#include" // void Judge(const string &number, const string in_json, string *out_json) { // LOG(DEBUG)<<in_json<<"\nnumber:"<<number<<endl; // 0.根据题目编号,直接拿到对应的题目细节 struct Question q; _model.GetOneQuestion(number, &q); // 1.in_json进行反序列化,得到题目id,得到用户提交的源代码,input Json::Reader reader; Json::Value in_value; reader.parse(in_json, in_value); string code = in_value["code"].asString(); // 2.重新拼接用户代码+测试代码用例,形成新代码 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; string compile_string = writer.write(compile_value); // 3.选择负载最低的主机 // 规则: // 一直选择,直到主机可用,否则,就是全部挂掉 while (true) { int id = 0; Machine *m = nullptr; if (!_load_balance.SmartChoice(&id, &m)) { break; } // 4.然后发起http请求,得到结果 m->IncLoad(); Client client(m->ip, m->port); LOG(INFO) << "选择主机成功,主机id:" << id << " 详情" << m->ip << ":" << m->port << "当前主机的负载为:" << m->GetLoad() << endl; if (auto resp = client.Post("/compile_and_run", compile_string, "application/json;charset=utf-8")) { // 5.将结果赋值给out_json if (resp->status == 200) { *out_json = resp->body; m->DecLoad(); LOG(INFO) << "请求编译运行服务成功...." << endl; break; } m->DecLoad(); // 请求失败(状态码不等于200),负载 } else { // 请求失败 LOG(ERROR) << "当前请求的主机id:" << id << " 详情:" << m->ip << ":" << m->port << "可能已经离线" << endl; // m->IncLoad();//可以没有必要,因为离线后,我们会将负载清零 _load_balance.OfflineMachine(id); _load_balance.ShowMachinees(); // 仅仅是用来调试的 } } } }; }
4.oj_server.cc
形成在线OJ的网络服务
#include <iostream> #include <signal.h> #include "../comm/httplib.h" #include "oj_control.hpp" using namespace httplib; using namespace ns_control; using namespace std; static Control *ctrl_ptr = nullptr; void Recovery(int signno) { ctrl_ptr->Recovery(); } int main() { //这个信号用于将所有的主机上线 signal(SIGQUIT, Recovery); // 用户请求的服务路由功能 Server svr; Control ctrl; ctrl_ptr = &ctrl; // 获取所有的题目列表 svr.Get("/all_questions", [&ctrl](const Request &req, Response &resp) { //resp.set_content("这是所有的题目列表", "text/plain;charset=utf-8"); //返回一张含有所有题目的html网页 string html; ctrl.AllQuestions(&html); resp.set_content(html, "text/html;charset=utf-8"); }); // 用户要根据题目编号,获取题目类容 /// questions/100->正则匹配(/d+是正则表达式) // R"()",原始字符串,保持字符串内容的原貌,不用做相关的转义 svr.Get(R"(/question/(\d+))", [&ctrl](const Request &req, Response &resp) { //resp.set_content("这是指定的一道题"+number, "text/plain;charset=utf-8"); string number=req.matches[1];//??? string html; ctrl.Question(number,&html); resp.set_content(html, "text/html;charset=utf-8"); }); // 用户提交代码,使我们的判题功能(1.每道题的测试用例 2.compile_and_run) svr.Post(R"(/judge/(\d+))", [&ctrl](const Request &req, Response &resp) { string number = req.matches[1]; //??? string result_json; ctrl.Judge(number, req.body, &result_json); resp.set_content(result_json, "application/json;charset=utf-8"); // resp.set_content("指定题目判题"+number, "text/plain;charset=utf-8"); }); // 设置首页的内容 svr.set_base_dir("./wwwroot"); // 设置静态资源的根目录 svr.listen("0.0.0.0", 8080); return 0; }
R"()"作用就是保持字符串的原貌,在 C++11 中引入了一种新的字符串字面量语法,称为原始字符串字面量(raw string literal)。原始字符串字面量使用 R"() 和 )" 作为界定符来定义字符串,其中 ) 和 ( 之间的任何字符都作为字符串内容。
5.questions文件版题库
1)1
存储1号题详细描述,头部代码,测试代码
[1]desc
输入:x = -121 输出:false 解释:从左向右读, 为 -121 。 从右向左读, 为 121- 。因此它不是一个回文数。 示例 3: 输入:x = 10 输出:false 解释:从右向左读, 为 01 。因此它不是一个回文数。
[2]header.cpp
#include<iostream> #include<string> #include<vector> #include<map> #include<algorithm> #include <unordered_map> using namespace std; class Solution{ public: bool isPalindrome(int x) { //将你的代码写在下面 return true; } };
[3]tail.cpp
#ifndef COMPILER_ONLINE #include"header.cpp" #endif void Test1() { //通过定义临时对象,来完成方法的调用 bool ret=Solution().isPalindrome(121); if(ret) { cout<<"通过了用例1,测试121通过....OK!"<<endl; } else { cout<<"没有通过用例1,测试121失败....ERROR!"<<endl; } } void Test2() { //通过定义临时对象,来完成方法的调用 bool ret=Solution().isPalindrome(-10); if(!ret) { cout<<"通过了用例2,测试-10通过....OK!"<<endl; } else { cout<<"没有通过用例2,测试-10失败....ERROR!"<<endl; } } int main() { Test1(); Test2(); return 0; }
2)questions.list
用于存储题目详情,包含
- 题号
- 难度
- 题目简单描述
- 时间限制
- 空间限制
1 判断回文数 简单 1 30000 2 两数之和 简单 1 30000
6.wwwroot主页
在线OJ系统的网页首页,index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>这是我的个人oj系统</title> <style> /* 保证我们的样式设置可以不受默认影响 */ /* "*"是选中所有的标签 */ * { /* 消除网页的默认外边距 */ margin: 0px; /*消除网页的默认内边距 */ padding: 0; } html, body { /* 设置网页的宽高 */ width: 100%; height: 100%; } .container .navbar { width: 100%; height: 50px; background-color: black; /* 给父级标签设置overflow属性,可以防止子级标签溢出 */ overflow: hidden; } .container .navbar a { /* 设置a标签为行内块元素,可以设置宽度和高度等属性 */ display: inline-block; /* 设置a标签的宽度 a标签是一个行内元素,无法设置宽度*/ width: 100px; /* 设置颜色 */ 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: 0 auto; /* 设置文字居中 */ text-align: center; /* 设置上外边距 */ margin-top: 200px; } .container .content .font_ { /* 设置标签为块级元素,独占一行,可以设置高度宽度等属性 */ display: block; /* 设置每个文字的上外边距 */ margin: 20px; /* 去掉a标签的下划线 */ text-decoration: none; /* 设置字体大小 font-size:larger; */ } </style> </head> <body> <div class="container"> <!-- 导航栏 功能不实现(我们没有这些功能) --> <div class="navbar"> <a href="/">首页</a> <a href="/all_questions">题库</a> <a href="#">讨论</a> <a href="#">竞赛</a> <a href="#">求职</a> <a class="login" href="/">登陆</a> </div> <!-- 网页内容 --> <div class="content"> <h1 class="font_ ">欢迎来到我们的OnlineJudge平台</h1> <p class="font_">这个是我个人独立开发的一个在线OJ平台</p> <a class="font_" href="/all_questions">点击我开始编程啦!</a> </div> </div> </body> </html>
7.template_html
1)all_questions.html
题库网络页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>在线OJ-题目列表</title> <style> /* 保证我们的样式设置可以不受默认影响 */ /* "*"是选中所有的标签 */ * { /* 消除网页的默认外边距 */ margin: 0px; /*消除网页的默认内边距 */ padding: 0; } html, body { /* 设置网页的宽高 */ width: 100%; height: 100%; } .container .navbar { width: 100%; height: 50px; background-color: black; /* 给父级标签设置overflow属性,可以防止子级标签溢出 */ 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: 50px; width: 800px; height: 100%; 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: rgb(243, 248, 246); } .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: rgb(31, 31, 190); font-size: larger; text-decoration: underline; } .container .footer{ width: 100%; height: 50px; text-align: center; line-height: 50px; color: #ccc; margin-top: 15px; } </style> </head> <body> <div class="container"> <!-- 导航栏 功能不实现(我们没有这些功能) --> <div class="navbar"> <a href="/">首页</a> <a href="/all_questions">题库</a> <a href="#">讨论</a> <a href="#">竞赛</a> <a href="#">求职</a> <a class="login" href="/">登陆</a> </div> <div class="question_list"> <h1>OnlineJudge题目列表</h1> <table> <tr> <th class="item">题目编号</th> <th class="item">题目名称</th> <th class="item">题目难度</th> </tr> {{#question_list}} <tr> <td class="item">{{number}}</td> <td class="item"><a href="/question/{{number}}">{{title}}</a></td> <td class="item">{{star}}</td> </tr> {{/question_list}} </table> </div> <div class="footer"> <h4>@毕财华</h4> </div> </div> </body> </html>
2)one_question.html
指定题目的网络页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{{number}}.{{title}}</title> <!-- 引入cdn --> <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ace.js" type="text/javascript" charset="utf-8"> </script> <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ext-language_tools.js" type="text/javascript" charset="utf-8"> </script> <!-- 引⼊jquery CDN --> <script src="http://code.jquery.com/jquery-2.1.1.min.js"></script> <style> * { margin: 0; padding: 0; } html, body { width: 100%; height: 100%; } /* div .ace_editor { width: 100%; } */ .container .navbar { width: 100%; height: 50px; background-color: black; /* 给父级标签设置overflow属性,可以防止子级标签溢出 */ overflow: hidden; } .container .navbar a { /* 设置a标签为行内块元素,可以设置宽度和高度等属性 */ display: inline-block; /* 设置a标签的宽度 a标签是一个行内元素,无法设置宽度*/ width: 100px; /* 设置颜色 */ 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 pre { padding-top: 10px; padding-left: 10px; font-size: medium; font-family:'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', 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: 120px; height: 50px; font-size: large; float: right; background: #26bb9c; color: white; /* 给按钮带上圆角 */ /* border-radius:1ch; */ border: 0px; margin-top: 10px; margin-right: 10px; } .container .part2 button:hover { color: red; } .container .part2 .result{ margin-top: 15px; margin-left: 15px; } .container .part2 .result pre{ font-size: large; } </style> </head> <body> <div class="container"> <div class="navbar"> <!-- 导航栏 功能不实现(我们没有这些功能) --> <a href="/">首页</a> <a href="/all_questions">题库</a> <a href="#">讨论</a> <a href="#">竞赛</a> <a href="#">求职</a> <a class="login" href="/">登陆</a> </div> <!-- 左右呈现,题目描述和预设代码 --> <div class="part1"> <!-- 1.题目描述 2.代码编辑区 --> <div class="left_desc"> <h3><span id="number">{{number}}</span>.{{title}}.{{star}}</h3> <pre>{{desc}}</pre> </div> <!-- 按钮 --> <div class="right_code"> <pre id="code" class="ace_editor"><textarea class="ace_text-input">{{pre_code}}</textarea></pre> </div> </div> <!-- 提交且得到结果 --> <div class="part2"> <div class="result"> <p>运行结果</p> </div> <button class="btn-submit" onclick="submit()">提交代码</button> </div> </div> <script> //初始化对象 editor = ace.edit("code"); //设置⻛格和语⾔(更多⻛格和语⾔,请到github上相应⽬录查看) // 主题⼤全:http://www.manongjc.com/detail/25-cfpdrwkkivkikmk.html editor.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() { //alert("嘿嘿!"); //1.收集当前页面的有关数据,1.题号 2.代码 var code = editor.getSession().getValue(); // console.log(code); var number = $(".container .part1 .left_desc h3 #number").text(); var judge_url = "/judge/" + number; //2.构建json 并通过ajax向后台发送请求 $.ajax({ method: "Post",//向后端发起请求的方式 url: judge_url,//向后端指定的url发起请求 dataType: "json",//告知server,我需要什么格式 contentType: "application/json;charset=utf-8",//告知server,我给你的是什么格式 data: JSON.stringify({ "code": code, "input": "" }), success: function (data) { //成功的结果放在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 reason_lable = $("<p>", { text: _reason }); reason_lable.appendTo(result_div); if (status == 0) { //请求是成功的,编译运行过程没有出问题,但是结果得看是否通过测试用例的结果 var _stdout = data.stdout; var _stderr = data.stderr 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>
8.conf
service_machine.conf中用于存储端口号信息,决定有几个编译服务
127.0.0.1:8081 127.0.0.1:8082 127.0.0.1:8083
这里只有三个编译服务,且都在本地,根据自己的需求进行更改
9.Makefile
oj_server:oj_server.cc g++ -o $@ $^ -std=c++11 -lpthread -lctemplate -ljsoncpp .PHONY:clean clean: rm -f oj_server
因为使用了jsconcpp库需要在编译的时候进行链接,还有httplib库中使用了多线程需要链接pthread库,使用渲染模块需要链接ctemplate
七、MySQL版本的实现
文件版的在线OJ系统,日常维护难度较高,我们建议使用数据库的版本,由文件版本转化为数据库版本,只需用oj_model_m.hpp要替换掉oj_model.hpp,更改一下oj_view.hpp中的头文件,删除#include"oj_model.hpp",添加#include"oj_model_m.hpp",更改一下oj_control.hpp中的头文件,和oj_view.hpp的操作即可实现MySQL的切换
注意:还需要添加mysql的库和头文件
1.oj_model_m
#pragma once // mysql版 #include "../comm/log.hpp" #include <iostream> #include <string> #include <unordered_map> #include <cassert> #include <vector> #include <fstream> #include "../comm/util.hpp" #include <cstdlib> #include "include/mysql.h" //连接mysql库 // 根据题目list文件,加载所有的题目信息到内存中 // model: 主要用来和数据进行交互,对外提供访问数据的接口 namespace ns_model { using namespace std; using namespace ns_log; using namespace ns_util; struct Question { string number; // 题号 string title; // 题目标题 string star; // 难度 string desc; // 题目描述 string header; // 题目预设给用户在线编辑器的代码 string tail; // 题目的测试用例,需要和header拼接,形成完整的代码 int cpu_limit; // 时间限制 int mem_limit; // 内存限制 }; const string oj_questions = "oj_questions"; const string host = "127.0.0.1"; const string user = "oj_client"; const string passwd = "123456"; const string db = "oj"; const int port = 3306; class Model { public: Model() { } bool QueryMysql(const string &sql, vector<Question> *out) { // 创建mysql句柄 MYSQL *my = mysql_init(nullptr); // 连接数据库 if (nullptr == mysql_real_connect(my, host.c_str(), user.c_str(), passwd.c_str(), db.c_str(), port, nullptr, 0)) { LOG(FATAL) << " " << mysql_error(my) << endl; return false; } LOG(INFO) << "连接数据库成功" << endl; // 一定要设置连接编码格式 mysql_set_character_set(my, "utf8"); // 执行sql语句 if (0 != mysql_query(my, sql.c_str())) { LOG(WARNING) << sql << "execute error!" << endl; return false; } // 提取结果 MYSQL_RES *res = mysql_store_result(my); // 分析结果 int rows = mysql_num_rows(res); // 获取行数量 int cols = mysql_num_fields(res); // 获取列数量 for (int i = 0; i < rows; i++) { MYSQL_ROW row = mysql_fetch_row(res); struct Question q; 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); } // 释放结果空间 free(res); // 关闭mysql连接 mysql_close(my); return true; } bool GetAllQuestions(vector<struct Question> *out) { string sql = "select * from "; sql += oj_questions; return QueryMysql(sql, out); } bool GetOneQuestion(const string &number, Question *q) { bool ret = false; string sql = "select * from "; sql += oj_questions; sql += " where number="; sql += number; vector<Question> result; if (QueryMysql(sql, &result)) { if (result.size() == 1) { *q = result[0]; ret = true; } } return ret; } ~Model() { } }; }
2.include
用于存放mysql所需要使用的头文件,需要在MySQL官网下载mysql安装包
3.lib
用于存放mysql所需要使用的库,需要在MySQL官网下载mysql安装包
4.linux下载mysql服务
linux服务器下载的mysql操作和上面在官网下载mysql不一样,上面下载的是mysql库,实现C/C++语言和sql进行交互,而现在下载的mysql服务是对一个mysql数据库进行操作
1)安装MySQL:使用 apt 包管理器安装MySQL服务器。如果您还没有安装MySQL,请使用以下命令进行安装:
sudo apt update sudo apt install mysql-server
2)启动MySQL服务:安装完成后,MySQL服务通常会自动启动。您可以使用以下命令检查其状态:
sudo systemctl status mysql
3)如果MySQL未运行,您可以使用以下命令启动它:
sudo systemctl start mysql
4)安全设置:MySQL安装后,执行以下命令以提高安全性并设置root用户的密码:
sudo mysql_secure_installation
这个命令将引导您完成一系列安全设置步骤,包括设置root密码、删除匿名用户、禁止root远程登录等。按照提示操作即可。
更改root密码:登录后,您可以使用以下命令来更改root用户的密码:
ALTER USER 'root'@'localhost' IDENTIFIED BY 'new_password';
将 new_password 替换为您想要设置的新密码。
刷新权限:密码更改后,您需要刷新MySQL的权限以使更改生效:
FLUSH PRIVILEGES;
5)第一步:
登录MySQL:安装完成并设置好密码后,您可以使用以下命令登录到MySQL服务器:
sudo mysql -u root -p
然后输入您设置的root密码。(第一次登录的时候没有密码)
第二步:
进入MySQL后,对root的密码进行修改更改root密码:连接到MySQL后,使用以下命令更改root密码。替换 new_password 为您要设置的新密码:
ALTER USER 'root'@'localhost' IDENTIFIED BY 'new_password';
刷新权限:密码更改后,刷新MySQL的权限:
FLUSH PRIVILEGES;
6)创建新用户(可选):出于安全考虑,建议不要总是使用root用户来管理数据库。您可以创建一个新的MySQL用户,并为其分配适当的权限。例如,您可以使用以下命令创建一个新用户,并允许他从本地主机登录:
CREATE USER 'newuser'@'localhost' IDENTIFIED BY 'password'; GRANT ALL PRIVILEGES ON *.* TO 'newuser'@'localhost' WITH GRANT OPTION;
将 newuser 替换为您要创建的用户名,password 替换为密码。
退出MySQL:完成操作后,您可以键入 exit 或 \q 来退出MySQL命令行界面。
5.在Ubuntu上设置MySQL可以远程登录用户
1)编辑mydqld.cnf文件
sudo vim /etc/mysql/mysql.conf.d/mysqld.cnf 找到bind-address字段,将其设置:bind-address = 0.0.0.0
2)创建用户
create user oj_client@'%' identified by '123456'; 数据库:oj
3)给用户赋权(只让用户操作oj这个数据库)
grant all on oj.* to oj_client@'%';
4)完成权限更改后,必须刷新MySQL以使更改生效:
FLUSH PRIVILEGES;
6.创建表结构
CREATE TABLE if not EXISTS `oj_questions`( `number` int PRIMARY key auto_increment COMMENT '题目编号', `title` varchar(128) NOT NULL COMMENT '题目的标题', `star` VARCHAR(8) NOT NULL COMMENT'题目难度', `desc` text NOT NULL COMMENT'题目的描述', `header` text NOT NULL COMMENT'预设给用户的代码', `tail` text NOT NULL COMMENT'测试用例的代码', `cpu_limit` int default 1 COMMENT '题目的cpu超时时间', `mem_limit` int default 50000 COMMENT '题目的memery超限空间' )engine=INNODB default charset=utf8;
八、综合
1.安装 jsoncpp
sudo apt update sudo apt install libjsoncpp-dev
2.安装 cpp-httplib
最新的cpp-httplib在使⽤的时候,如果gcc不是特别新的话有可能会有运⾏时错误的问题
建议:cpp-httplib 0.7.15
下载zip安装包,上传到服务器即可
cpp-httplib gitee链接:https://gitee.com/yuanfeng1897/cpp-httplib?
_from=gitee_search
v0.7.15版本链接: https://gitee.com/yuanfeng1897/cpp-httplib/tree/v0.7.15
把httplib.h拷⻉到我们的项⽬中即可,就这么简单3.安装boost库
sudo apt update sudo apt install libboost-all-dev
4.安装ctemplate
https://gitee.com/src-oepkgs/ctemplate $ git clone https:https://gitee.com/src-oepkgs/ctemplate $ ./autogen.sh $ ./configure $ make //编译 $ make install //安装到系统中
5.结果展示
🌸🌸🌸如果大家还有不懂或者建议都可以发在评论区,我们共同探讨,共同学习,共同进步。谢谢大家! 🌸🌸🌸