毕设项目:基于BS模型的在线OJ系统
系列文章目录
文章目录
- 系列文章目录
- 前言
- 一、在线OJ系统描述
- 二、在线编译模块
- 1.搭建一个HTTP服务器完成在线编译
- 2.收到HTTP请求,进行数据格式转化(HTTP中body的内容转换为JSON格式的字符串)
- 3.compile_server.cpp浏览器提交JSON数据请求服务器,服务器调用在线编译模块编译,把结果返回给浏览器
- 4.util.hpp工具类
- 1.TimeUtil类时间戳获取工具TimeUtil标识文件的不同
- 2.打印日志的工具
- 3.文件类FileUtil把文件所有内容读取出来,放到content字符串中
- 4.URL body解析模块
- 5.查找用户代码中是否有危害服务器的语句
- 5.compile.hpp在线编译类
- 1.源代码的文件
- 2.编译错误文件
- 3.可执行程序文件
- 4.标准输入文件
- 5.标准输出文件
- 6.标准错误文件
- 7.CompileAndRun函数
- 8.WriteTmpFile函数
- 9.Compile函数
- 10.Run函数
- 6.post提交到所有问题界面
- 7.在线编译模块小结
- 三、题目管理模块
- 1.oj_data存放题目的文件夹
- 1.oj_config.cfg每一行都代表一个题目
- 2.header.cpp代码框架
- 3.tail.cpp代码测试用例
- 4.desc.txt题目详细描述
- 2.MVC中的M负责存储数据 oj_model.hpp这个oj_model模块做的事情,就是把刚才文件中存储的题目信息加载起来,供服务器使用
- 1.描述题目的类question
- 2.load函数表示把文件中的数据加载到内存中, 加到哈希表中
- 3.GetAllQuestion获取所有题目
- 4.GetQuestion获取某个具体的题目
- 3.MVC中的C controller: 核心业务逻辑 oj_server.cpp作为服务器的核心逻辑,需要创建好对应的服务器框架代码,在这个框架中来组织逻辑
- 1.all_questions核心业务逻辑
- 2.question核心业务逻辑
- 3.compile核心业务逻辑
- 4.oj_server.hpp对于oj_server.cpp的实现部分controller 作为服务器的核心逻辑,需要创建好对应的服务器框架代码,在这个框架中来组织逻辑
- 1.server.Get("/all_questions")
- 2.server.Get(R"(/question/(\d+))"
- 3.server.Post(R"(/compile/(\d+))"
- 5.MVC中的v V => view : 负责显示界面 oj_view.hpp根据数据,生成html这个动作,通常叫做网页渲染(render)
- 1.RenderAllQuestion渲染所有问题界面
- 2.RenderQuestion渲染单个问题界面
- 3.RenderCompileResult渲染结果界面
- 三、前端结果界面
- 1.all_question.html
- 2.question.html
- 3.result.html
- 总结
前言
一、在线OJ系统描述
实现一个在线OJ系统类似于力扣或者牛客网的核心部分刷题代码练习功能,提供了用户一个可以在线刷题编写代码并且能够进行编译运行的环境,题目通过序号排序,题目也有难度等级的划分,测试用例等等。在编写代码的同时提供了语法纠错、代码高亮、自动补全等基本功能。
用户可以通过域名加上端口号访问服务器,系统内置了多道编程题,用户点击对应题目就可以进行练习,并且题目内含有大量测试样例。服务器端会根据用户编写代码会进行用例的测试,检测用户代码是否符合题意,并且可以将编译成功结果或者编译出错的原因返回给浏览器端。
二、在线编译模块
在现编译模块的实现:此模块的核心完成"在线",用户把写好的代码通过网页提交到服务器上,服务器调用g++完成编译过程,并且调用刚生成的可执行程序,验证程序结果,返回给用户提交的浏览器上。
1.搭建一个HTTP服务器完成在线编译
搭建一个HTTP服务器来完成在线编译的核心功能。
此处开源的Httplib源代码:cpp-httplib
或者直接git clone: git clone https://github.com/yhirose/cpp-httplib
A C++11 single-file header-only cross platform HTTP/HTTPS library.It's extremely easy to setup. Just include the httplib.h file in your code!NOTE: This is a multi-threaded 'blocking' HTTP library. If you are looking for a 'non-blocking' library, this is not the one that you want.翻译:一个C++11单文件头文件跨平台HTTP/HTTPS库。<--意思就是只有头文件-->它非常容易安装。只需在代码中包含httplib.h文件即可!注意:这是一个多线程的“阻塞”HTTP库。如果你正在寻找一个“非阻塞”库,这不是你想要的。
快速上手一个开源项目小技巧:
2.收到HTTP请求,进行数据格式转化(HTTP中body的内容转换为JSON格式的字符串)
3.compile_server.cpp浏览器提交JSON数据请求服务器,服务器调用在线编译模块编译,把结果返回给浏览器
- Json如何从req请求中获取到Json请求?
- 从req对象中获取到。
- Json如何和Http协议结合?
- 需要的请求格式是json格式,HTTP能够提供的格式,是另外一种键值对的 格式,所以要对HTTP提供的格式进行格式的转换。
- 此处由于提交的代码中可能会包含一些特殊符号,这些特殊符号要正确传 输时,就会进行urlencode,这一步由浏览器自动完成。
- 服务器收到请求之后的第一件事就是切分键值对,再urldecode,然后解> 析数据,整理成需要的Json格式。
- Json如何进行解析和构造?
- 使用jsoncpp第三方库。
jsoncpp第三方库获取办法:
1 #include <unordered_map>2 3 #include "httplib.h"4 #include "compile.hpp"5 //#include "jsoncpp/json/json.h"6 7 #include<jsoncpp/json/json.h>8 9 int main()10 {11 using namespace httplib;12 13 Server server;14 15 // Get注册一个回调函数,这个函数的调用机制是处理Get方法时16 // lambda表达式 就是一个匿名函数17 18 // 路由过程19 // 接收请求对象,根据请求进行处理,并且将响应返回给客户端20 //21 // 此处get改成post,代码放到body里面22 server.Post("/compile", [](const Request& req, Response& resp) {23 // 根据具体的问题场景,根据请求,计算出响应结果24 (void)req;25 26 // 如何从req请求中获取到Json请求27 // Json如何和Http协议结合28 29 // 需要的请求格式是json格式,HTTP能够提供的格式,是另外一种键值对的格式,所以要对HTTP提供的格式进行格式的转换30 31 // 此处由于提交的代码中可能会包含一些特殊符号,这些特殊符号要正确传输时,就会进行urlencode,这一步由浏览器自动完成32 // 服务器收到请求之后的第一件事就是切分键值对,再urldecode,然后解析数据,整理成需要的Json格式33 34 // 此函数将Http中的body,解析成键值对,存入body_kv35 std::unordered_map<std::string, std::string> body_kv;36 UrlUtil::ParseBody(req.body, &body_kv);37 38 // Json如何进行解析和构造? 使用jsoncpp第三方库39 // 40 // 下方调用CompileAndRun41 42 Json::Value req_json; // 从req对象中获取到43 44 /* for(std::unordered_map<std::string,std::string>::iterator it=body_kv.begin();it!=body_kv.end();++it)45 {46 req_json[it->first]=it->second;47 }48 */49 50 for (auto e : body_kv)51 {52 // e的类型和 *it 是一致的53 req_json[e.first] = e.second;54 }55 56 Json::Value resp_json; // resp_json发到响应中57 58 // resp_json 输出型参数59 Compiler::CompileAndRun(req_json, &resp_json);60 61 // 把Json::Value对象序列化成为字符串,才能返回62 Json::FastWriter writer;63 resp.set_content(writer.write(resp_json), "text/plain");64 });65 66 // 让浏览器能访问到一个静态页面67 // 静态页面: index.html 不会发生变化68 // 动态页面: 编译结果 随着参数的不同而发生变化69 //70 // 加上这个目录是为了浏览器能够访问到静态页面71 server.set_base_dir("../wwwroot", "");72 server.listen("0.0.0.0", 9092);73 74 return 0;75 }
4.util.hpp工具类
1.TimeUtil类时间戳获取工具TimeUtil标识文件的不同
18 class TimeUtil19 {20 public:21 // 获取当前时间戳22 static int64_t TimeStamp()23 {24 struct timeval tv;25 ::gettimeofday(&tv, nullptr);26 27 return tv.tv_sec;28 }29 30 static int64_t TimeStampMS()31 {32 struct timeval tv;33 ::gettimeofday(&tv, nullptr);34 35 return tv.tv_sec * 1000 + tv.tv_usec / 1000;36 }37 };
2.打印日志的工具
39 40 // 打印日志的工具41 42 // 期望打印出的日志格式: 43 // [I时间戳 util.hpp:31] hello44 // 日志的使用方式形如: LOG(INFO) << "hello" << "\n";45 // 日志的级别: 46 // FATAL 致命47 // ERROR 错误48 // WAENING 警告49 // INFO 提示50 51 enum Level 52 {53 INFO, 54 WARNING, 55 ERROR,56 FATAL57 };58 59 60 inline std::ostream& Log(Level level, const std::string& file_name, int line_num)61 {62 //前缀63 std::string prefix = "[";64 65 if (level == INFO)66 {67 prefix += "I";68 }69 else if (level == WARNING)70 {71 prefix += "W";72 }73 else if (level == ERROR)74 {75 prefix += "E";76 }77 else if (level == FATAL)78 {79 prefix += "F";80 }81 82 prefix += std::to_string(TimeUtil::TimeStamp());83 prefix += " ";84 prefix += file_name;85 prefix += ":";86 prefix += std::to_string(line_num);87 prefix += "] ";88 89 90 std::cout << prefix;91 return std::cout;92 }93 94 #define LOG(level) Log(level, __FILE__, __LINE__)
3.文件类FileUtil把文件所有内容读取出来,放到content字符串中
97 /98 // 文件相关工具类99 100 class FileUtil 101 {102 public:103 104 // 传入一个文件路径,把文件所有内容读取出来,放到content字符串中105 // 下面这个函数参数是 输出型参数106 //107 // 输入型参数用const引用108 // 输出型参数用指针109 // 输入输出型参数用引用110 // 111 static bool Read(const std::string& file_path, std::string* content)112 {113 //content->clear();114 (*content).clear();115 std::ifstream file(file_path.c_str());116 if (!file.is_open())117 {118 return false;119 }120 121 std::string line;122 while (std::getline(file, line))123 {124 *content += line + "\n";125 }126 127 file.close();128 return true;129 }130 131 static bool Write(const std::string& file_path, const std::string& content)132 {133 std::ofstream file(file_path.c_str());134 if (!file.is_open())135 {136 return false;137 }138 139 file.write(content.c_str(), content.size());140 141 file.close();142 return true;143 }144 };
4.URL body解析模块
首先安装boost标准库来进行字符串切分。
146 ///147 // URL / body解析模块148 149 // 使用boost库中的函数完成字符串某些操作150 class StringUtil151 {152 public:153 // 使用boost split进行字符串的切分154 // aaa bbb ccc 按照1个 空格切分 切分成3个部分155 // aaa bbb ccc 切分成3或者4 156 // is_any_of 表示多个字符切割 & =157 // split中有一个参数叫做 token_compress_off 标识是否打开还是关闭分隔符压缩就是4个,如果打开上述就会切分为3部分,token_compress_on158 static void Split(const std::string& input, const std::string& split_char, std::vector<std::string>* output)159 {160 boost::split(*output, input, boost::is_any_of(split_char), boost::token_compress_off);161 }162 };163 164 // 对url body的解析模块165 class UrlUtil166 {167 public:168 static void ParseBody(const std::string& body, std::unordered_map<std::string, std::string>* params)169 {170 // 1.先对body字符串进行切分,切分成键值对形式171 // 1.1 先按照 & 切分172 // 1.2 再按照 = 切分173 // 使用boost split进行切分174 std::vector<std::string> kvs;175 StringUtil::Split(body, "&", &kvs);176 177 for (size_t i = 0; i < kvs.size(); i++)178 { 179 std::vector<std::string> kv;180 181 // kvs[i]存的是一个键值对182 StringUtil::Split(kvs[i], "=", &kv);183 184 //kv[0]=key kv[1]=value185 if (kv.size() != 2)186 {187 continue;188 }189 190 // 出参,将切分好的键值对,传给调用位置191 // unordered_map [] 操作,如果key不存在则新增,如果key存在,则获取到value192 // 2.对键值对中的转义过的字符进行urldecode193 // 只用对value转义,key不用转义194 (*params)[kv[0]] = UrlDecode(kv[1]);195 }196 }197 198 199 static unsigned char ToHex(unsigned char x) 200 { 201 return x > 9 ? x + 55 : x + 48; 202 } 203 204 static unsigned char FromHex(unsigned char x) 205 { 206 unsigned char y; 207 if (x >= 'A' && x <= 'Z') y = x - 'A' + 10; 208 else if (x >= 'a' && x <= 'z') y = x - 'a' + 10; 209 else if (x >= '0' && x <= '9') y = x - '0'; 210 else assert(0); 211 return y; 212 } 213 214 static std::string UrlEncode(const std::string& str) 215 { 216 std::string strTemp = ""; 217 size_t length = str.length(); 218 for (size_t i = 0; i < length; i++) 219 { 220 if (isalnum((unsigned char)str[i]) || 221 (str[i] == '-') || 222 (str[i] == '_') || 223 (str[i] == '.') || 224 (str[i] == '~')) 225 strTemp += str[i]; 226 else if (str[i] == ' ') 227 strTemp += "+"; 228 else 229 { 230 strTemp += '%'; 231 strTemp += ToHex((unsigned char)str[i] >> 4); 232 strTemp += ToHex((unsigned char)str[i] % 16); 233 } 234 } 235 return strTemp; 236 } 237 238 static std::string UrlDecode(const std::string& str) 239 { 240 std::string strTemp = ""; 241 size_t length = str.length(); 242 for (size_t i = 0; i < length; i++) 243 { 244 if (str[i] == '+') strTemp += ' '; 245 else if (str[i] == '%') 246 { 247 assert(i + 2 < length); 248 unsigned char high = FromHex((unsigned char)str[++i]); 249 unsigned char low = FromHex((unsigned char)str[++i]); 250 strTemp += high*16 + low; 251 } 252 else strTemp += str[i]; 253 } 254 return strTemp; 255 }256 };
5.查找用户代码中是否有危害服务器的语句
258 // 查找用户代码中是否有危害服务器的语句259 class CheckUserCode260 {261 public:262 // 目前只屏蔽了system,后续可以继续添加屏蔽字263 static bool isHaveSystem(const std::string& user_code)264 {265 if (std::string::npos != user_code.find("system"))266 {267 return true; 268 }269 270 return false;271 }272 };
5.compile.hpp在线编译类
该程序会生成以下的文件: 此处本质使用文件完成进程间通信。
1.源代码的文件
42 // 1.源代码文件,此处的name表示当前的请求名字43 44 // 请求和请求之间name必须不同45 // name必须唯一,因为可能有多个请求46 // 形如: tmp_时间戳.计数器序号.cpp 47 static std::string SrcPath(const std::string& name)48 { 49 return "../tmp_files/" + name + ".cpp";50 }
2.编译错误文件
51 // 2.编译错误文件52 static std::string CompileErrorPath(const std::string& name)53 {54 return "../tmp_files/" + name + ".compile_err";55 }
3.可执行程序文件
56 // 3.可执行程序文件57 static std::string ExePath(const std::string& name)58 {59 return "../tmp_files/" + name + ".exe";60 }
4.标准输入文件
61 // 4.标准输入文件62 static std::string StdinPath(const std::string& name)63 {64 return "../tmp_files/" + name + ".stdin";65 }
5.标准输出文件
66 // 5.标注输出文件67 static std::string StdoutPath(const std::string& name)68 {69 return "../tmp_files/" + name + ".stdout";70 }
6.标准错误文件
71 // 6.标准错误文件72 static std::string StderrPath(const std::string& name)73 {74 return "../tmp_files/" + name + ".stderr";75 }
4 5 6号文件目的是为了通知可执行程序文件,所以可以使用进程间通信进行 ,完成可以不需要创建文件。
7.CompileAndRun函数
90 static bool CompileAndRun(const Json::Value& req, Json::Value* resp/*出参*/)91 {92 // 1.根据json请求对象,生成源代码文件和标准输入文件93 // 2.调用 g++进行编译(fork+exec /system)94 // 生成可执行程序95 // 如果编译出错,96 // 需要把编译错误记录下来(重定向到文件中)97 // 3.调用可执行程序,把标准输入记录到文件中,然后把文件98 // 中的内容重定向到可执行程序中,可执行程序的标准输出99 // 和标准错误内容也要重定向输出记录到文件中100 // 4.把程序最终结果进行返回,构造resp对象101 102 103 // 1.根据json请求对象,生成源代码文件104 // 代码段为空的情况下105 if (req["code"].empty())106 {107 (*resp)["error"] = 3;108 (*resp)["reason"] = "code empty";109 LOG(ERROR) << "code empty" << std::endl;110 return false;111 }112 113 // req["code"] 根据可以取出value,value类型也是Json::Value, 通过asSting() 转成字符串114 const std::string& code = req["code"].asString(); // 不会进行拷贝操作,提高代码效率115 116 // std::string& code = req["code"].asString();117 118 // 通过这个函数将用户提交代码和用户输入写到文件中去119 std::string file_name = WriteTmpFile(code, req["stdin"].asString());120 121 // 2.调用g++进行编译(fork + exec)122 // 生成一个可执行程序,如果编译出错需要记录下来(重定向到文件)123 bool ret = Compile(file_name);124 if (!ret)125 {126 // 错误处理127 (*resp)["error"] = 1;128 // 从文件中读取错误原因129 std::string reason;130 FileUtil::Read(CompileErrorPath(file_name), &reason);131 (*resp)["reason"] = reason;132 133 // 虽然是编译出错,这样的错误是用户的错误,并不是服务器的错误,对于服务器来说,这样的编译错误不是错误,所以把它叫做info134 LOG(INFO) << "Compile failed!" << std::endl;135 return false;136 }137 138 // 3.调用可执行程序,139 // 把标准输入记录到文件中,把文件中的内容重定向给可执行程序,140 // 可执行程序的标准输出重定向至文件141 //142 // 返回一个信号143 int sig = Run(file_name);144 if (sig != 0)145 {146 // 错误处理147 (*resp)["error"] = 2;148 149 // 运行时错误,程序终止进行一定会发出信号,所以信号可以判断进程是因为什么原因退出150 (*resp)["reason"] = "Program exit by signo: " +std::to_string(sig);151 152 // 虽然是编译出错,这样的错误是用户的错误,并不是服务器的错误,对于服务器来说,这样的编译错误不是错误,所以把它叫做info153 LOG(INFO) << "Program exit by signo: " << std::to_string(sig) << std::endl;154 return false;155 }156 157 // 4.没有错误,把程序中的最终结果进行返回,构造resp_json对象158 (*resp)["error"] = 0;159 (*resp)["reason"] = "";160 161 // 运行的最终结果也在文件中存储162 std::string str_stdout;163 FileUtil::Read(StdoutPath(file_name), &str_stdout);164 (*resp)["stdout"] = str_stdout;165 166 std::string str_stderr;167 FileUtil::Read(StderrPath(file_name), &str_stderr);168 (*resp)["stderr"] = str_stderr;169 170 LOG(INFO) << "Program " << file_name << " Done" << std::endl;171 return true;172 }
8.WriteTmpFile函数
174 private:
175
176 // 1.将代码写到文件中
177 // 2.给这个请求分配唯一的一个名字,通过返回值返回
178 // 分配名字形如: tmp_时间戳.计数器序号.xxx
179 static std::string WriteTmpFile(const std::string& code, const std::str ing& str_stdin)
180 {181 // 将id变为原子性的操作,可以省去互斥锁的带来的开销
182 // 原子操作依赖于CPU的支持,保持每个请求的唯一性
183 static std::atomic_int id(0);
184 ++id;
185
186 std::string file_name = "tmp_" + std::to_string(TimeUtil::TimeStamp ()) + "." + std::to_string(id);
187
188 // 将代码写到文件中
189 FileUtil::Write(SrcPath(file_name), code);
190
191 // 把用户的输入也写到文件中
192 FileUtil::Write(StdinPath(file_name), str_stdin);
193
194 return file_name;
195 }
9.Compile函数
197 static bool Compile(const std::string& file_name)198 {199 // 1.先构造出编译指令200 // g++ file_name.cpp -o file_name.exe -std=c++11201 char* command[20] = { 0 };202 // 必须保证command的指针都是指向有效内存203 char buf[20][50] = { {0} };204 for (int i = 0; i < 20; i++)205 {206 command[i] = buf[i];207 }208 209 sprintf(command[0], "%s", "g++");210 sprintf(command[1], "%s", SrcPath(file_name).c_str());211 sprintf(command[2], "%s", "-o");212 sprintf(command[3], "%s", ExePath(file_name).c_str());213 sprintf(command[4], "%s", "-std=c++11");214 command[5] = nullptr;215 216 // 2.创建子进程217 int ret = fork(); 218 if (ret > 0)219 {220 // 3.父进程进行进行等待,防止出现僵尸221 waitpid(ret, nullptr, 0);222 }223 else224 {225 // 4.子进程进行程序替换226 // 将编译时错误的信息输出到文件中227 int fd = open(CompileErrorPath(file_name).c_str(), O_WRONLY | O_CREAT, 0666);228 if (fd < 0)229 {230 LOG(ERROR) << "Open Compile file error" << std::endl;231 exit(1);232 }233 // 将文件描述符中的标准错误的内容,拷贝至文件中234 dup2(fd, 2);235 236 execvp(command[0], command); //command[0]是g++237 238 // 此处子进程执行失败直接退出239 exit(0);240 }241 242 // 5.代码执行到这里,如何知道编译成功?243 // 可以判定可执行文件是否存在, stat(ls),stat与ls的功能差不多244 // ls指令基于stat实现的245 struct stat st;246 ret = stat(ExePath(file_name).c_str(), &st);247 if (ret < 0)248 {249 // stat 执行失败,说明该文件不存在250 LOG(INFO) << "Compile " << file_name << " Failed!" << std::endl;251 return false;252 }253 LOG(INFO) << "Compile " << file_name << " OK!" << std::endl;254 255 return true;256 }
10.Run函数
258 static int Run(const std::string& file_name)259 {260 // 1.创建子进程261 int ret = fork();262 263 if (ret > 0)264 {265 // 2.父进程进行等待266 // 如果程序执行异常,则返回状态码,根据状态码判断出错原因267 int status = 0;268 waitpid(ret, &status, 0);269 270 // 取出位图的后7位271 return status & 0x7f;272 }273 else 274 {275 // 3.重定向 标准输入,标准输出,标准错误276 int fd_stdin = open(StdinPath(file_name).c_str(), O_RDONLY);277 dup2(fd_stdin, 0);278 279 int fd_stdout = open(StdoutPath(file_name).c_str(), O_WRONLY | O_CREAT, 0666);280 dup2(fd_stdout, 1);281 282 int fd_stderr = open(StderrPath(file_name).c_str(), O_WRONLY | O_CREAT, 0666);283 dup2(fd_stderr, 2);284 285 // 4.子进程进行程序替换286 execl(ExePath(file_name).c_str(), ExePath(file_name).c_str(), nullptr);287 exit(0);288 }289 }290 };
6.post提交到所有问题界面
加上这个目录是为了浏览器能够访问到静态页面,index.html 。
这里点击会跳转下一个所有问题界面。
1 <!DOCTYPE html>2 <html>3 <head>4 <meta charset="UTF-8">5 <title>online-OJ</title>6 <style type="text/css">7 * {8 margin: 0;9 padding: 0;10 list-style: none;11 }12 13 body {14 /*background-color: #9a9a9a;*/15 background: rgba(29, 111, 145, 0.9);16 }17 18 #login_page {19 width: 500px;20 height: 420px;21 background-color:white;22 position: absolute;23 top: 50%;24 left: 50%;25 transform: translate(-50%, -50%); 26 }27 #login_inner {28 width: 400px;29 position: relative;30 margin: 0 auto;31 }32 p {33 margin-top: 50px;34 font-family: "黑体";35 font-size: 30px;36 color: black;37 text-align: center;38 }39 .btn {40 width: 400px;41 height: 50px;42 font-size: 16px;43 font-family: "黑体";44 margin-top: 10px;45 border: none;46 }47 #loginbtn {48 background-color:green;49 color: white;50 margin-top: 40px;51 margin-bottom: 10px;52 }53 #abc54 {55 56 background-color:green;57 color: white;58 width: 400px;59 height: 50px;60 }61 </style>62 </head>63 <body>64 <div id="login_page">65 <div id="login_inner">66 <p>欢迎来到在线OJ训练营</p>67 <p>点击下方按钮即刻开始旅程</p>68 <form action="/all_questions" method="GET">69 <input type="submit" class="btn" id="loginbtn" value="开启OJ之旅">70 </form>71 <center> <button id="abc" onclick="window.location.href='https://blog.csdn.net/qq_44918090?spm=1019.2139.3001.5343'">在线OJ博客地址</button>72 </center>73 </div>74 </div>75 </body>76 </html>
7.在线编译模块小结
三、题目管理模块
1.oj_data存放题目的文件夹
1.oj_config.cfg每一行都代表一个题目
按行读取,每一行就是一个题目。
1 1 回文数 简单 ../oj_data/12 2 测试题 困难 ../oj_data/23 3 两数之和 简单 ../oj_data/3
2.header.cpp代码框架
1 #include<iostream>2 #include<string>3 #include<vector>4 5 class Solution {6 public:7 bool isPalindrome(int x) {8 9 }10 };
3.tail.cpp代码测试用例
1 2 // tail.cpp 不给用户看,最终编译时,会把用户提交的代码和tail.cpp合并成一个文件再进行编译3 4 void Test1()5 {6 Solution s;7 bool ret = s.isPalindrome(121);8 if (ret) 9 {10 std::cout << "Test1 OK" << std::endl;11 }12 else 13 {14 std::cout << "Test1 failed" << std::endl;15 }16 }17 18 void Test2()19 {20 Solution s;21 bool ret = s.isPalindrome(-121);22 if (!ret) 23 {24 std::cout << "Test2 OK" << std::endl;25 }26 else 27 {28 std::cout << "Test2 failed" << std::endl;29 }30 }31 32 int main()33 {34 Test1();35 Test2();36 37 return 0;38 }
4.desc.txt题目详细描述
1 给你一个整数 x ,如果 x 是一个回文整数,返回 true ;否则,返回 false 。2 回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。例如,121 是回文,而 123 不是。3 4 示例 1:5 输入:x = 1216 输出:true7 8 示例?2:9 输入:x = -12110 输出:false11 解释:从左向右读, 为 -121 。 从右向左读, 为 121- 。因此它不是一个回文数。12 13 示例 3:14 输入:x = 1015 输出:false16 解释:从右向左读, 为 01 。因此它不是一个回文数。17 18 示例 4:19 输入:x = -10120 输出:false21 ?22 提示:23 -231?<= x <= 231?- 124 25 进阶:你能不将整数转为字符串来解决这个问题吗?
2.MVC中的M负责存储数据 oj_model.hpp这个oj_model模块做的事情,就是把刚才文件中存储的题目信息加载起来,供服务器使用
1.描述题目的类question
1 // MVC(软件设计方式)2 // M => model: 负责数据存储3 // V => view : 负责显示界面4 // C => controller: 核心业务逻辑5 6 #pragma once7 8 #include <string>9 #include <vector>10 #include <map>11 #include <fstream>12 13 #include "util.hpp"14 15 // model这个模块做的事情,就是把刚才文件中存储的题目信息加载起来,供服务器使用16 17 //基于文件的方式完成题目的存储18 //约定每个题目对应一个目录,目录的名字就是题目的id19 //目录里面包含一下几个文件:20 //1)header.cpp 代码框架21 //2)tail.cpp 代码测试用例22 //desc.txt 题目的详细描述23 //除此之外,在搞一个oj_config.cfg文件,作为一个总的入口文件24 //这个文件是一个行文本文件25 //这个文件的每一行对应一个需要被服务器加载起来的题目26 //这一行里面包含以下几个信息:题目的id,题目的名字,题目的难度,题目对应的目录27 //28 29 struct Question 30 {31 std::string id;32 std::string name;33 std::string dir; // 题目对应的目录, 目录就包含了题目的描述 题目的框架 题目的测试用例34 std::string star; // 题目的难度35 36 // 以下字段通过 dir 进行获取37 std::string desc; // 题目的描述38 std::string header_cpp; // 题目的代码框架中的代码39 std::string tail_cpp; // 题目的测试用例代码40 };
2.load函数表示把文件中的数据加载到内存中, 加到哈希表中
// 完成核心操作,获取题目信息
// 函数中的参数 &输入性参数 *输出型参数
class OjModel
{public:// 把文件中的数据加载到内存中, 加到哈希表中bool Load(){// 1.先打开oj_config.cfg文件std::ifstream file("../oj_data/oj_config.cfg");if (!file.is_open()){LOG(ERROR) << "open oj_config failed!" << std::endl;return false;}// 2.按行读取 oj_config.cfg文件,并进行解析,// 一行包含4个字段,字段间的分隔是 \tstd::string line;while (std::getline(file, line)){// 3.根据解析结果拼装成 Question结构体std::vector<std::string> tokens;StringUtil::Split(line, "\t", &tokens);// 因为设定的是4个部分if (tokens.size() != 4){LOG(ERROR) << "config file format error!\n";continue;}// 4.把结构体加入到hash表Question q;q.id = tokens[0];q.name = tokens[1];q.star = tokens[2];q.dir = tokens[3];FileUtil::Read(q.dir + "/desc.txt", &q.desc);FileUtil::Read(q.dir + "/header.cpp", &q.header_cpp);FileUtil::Read(q.dir + "/tail.cpp", &q.tail_cpp);// 将数据插入值哈希表中// [] 如果可以不存在就创建新的键值对,如果key存在就查找对应的valuemodel_[q.id] = q;}file.close();LOG(INFO) << "Load " << model_.size() << " question" << std::endl;return true;}
3.GetAllQuestion获取所有题目
// 获取所有题目bool GetAllQuestion(std::vector<Question>* question/*输出型参数*/) const{// 遍历哈希表就可以拿到所有题目question->clear();for (const auto& kv : model_){// auto推导出的类型是一个键值对question->push_back(kv.second);}return true;}
4.GetQuestion获取某个具体的题目
// 获取某个具体的题目bool GetQuestion(const std::string& id, Question* q) const{// 返回值是一个迭代器 auto pos = model_.find(id);if (pos == model_.end()){return false;}*q = pos->second;return true;}private:// MD5 SHA1 计算字符串的哈希值// 1.计算的哈希值非常均匀(两个字符串哪怕只差一个字符,计算得到的哈希值,差别也会很大)// 2.不可逆,通过字符串算hash值很容易,但是给你hash值找到对应的原串,理论上是不可能的// 3.固定长度(不管字符串多长,得到的哈希值都是固定长度)std::map<std::string, Question> model_; // 存放题目id对应的题目信息的键值对
};
3.MVC中的C controller: 核心业务逻辑 oj_server.cpp作为服务器的核心逻辑,需要创建好对应的服务器框架代码,在这个框架中来组织逻辑
1.all_questions核心业务逻辑
1 #include "httplib.h"2 #include <jsoncpp/json/json.h>3 4 #include "compile.hpp"5 #include "util.hpp"6 #include "oj_model.hpp"7 #include "oj_view.hpp"8 9 // controller 作为服务器的核心逻辑,需要创建好对应的服务器框架代码,在这个框架中来组织逻辑10 11 int main()12 {13 // 服务器启动只加载一次数据14 OjModel model;15 model.Load();16 17 using namespace httplib;18 19 Server server;20 // [&model] lamda表达式按照引用来捕获匿名函数外的变量// 按引用来捕获,不写&就是按值来捕获21 server.Get("/all_questions", [&model](const Request& req, Response& resp) {22 (void)req;23 24 // 数据来自于Model25 std::vector<Question> all_question;26 model.GetAllQuestion(&all_question);27 28 // 如何借助 all_question 数据得到最终的html,用oj_view.hpp29 std::string html;30 OjView::RenderAllQuestion(all_question, &html);31 resp.set_content(html, "text/html");32 });
2.question核心业务逻辑
34 // 登录请求35 // server.Get();36 37 // 注册请求38 // server.Get();39 40 // R"()" C++11引入的语法,标识原始字符串(忽略字符串中的转义字符)41 // \d+ 正则表达式,用特殊符号表示字符串满足啥样的条件42 server.Get(R"(/question/(\d+))", [&model](const Request& req, Response& resp) {43 Question question;44 model.GetQuestion(req.matches[1].str(), &question);45 46 std::string html;47 OjView::RenderQuestion(question, &html);48 49 resp.set_content(html, "text/html");50 });
3.compile核心业务逻辑
server.Post(R"(/compile/(\d+))", [&model](const Request& req, Response& resp) {54 // 1.先根据id获取到题目信息55 Question question;56 model.GetQuestion(req.matches[1].str(), &question); 57 58 // 2.解析body,获取到用户提交的代码59 // 此处实现代码与compile_server类似60 // 此函数将Http中的body,解析成键值对,存入body_kv61 std::unordered_map<std::string, std::string> body_kv;62 UrlUtil::ParseBody(req.body, &body_kv);63 const std::string& user_code = body_kv["code"];64 65 // 3.构造JSon结构的参数66 Json::Value req_json; // 从req对象中获取到67 // 需要编译的代码,是用户提交代码 + 题目的测试用例代码68 req_json["code"] = user_code + "#define system void\n" + question. tail_cpp;69 70 // 4.调用编译模块进行编译71 Json::Value resp_json;72 Compiler::CompileAndRun(req_json, &resp_json);73 74 // 5.根据编译结果构成最终的网页75 std::string html;76 OjView::RenderCompileResult(resp_json["stdout"].asString(), resp_js on["reason"].asString(), &html);77 78 resp.set_content(html, "text/html");79 });80 81 server.set_base_dir("../wwwroot", "");82 server.listen("0.0.0.0", 9092);83 }
4.oj_server.hpp对于oj_server.cpp的实现部分controller 作为服务器的核心逻辑,需要创建好对应的服务器框架代码,在这个框架中来组织逻辑
1.server.Get("/all_questions")
1 #pragma once2 #include "httplib.h"3 #include <jsoncpp/json/json.h>4 5 #include "compile.hpp"6 #include "util.hpp"7 #include "oj_model.hpp"8 #include "oj_view.hpp"9 #include "database.hpp"10 #include "session.hpp"11 // controller 作为服务器的核心逻辑,需要创建好对应的服务器框架代码,在这个框架中来组织逻辑12 13 #define OJ_SVR_IP "0.0.0.0"14 #define OJ_SVR_PORT 909215 16 class OJServer17 {18 public:19 /*20 OJServer()21 {22 svr_ip_ = OJ_SVR_IP;23 svr_port_ = OJ_SVR_PORT;24 25 db_svr_ = nullptr;26 all_sess_ = nullptr;27 }28 */29 ~OJServer()30 {}31 32 /*33 int InitOJServer(std::string db_ip, std::string db_user, std::string db_password, std::string db_db, std::string ip = OJ_SVR_IP, std::uint16_t port = OJ_SVR_PORT)34 {35 svr_ip_ = ip;36 svr_port_ = port;37 38 // 数据库连接初始化,传入数据库连接所需要的参数39 db_svr_ = new DataBaseSvr(db_ip, db_user, db_password, db_db);40 if (db_svr_ == nullptr)41 {42 return -1;43 }44 if(db_svr_->ConnectToMysql() < 0)45 {46 return -2;47 }48 all_sess_ = new AllSessionInfo();49 if (all_sess_ == nullptr)50 {51 LOG(INFO) << "sessionInfo Init failed!" << std::endl;52 return -3;53 }54 55 return 0;56 }57 */58 void StartOJServer()59 {60 using namespace httplib;61 // 服务器启动只加载一次数据62 OjModel model;63 model.Load();64 65 /*66 // 连接数据库67 if(this->db_svr_->ConnectToMysql())68 {69 LOG(ERROR) << "MySQL connect failed" << std::endl;70 }71 else72 {73 LOG(INFO) << "MySQL connect success!" << std::endl;74 }75 */76 77 /*78 // [&model] lamda表达式按照引用来捕获匿名函数外的变量79 server.Post("/register", [this](const Request& req, Response& resp){80 //1.将用户提交的数据, 继续反序列化, 拿到一个json对象81 Json::Reader r;82 Json::Value v;83 r.parse(req.body, v);84 85 Json::Value resp_json;86 //2.需要将浏览器提交的数据继续持久化(保存在数据库当中)87 resp_json["status"] = db_svr_->InsertUserInfo(v);88 89 Json::FastWriter w;90 resp.body = w.write(resp_json);91 resp.set_header("Content-Type", "application/json");92 });93 94 server.Post("/login", [this](const Request& req, Response& resp){95 /*96 * 1.将浏览器提交的json串,反序列化成为json对象97 * 2.调用数据库模块的函数, 进行查找和比对98 * 有邮箱继续查找, 用密码进行比对99 * 3.根据登录状态, 判断是否生成会话信息100 * 4.返回给浏览器一个结果101 * 4.1 登录失败102 * 4.2 登录成功103 * 需要返回会话ID104 * */105 /*106 Json::Reader r;107 Json::Value v;108 r.parse(req.body, v);109 110 Json::Value resp_json;111 int user_id = db_svr_->QueryUserExist(v);112 string session_id = "";113 if(user_id > 0)114 {115 Session sess(v, user_id);116 //保存该会话信息117 session_id = sess.GetSessionID();118 all_sess_->InsertSessionInfo(session_id, sess);119 }120 resp_json["login_status"] = user_id > 0 ? true : false;121 Json::FastWriter w;122 resp.body = w.write(resp_json);123 resp.set_header("Set-Cookie", session_id.c_str());124 resp.set_header("Content-Type", "application/json");125 });126 127 */128 129 server.Get("/all_questions", [&model, this](const Request& req, Response& resp) {130 131 int loginStatus = 0;132 // std::string username = "";133 134 // int user_id = all_sess_->CheckSession(req); 135 /*136 if (user_id > 0)137 {138 // 会话校验成功,用户登录则显示个人中心139 loginStatus = 1;140 141 // 并且根据用户ID查询对应的用户名,发送给前台页面进行显示142 username = db_svr_->SelectUsernameByID(user_id); 143 }144 */145 // 数据来自于Model146 std::vector<Question> all_question;147 model.GetAllQuestion(&all_question);148 149 // 如何借助 all_question 数据得到最终的html150 std::string html;151 OjView::RenderAllQuestion(all_question, &html);152 resp.set_content(html, "text/html");153 });
2.server.Get(R"(/question/(\d+))"
155 // R"()" C++11引入的语法,标识原始字符串(忽略字符串中的转义字符)156 // \d+ 正则表达式,用特殊符号标识字符串满足啥样的条件157 server.Get(R"(/question/(\d+))", [&model](const Request& req, Response& resp) {158 Question question;159 model.GetQuestion(req.matches[1].str(), &question);160 161 std::string html;162 OjView::RenderQuestion(question, &html);163 164 resp.set_content(html, "text/html");165 });
3.server.Post(R"(/compile/(\d+))"
server.Post(R"(/compile/(\d+))", [&model](const Request& req, Response& resp) {168 // 1.先根据id获取到题目信息169 Question question;170 model.GetQuestion(req.matches[1].str(), &question); 171 172 // 2.解析body,获取到用户提交的代码173 // 此处实现代码与compile_server类似174 // 此函数将Http中的body,解析成键值对,存入body_kv175 std::unordered_map<std::string, std::string> body_kv;176 UrlUtil::ParseBody(req.body, &body_kv);177 const std::string& user_code = body_kv["code"];178 179 // 2.1 检测用户代码中是否有敏感词汇,防止对服务器造成伤害180 bool sesitiveFlag = CheckUserCode::isHaveSystem(user_code);181 std::string html;182 183 if (sesitiveFlag)184 {185 // 包含有敏感词汇186 OjView::RenderCompileResult("无法完成编译", "代码中含有非法语句,请仔细检查代码!", &html);187 }188 else189 {190 // 3.构造JSon结构的参数191 Json::Value req_json; // 从req对象中获取到192 // 需要编译的代码,是用户提交代码 + 题目的测试用例代码193 req_json["code"] = user_code + "#define system void\n" + question.tail_cpp;194 195 // 4.调用编译模块进行编译196 Json::Value resp_json;197 Compiler::CompileAndRun(req_json, &resp_json);198 199 // 5.根据编译结果构成最终的网页200 OjView::RenderCompileResult(resp_json["stdout"].asString(), resp_json["reason"].asString(), &html);201 }202 203 resp.set_content(html, "text/html");204 });205 206 server.set_base_dir("../wwwroot", "");207 server.listen(svr_ip_.c_str(), svr_port_);208 }209 210 private:211 // httplib 种的server212 httplib::Server server;213 214 std::string svr_ip_;215 uint16_t svr_port_;216 217 // 数据库操作218 // DataBaseSvr* db_svr_;219 220 // 关于所有登录用户的session的类221 // AllSessionInfo* all_sess_;222 };
5.MVC中的v V => view : 负责显示界面 oj_view.hpp根据数据,生成html这个动作,通常叫做网页渲染(render)
这里要借用首先包含 google 开源库。
开源库下载地址:google开源库
1.RenderAllQuestion渲染所有问题界面
1 #pragma once2 // google 开源库3 #include <ctemplate/template.h>4 #include "oj_model.hpp"5 6 class OjView7 {8 public:9 // 根据数据,生成html这个动作,通常叫做网页渲染(render)10 static void RenderAllQuestion(const std::vector<Question>& all_question, std::string* html)11 { 12 ctemplate::TemplateDictionary dict("all_question");13 dict.SetValue("rows", std::to_string(all_question.size()));14 15 // 根据传入的用户名判断用户是否登录,如果登录显示用户名,不显示登录注册按钮16 dict.SetValue("hiddenFlag", "false");17 /*18 if (username != "")19 {20 dict.SetValue("hiddenFlag", "true");21 dict.SetValue("username", username);22 }23 */24 for (const auto& question : all_question)25 {26 ctemplate::TemplateDictionary* table_dict = dict.AddSectionDictionary("question");27 table_dict->SetValue("id", question.id);28 table_dict->SetValue("name", question.name);29 table_dict->SetValue("star", question.star);30 }31 32 ctemplate::Template* tpl;33 tpl = ctemplate::Template::GetTemplate("../template/all_question.html", ctemplate::DO_NOT_STRIP);34 tpl->Expand(html, &dict);35 }
2.RenderQuestion渲染单个问题界面
37 static void RenderQuestion(const Question& question, std::string* html)38 {39 ctemplate::TemplateDictionary dict("question");40 dict.SetValue("id", question.id);41 dict.SetValue("name", question.name);42 dict.SetValue("star", question.star);43 dict.SetValue("desc", question.desc);44 dict.SetValue("header", question.header_cpp);45 46 ctemplate::Template* tpl;47 tpl = ctemplate::Template::GetTemplate("../template/question.html", ctemplate::DO_NOT_STRIP);48 tpl->Expand(html, &dict);49 }
3.RenderCompileResult渲染结果界面
51 static void RenderCompileResult(const std::string& str_stdout, const std::string& reason, std::string* html)52 {53 ctemplate::TemplateDictionary dict("result");54 dict.SetValue("stdout", str_stdout);55 dict.SetValue("reason", reason);56 57 ctemplate::Template* tpl;58 tpl = ctemplate::Template::GetTemplate("../template/result.html", ctemplate::DO_NOT_STRIP);59 tpl->Expand(html, &dict);60 }61 };
三、前端结果界面
1.all_question.html
1 <!DOCTYPE html>2 <html lang="en">3 <head>4 <meta charset="utf-8">5 <title>OJ系统</title>6 <meta name="description" content="Charcoal is a free Bootstrap 4 UI kit build by @attacomsian at Wired Dots." />7 <meta name="viewport" content="width=device-width, initial-scale=1.0">8 <!--Bootstrap 4-->9 <link rel="stylesheet" href="css/bootstrap.min.css">10 <style>11 body{12 background-color:#EE82EE;13 14 15 16 }17 </style>18 </head>19 <body background="./images/3.png">20 21 <nav class="navbar navbar-expand-md navbar-dark fixed-top sticky-navigation">22 <a class="navbar-brand font-weight-bold" href="#">OJ系统</a>23 24 25 26 </nav>27 28 <!--hero section-->29 <section class="bg-hero">30 <div class="container">31 <div class="row vh-100">32 <div class="col-sm-12 my-auto text-center">33 <h1>OJ系统</h1>34 <p class="lead text-capitalize my-4">35 基于C++实现的在线OJ系统36 </p>37 <a href="https://blog.csdn.net/qq_44918090?spm=1010.2135.3001.5343" class="btn btn-outline-light btn-radius btn-lg">博客地址</a>38 </div>39 </div>40 </div>41 </section>42 43 <!--components-->44 <section class="my-5 pt-5">45 <div class="container">46 47 48 49 <!-- Tables -->50 <div class="row mb-5" id="tables">51 <div class="col-sm-12">52 53 <div class="mt-3 mb-5">54 <h3>题目列表</h3>55 <table class="table table-hover">56 <thead>57 <tr>58 <th>题目序号</th>59 <th>题目名称</th>60 <th>题目难度</th>61 </tr>62 </thead>63 64 <tbody>65 {{#question}}66 <tr>67 <td>{{id}}</td>68 <td><a href="/question/{{id}}">{{name}}</a></td>69 <td>{{star}}</td>70 </tr>71 {{/question}}72 73 </table>74 </div>75 76 77 78 </div>79 </div>80 81 82 83 </div>84 </section>85 86 <!--footer-->87 <section class="py-5 bg-dark">88 <div class="container">89 <div class="row">90 <div class="col-md-6 offset-md-3 col-sm-8 offset-sm-2 col-xs-12 text-center">91 <!-- <h3>Upgrade to Pro Version</h3>92 <p class="pt-2">93 We are working on <b>Charcoal Pro</b> which will be released soon. The pro version 94 will have a lot more components, sections, icons, plugins and example pages. 95 Join the waiting list to get notified when we release it (plus discount code).96 </p>97 <a class="btn btn-warning" href="https://wireddots.com/newsletter">Join Waiting List</a>98 <hr class="my-5"/> -->99 <center>100 <p class="pt-2 text-muted">101 归属@2022->CSDN万粉博主:森明帮大于黑虎帮 102 </p>103 104 </center>105 </div>106 </div>107 </div>108 </section>109 110 <script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>111 <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js"></script>112 <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js"></script>113 <script src="js/app.js"></script>114 </body>115 </html>
2.question.html
1 <html>2 <head>3 <meta charset="utf-8">4 <title>{{id}}.{{name}}</title>5 </head>6 7 <style type="text/css">8 * {9 margin: 0;10 padding: 0;11 list-style: none;12 }13 14 #left {15 width: 38%;16 float: left;17 word-wrap: break-word;18 19 position: absolute;20 top: 10px;21 left: 20px;22 }23 #right {24 width: 60%;25 height: 100%;26 float: right;27 background-color: #F7F7F7;28 }29 pre {30 white-space:pre-wrap;31 white-space:-moz-pre-wrap;32 white-space:-pre-wrap;33 white-space:-o-pre-wrap;34 word-wrap:break-word;35 }36 37 #codeEditor {38 display: flex;39 align-items: stretch;40 41 width: 94%;42 height: 85%;43 border: 0;44 border-radius: 5px;45 46 position: relative;47 left: 0;48 right: 0;49 bottom: 0;50 top:10px;51 margin: auto;52 }53 54 #codeArea {55 width: 95%;56 padding-top: 5px;57 }58 59 #subbutton {60 border: 0;61 border-radius: 5px;62 width: 120px;63 height: 40px;64 color: white;65 font-size: 15px;66 background-color: #2DB55D;67 68 float: right;69 position: absolute;70 right: 20px;71 bottom: 10px;72 }73 74 </style>75 76 <body >77 78 <div id="left">79 <h2 style="border-bottom:2px; width:100%;">题目描述</h2>80 <br>81 82 <div><font style="font-size: 23px">{{id}}.{{name}}</font></div>83 <font style="font-size:18px">难度:</font> {{star}}<br><br><br>84 <div><pre>{{desc}}</pre></div>85 </div>86 87 <div id="right">88 <form action="/compile/{{id}}" method="POST">89 <font style="font-size:20px; padding-top: 10px;"> 代 码 编 写:</font>90 91 <div id="codeEditor" style="min-height:400px"><textarea id="codeArea" type="text" name="code" rows="30" cols="100">{{header}}</textarea>92 </div>93 94 <input id="subbutton" type="button" value="提交代码" onclick="submit({{id}})">95 </form>96 </div>97 98 </body>99 100 </html>
3.result.html
1 <html>2 <head>3 <meta charset="utf-8">4 5 <style type="text/css">6 body {7 background-color: rgba(29, 111, 145, 0.9);8 font-family: "黑体";9 }10 11 div {12 width: 100%;13 text-align: center;14 font-size: 25px;15 }16 </style>17 18 </head>19 20 <body>21 <h1 align="center" style="color: white">运行结果</h2>22 <div>23 <pre style="color:yellow">{{stdout}}</pre>24 </div>25 <div>26 <pre style="color:orange">{{reason}}</pre>27 </div>28 </body>29 </html>
总结
毕设项目:基于BS模型的在线OJ系统相关推荐
- SSM毕设项目 - 基于SSM的电影院在线售票系统(含源码+论文)
文章目录 1 项目简介 2 实现效果 2.1 界面展示 3 设计方案 3.1 概述 3.2 开发环境 3.3 系统流程 3.3.1 系统开发流程 3.3.2 用户登录流程 3.3.3 系统操作流程 3 ...
- node.js+uni计算机毕设项目基于微信小程序在线抽签系统(程序+小程序+LW)
该项目含有源码.文档.程序.数据库.配套开发软件.软件安装教程.欢迎交流 项目运行 环境配置: Node.js+ Vscode + Mysql5.7 + HBuilderX+Navicat11+Vue ...
- ssm毕设项目基于Java的医疗器械销售系统oy281(java+VUE+Mybatis+Maven+Mysql+sprnig)
ssm毕设项目基于Java的医疗器械销售系统oy281(java+VUE+Mybatis+Maven+Mysql+sprnig) 项目运行 环境配置: Jdk1.8 + Tomcat8.5 + Mys ...
- ssm毕设项目基于JAVAEE的车检预约系统846ks(java+VUE+Mybatis+Maven+Mysql+sprnig)
ssm毕设项目基于JAVAEE的车检预约系统846ks(java+VUE+Mybatis+Maven+Mysql+sprnig) 项目运行 环境配置: Jdk1.8 + Tomcat8.5 + Mys ...
- JavaWeb项目——基于Servlet实现的在线OJ平台 (项目问答+代码详解)
文章目录 项目演示 预先知识 请问 在处理用户同时提交代码时是 多进程处理还是 多线程处理? 你是如何创建多进程的逻辑的 如何获取到编译与运行后的结果? 编译运行模块 子进程之间如何并发? 文件读写操 ...
- Java项目-基于Springboot实现英语在线学习系统
项目编号:BS-GX-025 运行环境: 开发工具:IDEA /ECLIPSE 数据库:MYSQL5.7 应用服务:Tomcat8.5.31 项目构建:Maven 后台开发技术:Springboot ...
- VUE毕设项目 - 基于SSM的网上租车系统(含源码+论文)
文章目录 1 项目简介 2 实现效果 2.1 界面展示 3 设计方案 3.1 概述 3.2 系统流程 3.2.1 系统开发流程 3.2.2 登录流程 3.3 系统结构设计 4 项目获取 1 项目简介 ...
- SSM毕设项目 - 基于SSM的大学生兼职跟踪系统(含源码+论文)
文章目录 1 项目简介 2 实现效果 2.1 界面展示 3 设计方案 3.1 概述 3.2 系统流程 3.2.1 系统开发流程 3.2.2 学生登录流程 3.2.3 系统操作流程 3.3 系统结构设计 ...
- 毕设项目 - 基于SSM的大学生兼职跟踪系统(含源码+论文)
文章目录 1 项目简介 2 实现效果 2.1 界面展示 3 设计方案 3.1 概述 3.2 系统流程 3.2.1 系统开发流程 3.2.2 学生登录流程 3.2.3 系统操作流程 3.3 系统结构设计 ...
最新文章
- vs oracle带参数更新,Oracle vs PostgreSQL Develop(23) - PL(pg)sql(参数声明)
- 七、Java编码字符集和转义符介绍
- 读取带空格字符串小结
- mysql把select结果存到变量中_mysql实例 select into保存到变量的例子
- 成员变量(全局变量)和局部变量区别
- 如何正确的开始用Go编程
- python绘制函数图像
- SSM服装管理系统毕业设计源码080948
- 基于STM32F103系列单片机四路定时器电机编码器模式配置过程附源码
- 牧师与魔鬼 -- version2 动作分离
- power_supply子系统
- Broadcom BCM4312 无线网卡驱动安装
- 再摘一枚重要奖项!腾讯安全获得云安全联盟CSA 2022安全金盾奖
- Weblogic创建domain域
- js实现table 表头顶部固定方法
- 2022 考研上岸中科大大数据学院一点点经验
- 普朗克公式matlab,用MATLAB实现普朗克函数积分的快捷计算.pdf
- Mastercam 2017四轴五轴编程加工实例视频教程
- html页面加载蒙版_javascript 窗口加载蒙板 内嵌网页内容
- C语言职工管理系统课程设计
热门文章
- linux加载虚拟sriov网卡,网卡直通SR-IOV技术
- 下一代互联网:三网融合下的美丽画卷
- ttyLinux安装完整指南
- windows系统下载安装JDK8
- 抢滩大数据金融“蓝海”
- 抽样检验规范——多次(多轮)抽样的理解
- 量子计算机 基因工程,2020 ASC世界大学生超级计算机竞赛聚焦量子计算和语言智能...
- Windows Vista With Service Pack 2(x86 / x64)官方简体中文版(ISO)光盘镜像
- MySQL 中的系统库之sys 系统库
- Windows下Tomcat的搭建步骤