在线OJ项目(三)

在线OJ项目(三)

一、回顾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. 对源文件进行编译

需要创建子进程完成来执行编译命令,并对子进程进行进程程序替换

compile.hpp:

	//const std::string& filename:代表源码文件的前缀
	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");
            snprintf(Command[5], 49, "%s", "-lpthread");
            Command[5] = 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);
                //重定向,把标准错误重定向到文件
                dup2(fd, 2);
                //程序替换
                execvp(Command[0], Command);
                //注意这个exit是非常重要的,如果替换失败,就直接exit掉
                exit(0);
            }
            else
            {
                //father
                waitpid(pid, NULL, 0);
            }
            //3.验证是否生产可执行程序
        }

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

		static bool Run()
        {

        }

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

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

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

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"

class Compiler
{
    public:
        //有可能浏览器对不同的题目提交的数据是不同的
        //code="xxx"
        //code="xxx"&stdin="xxx"  code + tail.cpp
        static void CompileAndRun(Json::Value Req, Json::Value* Resp)
        {
            //1.判空
            //{"code":"xxx", "stdin":"xxx"}
            if(Req["code"].empty())
            {
                LOG(ERROR, "Request code is Empty") << std::endl;
                return;
            }

            //2.将代码写到文件当中去
            std::string code = Req["code"].asString();
            //文件名称进行约定 tmp_时间戳.cpp
            std::string tmp_filename = WriteTmpFile(code);
            if(tmp_filename == "")
            {
                LOG(ERROR, "Write Source failed");
                return;
            }
            //3.编译
            if(!Compile())
            {

            }
            //4.运行
            if(!Run())
            {

            }
            //5.构造响应
        }
   	
   		//生成对应文件后缀的文件名
        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";
        }
};

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

接收用户提交代码的完整代码:
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.构造响应,json
            std::string html = "1";
            resp.set_content(html,"text/html; charset=UTF-8");
            });

未完待续…

发布了150 篇原创文章 · 获赞 89 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/wolfGuiDao/article/details/105243393