标签搜索

目 录CONTENT

文章目录

在线OJ项目(三).md

小小城
2021-08-22 / 0 评论 / 0 点赞 / 5 阅读 / 12,231 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2022-05-02,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

在线OJ项目(三)

@[toc]

一、回顾oj_server模块整体框架:

oj_server.cpp

#include <stdio.h>
#include <string>
#include <string.h>
#include "httplib.h"
#include "oj_model.hpp"
#include "oj_view.hpp"
#include "oj_log.hpp"
#include "compile.hpp"

int main()
{
    using namespace httplib;
    Server svr;
    //1.获取题目列表接口
    
    //2.获取单个题目接口
    
    //3.服务器接收用户通过浏览器提交code接口 
    svr.listen("0.0.0.0", 19999);
    return 0;
}

在线OJ项目(二)当中已经完成了第二步:获取单个题目接口并返回给浏览器。

二、提供用户向服务器提交代码的接口

1. 提供一个数据解码接口

前面已经实现用户可以获取单个题目的详细信息,并且可以进行作答,现在我们应该在oj_server模块当中提供一个接收用户提交的代码

但是用户在写完代码后,通过浏览器向服务器提交代码,浏览器会对用户提交的内容进行‘转译’(原因是提交的数据当中可能有一些特殊符号),所以服务器收到的数据是经过’转译‘后的,所以我们应该将其解码

在工具模块提供数据解码的接口

tool.hpp:

class UrlUtil
{
    public:
    	//const std::string& body:待解码的数据
    	//std::unordered_map<std::string, std::string>* pram:代表保存
    	//解码出来的数据,是K-V型
        static void PraseBody(const std::string& body, std::unordered_map<std::string, std::string>* pram)
        {
        	//这里进行切割是为了后续项目的拓展,比如力扣是支持stdin的
            //code=xxx&stdin=xxxx
            //进行切割
            std::vector<std::string> tokens;
            StringTools::Split(body, "&", &tokens);
            for(const auto& token:tokens)
            {
                //code=xxxx(xxx是真正用户提交的代码)
                std::vector<std::string> vec;
                StringTools::Split(token, "=", &vec);
				//代表切割后的数据不完整
                if(vec.size() != 2)
                {
                    continue;
                }
                //k:vec[0] ---》name
                //v:UrlDecode(vec[1])---》xxx
                (*pram)[vec[0]] = UrlDecode(vec[1]);
            }
        }
    private:
        static unsigned char ToHex(unsigned char x) 
        { 
            return  x > 9 ? x + 55 : x + 48; 
        }

        static unsigned char FromHex(unsigned char x) 
        { 
            unsigned char y;
            if (x >= 'A' && x <= 'Z') y = x - 'A' + 10;
            else if (x >= 'a' && x <= 'z') y = x - 'a' + 10;
            else if (x >= '0' && x <= '9') y = x - '0';
            else assert(0);
            return y;
        }


        static std::string UrlEncode(const std::string& str)
        {
            std::string strTemp = "";
            size_t length = str.length();
            for (size_t i = 0; i < length; i++)
            {
                if (isalnum((unsigned char)str[i]) || 
                        (str[i] == '-') ||
                        (str[i] == '_') || 
                        (str[i] == '.') || 
                        (str[i] == '~'))
                    strTemp += str[i];
                else if (str[i] == ' ')
                    strTemp += "+";
                else
                {
                    strTemp += '%';
                    strTemp += ToHex((unsigned char)str[i] >> 4);
                    strTemp += ToHex((unsigned char)str[i] % 16);

                }

            }
            return strTemp;

        }

        static std::string UrlDecode(const std::string& str)
        {
            std::string strTemp = "";
            size_t length = str.length();
            for (size_t i = 0; i < length; i++)
            {
                if (str[i] == '+') strTemp += ' ';
                else if (str[i] == '%')
                {
                    assert(i + 2 < length);
                    unsigned char high = FromHex((unsigned char)str[++i]);
                    unsigned char low = FromHex((unsigned char)str[++i]);
                    strTemp += high*16 + low;

                }
                else strTemp += str[i];
            }
            return strTemp;
        }
};

2. 对用户提交的代码和题目的main函数进行拼接

因为用户提交上来的代码可能只是一个函数接口,我们需要在服务器后台oj_model模块提供一个接口,把这道题的相关头文件及main函数信息进行拼接成一份完整的代码

oj_model.hpp:

	//std::string user_code:用户提交并经过解码后的代码
	//const std::string& ques_id:这道题的id,根据这个id找对应的tail.cpp
	//std::string* code:将拼接完成后的完整代码返回
bool SplicingCode(std::string user_code, const std::string& ques_id, std::string* code)
        {
            //1.查找下对应id的题目是否存在
            auto iter = model_map_.find(ques_id);
            if(iter == model_map_.end())
            {
                LOG(ERROR, "can not find question id is ") << ques_id << std::endl;
                return false;
            }
			
			//获取tail.cpp内容
            std::string tail_code;
            int ret = FileOper::ReadDataFromFile(TailPath(iter->second.path_), &tail_code);
            if(ret < 0)
            {
                LOG(ERROR, "Open tail.cpp failed");
                return false;
            }

			//拼接
            *code = user_code + tail_code;
            return true;
        }

3. 将code写入到文件并对文件进行命名的接口

服务器可能会在同一时刻收到多个用户提交的代码,都需要进行编译,但是我们是用的g++编译器,所以用时间戳来对编译文件进行命名。

compile.hpp:

 private:
 		//const std::string& code:代表完整的代码
        static std::string WriteTmpFile(const std::string& code)
        {
            //1.组织文件名称,组织文件的前缀名称,用来区分源码文件,可执行文件是同一组数据
            std::string tmp_filename = "tmp_" + std::to_string(LogTime::GetTimeStamp());
            //写文件
            int ret = FileOper::WriteDataToFile(SrcPath(tmp_filename), code); 
            if(ret < 0)
            {
                LOG(ERROR, "Write code to source failed");
                return "";
            }
            //返回这个命名文件的前缀,后面对文件进行命名的时候会用到,用来区分是否是同一个用户请求的数据
            return tmp_filename;
        }

4. 对源文件进行编译

  1. 需要创建子进程完成来执行编译命令,并对子进程进行进程程序替换
  2. 判断是否成功编译出可执行文件(stat函数)

stat函数通过文件名称来获取文件信息

int stat(const char *path, struct stat *buf);
参数解释
const char *path文件的路径及名称
struct stat *buf保存文件的信息
返回值成功返回0,失败返回-1,ENOENT错误码:文件不存在;ENOTDIR:传入文件的路径错误

stat结构体

 struct stat {
               dev_t     st_dev;     /* ID of device containing file */
               ino_t     st_ino;     /* inode number */
               mode_t    st_mode;    /* protection */
               nlink_t   st_nlink;   /* number of hard links */
               uid_t     st_uid;     /* user ID of owner */
               gid_t     st_gid;     /* group ID of owner */
               dev_t     st_rdev;    /* device ID (if special file) */
               off_t     st_size;    /* total size, in bytes */
               blksize_t st_blksize; /* blocksize for file system I/O */
               blkcnt_t  st_blocks;  /* number of 512B blocks allocated */
               time_t    st_atime;   /* time of last access */
               time_t    st_mtime;   /* time of last modification */
               time_t    st_ctime;   /* time of last status change */
           };

compile.hpp:

	static bool Compile(const std::string& filename)
        {
            //1.构造编译命令--》g++ src -o [exec] -std=c++11
            const int commandcount = 20;
            char buf[commandcount][50] = {{0}};
            char* Command[commandcount] = {0}; 
            for(int i = 0; i < commandcount; i++)
            {
                Command[i] = buf[i];
            }
            snprintf(Command[0], 49, "%s", "g++");
            snprintf(Command[1], 49, "%s", SrcPath(filename).c_str());
            snprintf(Command[2], 49, "%s", "-o");
            snprintf(Command[3], 49, "%s", ExePath(filename).c_str());
            snprintf(Command[4], 49, "%s", "-std=c++11");
            //-D是用命令行创建一个宏CompileOnline,这个宏的作用就是为了编译时不报错,没有其他大作用
            snprintf(Command[5], 49, "%s", "-D");
            snprintf(Command[6], 49, "%s", "CompileOnline");
            Command[7] = NULL;
            //2.创建子进程
            //   2.1 父进程 --》 等待子进程退出
            //   2.2 子进程 --》 进程程序替换--》 g++
            int pid = fork();
            if(pid < 0)
            {
                LOG(ERROR, "Create child process failed");
                return false;
            }
            else if(pid == 0)
            {
                //child
                //如果程序编译错误,就将错误信息写入到对应文件前缀的错误文件当中
                int fd = open(ErrorPath(filename).c_str(), O_CREAT | O_RDWR, 0664);
                if(fd < 0)
                {
                    LOG(ERROR, "open Compile errorfile failed") << ErrorPath(filename) << std::endl;
                    exit(1);
                }

                //重定向
                dup2(fd, 2);
                //程序替换
                execvp(Command[0], Command);
                perror("execvp");
                LOG(ERROR, "execvp failed") << std::endl;
                exit(0);
            }
            else
            {
                //father
                waitpid(pid, NULL, 0);
            }
            //3.验证是否生产可执行程序
           struct stat st; 
           int ret = stat(ExePath(filename).c_str(), &st);
           if(ret < 0)
           {
               LOG(ERROR, "Compile ERROR! Exe filename is ") << ExePath(filename) << std::endl;
               return false;
           }
           return true;
        }

5. 对编译出来的结果运行

  1. 同样是fork出子进程,让子进程进行进程程序替换,执行编译出来的可执行程序
  2. 执行成功,需要把结果重定向到标准输出文件;执行错误,需要把结果重定向到标准错误文件
  3. 对程序对运行时间和内存进行限制

setrlimit函数对进程进行限制

int setrlimit(int resource, const struct rlimit *rlim);
参数解释
int resource代表将要改变的值,RLIMIT_AS:进程的最大虚拟内存空间,用来限制进程运行的内存大小;RLIMIT_DATA:进程数据段的大小;RLIMIT_STACK:进程最大的线程栈大小,都是以字节为单位
const struct rlimit *rlim具体的限制内容

rlimit结构体:

struct rlimit {
               rlim_t rlim_cur;  /* 软限制Soft limit:内核给当前进程资源的限制 */
               rlim_t rlim_max;  /* 硬限制Hard limit (ceiling for 
               					rlim_cur):操作系统当前所能给进程提供的最大资源,RLIM_INFINITY代表无限制 */
           };

compile.hpp:

		static int Run(std::string& filename)
        {
            //可执行程序
            //1.创建子进程
            //  父进程 进程等待
            //  子进程 替换编译出来的程序
            int pid  = fork();
            if(pid < 0)
            {
                LOG(ERROR, "Exec pragma failed! Create child process failed") << std::endl;
                return -1;
            }
            else if(pid == 0)
            {
                //对于子进程执行的限制
                //1.时间限制
                //    alarm
                alarm(1);
                
                //2.内存大小限制
                struct rlimit rl;
                rl.rlim_cur = 1024 * 30000;
                rl.rlim_max = RLIM_INFINITY; //无限制
                setrlimit(RLIMIT_AS, &rl);
                
                //child
                //  获取 :标准输出--》重定向到文件
                int stdout_fd = open(StdoutPath(filename).c_str(), O_CREAT | O_RDWR, 0664);
                if(stdout_fd < 0)
                {
                    LOG(ERROR, "Open stdout file failed") << StdoutPath(filename) << std::endl;
                    return -1;
                }
                dup2(stdout_fd, 1);
                //  标准错误--》重定向到文件
                int stderr_fd = open(StderrPath(filename).c_str(), O_CREAT | O_RDWR, 0664);
                if(stderr_fd < 0)
                {
                    LOG(ERROR, "Open stderr file failed") << StderrPath(filename) << std::endl;
                    return -1;
                }
                dup2(stdout_fd, 2);
                
                execl(ExePath(filename).c_str(), ExePath(filename).c_str(), NULL);
                exit(1);
            }

            //father
            int Status = -1;
            waitpid(pid, &Status, 0);
            //将是否收到信号的信息返回给调用者,如果调用者判断是0, 则正常运行完毕,否则收到看了某个信号异常结束的
            return Status & 0x7f;
        }

6. 构造运行结果的响应给浏览器

题目运行结果的html页面仍然使用谷歌的模版技术进行填充渲染:

oj_view.hpp:

//const std::string& errorno:错误原因
//const std::string& reason:返回的结果
//const std::string& stdout_reason:标准输出的结果
//std::string* html:渲染完成后的页面
static void ExpandReason(const std::string& errorno, const std::string& reason, const std::string& stdout_reason, std::string* html)
        {
            ctemplate::TemplateDictionary dict("reason");
            dict.SetValue("errorno", errorno);
            dict.SetValue("reason", reason);
            dict.SetValue("stdout", stdout_reason);

            ctemplate::Template* tpl = ctemplate::Template::GetTemplate("./template/reason.html", ctemplate::DO_NOT_STRIP);
            tpl->Expand(html, &dict);
        }
<html>
    <head>
        <title>oj</title>
    </head>
    <body>
        <div><pre>{{errorno}}</pre></div>
        <div><pre>{{reason}}</pre></div>
        <div><pre>{{stdout}}</pre></div>
    </body>
</html>

7. 清理文件函数

unlink:删除文件

int unlink(const char *pathname);

compile.hpp:

 	static void Clean(std::string filename)
        {
            unlink(SrcPath(filename).c_str());
            unlink(ExePath(filename).c_str());
            unlink(ErrorPath(filename).c_str());
            unlink(StdoutPath(filename).c_str());
            unlink(StderrPath(filename).c_str());
        }

8. 完整拼接代码并进行编译运行的接口

完整拼接代码并进行编译运行,并构造响应返回给浏览器的接口

compile.hpp:

#pragma once
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <string>
#include <json/json.h>

#include "oj_log.hpp"
#include "tools.hpp"

enum ErrorNo
{
    OK = 0,
    COMPILE_ERROR,
    RUN_ERROR,
    PRAM_ERROR,
    INTERNAL_ERROR
};

class Compiler
{
    public:
        //有可能浏览器对不同的题目提交的数据是不同的
        //code="xxx"
        //code="xxx"&stdin="xxx"  code + tail.cpp
        //有可能浏览器对不同的题目提交的数据是不同的
        //code="xxx"
        //code="xxx"&stdin="xxx"  code + tail.cpp
        static void CompileAndRun(Json::Value Req, Json::Value* Resp)
        {
        	//规定:
            //0.编译运行没有错误
            //1.编译错误
            //2.运行错误
            //3.参数错误
            //4.内存错误
            
            //1.判空
            //{"code":"xxx", "stdin":"xxx"}
            if(Req["code"].empty())
            {
                (*Resp)["errorno"] = PRAM_ERROR;
                (*Resp)["reason"] = "Pram error";
                LOG(ERROR, "Request Commande is Empty") << std::endl;
                return;
            }

            //2.将代码写到文件当中去
            std::string code = Req["code"].asString();
            //文件名称进行约定 tmp_时间戳.cpp
            std::string tmp_filename = WriteTmpFile(code);
            if(tmp_filename == "")
            {
                (*Resp)["errorno"] = INTERNAL_ERROR;
                (*Resp)["reason"] = "Create file failed";
                LOG(ERROR, "Write Source failed");
                return;
            }
            //3.编译
            if(!Compile(tmp_filename))
            {
                (*Resp)["errorno"] = COMPILE_ERROR;
                std::string reason;
                FileOper::ReadDataFromFile(ErrorPath(tmp_filename), &reason);
                (*Resp)["reason"] = reason; 
                LOG(ERROR, "Compile Error") << std::endl;
                return;
            }
            //4.运行
            //因为运行结果有两种情况,需要判断
            int sig = Run(tmp_filename);
            if(sig != 0) 
            {
                (*Resp)["errorno"] = RUN_ERROR;
                (*Resp)["reason"] = "Program exit by sig " + std::to_string(sig);
                LOG(ERROR, "Run Error") << std::endl;
                return;
            }
            //5.构造响应
            (*Resp)["errorno"] = OK;
            (*Resp)["reason"] = "Compile and run is okey!";
            
            //标准输出
            std::string stdout_reason;
            FileOper::ReadDataFromFile(StdoutPath(tmp_filename), &stdout_reason);
            (*Resp)["stdout"] = stdout_reason;
            
            //标准错误
            std::string stderr_reason;
            FileOper::ReadDataFromFile(StderrPath(tmp_filename), &stderr_reason);
            (*Resp)["stderr"] = stderr_reason;

            //6.清理掉临时文件
            Clean(tmp_filename);
            return;
        }
   	
   		//生成对应文件后缀的文件名
        static std::string SrcPath(const std::string& filename)
        {
            return "./tmp_files" + filename + ".cpp";
        }

        static std::string ErrorPath(const std::string& filename)
        {
            return "./tmp_files" + filename + ".err";
        }

        static std::string ExePath(const std::string& filename)
        {
            return "./tmp_files" + filename + ".executable";
        }
};

9. 接收用户提交的代码完整代码

接收用户提交代码的完整代码:
oj_server.cpp:

		 svr.Post(R"(/question/(\d+))", [&ojmodel](const Request& req, Response& resp){
            //key:value
            //1.从正文当中提取出来提交的内容。主要是提取code字段所对应的内容
            //  提交的内容当中有url编码--》提交内容进行 解码
            //  提取完成后的数据放到 unordered_map<std::string, std::string>
            std::unordered_map<std::string, std::string> pram;
            UrlUtil::PraseBody(req.body, &pram);
            //for(const auto& pr:pram)
            //{
            //    LOG(INFO, "code ") << pr.second << std::endl;
            //}
            
            //2.编译&运行
            //   2.1 需要给提交的代码增加头文件,测试用例,main函数
            std::string code;
            ojmodel.SplicingCode(pram["code"], req.matches[1].str(), &code); 
            //LOG(INFO, "code ") << code << std::endl;
            Json::Value req_json;
            req_json["code"] = code;
            //req_json["stdin"] = ""
            Json::Value Resp_json;
            Compiler::CompileAndRun(req_json, &Resp_json);
            
            //3.构造响应
            const std::string errorno = Resp_json["errorno"].asString();
            const std::string reason = Resp_json["reason"].asString();
            const std::string stdout_reason = Resp_json["stdout"].asString();
            std::string html;
            OjView::ExpandReason(errorno, reason, stdout_reason, &html);
            resp.set_content(html,"text/html; charset=UTF-8");
            });

THE END~~~

0

评论区