负载均衡式在线OJ
- 前言
- [1. 项目介绍](#1. 项目介绍)
- [2. 所用技术与环境](#2. 所用技术与环境)
- [3. 项目宏观结构](#3. 项目宏观结构)
-
- [3.1 项目核心模块](#3.1 项目核心模块)
- [3.2 项目的宏观结构](#3.2 项目的宏观结构)
- [4. comm公共模块](#4. comm公共模块)
-
- [4.1 日志(log.hpp )](#4.1 日志(log.hpp ))
-
- [4.1.1 日志主要内容](#4.1.1 日志主要内容)
- [4.1.2 日志使用方式](#4.1.2 日志使用方式)
- [4.1.2 日志代码](#4.1.2 日志代码)
- [4.2 工具(util.hpp)](#4.2 工具(util.hpp))
- [5. compiler_server设计](#5. compiler_server设计)
-
- 第一个功能:编译功能(compiler.hpp)
- 第二个功能:运行功能(runner.hpp)
- 第三个功能:编译并运行功能(compile_run.hpp)
- [第四个功能: 把编译并运行功能,形成网络服务(compile_server.cc)](#第四个功能: 把编译并运行功能,形成网络服务(compile_server.cc))
- [6. 基于MVC结构的OJ服务设计](#6. 基于MVC结构的OJ服务设计)
- [7. 文件版题目设计](#7. 文件版题目设计)
-
- [(1) 题目要求](#(1) 题目要求)
- [(2) 两批文件构成](#(2) 两批文件构成)
- [8. 前端页面设计](#8. 前端页面设计)
-
- [(1) 首页(index.html)](#(1) 首页(index.html))
- [(2) 所有题目的列表(all_questions.html)](#(2) 所有题目的列表(all_questions.html))
- [(3) 指定题目的编写代码的页面+代码提交(one_questions.html)](#(3) 指定题目的编写代码的页面+代码提交(one_questions.html))
前言
项目源代码 :负载均衡式在线OJ源代码
手机端如果打不开,可以复制下面链接到浏览器中访问
https://gitee.com/hou-shanlin/linux/tree/master/load-balanced-online-OJ
1. 项目介绍
- 本项目主要实现的是类似于 leetcode 的题目列表 + 在线编程功能。
- 该项目采用负载均衡算法 (轮询检测) 使得多个服务器协同处理大量的提交请求和编译请求。
- 可支持多用户在网站中同时选择题目、答题、提交代码、代码的编译与运行,以及查看题目的通过情况。
2. 所用技术与环境
所用技术栈
- C++ STL 标准库
- Boost 准标准库 (字符串切割)
- cpp-httplib 第三方开源网络库
- ctemplate 第三方开源前端网页渲染库
- jsoncpp 第三方开源序列化、反序列化库
- 负载均衡设计
- 多进程、多线程
- Ace 前端在线编辑器
- MySQL C connect
- html / css / js / jquery / ajax
开发环境
- Ubuntu 22.04.4 LTS (GNU/Linux 5.15.0-113-generic x86_64)云服务器
- VS Code
3. 项目宏观结构
3.1 项目核心模块
我们的项目核心是三个模块:
comm : 公共模块(主要包含:httplib<网络服务>、log<日志信息>、util<项目中都需要使用到的工具类的集合>)
compile_server : 编译与运行模块(主要包含:编译服务、运行服务、编译和运行服务)
oj_server : 获取题目列表,查看题目编写题目界面,负载均衡
3.2 项目的宏观结构
- B/S模式(Browser/Server):将应用程序分为两部分:客户端和服务器端。客户端通常是Web浏览器,用户通过浏览器与服务器进行交互。
- compile_server 和 oj_server 会采用网络套接字的方式进行通信,这样就能将编译模块部署在服务器后端的多台主机上。
- 而 oj_server 只有一台,这样子就会负载均衡的选择后台的编译服务。
4. comm公共模块
该模块主要为所有模块提供文件操作、字符串处理、网络请求、打印日志等公共功能
其中的 httplib.h 文件是第三方开源网络库 cpp-httplib 所提供的,因此之后不展示其代码。
文件名 | 功能 |
---|---|
httplib.h | 提供网络服务 |
log.hpp | 提供日志打印功能 |
util.hpp | 提供各种工具类 |
4.1 日志(log.hpp )
该模块主要是提供打印日志的功能,方便后续代码调试。
4.1.1 日志主要内容
- 日志等级
- 打印该日志的文件名
- 对应日志所在的行号
- 添加对应日志的时间
- 日志信息
4.1.2 日志使用方式
cpp
LOG(日志等级) << "message" << "\n"; // 如: LOG(INFO) << "这是一条日志" << "\n";
4.1.2 日志代码
cpp
#pragma once
#include <iostream>
#include "util.hpp"
namespace ns_log
{
using namespace ns_util;
// 日志等级
enum
{
INFO,
DEBUG,
WARNING,
ERROR,
FATAL
};
// level:等级
inline std::ostream &Log(const std::string &level, const std::string &file_name, int line)
{
// 添加日志等级
std::string message = "[";
message += level;
message += "]";
// 添加报错文件名称
message += "[";
message += file_name;
message += "]";
// 添加报错行
message += "[";
message += std::to_string(line);
message += "]";
// 日志时间
message += "[";
message += TimeUtil::TimeStampExLocalTime();
message += "]";
// cout内部包含缓冲区
std::cout << message; // 刷新
return std::cout;
}
// LOG(INFO)<<"message(信息)"
#define LOG(level) Log(#level, __FILE__, __LINE__)
//编译时,__FILE__ 会被替换为包含当前代码的文件名,__LINE__会被替换为当前所在行数
}
4.2 工具(util.hpp)
该模块主要提供的工具类及其说明如下:
类名 | 说明 | 提供的功能 |
---|---|---|
TimeUtil | 时间工具 | 获取当前时间 |
PathUtil | 路径工具 | 根据文件名和路径构建 .cpp 后缀的文件完整名 根据文件名和路径构建 .exe 后缀的完整文件名 根据文件名和路径构建 .compile_error 后缀的完整文件名 根据文件名和路径构建 .stdin 后缀的完整文件名 根据文件名和路径构建 .stdout 后缀的完整文件名 根据文件名和路径构建 .stderr 后缀的完整文件名 |
FileUtil | 文件工具 | 判断指定文件是否存在 用 毫秒级时间戳 + 原子性递增的唯一值 形成一个具有唯一性的文件名 将用户代码写到唯一的目标文件中, 形成临时的 .cpp 源文件 读取目标文件中的所有内容 |
StringUtil | 字符串工具 | 根据指定的分隔符切割字符串,并将切分出的子串用数组存储返回 |
工具类代码
cpp
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <unistd.h>
#include <atomic> //原子的
#include <fstream>
#include <vector>
#include <boost/algorithm/string.hpp>
namespace ns_util
{
// 时间功能
class TimeUtil
{
public:
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);
}
static std::string TimeStampExLocalTime()
{
time_t currtime = time(nullptr);
struct tm *curr = localtime(&currtime);
char time_buffer[128];
snprintf(time_buffer, sizeof(time_buffer), "%d-%d-%d %d:%d:%d",
curr->tm_year + 1900, curr->tm_mon + 1, curr->tm_mday,
curr->tm_hour, curr->tm_min, curr->tm_sec);
return time_buffer;
}
};
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_anme = temp_path;
path_anme += file_name;
path_anme += suffix;
return path_anme;
}
// 编译时需要的临时文件
// 构建源文件路径+后缀的完整文件名
// hsl -> ./temp/hsl.cpp
static 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 Stderr(const std::string &file_name)
{
return AddSuffix(file_name, ".stderr");
}
// 运行时需要的临时文件
// 构建编译时报错的文件路径
static std::string CompilerError(const std::string &file_name)
{
return AddSuffix(file_name, ".compilererror");
}
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");
}
};
// 文件操作功能
class FileUtil
{
public:
static bool IsFileExists(const std::string &path_name)
{
struct stat st;
if (stat(path_name.c_str(), &st) == 0)
{
// 获取文件属性成功,表示已经存在
return true;
}
return false;
}
static std::string UniqFileName()
{
std::atomic_uint id(0);
id++;
// 根据毫秒级时间戳+原子性递增唯一性:来保证唯一性
std::string ms = TimeUtil::GetTimeMs();
std::string uniq_id = std::to_string(id);
return ms + "_" + uniq_id;
}
// content:内容
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;
}
// keep:是否保留"\n"
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;
}
};
class StringUtil
{
public:
/***********************************************************************
* str: 传入的待切分的字符串
* target: 输出型参数,传回分割完毕的结果
* 分割标识符(空格,斜杠等等...)
************************************************************************/
static void SplitString(const std::string &str, std::vector<std::string> *target, std::string sep)
{
boost::split(*target, str, boost::is_any_of(sep), boost::algorithm::token_compress_on);
}
};
}
5. compiler_server设计
提供的服务:编译并运行代码,得到格式化的相关的结果
第一个功能:编译功能(compiler.hpp)
- 当用户提交代码的时候,需要为提交的代码提供编译服务,可以将提交的代码打包,使用进程替换的方式进行 g++ 编译。
- 为了防止远端代码是程序错误的代码或者恶意代码,需要 fork 出子进程去执行进程替换对用户提交的代码执行编译功能。
- 编译服务只关心编译有没有出错,如果出错,则需要知道是什么原因导致的错误。
- 需要形成一个临时文件,保存编译出错的结果。
cpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "../comm/log.hpp"
#include "../comm/util.hpp"
// 只负责代码编译
namespace ns_compiler
{
using namespace ns_util;
using namespace ns_log;
// 引入路径拼接功能
class Compiler
{
public:
Compiler()
{
}
// 返回值:编译成功:true,编译失败:false
// file_name: hsl
// hsl ->./temp/hsl.cpp
// hsl ->./temp/hsl.exe
// hsl ->./temp/hsl.stderr
static bool Compile(const std::string &file_name)
{
pid_t res = fork();
if (res < 0)
{
LOG(ERROR) << "内部错误,创建子进程失败" << "\n";
return false;
}
else if (res == 0) // 子进程
{
umask(0);
// 创建一个stderr文件,将编译错误信息重定向到该文件中
int _stderr = open(PathUtil::CompilerError(file_name).c_str(), O_CREAT | O_WRONLY, 0644);
if (_stderr < 0)
{
LOG(WARNING) << "没有成功形成compilererror文件" << "\n";
exit(1);
}
// 重定向错误到_stderr
dup2(_stderr, 2);
// 子进程:调用编译器,完成对代码的编译工作
// execlp :用于替换当前进程为一个新程序(不会创建子进程,所以要配合fork使用)
// g++ -o target(目标) src(源文件) -std=c++11
execlp("g++", "g++", "-o", PathUtil::Exe(file_name).c_str(), PathUtil::Src(file_name).c_str(), "-std=c++11","-D","COMPILER_ONLONE", nullptr);
LOG(ERROR) << "启动g++编译器失败,可能是参数错误" << "\n";
exit(2);
}
else
{
// 父进程
waitpid(res, nullptr, 0);
// 编译是否成功: 判断有没有生成可执行程序
if (FileUtil::IsFileExists(PathUtil::Exe(file_name)))
{
LOG(INFO) << PathUtil::Src(file_name) << " 编译成功" << "\n";
return true;
}
}
LOG(ERROR) << "程序编译失败,未形成可执行程序" << "\n";
return false;
}
~Compiler()
{
}
};
}
第二个功能:运行功能(runner.hpp)
- 编译完成也要能将代码运行起来才能知道代码的结果是否正确,因此还需要体提供运行服务。运行服务也是需要 fork 出子进程执行运行服务。
- 运行服务需要有的临时文件分别有 4 个:
- .exe 可执行程序,没有这个代码可没法运行,在编译时已经创建好了该文件,直接用就行。
- .stdin 标准输入文件,用来重定向保存用户的输入。
- .stdout 标准输出文件,只用来保存程序运行完成后的结果。
- .stderr 标准错误文件,如果用户代码在运行时发生错误了,需要用该文件保存运行时的错误信息。
- 运行服务只关心程序是否正常运行完成,有没有收到信号 (使用进程等待的方式查看) 即可。运行结果是否正确由测试用例决定。
- 同时还需要限制用户代码所占用的资源,不能让用户无限制的占用 CPU 资源以及内存资源。这就是平时刷题时最常见的资源限制。
- 可以借助 setrlimit() 函数去限制用户代码所占用的时空资源。
cpp
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <sys/resource.h>
#include "../comm/log.hpp"
#include "../comm/util.hpp"
namespace ns_runner
{
using namespace ns_log;
using namespace ns_util;
class Runner
{
public:
Runner() {}
~Runner() {}
public:
//提供设备进程占用资源大小的接口(限制一下使用的资源,防止死循环,内存消耗过大....)
static void SetProcLimit(int _cpu_limit,int _mem_limit)
{
//设置CPU时长
struct rlimit cpu_limit;
cpu_limit.rlim_cur=_cpu_limit;
cpu_limit.rlim_max=RLIM_INFINITY;
setrlimit(RLIMIT_CPU,&cpu_limit);
//设置内存大小
struct rlimit mem_limit;
mem_limit.rlim_cur=_mem_limit*1024;
mem_limit.rlim_max=RLIM_INFINITY;
setrlimit(RLIMIT_AS,&mem_limit);
}
// 指明文件名即可
/**************************************************************
* 返回值 > 0:程序异常了,退出时收到了信号,返回值就是对应的信号编号
* 返回值 == 0:正常运行完毕,结果保存到了对应的临时文件
* 返回值 < 0:内部错误
*
* cpu_limit: 该程序运行时,可以使用的最大CPU上限(运行时间【秒】)
* mem_limit: 该程序运行时,可以使用的最大内存上限【KB】
***************************************************************/
static int Run(const std::string &file_name, int cpu_limit, int mem_limit)
{
/**************************************************************
* 运行有三种情况:
* 1.运行成功,通过
* 2.运行成功,不通过
* 3.编译出错,运行不了
* 我们只管编译运行成功,具体代码有没有通过题目要求,是另外模块的事情
* 需要创建stdin,stdout,stderr三个文件来保存运行信息
***************************************************************/
// execute:执行
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 (_stderr_fd < 0 || _stdin_fd < 0 || _stdout_fd < 0)
{
LOG(ERROR) << "运行时打开标准文件失败" << std::endl;
return -1; // 文件打开失败
}
pid_t pid = fork();
if (pid < 0)
{
close(_stderr_fd);
close(_stdout_fd);
close(_stdin_fd);
return -2; // 创建子进程失败
}
else if (pid == 0) // child
{
dup2(_stdin_fd, 0);
dup2(_stdout_fd, 1);
dup2(_stderr_fd, 2);
SetProcLimit(cpu_limit, mem_limit);
execl(execute.c_str() /*我要执行谁*/, execute.c_str() /*怎么执行*/, nullptr);
exit(1);
}
else
{
close(_stderr_fd);
close(_stdout_fd);
close(_stdin_fd);
int status = 0;
waitpid(pid, &status, 0);
LOG(INFO) << "运行完毕,退出码: info: " << (status & 0x7F) << std::endl;
return status & 0x7F;
}
}
};
}
第三个功能:编译并运行功能(compile_run.hpp)
该模块需要整合编译和运行功能、适配用户请求,定制通信协议字段并正确的调用 compile 和 run 模块。
cpp
#pragma once
#include <jsoncpp/json/json.h>
#include <signal.h>
#include <unistd.h>
#include "runner.hpp"
#include "compiler.hpp"
#include "../comm/log.hpp"
#include "../comm/util.hpp"
namespace ns_compile_and_run
{
using namespace ns_compiler;
using namespace ns_runner;
using namespace ns_log;
using namespace ns_util;
// 考虑的都是整体
class CompileAndRun
{
public:
// 清理临时文件(unlink函数)
static void RemoveTempFile(const std::string &file_name)
{
// 清理文件个数不确定,但是有哪些文件我们清楚
std::string _src = PathUtil::Src(file_name);
if (FileUtil::IsFileExists(_src))
unlink(_src.c_str());
std::string _execute = PathUtil::Exe(file_name);
if (FileUtil::IsFileExists(_execute))
unlink(_execute.c_str());
std::string _compiler_error = PathUtil::CompilerError(file_name);
if (FileUtil::IsFileExists(_compiler_error))
unlink(_compiler_error.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 > 0 : 进程收到了信号导异常崩溃
* code < 0 : 整个过程运行报错(代码为空,编译报错等)
* code==0 : 整个过程运行完成
*
*********************************************************************/
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:
// desc = "编译时发生错误";
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;
}
/****************************************************************
* 输入:
* code:用户提交的代码
* input:用户提交的代码对应的输入,不做处理
* cpu_limit:时间要求
* mem_limit:空间要求(内存)
*
* 输出:
* 必填
* status:状态码
* reason:请求结果(出错原因)
*
* 选填
* stdout:我的程序运行完的结果
* stderr:我的程序运行完的错误结果
* 参数:
* in_json: {"code": "#include <iostream>...","input": "","cpu_limit": "1","mem_limit":"10240"}
* out_json:{"status":"0","reason":"","stdout":"","stderr":""}
****************************************************************/
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); // 反序列化(差错处理)
std::string code = in_value["code"].asString();
std::string input = in_value["input"].asString();
int cpu_limit = in_value["cpu_limit"].asInt();
int mem_limit = in_value["mem_limit"].asInt();
int status_code = 0;
Json::Value out_value;
int run_result = 0;
std::string file_name;
if (code.size() == 0)
{
// 代码为空
status_code = -1;
goto END;
}
//编译服务可能随时被多人请求,必须保证传递上去的code,形成源文件名称的时候,要具有唯一性,不然多个用户之间会相互影响
// 根据毫秒级时间戳+原子性递增唯一性:来保证唯一性
file_name = FileUtil::UniqFileName(); // 唯一的文件名(只有文件ming)
// 形成临时源文件(.cpp)
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;
}
// out_value["emitUTF8"] = true;
Json::StyledWriter writer;
*out_json = writer.write(out_value); // 序列化
// 清理临时文件
RemoveTempFile(file_name);
}
};
}
第四个功能: 把编译并运行功能,形成网络服务(compile_server.cc)
cpp
#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; // 创建一个 Server 对象 svr
// 注册 GET 请求处理器(测试)
// svr.Get("/hello",[](const Request &req, Response &resp){
// // 当收到 GET 请求到 "/hello" 时,设置响应内容
// resp.set_content("hello http!!你好","text/plain;charset=utf-8");
// });
// 注册 POST 请求处理器(只是设置好回调函数,当有请求来的时候才会执行)
svr.Post("/compile_and_run", [](const Request &req, Response &resp)
{
// 用户请求的服务正文是我们想要的 JSON 字符串
std::string in_json = req.body; // 获取请求体中的 JSON 字符串(表示请求的主体内容)
std::string out_json; // 用于存储响应的 JSON 字符串
// 如果输入 JSON 字符串不为空
if (!in_json.empty())
{
// 调用 CompileAndRun::Start 函数进行编译和运行
CompileAndRun::Start(in_json, &out_json);
// 设置响应内容为输出的 JSON 字符串
resp.set_content(out_json, "application/json;charset=utf-8");
} });
// 启动 HTTP 服务,监听所有地址的argv[1]端口
svr.listen("0.0.0.0", atoi(argv[1])); // 启动 HTTP 服务
// listen
// 用于启动服务器并开始监听指定地址和端口的客户端请求。
// 它使得服务器能够接收和处理来自客户端的连接。
return 0; // 返回 0,表示程序正常结束
}
6. 基于MVC结构的OJ服务设计
本质:建立一个小型网站
- 获取首页,用题目列表充当
- 编辑区域
- 提交判题功能(编译并运⾏)
MVC结构是什么?
MVC(Model-View-Controller)结构是一种广泛使用的软件设计模式,主要用于构建用户界面。它将应用程序分为三个主要组成部分,以实现关注点分离,增强可维护性和可扩展性
M: Model :通常是和数据交互的模块,比如,对题库进行增删改查(文件版,MySQL)
V: view :通常是拿到数据之后,要进行构建网页,渲染网页内容,展示给用户的(浏览器)
C: control: 控制器,就是我们的核心业务逻辑
第一个功能:用户请求的服务路由功能(oj_server.cc)
cpp
#include <iostream>
#include <signal.h>
#include <ctemplate/template.h>
#include "../comm/httplib.h"
#include "oj_control.hpp"
using namespace httplib;
using namespace ns_control;
static Control *ctrl_ptr = nullptr;
void Recovery(int signo)
{
//Ctrl + C全部上线
ctrl_ptr->RecoveryMachine();
}
int main()
{
// 1.用户请求的服务器路由功能
signal(SIGQUIT, Recovery);
//用户请求的服务路由功能
Server svr;
Control ctrl;
ctrl_ptr = &ctrl;
// 获取所有的题目列表(返回一张包含所有题目的html网页)
svr.Get("/all_questions", [&ctrl](const Request &req, Response &resp)
{
std::string html;
ctrl.AllQuestions(&html);
resp.set_content(html, "text/html;charset=utf-8");
});
// question:问题,
// 2.用户根据题目编号,获取题目内容(返回包含题目具体内容的网页)
// questions/100(100是题目编号)
// R"()" :原始字符串,保持字符串的原貌,不用做相关的转义
svr.Get(R"(/questions/(\d+))", [&ctrl](const Request &req, Response &resp)
{
std::string number = req.matches[1];
std::string html;
ctrl.Question(number,&html);
resp.set_content(html, "text/html;charset=utf-8"); });
// 3.用户提交代码,使用我们的判题功能(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;charset=utf-8"); });
// 设置默认根目录
svr.set_base_dir("./wwwroot");
svr.listen("0.0.0.0", 8888);
return 0;
}
第二个功能:model功能,提供对数据的操作(oj_model.hpp)
cpp
#pragma once
#include "../comm/log.hpp"
#include "../comm/log.hpp"
#include <iostream>
#include <string>
#include <unordered_map>
#include <cassert>
#include <vector>
#include <stdlib.h>
// 根据list文件,加载所有的题目信息到内存中
// 和数据交互的模块,对外提供数据访问的接口,⽐如,对题库进⾏增删改查
namespace ns_model
{
using namespace std;
using namespace ns_log;
using namespace ns_util;
struct Question
{
std::string number; // 题目的编号,唯一
std::string title; // 题目的标题
std::string star; // 难度:简单,中等,困难
int cpu_limit; // 题目的时间要求(s)
int mem_limit; // 题目的空间要求(kb)
std::string desc; // 题目的描述
std::string header; // 题目预设给用户在线编辑器的代码
std::string tail; // 题目的测试用例,需要和header拼接,形成完整代码
};
const string question_list = "./questions/questions.list";
const string questions_path = "./questions/";
class Model
{
public:
Model()
{
assert(LoadQuestionList(question_list));
}
bool LoadQuestionList(const string &question_list)
{
// 加载配置文件:./questions/questions.list + 题目编号文件
ifstream in(question_list);
if (!in.is_open())
{
LOG(FATAL) << "加载题库失败,请检查是否存在题库文件" << "\n";
return false;
}
string line;
while (getline(in, line))
{
vector<string> tokens;
StringUtil::SplitString(line, &tokens, " ");
// 1 判断回文数 简单 1 30000
if (tokens.size() != 5)
{
LOG(WARNING) << "加载部分题目失败,请检查文件格式" << "\n";
continue;
}
Question q;
q.number = tokens[0];
q.title = tokens[1];
q.star = tokens[2];
q.cpu_limit = atoi(tokens[3].c_str());
q.mem_limit = atoi(tokens[4].c_str());
string path = questions_path;
path += q.number;
path += "/";
FileUtil::ReadFile(path + "desc.txt", &(q.desc), true);
FileUtil::ReadFile(path + "header.cpp", &(q.header), true);
FileUtil::ReadFile(path + "tail.cpp", &(q.tail), true);
questions.insert({q.number, q});
}
LOG(INFO) << "加载题库成功" << "\n";
in.close();
return true;
}
// 获取全部题目
bool GetAllQuestions(vector<Question> *out)
{
if (questions.size() == 0)
{
LOG(ERROR) << "获取题库失败" << "\n";
return false;
}
for (auto &q : questions)
{
out->push_back(q.second);
}
return true;
}
// 获取指定一个题目
bool GetOneQuestions(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()
{
}
private:
// 题号 : 题目细节
unordered_map<string, Question> questions;
};
}
第三个功能:control,逻辑控制模块(oj_control.hpp)
cpp
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <mutex>
#include <cassert>
#include <fstream>
#include <jsoncpp/json/json.h>
#include <algorithm>
#include "../comm/log.hpp"
#include "../comm/util.hpp"
#include "../comm/httplib.h"
#include "oj_model.hpp"
#include "oj_view.hpp"
#include "../comm/httplib.h"
namespace ns_control
{
using namespace std;
using namespace ns_log;
using namespace ns_util;
using namespace ns_model;
using namespace ns_view;
using namespace httplib;
// 提供服务的主机【Machine(机器)】
class Machine
{
public:
Machine() : ip(""), port(0), load(0), mtx(nullptr)
{
}
// 提升主机负载
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;
}
~Machine()
{
}
public:
std::string ip; // 编译服务的ip
int port; // 编译服务的port
uint64_t load; // 编译服务的负载(加载)
std::mutex *mtx; // mutex禁止拷贝
};
// 【service(服务)】
const std::string service_machine = "./conf/service_machine.conf";
// 【LoadBlancer(负载均衡)】
// 负载均衡模块
class LoadBlancer
{
public:
LoadBlancer()
{
assert(LoadConf(service_machine));
LOG(INFO) << "加载" << service_machine << "成功" << "\n";
}
// 加载配置
bool LoadConf(const std::string &machine_list)
{
std::ifstream in(machine_list);
if (!in.is_open())
{
LOG(FATAL) << "加载: " << machine_list << "失败" << "\n";
return false;
}
std::string line;
while (std::getline(in, line))
{
vector<std::string> tokens;
StringUtil::SplitString(line, &tokens, ":");
if (tokens.size() != 2)
{
LOG(WARNING) << "切分" << line << "失败" << "\n";
continue;
}
Machine m;
m.ip = tokens[0];
m.port = atoi(tokens[1].c_str());
m.load = 0;
m.mtx = new 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. 随机数 + hash
// 2. 轮询 + hash(选择这种)
int online_num = online.size();
if (online_num == 0)
{
mtx.unlock();
LOG(FATAL) << "所有的后端编译主机已经离线,运维的牛马快来修" << "\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();
// 要离线的主机找到了
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 test
void ShowMachines()
{
mtx.lock();
cout << "当前在线主机列表:";
for (auto &iter : online)
{
cout << iter << " ";
}
cout << endl;
cout << "当前离线主机列表:";
for (auto &iter : offline)
{
cout << iter << " ";
}
cout << endl;
mtx.unlock();
}
~LoadBlancer()
{
}
private:
// 可以给我们提供服务的所有主机
// 每台主机都有自己的下标。充当当前主机的id
std::vector<Machine> machines;
// 所有在线的主机
std::vector<int> online;
// 所有离线的主机
std::vector<int> offline;
// 给保证LoadBlance它的数据安全
std::mutex mtx;
};
// 业务核心逻辑的控制器
class Control
{
public:
Control()
{
}
~Control()
{
}
public:
// 根据题目数据构建网页
void RecoveryMachine()
{
_load_blance.OnlineMachine();
}
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
{
*html = "获取题目失败, 形成题目列表失败";
ret = false;
}
return ret;
}
// 根据题目内容构建网页
bool Question(const string &number, string *html)
{
bool ret = true;
struct Question q;
if (_model.GetOneQuestions(number, &q))
{
// 获取题目信息成功,将指定的题目数据构建成网页
_view.OneExpandHtml(q, html);
}
else
{
*html = "指定题目不存在";
ret = false;
}
return ret;
}
// 把判题内容构建成网页
// id : 100
// code: include....
// input: "..."
void Judge(const std::string &number, const std::string in_json, std::string *out_json)
{
// 0. 根据题目编号直接拿到对应的题目细节
struct Question q;
_model.GetOneQuestions(number, &q);
// 1. in_json进行反序列化,得到题目id,得到用户提交的题目代码,输出
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;
std::string compile_string = writer.write(compile_value);
// 3. 选择负载最低的主机(差错处理)
// 规则:一直选择,直到主机可用,否则就是全部挂掉
while (true)
{
int id = 0;
Machine *m = nullptr;
if (!_load_blance.SmartChoice(&id, &m))
{
break;
}
// 4. 然后发起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;charset=utf-8"))
{
// 5. 将结果赋值给out_json
if (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.ShowMachines(); // 调试
}
}
}
private:
Model _model; // 提供后台数据
View _view; // 提供html网页渲染功能
LoadBlancer _load_blance; // 核心负载均衡器
};
}
第四个功能:网页渲染功能(oj_view.hpp)
cpp
#pragma once
#include <iostream>
#include <string>
#include <ctemplate/template.h>
#include "oj_model.hpp"
namespace ns_view
{
using namespace std;
using namespace ns_model;
const std::string template_path = "./template_html/";
class View
{
public:
View() {}
//构建全部题目显示的网页
void AllExpandHtml(const vector<struct Question>& questions, string *html)
{
//题目编号,标题,难度
//1.形成路径
std::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.获取被渲染的网页
ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html,ctemplate::DO_NOT_STRIP);
//4.完成渲染功能(开始渲染)
tpl->Expand(html,&root);
}
//构建指定题目内容的网页
void OneExpandHtml(const struct Question &q,string *html)
{
//1.形成路径
std::string src_html = template_path +"one_questions.html";
//2.形成数字典
ctemplate::TemplateDictionary root("one_questions");
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);
}
~View() {}
private:
};
}
7. 文件版题目设计
(1) 题目要求
- 题目的编号
- 题目的标题
- 题目的难度
- 题目的描述,题面
- 时间要求(内部处理)
- 空间要求(内部处理)
(2) 两批文件构成
- 第一个:questions.list : 题目列表(不需要题目的内容)
- 第二个:题目的描述,题目的预设置代码(header.cpp), 测试用例代码(tail.cpp)
这两个内容是通过题目的编号,产生关联的
当用户提交自己代码的时候,看到的代码是这样的(header.cpp):
cpp
#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <algorithm>
using namespace std;
class Solution
{
public:
int search(vector<int>& nums, int target) {
//将你的代码写在此处
}
};
而OJ不只是把header.cpp交给compile_and_run,而是会把测试用例文件(tail.cpp)和header.cpp整合到一起打包给compile_and_run
tail.cpp:
cpp
#ifndef COMPILER_ONLONE
#include "header.cpp"
#endif
void Test1()
{
vector<int> nums={-1,0,3,5,9,12};
int target =9;
int ret = Solution().search(nums,target);
if(ret==4)
{
std::cout<<"通过测试用例1, 测试(nums = [-1,0,3,5,9,12], target = 9)通过 OK!"<<std::endl;
}
else
{
std::cout<<"未通过测试用例1, 测试的值为: (nums = [-1,0,3,5,9,12], target = 9)"<<std::endl;
}
}
void Test2()
{
vector<int> nums={-1,0,3,5,9,12};
int target =2;
int ret = Solution().search(nums,target);
if(ret==-1)
{
std::cout<<"通过测试用例2, 测试(nums = [-1,0,3,5,9,12], target = 2)通过 OK!"<<std::endl;
}
else
{
std::cout<<"未通过测试用例2, 测试的值为: (nums = [-1,0,3,5,9,12], target = 2)"<<std::endl;
}
}
int main()
{
Test1();
Test2();
}
最终提交给后台编译运行服务的代码是:
cpp
//header.cpp
#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <algorithm>
using namespace std;
class Solution
{
public:
int search(vector<int>& nums, int target) {
//用户(你)提交的代码在这儿
}
};
//tail.cpp
#ifndef COMPILER_ONLONE
#include "header.cpp"
#endif
void Test1()
{
vector<int> nums={-1,0,3,5,9,12};
int target =9;
int ret = Solution().search(nums,target);
if(ret==4)
{
std::cout<<"通过测试用例1, 测试(nums = [-1,0,3,5,9,12], target = 9)通过 OK!"<<std::endl;
}
else
{
std::cout<<"未通过测试用例1, 测试的值为: (nums = [-1,0,3,5,9,12], target = 9)"<<std::endl;
}
}
void Test2()
{
vector<int> nums={-1,0,3,5,9,12};
int target =2;
int ret = Solution().search(nums,target);
if(ret==-1)
{
std::cout<<"通过测试用例2, 测试(nums = [-1,0,3,5,9,12], target = 2)通过 OK!"<<std::endl;
}
else
{
std::cout<<"未通过测试用例2, 测试的值为: (nums = [-1,0,3,5,9,12], target = 2)"<<std::endl;
}
}
int main()
{
Test1();
Test2();
}
8. 前端页面设计
编写页面的时候,需要三剑客: html/css/js
(1) 首页(index.html)
cpp
<!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>这是我的个人OJ系统</title>
<style>
/* 起手式, 100%保证我们的样式设置可以不受默认影响 */
* {
/* 消除网页的默认外边距 */
margin: 0px;
/* 消除网页的默认内边距 */
padding: 0px;
}
html,
body {
width: 100%;
height: 100%;
background-image: url('hsl.jpg'); /* 添加背景图 */
background-size: cover; /* 使背景图覆盖整个元素 */
background-position: center; /* 背景图居中显示 */
}
.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 .font_ {
/* 设置标签为块级元素,独占一行,可以设置高度宽度等属性 */
display: block;
/* 设置每个文字的上外边距 */
margin-top: 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>
(2) 所有题目的列表(all_questions.html)
cpp
<!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>在线OJ-题目列表</title>
<style>
/* 起手式, 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: 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: blue;
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>OnlineJuge题目列表</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="/questions/{{number}}">{{title}}</a></td>
<td class="item">{{star}}</td>
</tr>
{{/question_list}}
</table>
</div>
<div class="footer">
<!-- <hr> -->
<h4>@HSL</h4>
</div>
</div>
</body>
</html>
(3) 指定题目的编写代码的页面+代码提交(one_questions.html)
cpp
<!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>
<!-- 引入ACE插件 -->
<!-- 官网链接:https://ace.c9.io/ -->
<!-- CDN链接:https://cdnjs.com/libraries/ace -->
<!-- 使用介绍:https://www.iteye.com/blog/ybc77107-2296261 -->
<!-- https://justcode.ikeepstudying.com/2016/05/ace-editor-%E5%9C%A8%E7%BA%BF%E4%BB%A3%E7%A0%81%E7%BC%96%E8%BE%91%E6%9E%81%E5%85%B6%E9%AB%98%E4%BA%AE/ -->
<!-- 引入ACE 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%;
}
.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:'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-color: #26bb9c;
color: #FFF;
/* 给按钮带上圆角 */
/* border-radius: 1ch; */
border: 0px;
margin-top: 10px;
margin-right: 10px;
}
.container .part2 button:hover {
color:green;
}
.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">
<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"></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();
// console.log(number);
var judge_url = "/judge/" + number;
// console.log(judge_url);
// 2. 构建json,并通过ajax向后台发起基于http的json请求
$.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){
//成功得到结果
// 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{
// 编译运行出错,do nothing
}
}
}
</script>
</body>
</html>
(优化持续更新中...)