• C++项目:【负载均衡式在线OJ】


    文章目录

    一、项目介绍

    二、技术栈与开发环境

    1.所用技术:

    2.开发环境:

    三、项目演示

    1.运行代码

    2.进入项目首页

    3.题目列表

    4.点击具体一道题

    5.编辑代码并提交

    四、项目思维导图

    五、项目宏观结构

    六、Comm公共模块

    1.日志工具log.hpp

    2.其他工具util.hpp

    七、CompilerServer模块

    1.整体层次如图

    2.编译模块compiler.hpp

    3.运行模块runner.hpp

    4.编译+运行compile_run.hpp

    5.Compiler模块compiler_server.cc

    八、基于MVC结构的OJServer模块

    1. 什么是MVC结构

    2.整体层次如图

    3.Model模块oj_model.hpp​编辑

    3.1文件版本

    3.2数据库版本

    4.题库的结构

    5.view模块oj_view.hpp

    6.oj_control模块oj_control.hpp

    6.1.构建题目列表和单个题目网页

    6.2.负载均衡模块

    6.2.1机器类的设计

    6.2.2负载均衡模块设计

    6.2.3负载均衡的实现:

    6.2.4判题模块

    7.oj_server模块oj_server.cc

    九、前端页面的设计(了解)

    1. indx.html

    2. all_questions.html

    3. one_question.html

    十、项目扩展

    十一、项目所需工具

    1.升级 gcc

    2.安装 jsoncpp

    3.安装 cpp-httplib

    4.安装boost库

    5.安装与测试 ctemplate

    6.使用Ace在线编辑器

    7.MySQL 建表

    十二、项目源码


    一、项目介绍

            这个项目是基于负载均衡的在线OJ平台,用户可以在浏览器访问各个题目,在编辑区编写代码提交,后端能够自动分配服务器资源,保持平衡的情况下为用户提供良好的编程运行环境,让代码快速运行和提交。


    二、技术栈与开发环境

    1.所用技术:

    • C++ STL 标准库
    • Boost 准标准库(字符串切割)
    • cpp-httplib 第三方开源网络库
    • ctemplate 第三方开源前端网页渲染库
    • jsoncpp 第三方开源序列化、反序列化库
    • 负载均衡设计
    • 多进程、多线程
    • MySQL C connect
    • Ace前端在线编辑器(简单使用)
    • html/css/js/jquery/ajax (简单使用)

    2.开发环境:

    • Centos 7 云服务器
    • vscode
    • Mysql Workbench

    三、项目演示

    1.运行代码


    2.进入项目首页


    3.题目列表

    可以根据需求增加更多的题目 


    4.点击具体一道题


     5.编辑代码并提交


    四、项目思维导图

    超清图片我放在了Gitee上面,需要可以自行下载(文章末尾有链接)


    五、项目宏观结构

    我的项目核心是三个模块:

    1. comm : 公共模块,提供一些时间戳获取,路径拼接,文件操作,字符串处理,网络请求,日志等项目里常用的功能。
    2. compile_server : 编译与运行模块,让用户提交的代码与测试用例拼接后在服务器上进行编译,运行,得到结果返回给用户。
    3. oj_server : 请求题目列表;请求一个具体题目,且有编辑区 ;提交判题请求。采用MVC的设计模式,使用负载均衡,访问文件或数据库,调用编译模块,以及把题目列表和编辑界面展示给用户。

    注意:我们只实现类似 leetcode 的题目列表+在线编程功能。

            用户直接访问的是OJServer模块,OJServer收到请求后会进行功能路由,根据不同的请求给用户返回不同的结果,如果用户是编写题目提交代码,那么OJServer模块会根据后端的CompilerServer服务器的负载情况,负载均衡地选择主机提供的编译运行服务,然后拿到编译运行结果返回给用户。Compiler服务器和OJ服务器,两个模块之间采用http网络通信,把编译运行模块部署在多台服务器上,OJ服务器只需要一台,能够把用户的请求发送给后端CompilerServer。


    六、Comm公共模块

    comm : 公共模块,提供一些时间戳获取,路径拼接,文件操作,字符串处理,网络请求,日志等项目里常用的功能。


    1.日志工具log.hpp

    1. namespace ns_log
    2. {
    3. // 引入公共功能
    4. using namespace ns_util;
    5. // 日志等级
    6. enum
    7. {
    8. INFO, // 常规,正常的
    9. DEBUG, // 调试
    10. WARNING, // 告警
    11. ERROR, // 错误
    12. FATAL // 致命错误
    13. };
    14. // 打印日志
    15. // 参数:日志等级,在哪一个文件,当前在哪一行
    16. // 使用方法:Log() << "message"
    17. inline std::ostream &Log(const std::string &level, const std::string &file_name, int line)
    18. {
    19. // 添加日志等级
    20. std::string message = "[";
    21. message += level;
    22. message += "]";
    23. // 添加报错文件名称
    24. message += "[";
    25. message += file_name;
    26. message +="]";
    27. // 添加报错行
    28. message += "[";
    29. message += std::to_string(line);
    30. message += "]";
    31. // 添加日志时间戳
    32. message += "[";
    33. message += TimeUtil::GetTimeStamp(); // 获取时间戳
    34. message += "]";
    35. std::cout << message; // 不要endl进行刷新
    36. return std::cout;
    37. }
    38. // 再用宏封装一下
    39. // 使用方法:例如:LOG(INFO) << "message" << "\n"
    40. #define LOG(level) Log(#level, __FILE__, __LINE__)
    41. }

    2.其他工具util.hpp

    1. // 传入一个文件名,自动形成路径与后缀
    2. namespace ns_util
    3. {
    4. // 引入存储临时文件的路径
    5. const std::string temp_path = "./temp/";
    6. // 时间功能
    7. class TimeUtil
    8. {
    9. public:
    10. // 获取秒级别时间戳
    11. static std::string GetTimeStamp()
    12. {
    13. struct timeval _time;
    14. gettimeofday(&_time, nullptr); // 时区不关心,所以设置成nullptr
    15. return std::to_string(_time.tv_sec);
    16. }
    17. // 获取毫秒级别时间戳
    18. static std::string GetTimeMs()
    19. {
    20. struct timeval _time;
    21. gettimeofday(&_time, nullptr);
    22. return std::to_string(_time.tv_sec * 1000 + _time.tv_usec / 1000); // 秒->毫秒=秒*1000,微秒->毫秒=微秒/1000
    23. }
    24. };
    25. // 对路径操作的方法
    26. class PathUtil
    27. {
    28. public:
    29. // 构建文件路径+后缀的完整文件名
    30. static std::string AddSuffix(const std::string &file_name, const std::string &suffix)
    31. {
    32. std::string path_name = temp_path; // "./temp/"
    33. path_name += file_name; // "./temp/12345"
    34. path_name += suffix; // "./temp/12345.后缀"
    35. return path_name;
    36. }
    37. /**************************编译时需要有的临时文件**************************/
    38. // 构建源文件路径+后缀的完整文件名
    39. // 例如:12345 -> ./temp/12345.cpp
    40. static std::string Src(const std::string &file_name)
    41. {
    42. return AddSuffix(file_name, ".cpp");
    43. }
    44. // 构建可执行程序的完整路径+后缀名
    45. // 例如:12345 -> ./temp/12345.exe
    46. static std::string Exe(const std::string &file_name)
    47. {
    48. return AddSuffix(file_name, ".exe");
    49. }
    50. // 构建该程序对应的编译时错误文件的完整路径+后缀名
    51. // 例如:12345 -> ./temp/12345.compile_error
    52. static std::string CompilerError(const std::string &file_name)
    53. {
    54. return AddSuffix(file_name, ".compile_error");
    55. }
    56. /**************************运行时需要有的临时文件**************************/
    57. // 构建标准输入文件
    58. static std::string Stdin(const std::string &file_name)
    59. {
    60. return AddSuffix(file_name, ".stdin");
    61. }
    62. // 构建标准输出文件
    63. static std::string Stdout(const std::string &file_name)
    64. {
    65. return AddSuffix(file_name, ".stdout");
    66. }
    67. // 构建标准错误文件
    68. static std::string Stderr(const std::string &file_name)
    69. {
    70. return AddSuffix(file_name, ".stderr");
    71. }
    72. };
    73. // 对文件操作的方法
    74. class FileUtil
    75. {
    76. public:
    77. // 判断文件是否存在
    78. static bool IsFileExists(const std::string &path_name)
    79. {
    80. struct stat st;
    81. if (stat(path_name.c_str(), &st) == 0)
    82. {
    83. // 获取属性成功,表示文件已经存在
    84. return true;
    85. }
    86. return false;
    87. }
    88. // 形成一个唯一的文件名,没有目录没有后缀
    89. // 毫秒级时间戳+原子性递增唯一值: 来保证唯一性
    90. static std::string UniqFileName()
    91. {
    92. static std::atomic_uint id(0); // 原子性的计数器
    93. id++; // 计数器++
    94. std::string ms = TimeUtil::GetTimeMs(); // 毫秒级时间戳
    95. std::string uniq_id = std::to_string(id); // 获得唯一id
    96. return ms + "_" + uniq_id;
    97. }
    98. // 将代码写入该文件
    99. // 参数:target为要写入的文件名,content为要写入的内容
    100. static bool WriteFile(const std::string &target, std::string &content)
    101. {
    102. std::ofstream out(target);
    103. if (!out.is_open())
    104. {
    105. // 如果没有被打开成功
    106. return false;
    107. }
    108. out.write(content.c_str(), content.size()); // 写入内容
    109. out.close(); // 关闭文件
    110. return true;
    111. }
    112. // 把所有的文件内容读出来
    113. // 参数:target为要读取的文件名,content用于存储读到的数据, keep用于判断是否保留\n
    114. static bool ReadFile(const std::string &target, std::string *content, bool keep = false)
    115. {
    116. (*content).clear(); // 先清空content,为了不影响第一次读取
    117. std::ifstream in(target, std::ios::binary);
    118. if (!in.is_open())
    119. {
    120. return false;
    121. }
    122. std::string line;
    123. // 注意:
    124. // getline:不能保存行分隔符
    125. // getline:内部重载了强制类型转化
    126. while (std::getline(in, line))
    127. {
    128. (*content) += line;
    129. (*content) += (keep ? "\n" : ""); // 判断是否保留\n
    130. }
    131. in.close(); // 关闭文件
    132. return true;
    133. }
    134. };
    135. // 字符串工具
    136. class StringUtil
    137. {
    138. public:
    139. // 切分字符串
    140. /******************************************************
    141. * 参数:
    142. * str:输入型,目标要切分的字符串
    143. * target:输出型,保存切分完毕的结果
    144. * sep:指定的分隔符
    145. ******************************************************/
    146. static void SplitString(const std::string &str, std::vector *target, const std::string sep)
    147. {
    148. // 用boost库里面的字符串切分功能
    149. boost::split((*target), str, boost::is_any_of(sep), boost::algorithm::token_compress_on);
    150. }
    151. };
    152. }

    七、CompilerServer模块

    compile_server : 编译与运行模块,让用户提交的代码与测试用例拼接后在服务器上进行编译,运行,得到结果返回给用户。

    1.整体层次如图


    2.编译模块compiler.hpp

    1. // 只负责进行代码的编译
    2. namespace ns_compiler
    3. {
    4. using namespace ns_util; // 引入路径拼接功能
    5. using namespace ns_log; // 引入日志打印功能
    6. // 编译功能
    7. class Compiler
    8. {
    9. public:
    10. Compiler()
    11. {}
    12. ~Compiler()
    13. {}
    14. // 编译代码:
    15. // 返回值:编译成功:true,编译错误:false
    16. // 输入参数:编译的文件名:file_name
    17. // 例如:file_name: 12345(只传进来一个文件名,不带后缀,后缀我们自己添加)
    18. // 我们会生成三个文件
    19. // 12345 -> ./temp/12345.cpp 源文件
    20. // 12345 -> ./temp/12345.exe 可执行文件
    21. // 12345 -> ./temp/12345.stderr 标准错误文件
    22. static bool Compile(std::string &file_name)
    23. {
    24. pid_t pid = fork(); // 创建一个子进程进行编译代码
    25. if (pid < 0)
    26. {
    27. // 如果创建子进程失败就直接退出
    28. LOG(ERROR) << "内部错误,创建子进程失败" << "\n";
    29. return false;
    30. }
    31. else if (pid == 0)
    32. {
    33. umask(0); // 先将umask清零,让其不受平台的影响
    34. // 先将编译时错误文件打开
    35. int _compile_error = open(PathUtil::CompilerError(file_name).c_str(), O_CREAT | O_WRONLY, 0644);
    36. if (_compile_error < 0)
    37. {
    38. // 如果打开文件失败
    39. LOG(WARNING) << "没有成功形成compile_error文件" << "\n";
    40. exit(1);
    41. }
    42. // 重定向标准错误到_compile_error
    43. dup2(_compile_error, 2);
    44. // 注意:程序替换,并不影响进程的文件描述符表
    45. // 子进程:调用编译器,完成对代码的编译工作
    46. // g++ -o target src -std=c++11
    47. execlp("g++", "g++", "-o", PathUtil::Exe(file_name).c_str(),\
    48. PathUtil::Src(file_name).c_str(), "-D", "COMPILER_ONLINE", "-std=c++11", nullptr/*不要忘记写nullptr*/); // 用g++替换当前的子进程
    49. // 如果程序替换失败
    50. LOG(ERROR) << "启动编译器g++失败,可能是参数错误" << "\n";
    51. exit(2);
    52. }
    53. else
    54. {
    55. // 父进程
    56. waitpid(pid, nullptr, 0);
    57. // 判断编译是否成功(判断是否生成可执行文件)
    58. if (FileUtil::IsFileExists(PathUtil::Exe(file_name)))
    59. {
    60. LOG(INFO) << PathUtil::Src(file_name) << " 编译成功!" << "\n";
    61. return true;
    62. }
    63. }
    64. // 如果父进程走到这里,就说明编译失败,没有形成可执行文件
    65. LOG(ERROR) << "编译失败,没有形成可执行文件!" << "\n";
    66. return false;
    67. }
    68. };
    69. }


    3.运行模块runner.hpp

    1. namespace ns_runner
    2. {
    3. using namespace ns_util; // 引入路径拼接功能
    4. using namespace ns_log; // 引入日志打印功能
    5. class Runner
    6. {
    7. public:
    8. Runner()
    9. {}
    10. ~Runner()
    11. {}
    12. public:
    13. // 提供设置进程占用资源大小的接口
    14. static void SetProcLimit(int _cpu_limit, int _mem_limit)
    15. {
    16. // 设置cpu占用时长
    17. struct rlimit cpu_rlimit;
    18. cpu_rlimit.rlim_max = RLIM_INFINITY;
    19. cpu_rlimit.rlim_cur = _cpu_limit;
    20. setrlimit(RLIMIT_CPU, &cpu_rlimit);
    21. // 设置内存大小
    22. struct rlimit mem_rlimit;
    23. mem_rlimit.rlim_max = RLIM_INFINITY;
    24. mem_rlimit.rlim_cur = _mem_limit * 1024; // 乘以1024,让单位KB变成字节
    25. setrlimit(RLIMIT_AS, &mem_rlimit);
    26. }
    27. // 运行并判断运行是否成功
    28. // 注意:我们这里实现的功能,也只需要指明文件名即可,不需要带路径和后缀
    29. /***************************************************************
    30. * 将这里的返回值设置成int而不设置成bool,是因为:
    31. * 返回值 > 0 : 程序异常了,退出时收到了信号,返回值就是对应的信号编号
    32. * 返回值 == 0 : 程序正常运行完毕的,结果保存到了对应的临时文件中
    33. * 返回值 < 0 : 内部错误:打开文件失败、创建子进程失败等
    34. *
    35. * 参数介绍:
    36. * cpu_limit : 该程序运行的时候,可以使用的最大cpu资源上限
    37. * mem_limit : 该程序运行的时候,可以使用的最大的内存大小(KB)
    38. ***************************************************************/
    39. static int Run(const std::string &file_name, int cpu_limit, int mem_limit )
    40. {
    41. // 获取运行需要的文件
    42. std::string _execute = PathUtil::Exe(file_name); // 获得可执行程序的完整文件名
    43. std::string _stdin = PathUtil::Stdin(file_name); // 获取标准输入文件
    44. std::string _stdout = PathUtil::Stdout(file_name); // 获取标准输出文件
    45. std::string _stderr = PathUtil::Stderr(file_name); // 获取标准错误文件
    46. umask(0); // 先将umask清零,让其不受平台的影响
    47. // 打开运行需要的文件
    48. int _stdin_fd = open(_stdin.c_str(), O_CREAT | O_RDONLY, 0644);
    49. int _stdout_fd = open(_stdout.c_str(), O_CREAT | O_WRONLY, 0644);
    50. int _stderr_fd = open(_stderr.c_str(), O_CREAT | O_WRONLY, 0644);
    51. // 如果有任何一个文件打开失败,就退出程序(因为如果打开失败就无法获取相应的数据和输出结果,再继续运行就没有意义了)
    52. if (_stdin_fd < 0 || _stdout_fd < 0 || _stderr_fd < 0)
    53. {
    54. LOG(ERROR) << "运行时打开标准文件失败" << "\n";
    55. return -1; // 代表文件打开失败
    56. }
    57. // 创建一个子进程运行程序
    58. pid_t pid = fork();
    59. if (pid < 0)
    60. {
    61. LOG(ERROR) << "运行时创建子进程失败" << "\n";
    62. // 如果创建失败,关闭需要的文件
    63. close(_stdin_fd);
    64. close(_stdout_fd);
    65. close(_stderr_fd);
    66. return -2; // 代表创建子进程失败
    67. }
    68. else if (pid == 0)
    69. {
    70. // 子进程创建成功
    71. // 将标准输入,标准错误,标准错误都进行重定向到刚才打开的文件
    72. dup2(_stdin_fd, 0);
    73. dup2(_stdout_fd, 1);
    74. dup2(_stderr_fd, 2);
    75. // 设置资源限制
    76. SetProcLimit(cpu_limit, mem_limit);
    77. // 然后替换程序,将子进程替换成需要的可执行文件
    78. // 参数:带路径的可执行文件,要怎么执行......
    79. execl(_execute.c_str()/*我要执行谁*/, _execute.c_str()/*我想在命令行上如何执行该程序*/, nullptr);
    80. // 如果替换失败
    81. exit(1);
    82. }
    83. else
    84. {
    85. // 父进程用不到这些文件,所以先将其关闭
    86. close(_stdin_fd);
    87. close(_stdout_fd);
    88. close(_stderr_fd);
    89. // 让父进程等待子进程
    90. int status = 0; // 拿到子进程的退出结果
    91. waitpid(pid, &status, 0);
    92. // 程序如果运行异常,一定是因为收到了信号!
    93. LOG(INFO) << "运行完毕,info:" << (status & 0x7f) << "\n";
    94. return status & 0x7f;
    95. }
    96. }
    97. };
    98. }

    4.编译+运行compile_run.hpp

    1. namespace ns_compile_and_run
    2. {
    3. using namespace ns_log; // 引入日志打印功能
    4. using namespace ns_util; // 引入路径拼接功能
    5. using namespace ns_compiler; // 引入编译功能
    6. using namespace ns_runner; // 引入运行功能
    7. class CompileAndRun
    8. {
    9. public:
    10. // 清空所有的临时文件
    11. static void RemoveTempFile(const std::string &file_name)
    12. {
    13. // 特点:清理文件的个数是不确定的,但是有哪些我们是知道的
    14. // 删除源文件
    15. std::string _src = PathUtil::Src(file_name); // 获取源文件的完整路径
    16. if (FileUtil::IsFileExists(_src)) unlink(_src.c_str()); // 如果文件存在,就将其移除
    17. // 删除编译错误文件
    18. std::string _compile_error = PathUtil::CompilerError(file_name);
    19. if (FileUtil::IsFileExists(_compile_error)) unlink(_compile_error.c_str());
    20. // 删除可执行程序
    21. std::string _execute = PathUtil::Exe(file_name);
    22. if (FileUtil::IsFileExists(_execute)) unlink(_execute.c_str());
    23. // 删除标准输入
    24. std::string _stdin = PathUtil::Stdin(file_name);
    25. if (FileUtil::IsFileExists(_stdin)) unlink(_stdin.c_str());
    26. // 删除标准输出
    27. std::string _stdout = PathUtil::Stdout(file_name);
    28. if (FileUtil::IsFileExists(_stdout)) unlink(_stdout.c_str());
    29. // 删除标准错误
    30. std::string _stderr = PathUtil::Stderr(file_name);
    31. if (FileUtil::IsFileExists(_stderr)) unlink(_stderr.c_str());
    32. }
    33. // 将信号转化成为报错的原因
    34. // code > 0 : 进程收到了信号导致异常错误
    35. // code < 0 : 整个过程非运行报错(代码为空,编译报错等)
    36. // cod == 0 : 整个过程全部完成
    37. // 待完善...
    38. static std::string CodeToDesc(int code, const std::string file_name)
    39. {
    40. std::string desc;
    41. switch (code)
    42. {
    43. case 0:
    44. desc = "编译运行成功";
    45. break;
    46. case -1:
    47. desc = "用户提交的代码是空";
    48. break;
    49. case -2:
    50. desc = "未知错误";
    51. break;
    52. case -3: // 代码编译的时候发生了错误
    53. FileUtil::ReadFile(PathUtil::CompilerError(file_name), &desc, true); // 读取编译报错的文件
    54. break;
    55. case SIGABRT: // 信号6
    56. desc = "内存超过范围";
    57. break;
    58. case SIGXCPU: // 信号24
    59. desc = "CPU使用超时";
    60. break;
    61. case SIGFPE: // 信号8
    62. desc = "浮点数溢出";
    63. break;
    64. default:
    65. desc = "未知" + std::to_string(code);
    66. break;
    67. }
    68. return desc;
    69. }
    70. /*****************************************************************************
    71. * 输入:
    72. * code: 用户提交的代码
    73. * input: 用户给自己提交的代码对应的输入,不做处理
    74. * cpu_limit: 时间要求
    75. * mem_limit: 空间要求
    76. *
    77. * 输出:
    78. * 必填
    79. * status: 状态码
    80. * reason: 请求结果
    81. * 选填:
    82. * stdout: 我的程序运行完的结果
    83. * stderr: 我的程序运行完的错误结果
    84. *
    85. * 参数:
    86. * in_json: {"code": "#include...", "input": "","cpu_limit":1, "mem_limit":10240}
    87. * out_json: {"status":"0", "reason":"","stdout":"","stderr":"",}
    88. *****************************************************************************/
    89. static void Start(const std::string &in_json, std::string *out_json)
    90. {
    91. // 将拿到的数据做反序列化
    92. Json::Value in_value;
    93. Json::Reader reader;
    94. reader.parse(in_json, in_value);
    95. std::string code = in_value["code"].asString(); // 拿到用户提交的代码
    96. std::string input = in_value["input"].asString(); // 拿到用户输入的数据
    97. int cpu_limit = in_value["cpu_limit"].asInt(); // 拿到上层给的cpu占用时长
    98. int mem_limit = in_value["mem_limit"].asInt(); // 拿到上层给的内存大小
    99. // 创建输出返回的数据
    100. int status_code = 0; // 状态码
    101. Json::Value out_value;
    102. int run_result = 0; // 运行结果
    103. std::string file_name; // 需要内部形成的唯一文件名(因为goto语句之间不能出现定义,所以就在这里定义)
    104. if (code.size() == 0)
    105. {
    106. status_code = -1; // 代码为空
    107. goto END;
    108. }
    109. // 形成的文件名只具有唯一性,没有目录没有后缀
    110. file_name = FileUtil::UniqFileName();
    111. // 形成临时的src文件
    112. if (!FileUtil::WriteFile(PathUtil::Src(file_name), code))
    113. {
    114. // 如果写入文件失败
    115. status_code = -2; // 未知错误
    116. goto END;
    117. }
    118. // 编译
    119. if (!Compiler::Compile(file_name))
    120. {
    121. // 如果编译失败
    122. status_code = -3; // 代码编译的时候发生了错误
    123. goto END;
    124. }
    125. run_result = Runner::Run(file_name, cpu_limit, mem_limit); // 运行
    126. if (run_result < 0)
    127. {
    128. // 内部错误
    129. status_code = -2;
    130. ; // 未知错误
    131. goto END;
    132. }
    133. else if (run_result > 0)
    134. {
    135. // 程序运行崩溃了
    136. status_code = run_result;
    137. }
    138. else
    139. {
    140. // 运行成功
    141. status_code = 0;
    142. }
    143. END:
    144. out_value["status"] = status_code;
    145. out_value["reason"] = CodeToDesc(status_code, file_name); // 将错误码转化成错误描述
    146. if (status_code == 0)
    147. {
    148. // 整个过程全部成功:
    149. // 读取标准输出文件
    150. std::string _stdout;
    151. FileUtil::ReadFile(PathUtil::Stdout(file_name), &_stdout, true);
    152. out_value["stdout"] = _stdout;
    153. // 读取标准输出文件
    154. std::string _stderr;
    155. FileUtil::ReadFile(PathUtil::Stderr(file_name), &_stderr, true);
    156. out_value["stderr"] = _stderr;
    157. }
    158. // 序列化过程
    159. Json::StyledWriter writer;
    160. *out_json = writer.write(out_value);
    161. // 清空所有的临时文件
    162. RemoveTempFile(file_name);
    163. }
    164. };
    165. }

    5.Compiler模块compiler_server.cc

    1. // 使用手册
    2. void Usage(std::string proc)
    3. {
    4. std::cerr << "Usage: " << "\n\t" << proc << " port" << std::endl;
    5. }
    6. // 调用方式:./compile_server port
    7. int main(int argc, char *argv[])
    8. {
    9. if (argc != 2)
    10. {
    11. // 如果使用方法不对
    12. Usage(argv[0]);
    13. return 1;
    14. }
    15. Server svr; // 创建一个服务器
    16. // 参数:req:用户的需求,resp:服务器的相应
    17. svr.Post("/compile_and_run", [](const Request &req, Response &resp){
    18. // 用户请求的服务正文就是我们想要的json string
    19. // in_json: {"code": "#include...", "input": "","cpu_limit":1, "mem_limit":10240}
    20. // out_json: {"status":"0", "reason":"","stdout":"","stderr":"",}
    21. std::string in_json = req.body;
    22. std::string out_json;
    23. if (!in_json.empty())
    24. {
    25. // 编译并运行用户传过来的代码
    26. CompileAndRun::Start(in_json, &out_json);
    27. // 要响应的内容
    28. resp.set_content(out_json, "application/json;charset=utf_8");
    29. }
    30. });
    31. // 让服务器在所有ip,指定端口服务
    32. svr.listen("0.0.0.0", atoi(argv[1])); // 启动http服务
    33. return 0;
    34. }

    八、基于MVC结构的OJServer模块

            OJServer模块是直接和用户交互的,用户访问OJ系统,我需要有一个首页,其次需要有一个题目列表网页供用户选择题目,再者还需要一个可以给用户写代码做题的网页,并且可以提交代码,判断用户提交的代码是否正确。


    总结用户的请求分为三种:

    1. 请求题目列表
    2. 请求一个具体的题目,并且需要有编译区域
    3. 提交,判题请求 OJServer模块主要要根据这三种请求提供对应的功能。

    整个模块采用的是MVC的设计模式进行设计
    通过这个设计模式,把数据,业务逻辑和网页界面进行了分离。

    1. 什么是MVC结构

            经典MVC模式中,M是指业务模型,V是指用户界面(视图),C则是控制器,使用MVC的目的是将M和V的实现代码分离,从而使同一个程序可以使用不同的表现形式。其中,View的定义比较清晰,就是用户界面。 

    • M:model表示的是模型,代表业务规则。在MVC的三个部件中,模型拥有最多的处理任务。被模型返回的数据时中立的,模型与数据格式无关,这样一个模型就能够为多个视图提供数据,由于应用于模型的代码只需要写一次就可以被多个视图重用,所以减少了代码的重复性,
    • V:view表示的视图,代表用户看到并与之交互的界面。在视图中没有真正的处理发生,它只是作为一种输出数据并允许用户操作的方式。
    • C:controller表示的是控制器,控制器接收用户的输入并调用模型(M)和视图(V)去完成用户需求。控制器本身不输出任何东西和任何处理。它只接收请求并决定调用那个模型构建去处理请求,然后再确定用那个视图来显示返回的数据。

    整个模块就包含四个部分:

    1. oj_model模块:负责模块前两个功能的数据部分,通过与题库交互,得到所有题目的信息或者某一个题目的信息
    2. oj_view模块:负责渲染用户得到网页,根据用户提交的不同请求,渲染不同的题目信息
    3. oj_control模块:负责整个OJServer模块的业务逻辑控制。对下负责负载均衡式的选择主机请求编译服务,对上根据用户的三种请求,配合上面两个模块,完成对应的功能。
    4. oj_server模块:搭建http服务,根据用户的请求,完成功能路由,调用control模块的对应方法完成功能


    2.整体层次如图


    3.Model模块oj_model.hpp

            oj_model模块主要是和数据交互的,这里的数据就是我们后端文件或者数据库当中的题目信息,题目应该包含如下的信息:

    1. 题目的编号(1)
    2. 题目的标题(求最大值)
    3. 题目的难度(简单、中等、困难)
    4. 题目的时间要求(1s)
    5. 题目的空间要求(30000KB)
    6. 题目的描述(给定一个数组,求最大值)
    7. 题目预设给用户在线编辑的代码(#include...)
    8. 题目的测试用例

            到这里我们就需要有自己对应的题库,我们这个模块当中新增一个目录questions,用来存放我们的题库,这个questions目录下包含题目列表(文件形式)和每个题目的文件夹(其中又包含题目的描述、题目预设给用户在线编辑的代码header和题目的测试用例tail)


    3.1文件版本

    1. // 文件版
    2. namespace ns_model
    3. {
    4. using namespace std;
    5. using namespace ns_log;
    6. using namespace ns_util;
    7. struct Question
    8. {
    9. string number; // 题目编号(唯一)
    10. string title; // 题目的标题
    11. string star; // 题目的难度:简单 中等 困难
    12. int cpu_limit; // 题目的时间要求(S)
    13. int mem_limit; // 题目的空间要求(KB)
    14. string desc; // 题目的描述
    15. string header; // 题目预设给用户在线编辑器的代码
    16. string tail; // 题目的测试用例,需要和header拼接,形成完整代码
    17. };
    18. const string questions_list = "./questions/questions.list"; // 配置文件的路径
    19. const string question_path = "./questions/"; // 题库路径
    20. class Model
    21. {
    22. private:
    23. // 题号:题目细节
    24. unordered_map questions;
    25. public:
    26. Model()
    27. {
    28. // 将题目加载进来
    29. assert(LoadQuestionList(questions_list));
    30. }
    31. bool LoadQuestionList(const string &question_list)
    32. {
    33. // 加载配置文件:questions/questions.list + 题目编号文件
    34. ifstream in(question_list);
    35. if (!in.is_open())
    36. {
    37. // 如果配置文件打开失败
    38. LOG(FATAL) << "加载题库失败,请检查是否存在题库文件" << "\n";
    39. return false;
    40. }
    41. // 进行行读取题目
    42. string line;
    43. while (getline(in, line))
    44. {
    45. // 将一行的字符串(题目描述)进行切分
    46. vector tokens;
    47. StringUtil::SplitString(line, &tokens, " ");
    48. // 例如:1 判断回文数 简单 1 30000
    49. if (tokens.size() != 5)
    50. {
    51. // 如果配置的内容有问题
    52. LOG(WARNING) << "加载部分题目失败,请检查文件格式" << "\n";
    53. continue;
    54. }
    55. // 进行填充question
    56. Question q;
    57. q.number = tokens[0];
    58. q.title = tokens[1];
    59. q.star = tokens[2];
    60. q.cpu_limit = atoi(tokens[3].c_str());
    61. q.mem_limit = atoi(tokens[4].c_str());
    62. // 指定题库路径
    63. string path = question_path;
    64. path += q.number; // 加上题号
    65. path += "/";
    66. // 读取所有文件里面的内容,并填充到q中
    67. FileUtil::ReadFile(path+"desc.txt", &(q.desc), true);
    68. FileUtil::ReadFile(path+"header.cpp", &(q.header), true);
    69. FileUtil::ReadFile(path+"tail.cpp", &(q.tail), true);
    70. // 最后将单个题目q提交到题库中(哈希表)
    71. questions.insert({q.number, q});
    72. }
    73. LOG(INFO) << "加载题库...成功!" << "\n";
    74. in.close(); // 关闭配置文件
    75. return true;
    76. }
    77. // 获取所有的题目
    78. bool GetAllQuestions(vector *out)
    79. {
    80. if (questions.size() == 0)
    81. {
    82. // 如果没有题目
    83. LOG(ERROR) << "用户获取题库失败" << "\n";
    84. return false;
    85. }
    86. for (const auto &q : questions)
    87. {
    88. out->push_back(q.second); // first: key, second value
    89. }
    90. return true;
    91. }
    92. // 获取单个题目
    93. bool GetOneQuestion(const string &number, Question *q)
    94. {
    95. const auto& iter = questions.find(number);
    96. if (iter == questions.end())
    97. {
    98. // 如果没有找到该题目
    99. LOG(ERROR) << "用户获取题目失败,题目编号:" << number << "\n";
    100. return false;
    101. }
    102. (*q) = iter->second; // 获取题目成功
    103. return true;
    104. }
    105. ~Model()
    106. {}
    107. };
    108. }

    3.2数据库版本

    1. // MySQL版本
    2. // 根据题目list文件,加载所有的题目信息到内存中
    3. // model:主要用来和数据进行交互,对外提供访问数据的接口
    4. namespace ns_model
    5. {
    6. using namespace std;
    7. using namespace ns_log;
    8. using namespace ns_util;
    9. struct Question
    10. {
    11. string number; // 题目编号(唯一)
    12. string title; // 题目的标题
    13. string star; // 题目的难度:简单 中等 困难
    14. string desc; // 题目的描述
    15. string header; // 题目预设给用户在线编辑器的代码
    16. string tail; // 题目的测试用例,需要和header拼接,形成完整代码
    17. int cpu_limit; // 题目的时间要求(S)
    18. int mem_limit; // 题目的空间要求(KB)
    19. };
    20. const std::string oj_questions = "oj_questions"; // 要访问的表名
    21. const std::string host = "127.0.0.1"; // ip为本地服务器
    22. const std::string user = "oj_client"; // MySQL用户名
    23. const std::string passwd = "xujiaming+520"; // MySQL密码
    24. const std::string db = "oj"; // 要连接的数据库名
    25. const int port = 3306; // MySQL的端口号
    26. class Model
    27. {
    28. public:
    29. Model()
    30. {
    31. }
    32. // 查询MySQL
    33. // 参数:sql:sql查询语句,out:输出查询结果
    34. bool QueryMySql(const std::string &sql, vector *out)
    35. {
    36. // 创建MySQL句柄
    37. MYSQL *my = mysql_init(nullptr);
    38. // 连接数据库
    39. if (nullptr == mysql_real_connect(my, host.c_str(), user.c_str(), passwd.c_str(), db.c_str(), port, nullptr, 0))
    40. {
    41. // 如果连接失败
    42. LOG(FATAL) << "连接数据库失败!" << "\n";
    43. return false;
    44. }
    45. // 一定要设置该链接的编码格式,要不然会出现乱码问题
    46. mysql_set_character_set(my, "utf8");
    47. LOG(INFO) << "连接数据库成功!" << "\n";
    48. // 执行sql语句
    49. if (0 != mysql_query(my, sql.c_str()))
    50. {
    51. // 如果执行失败
    52. LOG(WARNING) << sql << " execute error!" << "\n";
    53. return false;
    54. }
    55. // 提取结果
    56. MYSQL_RES * res = mysql_store_result(my);
    57. // 分析结果
    58. int rows = mysql_num_rows(res); // 获得行数
    59. int cols = mysql_num_fields(res); // 获得列数
    60. // 获得每行每列的数据
    61. for (int i = 0; i < rows; ++i)
    62. {
    63. MYSQL_ROW row = mysql_fetch_row(res); // 拿到当前这一行的所有数据(这里的row是一个二级指针)
    64. Question q; // 用于保存结果
    65. // 拿到当前行,每列的所有数据
    66. q.number = row[0];
    67. q.title = row[1];
    68. q.star = row[2];
    69. q.desc = row[3];
    70. q.header = row[4];
    71. q.tail = row[5];
    72. q.cpu_limit = atoi(row[6]);
    73. q.mem_limit = atoi(row[7]);
    74. // 将当前题的所有信息放到返回数组里面
    75. out ->push_back(q);
    76. }
    77. // 释放结果空间
    78. free(res);
    79. // 关闭MySQL连接
    80. mysql_close(my);
    81. return true;
    82. }
    83. // 获取所有的题目
    84. bool GetAllQuestions(vector *out)
    85. {
    86. std::string sql = "select * from ";
    87. sql += oj_questions;
    88. return QueryMySql(sql, out);
    89. }
    90. // 获取单个题目
    91. bool GetOneQuestion(const string &number, Question *q)
    92. {
    93. bool res = false;
    94. std::string sql = "select * from ";
    95. sql += oj_questions;
    96. sql += " where number=";
    97. sql += number;
    98. vector result;
    99. if (QueryMySql(sql, &result)) // 判断是否获取题目成功
    100. {
    101. // 判断获取的题目个数是否只有1个
    102. if (result.size() == 1)
    103. {
    104. *q = result[0];
    105. res = true;
    106. }
    107. }
    108. return res;
    109. }
    110. ~Model()
    111. {
    112. }
    113. };
    114. }

    4.题库的结构

    题目的属性大致可以分为2类:

    1. 一种是题目编号,题目标题,题目难度,时间限制和内存限制这些字段,这些字段都比较小,可以把所有题目的这些信息存在一个文件里面。
    2. 另一种是题目描述,预置代码,测试用例等等,这类信息一般都比较大,可以根据题目编号给每道题建立一个与编号对应的文件夹,然后用三个文件保存这三个信息,到时候就可以通过题目编号找到题目对应的路径,然后读取对应的文件,不仅读取方便,还便于我们录题。
       


    5.view模块oj_view.hpp

            oj_view模块负责渲染给用户显示的网页。比如说用户请求访问题目列表,题目列表里的题目信息是从我们后端的题库中得到的,而把这些信息显示到网页上,这就是渲染网页。所有说view模块也应该提供两个接口,一个渲染题目列表,一个渲染单个题目的网页。


     我们需要引入一个第三方库ctemplate;功能如下:

    1. namespace ns_view
    2. {
    3. using namespace ns_model;
    4. const std::string template_path = "./template_html/"; // 要渲染的路径
    5. class View
    6. {
    7. public:
    8. View()
    9. {}
    10. ~View()
    11. {}
    12. public:
    13. // 将所有的题目数据构建成网页
    14. void AllExpandHtml(const vector<struct Question> &questions, std::string *html)
    15. {
    16. // 题目编号 题目标题 题目难度
    17. // 使用表格显示
    18. // 1. 形成路径
    19. std::string src_html = template_path + "all_questions.html"; // 要被渲染的网页
    20. // 2. 形成数据字典
    21. ctemplate::TemplateDictionary root("all_questions");
    22. // 形成一个子字典
    23. for (const auto &q : questions)
    24. {
    25. ctemplate::TemplateDictionary *sub = root.AddSectionDictionary("question_list");
    26. sub->SetValue("number", q.number);
    27. sub->SetValue("title", q.title);
    28. sub->SetValue("star", q.star);
    29. }
    30. // 3. 获取被渲染的网页
    31. ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP);
    32. // 4. 开始完成渲染功能
    33. tpl->Expand(html, &root); // 这个root字典里面会包含所有的子字典
    34. }
    35. // 将单个题目的所有数据构建成网页
    36. void OneExpand(const struct Question &q, std::string *html)
    37. {
    38. // 1. 形成路径
    39. std::string src_html = template_path + "one_question.html"; // 要被渲染的网页
    40. // 2. 形成数据字典
    41. ctemplate::TemplateDictionary root("one_questions");
    42. // 2.1 插入题目描述
    43. root.SetValue("number", q.number); // 题号
    44. root.SetValue("title", q.title); // 题目标题
    45. root.SetValue("star", q.star); // 题目难度
    46. root.SetValue("desc", q.desc); // 题目描述
    47. root.SetValue("pre_code", q.header); // 题目预设代码
    48. // 3. 获取被渲染的网页
    49. ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP);
    50. // 4. 开始完成渲染功能
    51. tpl->Expand(html, &root); // 这个root字典里面会包含所有的子字典
    52. }
    53. };
    54. }

    6.oj_control模块oj_control.hpp

            oj_control模块是整个OJSever模块的逻辑功能部分,在上层做好了功能路由之后,通过调用control模块实现各个功能,所有oj_control模块既要可以返回对应的网页,还要可以负载均衡的判题。

    提供三个功能:

    1. 构建好题目列表网页的接口
    2. 根据题目编号构建好单个题目网页的接口
    3. 判题接口。

    6.1.构建题目列表和单个题目网页

    1. // 根据题目数据构建网页
    2. // html:输出型参数
    3. bool AllQuestions(string *html)
    4. {
    5. bool ret = true;
    6. vector<struct Question> all;
    7. if (model_.GetAllQuestions(&all))
    8. {
    9. // 给题目按编号进行排序
    10. sort(all.begin(), all.end(), [](const struct Question &q1, const struct Question &q2){
    11. return atoi(q1.number.c_str()) < atoi(q2.number.c_str());
    12. });
    13. // 获取题目信息成功,将所有的题目数据构建成网页
    14. view_.AllExpandHtml(all, html);
    15. }
    16. else
    17. {
    18. // 获取失败
    19. *html = "获取题目失败,形成题目列表失败";
    20. ret = false;
    21. }
    22. return ret;
    23. }
    24. // 根据题号构建网页
    25. bool Question(const string &number, string *html)
    26. {
    27. bool ret = true;
    28. struct Question q;
    29. if (model_.GetOneQuestion(number, &q))
    30. {
    31. // 获取指定题目信息成功,将该题目的所有数据构建成网页
    32. view_.OneExpand(q, html);
    33. }
    34. else
    35. {
    36. // 获取失败
    37. *html = "指定题目:" + number + " 不存在!";
    38. ret = false;
    39. }
    40. return ret;
    41. }

    6.2.负载均衡模块

    6.2.1机器类的设计

            负载均衡模块,最重要的功能就是可以负载均衡式的选择主机,我如何得知有哪些主机可以供我选择,怎么实现负载均衡。

            所以模块内部要有一个结构包含提供服务的主机信息,用来表述主机的结构命名为Machine,然后用一个vector把所有提供服务的主机组织起来。

            Machine类里有主机的IP,端口,还有负载情况。负载均衡判断的依据就是看主机的负载,所有类里还要提供方法,在有新请求请求该机器时增加负载,服务结束时减少负载,如果中途服务主机突然挂了,还要可以清空负载。
    因为同一时刻可能有多个执行流在请求同一个主机,所有需要保证对负载操作的安全性,需要一个mutex互斥锁。


    6.2.2负载均衡模块设计

            将来选择主机可以在vector中选,在此之前需要知道有哪些主机可以选,在当前路径下加一个.conf文件里面会存放所有的可以提供服务的主机信息,包括了IP和端口,每一行是一个主机的信息,负载均衡模块在构建时就可以读取该文件,初始化自己的vector结构。
            然后就是选择主机功能,首先在同一时刻可能有很多执行流都在选择主机,所以对主机的选择需要加锁,也就是说负载均衡模块也需要一个互斥锁。
            设计在control模块调用负载均衡模块时,如果说后端的编译服务主机出问题挂了,不应该影响我的OJServer服务,OJ服务正常运行,编译服务如果恢复了,那我正常请求,如果有一部分挂了,那我请求别的主机,全挂我就不请求,提示后端。
            这个功能就由负载均衡模块负责,负载均衡模块除了可以选择主机,还要能够知道主机的情况,并能够根据情况更新。使用数组的下标表示每一个主机的编号,用两个数组,一个表示上线主机,元素的值就是主机编号,另一个表示下线的主机。提供方法,在后端编译服务重启时可以更新状态让主机上线,当请求主机失败时要更新状态让主机下线。


    6.2.3负载均衡的实现:

            负载均衡就是尽量让每一台机器负责的请求平均,那就需要从所有在线的主机中选择出对应的主机。一是随机挑选主机,但是这种方法不能排除有时候一直选中某几台,有几台又一直选不上。还有一种比较严格,遍历所有在线的主机,找出负载最小的。


     
    1. // 提供服务的主机
    2. class Machine
    3. {
    4. public:
    5. std::string ip; // 编译服务的ip
    6. int port; // 编译服务的端口号
    7. uint64_t load; // 编译服务的负载(计数器)
    8. std::mutex *mtx; // 用于保护计数器的锁(注意:cpp中的mutex是禁止拷贝的,所以我们这里定义指针来进行后面的拷贝)
    9. public:
    10. Machine()
    11. :ip(""), port(0), load(0), mtx(nullptr)
    12. {}
    13. ~Machine()
    14. {}
    15. public:
    16. // 提升主机负载
    17. void IncLoad()
    18. {
    19. if (mtx) mtx->lock(); // 加锁
    20. ++load;
    21. if (mtx) mtx->unlock(); // 解锁
    22. }
    23. // 减小主机负载
    24. void DecLoad()
    25. {
    26. if (mtx) mtx->lock(); // 加锁
    27. --load;
    28. if (mtx) mtx->unlock(); // 解锁
    29. }
    30. // 将主机的负载清零
    31. void ResetLoad()
    32. {
    33. if (mtx) mtx->lock();
    34. load = 0;
    35. if (mtx) mtx->unlock();
    36. }
    37. // 获取主机负载(没有太大的意义,只是为了统一接口)
    38. uint64_t Load()
    39. {
    40. uint64_t _load = 0;
    41. if (mtx) mtx->lock(); // 加锁
    42. _load = load;
    43. if (mtx) mtx->unlock(); // 解锁
    44. return _load;
    45. }
    46. };
    47. const std::string service_machine = "./conf/service_machine.conf"; // 提供服务的主机列表的路径
    48. // 负载均衡模块
    49. class LoadBalance
    50. {
    51. private:
    52. std::vector machines; // 可以给我们提供编译服务的主机(每一台主机都有自己的下标,充当当前主机的id)
    53. std::vector<int> online; // 所有在线的主机id
    54. std::vector<int> offline; // 所有离线的主机id
    55. std::mutex mtx; // mtx是LoadBalance的锁,是保证LoadBalance它的数据安全(注意:每个Machine也有自己的小锁,不要与这里的mtx搞混)
    56. public:
    57. LoadBalance()
    58. {
    59. assert(LoadConf(service_machine));
    60. LOG(INFO) << "加载 " << service_machine << " 成功" << "\n";
    61. }
    62. ~LoadBalance()
    63. {}
    64. public:
    65. // 将主机加载进来
    66. // 参数:machine_list:主机列表
    67. bool LoadConf(const std::string &machine_conf)
    68. {
    69. std::ifstream in(machine_conf);
    70. if (!in.is_open())
    71. {
    72. // 如果打开文件失败
    73. LOG(FATAL) << "加载:" << machine_conf << " 失败" << "\n";
    74. return false;
    75. }
    76. std::string line;
    77. while (std::getline(in, line))
    78. {
    79. // 按行读取machine_conf文件数据
    80. // 进行字符串切割:将字符串分成两部分:ip和port
    81. std::vector tokens;
    82. StringUtil::SplitString(line, &tokens, ":");
    83. if (tokens.size() != 2)
    84. {
    85. // 如果切分出来的字符串不是ip和port这两部分
    86. LOG(WARNING) << " 切分 " << line << " 失败" << "\n";
    87. continue;
    88. }
    89. Machine m;
    90. m.ip = tokens[0];
    91. m.port = atoi(tokens[1].c_str());
    92. m.load = 0;
    93. m.mtx = new std::mutex();
    94. online.push_back(machines.size()); // 上线当前主机
    95. machines.push_back(m);
    96. }
    97. in.close(); // 关闭文件
    98. return true;
    99. }
    100. // 智能选择合适的主机提供服务
    101. // 参数:
    102. // id:输出型参数
    103. // m:输出型参数
    104. bool SmartChoice(int *id, Machine **m)
    105. {
    106. // 1. 使用选择好的主机(更新该主机的负载)
    107. // 2. 我们需要可能离线的主机
    108. mtx.lock(); // 将选择功能加锁
    109. // 使用的负载均衡算法:轮询 + hash
    110. int online_num = online.size(); // 主机在线数
    111. if (online_num == 0)
    112. {
    113. // 如果所有的主机都离线了
    114. mtx.unlock(); // 将选择功能解锁
    115. LOG(FATAL) << " 所有的后端编译主机已经离线,请运维的人尽快查看" << "\n";
    116. return false;
    117. }
    118. // 通过遍历的方式,找到所有负载最小的机器
    119. *id = online[0]; // 默认最小负载的机器
    120. *m = &machines[online[0]]; // 默认最小负载主机的地址
    121. uint64_t min_load = machines[online[0]].Load(); // 默认最小负载数
    122. for (int i = 1; i < online_num; ++i)
    123. {
    124. uint64_t cur_load = machines[online[i]].Load();
    125. if (min_load > cur_load)
    126. {
    127. min_load = cur_load;
    128. *id = online[i];
    129. *m = &machines[online[i]];
    130. }
    131. }
    132. mtx.unlock(); // 将选择功能解锁
    133. return true;
    134. }
    135. // 离线指定主机
    136. void OfflineMachine(int which)
    137. {
    138. mtx.lock(); // 将离线功能加锁(因为在离线的同时,有可能有人正在进行智能选择)
    139. // 遍历在线主机列表,找到要离线的主机
    140. for (auto iter = online.begin(); iter != online.end(); ++iter)
    141. {
    142. if (*iter == which)
    143. {
    144. // 先将要离线的主机的负载清零,不然后面再将其上线的时候,负载还是和现在一样
    145. machines[which].ResetLoad();
    146. // 要离线的主机找到了,将其进行离线
    147. online.erase(iter);
    148. offline.push_back(which); // 注意:这里不能写成offline.push_back(*iter);因为这样为导致迭代器失效
    149. break; // 因为break的存在,所以我们暂时不用考虑迭代器失效的问题
    150. }
    151. }
    152. mtx.unlock(); // 将离线功能解锁
    153. }
    154. // 上线对应的主机
    155. void OnlineMachine()
    156. {
    157. // 规定:当所有主机都离线的时候,我们统一上线
    158. mtx.lock(); // 将上线功能加锁
    159. // 将离线列表里面的所有主机插入到上线列表里面,并删除离线列表里面的所有主机
    160. online.insert(online.end(), offline.begin(), offline.end());
    161. offline.erase(offline.begin(), offline.end());
    162. mtx.unlock(); // 将上线功能解锁
    163. LOG(INFO) << "所有的主机又上线啦!" << "\n";
    164. }
    165. // 显示所有在线和离线的主机(仅仅用于测试)
    166. void ShowMachines()
    167. {
    168. mtx.lock(); // 加锁
    169. std::cout << "当前在线主机列表: ";
    170. for (auto &id : online)
    171. {
    172. std::cout << id << " ";
    173. }
    174. std::cout << std::endl;
    175. std::cout << "当前离线主机列表: ";
    176. for (auto &id : offline)
    177. {
    178. std::cout << id << " ";
    179. }
    180. std::cout << std::endl;
    181. mtx.unlock(); // 解锁
    182. }
    183. };

    6.2.4判题模块

            得到的参数是需要判的题目编号和用户传进来的json串形式的代码,通过题目编号,调用model模块得到题目相关的信息,然后通过反序列化用户传来的代码,得到代码内容。有了题目的信息和用户的代码,就可以拼接出可以用来编译的源码内容,构建出CompilerServer需要的json串。请求后端编译服务器主机。
            选到主机之后通过主机的IP+端口,使用网络请求方式发起请求,除了通过请求的返回值判断请求是否成功,还需要判断请求的状态码,只有状态呢是200才表示请求成功。且需要更新请求时机器的负载情况。

    1. // 判题功能
    2. // 参数:
    3. // number:题号,in_json:客户上传上来的代码,out_json:要返回的结果
    4. void Judge(const std::string &number, const std::string in_json, std::string *out_json)
    5. {
    6. // LOG(DEBUG) << in_json << "\nnumber: " << number << "\n";
    7. // 0. 根据题目编号,直接拿到对应的题目细节
    8. struct Question q;
    9. model_.GetOneQuestion(number, &q);
    10. // 1. 将in_json进行反序列化,得到题目id,得到用户提交的源代码(input)
    11. Json::Reader reader;
    12. Json::Value in_value;
    13. reader.parse(in_json, in_value); // 参数:你想要反序列化谁,你想要反序列化的json_value是谁
    14. std::string code = in_value["code"].asString();
    15. // 2. 重新拼接用户代码+测试用例代码,形成新的代码
    16. Json::Value compile_value;
    17. compile_value["input"] = in_value["input"].asString();
    18. compile_value["code"] = code + "\n" + q.tail; // 注意这里需要加一个换行符,否则可能会将#ifndef拼接到 ‘;’ 后面,导致拼接错误
    19. compile_value["cpu_limit"] = q.cpu_limit;
    20. compile_value["mem_limit"] = q.mem_limit;
    21. Json::FastWriter writer;
    22. std::string compile_string = writer.write(compile_value); // 将拼接好的代码进行序列化
    23. // 3. 选择负载最低的主机(差错处理)
    24. // 规则:一直选择,直到主机可用,否则,就是全部挂掉
    25. while (true)
    26. {
    27. int id = 0;
    28. Machine *m = nullptr;
    29. // 进行智能选择
    30. if (!load_balance_.SmartChoice(&id, &m))
    31. {
    32. // 如果选择失败,那么所有的主机都已经挂掉了
    33. break;
    34. }
    35. // 4. 然后发起http请求,得到结果
    36. Client cli(m->ip, m->port);
    37. m->IncLoad(); // 增加主机负载
    38. LOG(INFO) << " 选择主机成功, 主机id: " << id << ", 详情: " << m->ip << ":" << m->port << ", 当前主机的负载是:" << m->Load() << "\n";
    39. if (auto res = cli.Post("/compile_and_run", compile_string, "application/json;charset=utf_8"))
    40. {
    41. if (res->status == 200)
    42. {
    43. // 5. 如果http请求成功,将编译运行后的结果赋值给out_json
    44. *out_json = res->body;
    45. m->DecLoad(); // 减少主机负载
    46. LOG(INFO) << "请求编译和运行服务成功..." << "\n";
    47. break;
    48. }
    49. m->DecLoad(); // 减少主机负载
    50. }
    51. else
    52. {
    53. // 请求失败
    54. LOG(ERROR) << " 当前请求的主机id: " << id << ", 详情: " << m->ip << ":" << m->port << ", 可能已经离线" << "\n";
    55. load_balance_.OfflineMachine(id); // 将当前主机离线(将主机离线后负载会自动清零)
    56. load_balance_.ShowMachines(); // 仅仅是为了用来调试
    57. }
    58. }
    59. }

    7.oj_server模块oj_server.cc

            搭建一个http服务,通过用户请求的不同资源,完成功能路由的任务,调用oj_control模块的功能。

    1. static Control *ctrl_ptr = nullptr; // 定义一个控制器指针,让其既可以局部使用,也可以全局使用
    2. // 使用手册
    3. void Usage(std::string proc)
    4. {
    5. std::cerr << "Usage: " << "\n\t" << proc << " port" << std::endl;
    6. }
    7. // 写一个恢复主机的回调方法
    8. void Recovery(int signo)
    9. {
    10. ctrl_ptr->RecoveryMachine();
    11. }
    12. // 调用方式:./compile_server port
    13. int main(int argc, char *argv[])
    14. {
    15. // 捕捉3号信号(ctrl + \).用快捷键(ctrl + \)一键上线所有离线的主机
    16. signal(SIGQUIT, Recovery);
    17. if (argc != 2)
    18. {
    19. // 如果使用方法不对
    20. Usage(argv[0]);
    21. return 1;
    22. }
    23. // 用户请求的服务路由功能
    24. Server svr; // 创建有个服务器
    25. Control ctrl; // 创建一个控制器
    26. ctrl_ptr = &ctrl;
    27. // 获取所有的题目列表
    28. // 参数:req:用户的需求,resp:服务器的相应
    29. svr.Get("/all_questions", [&ctrl](const Request &req, Response &resp){
    30. // 返回一张包含所有题目的html网页
    31. std::string html;
    32. ctrl.AllQuestions(&html);
    33. resp.set_content(html, "text/html; charset=utf-8");
    34. });
    35. // 用户要根据题目编号,获取题目的内容
    36. // /question/100 -> 正则匹配
    37. // R"()"作用:原始字符串raw string,保持字符串内容的原貌,不用做相关的转义
    38. svr.Get(R"(/question/(\d+))", [&ctrl](const Request &req, Response &resp){
    39. std::string number = req.matches[1]; // 拿到题号
    40. std::string html;
    41. ctrl.Question(number, &html);
    42. resp.set_content(html, "text/html; charset=utf-8");
    43. });
    44. // 用户提交代码,使用我们的判题功能(1. 每道题的测试用例 2. compile_and_run)
    45. svr.Post(R"(/judge/(\d+))", [&ctrl](const Request &req, Response &resp){
    46. std::string number = req.matches[1]; // 拿到题号
    47. std::string result_json;
    48. ctrl.Judge(number, req.body, &result_json);
    49. resp.set_content(result_json, "application/json;charset=utf-8");
    50. // resp.set_content("指定题目的判题:" + number, "text/plain;charset=utf-8");
    51. });
    52. svr.set_base_dir("./wwwroot");
    53. // 让服务器在所有ip,指定端口服务
    54. svr.listen("0.0.0.0", atoi(argv[1])); // 启动http服务
    55. return 0;
    56. }

    九、前端页面的设计(了解)

    前端简单使用:
    html/css/js/jquery/ajax
    Ace前端在线编辑器


    1. indx.html

    当用户访问根目录时显示的网页

    1. "UTF-8">
    2. "X-UA-Compatible" content="IE=edge">
    3. "viewport" content="width=device-width, initial-scale=1.0">
    4. 这是我的个人OJ系统
    5. class="container">
    6. class="navbar">
  • class="content">
  • class="font_">欢迎来到我的OnlineJudge平台

  • class="font_">这个我个人独立开发的一个在线OJ平台


  • 2. all_questions.html

    当用户获取题目列表的时候显示的网页 

    1. "en">
    2. "UTF-8">
    3. "X-UA-Compatible" content="IE=edge">
    4. "viewport" content="width=device-width, initial-scale=1.0">
    5. 在线OJ-题目列表
    6. class="container">
    7. class="navbar">
  • class="question_list">
  • OnlineJuge题目列表

  • {{#question_list}}
  • {{/question_list}}
  • class="item">编号class="item">标题class="item">难度
    class="item">{{number}}class="item">"/question/{{number}}">{{title}}class="item">{{star}}
  • class="footer">
  • @不一样的烟火a


  • 3. one_question.html

    当用户获取单道题目所显示的网页

    1. "en">
    2. "UTF-8">
    3. "X-UA-Compatible" content="IE=edge">
    4. "viewport" content="width=device-width, initial-scale=1.0">
    5. {{number}}.{{title}}
    6. class="container">
    7. class="navbar">
    8. class="part1">
    9. class="left_desc">
    10. "number">{{number}}.{{title}}_{{star}}

    11. {{desc}}
    12. class="right_code">
    13. "code" class="ace_editor">
    14. class="part2">
    15. class="result">

    十、项目扩展

    1. 基于注册和登陆的录题功能
    2. 业务扩展,自己写一个论坛,接入到在线OJ中
    3. 即便是编译服务在其他机器上,也其实是不太安全的,可以将编译服务部署在docker
    4. 目前后端compiler的服务我们使用的是http方式请求(仅仅是因为简单),但是也可以将我们的compiler服务,设计成为远程过程调用,推荐:rest_rpc,替换我们的httplib(建议,可以不做)
    5. 功能上更完善一下,判断一道题目正确之后,自动下一道题目
    6. navbar中的功能可以一个一个的都实现一下
    7. 其他
       

    十一、项目所需工具

    1.升级 gcc

    1. $ gcc -v
    2. Using built-in specs.
    3. COLLECT_GCC=gcc
    4. COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/lto-wrapper
    5. Target: x86_64-redhat-linux
    6. Configured with: ../configure --prefix=/usr --mandir=/usr/share/man --
    7. infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enablebootstrap --enable-shared --enable-threads=posix --enable-checking=release --with-systemzlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --
    8. enable-linker-build-id --with-linker-hash-style=gnu --enable-languages=c,c++,objc,objc++,java,fortran,ada,go,lto --enable-plugin --enable-initfini-array --disable-libgcj --
    9. with-isl=/builddir/build/BUILD/gcc-4.8.5-20150702/obj-x86_64-redhat-linux/isl-install --
    10. with-cloog=/builddir/build/BUILD/gcc-4.8.5-20150702/obj-x86_64-redhat-linux/cloog-install -
    11. -enable-gnu-indirect-function --with-tune=generic --with-arch_32=x86-64 --build=x86_64-
    12. redhat-linux
    13. Thread model: posix
    14. gcc version 4.8.5 20150623 (Red Hat 4.8.5-44) (GCC)
    15. cpp-httplib 用老的编译器,要么编译不通过,要么直接运行报错
    16. 百度搜索:scl gcc devsettool升级gcc
    17. //安装scl
    18. $ sudo yum install centos-release-scl scl-utils-build
    19. //安装新版本gcc,这里也可以把7换成8或者9,我用的是9,也可以都安装
    20. $ sudo yum install -y devtoolset-7-gcc devtoolset-7-gcc-c++
    21. $ ls /opt/rh/
    22. //启动: 细节,命令行启动只能在本会话有效
    23. $ scl enable devtoolset-7 bash
    24. $ gcc -v
    25. //可选:如果想每次登陆的时候,都是较新的gcc,需要把上面的命令添加到你的~/.bash_profile中
    26. $ cat ~/.bash_profile
    27. # .bash_profile
    28. # Get the aliases and functions
    29. if [ -f ~/.bashrc ]; then
    30. . ~/.bashrc
    31. fi
    32. # User specific environment and startup programs
    33. PATH=$PATH:$HOME/.local/bin:$HOME/bin
    34. export PATH
    35. #添加下面的命令,每次启动的时候,都会执行这个scl命令
    36. scl enable devtoolset-7 bash
    37. or
    38. scl enable devtoolset-8 bash
    39. or
    40. scl enable devtoolset-9 bash

    2.安装 jsoncpp

    1. $ sudo yum install -y jsoncpp-devel
    2. [sudo] password for whb:
    3. Loaded plugins: aliases, auto-update-debuginfo, fastestmirror, protectbase
    4. Repository epel is listed more than once in the configuration
    5. Loading mirror speeds from cached hostfile
    6. * base: mirrors.aliyun.com
    7. * epel-debuginfo: mirrors.tuna.tsinghua.edu.cn
    8. * extras: mirrors.aliyun.com
    9. * updates: mirrors.aliyun.com
    10. 0 packages excluded due to repository protections
    11. Package jsoncpp-devel-0.10.5-2.el7.x86_64 already installed and latest version
    12. //我已经安装了

    3.安装 cpp-httplib

    1. 最新的cpp-httplib在使用的时候,如果gcc不是特别新的话有可能会有运行时错误的问题
    2. 建议:cpp-httplib 0.7.15
    3. 下载zip安装包,上传到服务器即可
    4. cpp-httplib gitee链接:https://gitee.com/yuanfeng1897/cpp-httplib?_from=gitee_search
    5. v0.7.15版本链接: https://gitee.com/yuanfeng1897/cpp-httplib/tree/v0.7.15
    6. 把httplib.h拷贝到我们的项目中即可,就这么简单
    7. 使用样例:
    8. $ cat http_server.cc
    9. #include "httplib.h"
    10. int main()
    11. {
    12. httplib::Server svr;
    13. svr.Get("/hi", [](const httplib::Request &req, httplib::Response &rsp){
    14. rsp.set_content("你好,世界!", "text/plain; charset=utf-8");
    15. });
    16. svr.listen("0.0.0.0", 8080);
    17. return 0;
    18. }
    19. 更多的细节可以看gitee上面的使用手册

    4.安装boost库

    $ sudo yum install -y boost-devel //是boost 开发库

    5.安装与测试 ctemplate
     

    1. # 国内github镜像网站,如果挂掉可以私信老师要
    2. https://hub.fastgit.xyz/OlafvdSpek/ctemplate
    3. $ git clone https://hub.fastgit.xyz/OlafvdSpek/ctemplate.git
    4. $ ./autogen.sh
    5. $ ./configure
    6. $ make //编译
    7. $ make install //安装到系统中
    8. # 注意gcc版本
    9. # 如果安装报错,注意使用sudo
    10. 测试样例:
    11. 1. 建立文件
    12. $ ll
    13. total 8
    14. -rw-rw-r-- 1 whb whb 529 May 12 11:52 test.cc
    15. -rw-rw-r-- 1 whb whb 230 May 12 11:52 test.html
    16. 2. 编写ctemplate代码
    17. $ cat test.cc
    18. #include
    19. #include
    20. #include
    21. int main()
    22. {
    23. std::string html = "./test.html";
    24. std::string html_info = "Hello";
    25. //建立ctemplate参数目录结构
    26. ctemplate::TemplateDictionary root("test"); //unordered_map test;
    27. //向结构中添加你要替换的数据,kv的
    28. root.SetValue("info", html_info); //test.insert({key, value});
    29. //获取被渲染对象
    30. ctemplate::Template *tpl = ctemplate::Template::GetTemplate(html,
    31. ctemplate::DO_NOT_STRIP); //DO_NOT_STRIP:保持html网页原貌
    32. //开始渲染,返回新的网页结果到out_html
    33. std::string out_html;
    34. tpl->Expand(&out_html, &root);
    35. std::cout << "渲染的带参html是:" << std::endl;
    36. std::cout << out_html << std::endl;
    37. return 0;
    38. }
    39. 3. 编写简单html
    40. $ cat test.html
    41. {{info}}

    42. {{info}}

    43. {{info}}

    44. {{info}}

    45. 4. 编译
    46. $ g++ test.cc -o mytest -lctemplate -lpthread
    47. $ ls
    48. mytest test.cc test.html
    49. $ ./mytest
    50. 5. 对比结果
    51. 渲染前:
    52. {{info}}

    53. {{info}}

    54. {{info}}

    55. {{info}}

    56. 渲染后:
    57. 渲染的带参html是:
    58. Hello

    59. Hello

    60. Hello

    61. Hello


    6.使用Ace在线编辑器

    直接复制粘贴即可

    1. html>
    2. <html lang="en">
    3. <head>
    4. <meta charset="UTF-8">
    5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
    6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
    7. <title>Ace测试title>
    8. <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ace.js"
    9. type="text/javascript"
    10. charset="utf-8">script>
    11. <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ext-language_tools.js"
    12. type="text/javascript"
    13. charset="utf-8">script>
    14. <style>
    15. * {
    16. margin: 0;
    17. 比特就业课
    18. padding: 0;
    19. }
    20. html,
    21. body {
    22. width: 100%;
    23. height: 100%;
    24. }
    25. div .ace_editor {
    26. height: 600px;
    27. width: 100%;
    28. }
    29. style>
    30. head>
    31. <body>
    32. <div>
    33. <pre id="code" class="ace_editor"><textarea class="ace_textinput">#include<iostream>
    34. int
    35. main()
    36. {
    37. std::cout << "hello ace editor" << std::endl;
    38. return 0;
    39. }textarea>pre>
    40. <button class="bt" onclick="submit()">提交代码button><br/>
    41. div>
    42. <script>
    43. //初始化对象
    44. editor = ace.edit("code");
    45. //设置风格和语言(更多风格和语言,请到github上相应目录查看)
    46. // 主题大全:http://www.manongjc.com/detail/25-cfpdrwkkivkikmk.html
    47. editor.setTheme("ace/theme/monokai");
    48. editor.session.setMode("ace/mode/c_cpp");
    49. // 字体大小
    50. editor.setFontSize(16);
    51. // 设置默认制表符的大小:
    52. editor.getSession().setTabSize(4);
    53. // 设置只读(true时只读,用于展示代码)
    54. editor.setReadOnly(false);
    55. // 启用提示菜单
    56. ace.require("ace/ext/language_tools");
    57. editor.setOptions({
    58. enableBasicAutocompletion: true,
    59. enableSnippets: true,
    60. enableLiveAutocompletion: true
    61. });
    62. script>
    63. body>
    64. html>

    7.MySQL 建表

    1. CREATE TABLE IF NOT EXISTS `questions`(
    2. id int PRIMARY KEY AUTO_INCREMENT COMMENT '题目的ID',
    3. title VARCHAR(64) NOT NULL COMMENT '题目的标题',
    4. star VARCHAR(8) NOT NULL COMMENT '题目的难度',
    5. question_desc TEXT NOT NULL COMMENT '题目描述',
    6. header TEXT NOT NULL COMMENT '题目头部,给用户看的代码',
    7. tail TEXT NOT NULL COMMENT '题目尾部,包含我们的测试用例',
    8. time_limit int DEFAULT 1 COMMENT '题目的时间限制',
    9. mem_limit int DEFAULT 5000000 COMMENT '题目的空间限制'
    10. )ENGINE=INNODB DEFAULT CHARSET=utf8;

    十二、项目源码

            项目到这里就已经圆满结束了,但是由于篇幅已经很长了,项目中有些地方只给了文字说明与框架,具体的完整实现代码和项目的超清思维导图我会放在Gitee上供大家参考。

    项目源码icon-default.png?t=N7T8https://gitee.com/what-you-want-a/load-balanced-online-oj

  • 相关阅读:
    FreeRTOS使用总结
    基于JAVA+SpringBoot的学生成长管理评价系统
    Himall商城安装帮助类AES加密解密(2)
    JMeter笔记7 | JMeter脚本回放
    php-fpm自定义zabbix监控
    怎么给自己的网站主页配置SSL证书?
    再服务器上配置其他版本的DGL
    卡尔曼滤波(Kalman Filter)原理浅析-数学理论推导-1
    将java的项目jar包打成镜像
    注册登录首选,趣味滑块验证码
  • 原文地址:https://blog.csdn.net/qq_64042727/article/details/133611734