负载均衡的OJ系统

1 项目展示

启动服务之后,首先访问首页会得到首页界面
在这里插入图片描述
点击两个箭头指向的任意地方都可以打开题库
在这里插入图片描述
因为我的数据库只录了一道题,所以只显示了一道,后面可以自己再继续录题,最终的题目会根据编号排好序在列表里展示出来,点击题目标题就可以跳转到题目的编辑页面
在这里插入图片描述
编辑页面左侧有题目的介绍,右侧是代码编辑区,里面已经预置了一部分代码,用户可以直接在给定的函数里面编写,完成题目要求的函数功能。编辑完成后可以点击保存并提交,会把代码提交到后端进行编译,然后把结果返回,无论是代码编译错误还是运行错误,亦或是超时或者内存超出限制,都会把对应的结果与原因展示在网页的下面。

需要用到的技术栈:C++ STL、boost库的字符串分割,cpp-httplib第三方开源网络库,ctemplate第三方开源前端网页渲染库、jsoncpp序列化反序列化,负载均衡的设计算法,MySQL C语言连接数据库。
开发环境:centos 7云服务器,vscode

项目源码:https://gitee.com/tie-zhuTX/LoadBalancedOJ

2 项目基本结构

项目整体分为三大模块:
Comm公共模块,主要提供一些文件操作,字符串处理,网络请求,日志系统等功能,不做专门的编写,需要用到哪个个功能添加进去就可以。
CompilerServer编译运行模块,让用户的代码在自己的服务器上形成临时源文件,并且编译,运行,得到运行结果。
OJServer模块,采用MAC的设计模式,使用算法负载均衡式的调用编译模块以及访问文件或数据库,把题目列表和编辑界面展示给用户。

用户直接访问的是OJServer模块,OJServer收到请求后会进行功能路由,根据不同的请求给用户返回不同的结果,如果用户是提交代码,那么OJServer模块还会更具后端的CompilerServer服务器的负载负载均衡的选择主机提供的编译服务,然后拿到编译的结果在给用户返回。
在这里插入图片描述
编译服务器和OJ服务器,两个模块之间采用网络套接字的方式互相通信,就可以把编译模块部署在服务器的多台机器上,OJ服务只需要一台,能够以集群处理的方式对外输出在线OJ服务

3 CompilerServer模块设计

3.1 整体结构设计

整体编译运行模块的功能是:编译并运行客户端通过网络提交的代码,并得到运行的结果
把整个模块分为4个部分:
compiler模块:只负责代码的编译。拿到待编译代码的文件名,进行编译,并形成对应的可执行或者错误保存编译时错误信息的临时文件。
run模块:只负责运行代码。通过文件名执行指定的可执行程序,并且形成用于保存运行的结果的临时文件方便获得对应的内容。
compiler_run模块:整合编译模块和运行模块,解析用户发来的json串,得到json串形式的代码内容,形成一个可以用来编译的源文件,调用编译和运行两个模块完成功能,构建结果返回给编译服务模块。
compiler_server模块:负责搭建http服务,接收客户端发来的请求,提取出json串形式的代码,然后调用compiler_run模块编译运行,得到结构后返回给客户端。

以下是该模块的整体结构:
在这里插入图片描述下面就是每个模块的设计

#添加日志功能

日志功能整个项目都要使用,所以自然应该属于Comm公共模块,在Comm目录下创建一个专属的log.hpp文件。

首先将日志设计为五个等级,分别为:

NORMAL:表示正常的打印信息
DEBUG:表示用来dubug的信息
WARNING://警告,但是不影响继续使用
ERROR://错误,用户无法正常使用了
DEADLY://致命,别说用户了,服务器自己都不行了

可以定义枚举实现。

设计的的日志的使用方法为:LOG(level)<<“日志信息”<<endl;
根据用户输入的日志等级和消息,打印出该条日志打印的时间,等级,打印文件和所在行

可以编写一个Log函数,参数包括:日志等级,文件名,调用行信息。
然后构建一个对应的字符串,用cout打印但是不刷新,结果就会暂存在输出流的缓冲区中,返回一个标准输出流,这样就可以像cout一样调用日志函数,并且调用时还可以添加信息,谈话再刷新,就可以把完整的内容打印到频幕上。

日志函数就是单纯的给我们构建日志,就可以根据我们设计的结构进行构建,用一个string保存在日志函数构建的所有内容,包括等级,时间,文件和行数等内容,然后再把string输出到标准输出的缓冲区等待调用时拼接后半部分。

因为调用函数需要三个参数,而文件名和调用行可以用过宏得到,所有可以define一下日志函数,用户只需要输入日志等级,剩下两个参数直接调用宏
在这里插入图片描述

获取日期时间格式的时间戳

获取时间属于日志模块需要调用的一个功能,所有它不应该属于日志模块,而应该数据Comm公共模块中的util工具模块。

获取当前时间首先可以使用time函数或者系统调用gettimeofday接口获取当前的时间戳,我这里使用系统调用。

#include <sys/time.h>
int gettimeofday(struct timeval* tv,struct timezone* tz);

tv:输出形参数,是我们要获取到的时间
tz:时区,不关心可以设置为NULL

参数tv的结构如下

struct timeval{
	time_t tv_sec;//秒数
	suseconds_t tv_usec;//微秒
}

其中的tv_sec就是我们想要的秒数形式的时间戳。

但是如果想要打印的时间是日期时间格式,则需要用到strftime函数

#include <time.h>
size_t strftime(char* buf,size_t bufsize,const char* format,const struct tm* tm)

buf:保存转换过后的结果
bufsize:上一个参数的大小
format:转换的形式,一般为"%Y-%m-%d %H:%M:%S"
tm:是一个struct tm类型的数据,如果要使用这个函数,首先要把时间戳转换成struct tm类型。

#include <time.h>
struct tm* localtime(const time_t* timep);

timep:就是把time_t类型的时间戳转换成struct tm*类型

有了这些函数,就可以在buffer里得到调用这个函数的日期时间格式的时间戳了,然后把它转换成string返回出去,这个接口就完成了。

    class TimeUtil
    {
    public:
        static std::string GetTime()
        {
            //获取时间戳
            struct timeval tv;
            gettimeofday(&tv,nullptr);

            //localtime把时间戳转换为struct tm类型
            //调用strftime将tm类型转换成日期时间格式
            char datetime[64];
            strftime(datetime,sizeof(datetime),"%Y-%m-%d %H:%M:%S",localtime(&tv.tv_sec));
            return std::string(datetime);
        }
    };

在这里插入图片描述

3.2 compiler编译模块

编译服务的核心工作就是编译远端提交的代码形成的临时文件,得到得到编译结果。

实现的方法是创建子进程,让子进程执行程序替换,替换成编译代码,然后通过判断是否生成了对应的可执行程序来判断编译是否成功,如果失败就把错误信息保存到一个专门的错误文件中以便返回给用户。

模块拿到的只有对应临时源文件的文件名,在执行过程中可能会生成“错误文件”,可执行程序等临时文件,这些文件总不能乱摆,新建一个tempfile目录,里面专门存放这些临时文件

编译一个文件,源文件名和目标文件名是必不可少的,但是模块的参数只有文件名,就需要通过文件名,拼接出可以用来文件操作或编译的有路径和后缀的完整文件名,可以把这个功能封装成为一个路径工具类,属于是一种工具,可以在Comm模块的until模块进行编写

这里的错误文件时用来存放编译是发送的错误的,代码运行时也有可能发送错误,要把不同的错误消息存放在不同的的文件中,所以这里我规定存放编译时错误的文件名后缀为.cerr
规定三个文件的完整文件名的格式如下:

源文件:./tempfile/文件名.cpp
可执行:./tempfile/文件名.exe
编译错误文件:./tempfile/文件名.cerr

#路径工具类

在这里插入图片描述
编译模块首先创建子进程,子进程需要执行程序替换函数,但是如果编译失败了,我需要把错误信息存放到错误文件里,所有在执行替换函数之前,子进程首先要以写的方式打开一个对应的错误文件,然后把标准错误重定向到该文件,这样出错的信息就会直接输入到错误文件里面。

调用文件操作接口

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char* pathname,int flags,mode_t mode);

pathname:带路径的文件名,就是我们上面构建的完整文件名
flags:打开方式,我们选择只写O_CREAT | O_WRONLY
mode:权限,644我们自己可读可写,别人只能读

重定向接口

#include <unistd.h>
int dup2(int oldfd,int newfd);

让newfd指向的内容变成oldfd指向内容的一份拷贝,也就是说让newfd指向之前oldfd指向的文件,相当于给文件对应的描述符从oldfd换成newfd。

创建并设置好对应错误文件,子进程就要进行程序替换了
程序替换

#include <unistd.h>
int execlp(const char* file,const char* arg, ...);

第一个参数是要进行替换的程序的名字,因为我们是要编译代码,所以这个程序名就是g++。后面的参数是一个可变参数,我们在命令行怎么执行改程序,就怎么把参数传给这个替换函数,这就需要用到上面封装的路径工具类了。每个参数都是字符串类型,最后还有一个nullptr。

这里的编译选项还有一点需要注意,在编译模块只需要可以编译代码就可以了,但是我们整个代码的设计是分为两部分的,一部分预设代码,即在用户请求编译时已经存在的一部分代码,用户修改哪里的代码,然后提交给后端,后端通过用户提交的代码和题目对应的测试用例拼接起来,才形成一个完整的代码,而为了在测试用例一般会加入条件编译,具体原因我会在题库设计时讲解,所有编译时还要多加一个选项才能让代码正常编译

子进程执行完程序替换,父进程就等待子进程退出。并且父进程需要对编译的情况进行判断,可以通过可执行程序判断,但是这个判断属于是一种对文件的操作方法,也可以给他封装到工具模块的一个专门对文件操作的类里,将来所有的文件操作都可以封装到一起
获取文件属性

#include <sys/types>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char* path,struct stat* buf);

path:带路径的完整文件名
buf:获取文件属性
在这里插入图片描述
以上就是compiler编译模块的主要内容,还可以在一些地方加入对应的日志信息,就完成了该模块的编写

在这里插入图片描述

3.3 run运行模块

运行模块的功能就是根据文件名,找到对应的可执行程序并且运行。程序运行大致可以分为以下三种情况:

1.代码执行完,结果正确
2.代码执行完,结果不正确
3.代码没有执行完,中途崩溃*

其中代码的结果是否正确并不是我们运行模块需要考虑的内容,这个要交给上层的OJ模块根据测试用例去判断,所以运行模块只需要判断我的代码是否正常的执行完成了。

除了判断程序的执行情况,一般的在线OJ系统都会给每一道题目设置时间限制和空间限制,这样一是作为题目的一种要求,其次它可以防止用户可能输入的一些恶意代码,避免对服务器产生不利影响。

所以,该模块一共有两个主要的功能:一是对程序的进行一定的限制,二是判断程序的执行情况并返回给用户。

首先,要执行可执行程序就需要,可执行程序的名称。而要实现对进程的限制,就需要对应的时间限制空间限制。这就是我们需要的参数。

要把结果告知给用户,这个结果在代码运行正常时就是执行后得到的结果,在代码崩溃时就是报错信息,这两个信息在一般情况下是打印在标准输出和标准错误中的,而我要把这些信息返回给用户,就需要进行一下处理,把他们都保存在对应的临时文件中,方便上层提取。规定对应的文件格式为:

xxx.in ---- 程序的输入信息,暂时不处理
xxx.out ---- 保存程序正常退出的信息
xxx.rerr ---- 保存程序运行时的报错信息

这三个文件分别对应程序默认打开的三个文件:标准输入,标准输出,标准错误。就需要在运行程序之前对上面三个文件进行重定向。

实际实现时要运行可执行程序,首先就要能够找到对应的程序,那就可以用上面封装好的路径工具,找到对应的可执行程序,和编译模块类似,也是让子进程进行程序替换,父进程通过进程等待得到运行的结果。

一般程序崩溃终止的原因是因为系统给进行发送了信号杀掉了进程,可以通过父进程等待的方式获取进程退出时得到的信号,得知进程是因为什么原因导致的崩溃,也可以更好的告知上层运行的情况,我们可以用函数返回值的方式把这个信号返回给上层。规定:如果是我们模块内部的错误,比如打开文件失败等等,返回值就小于0,如果执行成功没有异常,返回值就等于0,如果代码崩溃收到信号,就把返回信号

在这里插入图片描述

#添加资源限制功能

根据两个参数,对进程进行时间和空间上的约束,如果资源不足,系统会给进程发送信号终止进程

使用的接口:

#include <sys/time.h>
#include <sys/resource.h>
int setrlimit(int resource,const struct rlimit* rlim);

resource有以下几种取值,表示你要做什么:

RLIMIT_AS:限制进行使用的虚拟地址的大小
RLIMIT_CPU:限制进程占用CPU的时间
RLIMIT_CORE:限制形成的core文件的大小
RLIMIT_DATA:限制数据段大小
等等

rlim是一个结构体

struct rlimit{
	rlim_t rlim_cur;//软限制
	rlim_t rlim_max;//硬限制
}

软限制设置的值不能超过硬限制,一般把硬限制设置成RLIM_INFINITY表示无限制,软限制设置成我想要的值

限制时间

#include <time.h>
#include <sys/resource.h>

int main()
{
    struct rlimit rt;
    rt.rlim_max=RLIM_INFINITY;//硬上限设置成无穷
    rt.rlim_cur=5;//软上限设置成1秒
    setrlimit(RLIMIT_CPU,&rt);
    while(1);
    return 0;
}

超出限制时间,系统会发送24号信号

限制内存

#include <time.h>
#include <sys/resource.h>
#include <iostream>

int main()
{
    struct rlimit rt;
    rt.rlim_max=RLIM_INFINITY;//硬上限设置成无穷
    rt.rlim_cur=1024*1024*30;//软上限设置最大虚拟空间
    setrlimit(RLIMIT_AS,&rt);
    int cnt=0;
    while(true)
    {
        int* p=new int[1024*1024];
        std::cout<<cnt++<<std::endl;
    }
    return 0;
}

超出限制内存,系统会发送6号信号

下面是我们的代码中的限制功能
在这里插入图片描述

3.4 compliler_run模块

首先明确该模块的功能:
因为涉及到网络服务,所以用户的代码会以json串的方式传给该模块,首先我要给每一份代码创建一个文件名具有唯一性的源文件,然后调用上面的编译模块和运行模块编译并执行该源文件,然后再把结果构建成json串返回给上层,两个参数,一个输入形的json串,一个输出形的json串。

3.4.1 定义结构

因为涉及到程序的资源限制,所以上层提交的参数可能会有很多部分,首先规定输入的json串里面会包含如下一个元素:

code:对应用户的代码
input:对应用户的自测输入
maxtime:对应该题目的时间限制
maxmem:对应该题目的内存限制

输出json串包含以下内容:

必有内容:
stat:我们规定的状态码
mean:状态码对应的含义
可能有:
out:运行正常的结果
rerr:运行时报错的结果

状态码用来表示编译运行的状态,规定:

-1:用户没有提交代码
-2:各种服务器的内部错误
-3:编译失败
0:编译运行成功
>0:进程运行崩溃时收到的信号

实现思路
对于拿到的代码json串,首先要更具协议规定进行反序列化,提取出对于的字段。

然后要给每一份提交的代码都生成一个临时源文件用于编译,这个文件的文件名需要具有唯一性,避免不同的客户之间相互干扰。

用获得的文件名,创建并把代码字段写入到该文件中形成源文件。

然后调用编译模块和运行模块,因为要针对每一中错误的情况把状态码和状态描述填写到返回json串的meam字段中并进行序列化,可以把所以的出错情况都集合再一起处理,用编写一个差错处理接口,根据状态码,构建对应的错误描述以供填写json串。

填写完成返回的json串之后进行一次序列化,然后可以编写一个接口用于清理掉代码执行过程中形成的所有临时文件

3.4.2 功能模块的编写

3.4.2.1 获取唯一性的文件名

获取文件名算是对文件的操作,可以封装在文件工具类

实现思路是通过拼接毫秒级别的时间戳和一个原子性递增的序号,使得拼接的文件名具有唯一性
原子性的序号可以用atomac_uint定义,而毫秒级的时间戳可以用编写日志获得时间戳的接口gettimeofday获得,其参数timeval结构体的第一个元素表示秒,第二个元素表示的就是微秒,可以使用这两个值计算出毫秒级别的时间戳:

Comm->util->TimeUtil

        //获得毫秒级别的时间戳
        static std::string GetMSTime()
        {
            struct timeval tv;
            gettimeofday(&tv,nullptr);
            //毫秒=秒*1000=微秒/1000
            return std::to_string(tv.tv_sec*1000+tv.tv_usec/1000);
        }

Comm->util->FileUtil

        //根据毫秒级时间戳和原子性递增的唯一值构建出一个可以保证唯一性的文件名
        static std::string GetUniqueName()
        {
            std::string ms=TimeUtil::GetMSTime();
            static std::atomic_uint key(0);
            key++;
            std::string key_s=std::to_string(key);
            return ms+key_s;
        }

3.4.2.2 读写文件操作

无论读取文件还是写入文件,都要使用文件操作,所以头文件必不可少。读写文件也属于工具类里的文件操作,所以他们的位置就应该是Comm->util->FileUtil。
下面是他们的实现方法:
向文件中写入内容

        static bool WriteFile(const std::string& file,const std::string& code)
        {
            std::ofstream out(file);//写方式打开文件
            if(!out.is_open())
            {
                return false;
            }
            //写入
            out.write(code.c_str(),code.size());

            out.close();
            return true;
        }

读取文件的内容

        //用iskeep设定读取的时候是否保留\n
        static bool ReadFile(const std::string& file,std::string* mean,bool iskeep=false)
        {
            (*mean).clear();//先清理一下

            std::ifstream in(file);//读方式打开文件
            if(!in.is_open())
                return false;

            //构建读取内容
            std::string line;
            //getline不保存\n
            //getline重载和强制类型转换,所以可以写在while判断语句这里
            while(std::getline(in,line)){
                (*mean)+=line;
                (*mean)+=(iskeep ? "\n" : "");
            }

            in.close();
            return true;
        }

3.4.2.3 清理临时文件

因为无论是编译还是运行都会生成临时文件,每一个请求生成一组,用不来哦几组临时文件的数量就会变得非常多,并且这些临时文件只要我编译运行模块拿到了其中的内容,就没用了,所以可以在编译运行的最后清理一下这一次服务生成的临时文件,因为这个方法只有当前模块调用,所以就不需要写在工具类了,直接定义在当前模块就可以。

思路:定义是编译会生成3个临时文件,运行会生成3个临时文件,一共是6个。
可以使用接口unlink来删除文件

#include <unistd.h>
int unlink(const char* pathname);

参数就是要删除文件的路径+文件名+后缀组成的完整文件名。而路径工具类里面刚好就有得到对应完整文件名的方法,直接调用得到文件名然后删除就可以。

但是计划情况下是6个文件,还有可能有特殊情况,比如编译失败,就不会生成可执行程序,所以在删除文件的时候还要判断一下文件是否存在,存在了再删。

   	static void RemoveTempFile(const std::string& file)
    {
        //形成的临时文件个是是不确定的,但是最多只有我们定义的6个
        //判断是否存在,存在就移除,不存在就删除
        std::string src=PathUtil::BuildSrc(file);
        if(FileUtil::IsExists(src))
            unlink(src.c_str());

        std::string exe=PathUtil::BuildExe(file);
        if(FileUtil::IsExists(exe))
            unlink(exe.c_str());

        std::string cerr=PathUtil::BuildCErr(file);
        if(FileUtil::IsExists(cerr))
            unlink(cerr.c_str());
        
        std::string in=PathUtil::BuildIn(file);
        if(FileUtil::IsExists(in))
            unlink(in.c_str());

        std::string out=PathUtil::BuildOut(file);
        if(FileUtil::IsExists(out))
            unlink(out.c_str());

        std::string rerr=PathUtil::BuildRErr(file);
        if(FileUtil::IsExists(rerr))
            unlink(rerr.c_str());
    } 

3.4.2.4 获得状态码含义

根据最开始定义的规定,编译运行模块最终给上层返回的json串里面一定要包含状态码字段和状态码描述字段,状态码可以让上层代码得知编译运行的情况,状态码描述则是告诉用户是怎么回事,所以不同的状态码会有不同的含义,需要封装一个接口完成这个功能

	//用来获得状态码的含义
    static std::string MeanOfStat(int stat,const std::string& filename)
    {
        std::string mean;
        switch (stat)
        {
        case 0:
            mean="编译运行成功";
            break;
        case -1:
            mean="提交代码为空";
            break;
        case -2:
            mean="发生了未知错误";
            break;
        case -3://拿到编译错误
            FileUtil::ReadFile(PathUtil::BuildCErr(filename),&mean,true);
            break;
        case SIGABRT://6
            mean="内存超过限制";
            break;
        case SIGFPE://8
            mean="除零错误";
            break;
        case SIGXCPU://24
            mean="运行超时";
            break;
        default:
            mean="其他错误: "+std::to_string(stat);
            break;
        }
        return mean;
    }

这个代码中除了指明的一些定义号的错误和超过资源限制对应的两个错误原因之外,起始后期还可以把其他信号对应的崩溃原因添加进来,让用户得到的错误原因更加细致。

3.4.3 compiler_run模块的整体代码

给外界提供一个Start方法让上层调用编译运行模块。参数是一个输入形式的json串和一个要给上层返回的json串。

首先使用jsoncpp反序列化,解析输入的json串。调用形成唯一文件名的方法生成一个唯一的文件名,专门供这一次服务使用。然后使用解析出来的代码部分创建出一个源文件,把文件名交给编译模块进行编译,再把文件名和时间限制,内存限制传给运行模块运行,记录这个过程中的状态码。再最后还要序列化一个json串返还给用户,更具获得状态码含义的接口填写状态码含义,根据状态码判断是否需要填写运行成功结果和运行时报错的结果,然后把填好的结果返还给上层。最终调用一次清理临时文件接口把这一次服务生成的所有临时文件清空即可。
在这里插入图片描述

3.4.4 编译运行模块的整体测试

引入网络服务前,首先对编译模块,运行模块,编译运行模块进行一下功能上的测试,可以一个源文件中引入编译运行模块,然后输入代码测试一下:

#include "compiler_run.hpp"

using namespace ns_compiler_run;

int main()
{
    std::string json_in;
    Json::Value rootin;
    rootin["code"]=R"(#include<iostream>
    int main(){
        std::cout<<"测试编译运行"<<std::endl;
        int* p=new int[1024*1024*20];
        return 0;
        })";
    rootin["input"]="";
    rootin["maxtime"]=1;
    rootin["maxmem"]=1024*30;//30M

    Json::FastWriter writer;
    json_in=writer.write(rootin);
    std::cout<<json_in.c_str()<<std::endl;

    std::string json_out;//给用户返回的
    CompilerRun::Start(json_in,&json_out);
    std::cout<<json_out.c_str()<<std::endl;
    return 0;
}

可以更换其中的代码部分来测试不同的模块功能是否正常。

3.5.compiler_server模块

把整个模块打包成一个网络服务,用户使用POST方法请求服务器上的compiler_run服务,请求的正文就是我们编译运行模块需要的json串。服务器用过json串调用编译运行模块,得到返回的json串后见响应返回给用户。

CompilerServer作为一个可以在多个主机上部署的服务,我们需要把他的端口号暴露出来,通过命令行参数获取,以便于部署。
在这里插入图片描述

以上就完成了整个CompilerServer模块的编写,OJServer就可以通过网络请求的方式请求编译服务。

4 OJServer模块设计

4.1 整体结构设计

OJServer模块是直接和用户交互的,用户访问OJ系统,我需要有一个首页,其次我需要有一个题目列表网页供用户选择题目,再者我还需要一个可以给用户写代码做题的网页,并且可以判断用户提交的代码是否正确。总结用户的请求分为三种:
1.请求题目列表
2.请求一个具体的题目,并且需要有编译区域
3.提交,判题请求
我的OJServer模块主要要更具这三种请求提供对应的功能。

整个模块采用的是MVC的设计模式进行设计

M:Model,模型。和数据交互的模块,对题库进行增删改查,专门和数据库沟通
V:View,视图。拿到数据之后渲染网页内容,展示给用户(浏览器)
C:Control,控制器。就是业务逻辑

通过这个设计模式,把数据,业务逻辑和界面进行了分离。

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

4.2 oj_server模块

主要功能就是搭建一个http服务,通过用户请求不同的资源,完成功能路由的任务,调用oj_control模块的功能:
在这里插入图片描述

4.3.oj_model模块

4.3.1 整体接口的设计

oj_model模块的主要任务就是和后端的题库交互,主要是为了完成请求题目列表和请求单个题目的功能,得到题库中对应的题目信息。

为了表示题目的信息,需要使用一个结构体把题目有关的属性都组织起来,结构如下:

    //表示题目的属性信息
    struct attribute
    {
        std::string number;//题目编号
        std::string title;//题目标题
        std::string grade;//题目等级
        std::string desc;//题目描述
        //预设代码和测试用例拼接才能构成完整代码进行编译
        std::string preset;//给用户预设的代码
        std::string test;//测试用例
        int maxtime;//时间限制(s)
        int maxmem;//空间限制(KB)

    };

其中大部分字段都比较好理解,需要注意的是预设代码字段和测试用例字段。其中预设代码字段是用户请求编写题目时我编辑框里面本身就携带的代码,这部分代码一般是根据题目要求设计的一个函数,用户通过题目要求实现该函数,并且提交到服务上,在后端就可以根据包含用户代码的预设部分加上提前添加的测试用例,组合成为一份可以编译的代码,然后再交给后面的编译服务编译。

但是这部分字段model模块并不关心,最终只需要对外提供两个接口:一个是返回所有题目信息的接口,用来给用户构建题目列表,获得题库所有的题目信息用一个vector返回。第二个就是返回指定编号的题目信息,通过编号获得题库中对应的题目相关属性返回出去,用来构建用户请求的单个题目网页。

两个接口的实现都离不开题库,题库的实现有两种方案,一种是文件版本的题库,一种是数据库版本的题库。两种版本的题库设计会使得oj_model模块的实现有一些差别,但是整体功能和提供接口都是一样的。下面就是两种题库的设计和对应model模块的编写方法

4.3.2 文件版本题库

4.3.2.1 题库设计

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

我将题库命名为TopicBank,题库的整体结构如下图:
在这里插入图片描述
其中topic_list.txt的格式规定为每一行包含一个题目的信息,内容从前到后依次为:题目编号,题目标题,题目难度,时间限制,内存限制。每个字段之间用空格分割。

在录题时,我们要自己设计题目的描述,预设代码和测试用例,尽量让测试用例比较完善,设计方法以一道判断回文数的题为例:
preset代码:

#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <algorithm>

using namespace std;

class Solution {
public:
    /**
     * 
     * @param n int整型
     * @return bool布尔类型
     */
    bool isParlindrome(int n) {
        // write code here
    }
};

预设代码里面是展示给用户的代码,可以包含头文件,一些解释等内容,最终要的就是让用户实现我们给定的函数,测试用例的写发如下:
test代码:


#ifndef COMPILER_ONLINE
#include "preset.cpp"
#endif

void Test1()
{
    //定义临时对象进行调用
    bool ret=Solution().isParlindrome(1234321);
    if(ret)
        std::cout<<"测试用例1通过!"<<std::endl;
    else
        std::cout<<"测试用例1没有通过,用例值: 1234321"<<std::endl;

}

void Test2()
{
    //定义临时对象进行调用
    bool ret=Solution().isParlindrome(-10);
    if(!ret)
        std::cout<<"测试用例2通过!"<<std::endl;
    else
        std::cout<<"测试用例2没有通过,用例值: -10"<<std::endl;

}

int main()
{
    Test1();
    Test2();
    return 0;
}

我们需要调用preset里面的函数,然后添加进测试用例代码,去执行我们设计的测试用例,当然实际上的测试用例可能非常多,并且非常完善,这里只是举个例子,但是同样是两个源文件,测试用例引用其他文件的内容,我就需要include包含该文件,否则代码无法识别我们输入的内容,万一写错了就不好了。但是如果在测试用例里包含了头文件,将来OJ服务把两份代码拼接在一起时中间的这个头文件就会变成一个语法错误,所以选择条件编译的方式包含该头文件,所以编译模块在编译时才需要加上以下两个选项:
在这里插入图片描述
为的就是在编译时去掉测试用例中包含的preset文件,让代码可以正常编译。

4.3.2.2 oj_model_fd模块

4.3.2.2.1 整体结构的设计

根据文件题库的设计,我要实现接口的功能,那我每道题我都带读取两个文件某一个时包含题目的小字段信息的topic_list,还有就是每道题目对应的代码,描述信息,我要得到这些信息,首先就需要吧文件的内容都加载到内存,也就是说读取文件,填写没到题目的属性信息。在类内可以使用一个哈希表存储题目编号与题目属性的映射关系,在类创建的时候就自动读取所有题目的信息。然后不同的接口选择哈希表不同的内容给用户返回。

4.3.2.2.2 字符串切分功能

读取题库首先要读topic_list.txt,而它的内容是一行一行分割的,正好可以使用boost库的字符串分割函数,获取其中的每一个字段

Comm->util

    class StringUtil
    {
    public:
        //src:源字符串 ret:切分的结果都保存在vector sep:分隔符
        static void StringSplit(const std::string& src,std::vector<std::string>* ret,std::string sep)
        {
            boost::split(*ret,src,boost::is_any_of(sep),boost::algorithm::token_compress_on);
        }
    };
}
4.3.2.2.3 整体代码的编写

在这里插入图片描述

4.3.3 数据库版本题库

4.3.3.1 题库设计

和文件不同,数据库的读取比较简单,所有不需要把信息分开存储,可以使用一张表存储一个题目的所有信息,使用的是MySQL实现。

首先创建一个oj库当作题库,表结构的设计创建如下:

mysql> create table if not exists `oj_topics`(
    -> `number` int primary key auto_increment comment '题目编号',
    -> `title` varchar(128) not null comment '题目标题',
    -> `grade` varchar(8) not null comment '难度等级',
    -> `desc` text not null comment '题目描述',
    -> `preset` text not null comment '预置代码',
    -> `test` text not null comment '测试用例',
    -> `maxtime` int default 1 comment '时间限制',
    -> `maxmem` int default 51200 comment '内存限制(50M)'
    -> )engine =InnoDB default charset=utf8;
Query OK, 0 rows affected (0.03 sec)

mysql> desc oj_topics;
+---------+--------------+------+-----+---------+----------------+
| Field   | Type         | Null | Key | Default | Extra          |
+---------+--------------+------+-----+---------+----------------+
| number  | int(11)      | NO   | PRI | NULL    | auto_increment |
| title   | varchar(128) | NO   |     | NULL    |                |
| grade   | varchar(8)   | NO   |     | NULL    |                |
| desc    | text         | NO   |     | NULL    |                |
| preset  | text         | NO   |     | NULL    |                |
| test    | text         | NO   |     | NULL    |                |
| maxtime | int(11)      | YES  |     | 1       |                |
| maxmem  | int(11)      | YES  |     | 51200   |                |
+---------+--------------+------+-----+---------+----------------+
8 rows in set (0.00 sec)

然后创建一个专门的oj_client用户,把oj数据库的所有权限都给这个用户
创建用户

mysql> create user oj_client@'%' identified by '123456';

赋权

mysql> grant all on oj.* to oj_client@'%';

这里为了方便本地登录也可以创建一个同名的允许本地登录的用户,注意要让别人远程登录需要开放3306端口。

之后就可以向这里面录题了,可以直接在命令行操作,也可以使用MySQL官方提供的图形化界面程序MySQL bench录题。使用方法在后面。

题录好了,要在代码里使用还需要在MySQL的官网上下载并安装一些开发包才可以在代码里使用MySQL,安装方法也在后面有说明

4.3.3.2 oj_model-db模块

通过连接数据库,那么model模块的两个接口其实就是sql语句的不同,一个数据库的所有记录,一个是某一个记录,那就需要一个执行sql的接口,然后传入不同的sql语句,获得结果即可,代码如下:
在这里插入图片描述

4.4 oj_view模块

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

渲染网页首先要有对应的网页,所有就要在OJServer模块的目录下新建两个html文件,负责题目列表和单个题目的两张网页,整个模块的功能就是通过上层传来的题目属性attribute,提取出不同网页需要的字段,渲染到网页中。规定题目列表只显示题目的编号,标题,等级即可,而单个题目则需要题目的编号,标题,等级,描述和预设代码字段。

view模块通过把这些数据交给前端的网页,然后再根据前端部分构建出对应的网页,两者配合起来,形成给用户返回的页面。渲染工作通过ctemplate库实现,安装和使用方法在后面有写。

下面就是整个模块的代码
在这里插入图片描述

4.5 oj_control模块

4.5.1 整体结构设计

首先明确一下功能,oj_control模块是整个OJSever模块的逻辑功能部分,在上层做好了功能路由之后,是通过调用control模块去时间各个功能的,所有oj_control模块既要可以给上传返还对应的网页,还要可以负载均衡的判题。根据这三种请求,就要能够提供三个功能,即:一个可以构建好题目列表网页的接口,一个可以根据题目编号构建好单个题目网页的接口,还有一个判题接口

要完成这些功能就需要一些模块的配合,首先网页有关的两个功能肯定需要model模块view模块实现,一定要包含这两个类的对象。其中比较复杂的就是这个判题功能,判题功能需要调用CompilerServer模块,使用它的编译服务帮我完成判题,但是我不能单纯的去请求一个服务,根据项目的设计,我是要负载均衡的去选择对应的编译服务,这个负载均衡功能可以封装到一个类里面,专门供判题模块去调用。

4.5.2 构建题目列表和单个题目网页

这两个模块的功能在model模块和view模块以及基本实现了,control模块的任务就是调用上面的两个模块,首先通过model模块获取到网页需要的题目信息,列表或者是单个题目,然后通过这些信息,调用view模块,渲染对应的网页,然后返还给oj_server模块,再通过网络返还给用户即可。

只有一个需要注意的是当我构建题目列表的网页时,我通过model模块拿到的题目信息,它不一定是按照题目编号排好序的,有可能是乱序的,所有需要我们拿到题目信息之后,对vector数组根据题目编号进行一下排序,然后再渲染网页,否则用户看到的也是一个乱序的题目列表。下面是两个模块的代码
在这里插入图片描述

4.5.3 判题功能

4.5.3.1 负载均衡模块

4.5.3.1.1 整体功能设计

机器类的设计
负载均衡模块,最重要的功能就是可以负载均衡式的选择主机,这里面有两个问题,一个是我如何得知有哪些主机可以供我选择,其次我怎么知道怎么选就是负载均衡的。

所以在模块内部一定需要有一个结构包含了可以提供服务的主机信息,然后有一个数据结构把这些属性组织在一起。用来表述主机的结构我命名为Machine,然后我用一个vector把所有可以提供服务的主机组织起来。

Machine类里面一定包含的属性有主机的IP,端口,还有一个计数器表示主机的负载情况。我负载均衡判断的依据就是看主机的负载,所有类里还要提供方法在有新请求请求该机器时增加负载,服务结束时减少负载,为了调试还可以增加一个获取负载,如果中途服务主机突然挂了,还要可以清空负载
因为同一时刻可能有多个执行流在请求同一个主机,所有需要保证我对负载操作的安全性,就需要一个mutex互斥锁保护对负载的操作。
在这里插入图片描述

负载均衡模块设计
有了表示所有的主机,那么我首先需要一个vector来组织起所有的主机,将来选择主机就可以在vector中选,但是在此之前我需要知道有哪些主机我可以选,规定在当前路径下的conf文件加下的一个.conf文件里面会存放所有的可以提供服务的主机信息,包括了IP和端口,两个字段用”:“分隔,每一行是一个主机的信息,负载均衡模块在构建时就可以读取该文件,初始化自己的vector结构

然后就是选择主机功能,首先在同一时刻可能有很多执行流都在选择主机,所以对主机的选择需要加锁,也就是说负载均衡模块也需要一个互斥锁。

设计在control模块调用负载均衡模块时,如果说后端的编译服务主机出问题挂了,不应该影响我的OJServer服务,OJ服务正常运行,编译服务如果恢复了,那我正常请求,如果有一部分寄了,那我请求别人,全寄我就不请求,提示后端,这个功能就由负载均衡模块负责,就是说负载均衡模块除了可以选择主机,还要能够知道主机的情况,并且能够更具实际情况更新。规定我使用数组的下标表示每一个主机的编号,用两个数组,一个表示上线的主机,元素的值就是主机编号,另一个数组也类似,但是表示的是下线的主机。然后我要提供方法,在后端编译服务重启时我要可以更新状态让主机上线,当请求主机失败时我要更新状态让主机下线。

4.5.3.1.2 负载均衡模块的实现

负载均衡模块所谓的负载均衡,就是尽量让每一台机器负责的请求都大致一样,那就需要我们从所有在线的主机中选择出对应的主机,选择的方法有两种:
一是随机挑选主机,即每一次都是随机选,这些选中每一台主机的概率就是相等的,但是这种方法你不能排除有时候就是点背一直选中某几台,有几台又一直选不上,需要一定的运气成分。还有一种就比较严格,选择的时候遍历所有在线的主机,找出负载最小的,我使用的就是这种方法。

还有就是上线下线需要说明一下,负载均衡模块它的功能是掌握机器的状态,选择机器来提供服务,至于如何请求编译服务,这个模块不关心,我只负责选出那个合适的主机。编译主机如何上线下线我也不管,我只负责知道它现在是上线还是下线

下面是代码实现:
在这里插入图片描述

4.5.3.2 判题模块的编写

有了负载均衡模块,判题模块就比较明了了。首先我得到的参数肯定是我需要判的题目编号和用户传进来的json串形式的代码,我通过题目编号,调用model模块得到题目相关的信息,然后通过反序列化用户传来的代码,得到代码内容。

有了题目的信息和用户的代码,就可以拼接出可以用来编译的源码内容和对应的资源限制,构建出CompilerServer需要的json串。

在请求服务之前,需要死循环式的去选择最低负载的主机,因为你选中的主机你不一定知道它是否还在线,可能你选中的时候它已经挂了,那你再请求就会请求失败,如果请求失败,就调用负载均衡模块的下线主机方法更新这个主机的状态,然后重新选择下一个主机,没有找到主机,说明所有主机都挂了,那只能返回了。

选到主机之后通过主机的IP+端口,使用网络请求的方式发起编译请求,除了通过请求的返回值判断请求是否成功,还需要判断请求的状态码,只有状态呢是200时才表示请求成功。并且需要注意更新请求时机器的负载情况。

下面是代码内容:
在这里插入图片描述

4.5.4 更新主机上线状态

设计时如果编译服务的主机都挂了,OJ服务就无法提供判题服务,但是OJ服务不能挂,如果后端的编译服务被重启了,OJ服务要可以更新主机的状态然后请求,所有涉及当服务重启之后,再后端通过给OJ服务发送信号,来更新OJ服务中记录的编译服务器状态,这个功能已经在负载均衡模块中实现了,可以让oj_srever模块通过调用control模块使用该功能
在这里插入图片描述
以上就完成了oj_control模块的编写。

这部分代码写完,整个项目的 后端部分就基本完成了,剩下的就是前端页面的编写。

5. 前端页面设计

主要包含三个网页,一个是首页内容,首页里会包含访问题目列表的连接,通过题目列表的题目访问每一道题,后端会配合view模块和model模块对后面两个网页内容进行渲染响应给用户。因为前端的部分我也不是很熟悉,所有下面的内容就不做详细的解释了,注释会解释一些部分的含义。

5.1首页

OJServer->wwwroot->index.html

<!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>
        *{
            margin: 0px;/*消除网页默认外边距 */
            padding: 0px;/*消除网页默认内边距 */
        }
        html,
        body {
            width: 100%;
            height: 100%;
        }
        .container .navbar{
            width: 100%;
            height: 50px;
            background-color: rgb(0, 0, 0);
            /* 给父级别标签设置取消后续浮动带来的影响 */
            overflow: hidden;
        }
        .container .navbar a{
            /* 设置成行内块,允许设置宽度 */
            display: inline-block;
            /* 设置每个a标签的宽度 */
            width: 80px;
            /* 设置字体颜色 */
            color: rgb(255, 255, 255);
            /* 设置字体大小 */
            font-size: larger;
            /* 设置文字的高度 */
            line-height: 50px;
            /* 去掉下划线 */
            text-decoration: none;
            /* 设置文字居中 */
            text-align: center;
        }
        /* 添加鼠标事件 */
        .container .navbar a:hover{
            background-color: blueviolet;
        }
        .container .navbar .login{
            float: right;
        }
        .container .content{
            width: 800px;
            /* background-color: rgba(184, 175, 197, 0.947); */
            /* 边距居中 */
            margin: 0px auto;
            /* 文字居中 */
            text-align: center;
            /* 设置上外边距 */
            margin-top: 200px;
        }
        .container .content ._font{
            /* 设置标签为块级元素,可以设置高度宽度等属性 */
            display: block;
            margin-top: 20px;
            /* 去掉下划线 */
            text-decoration: none;
            /* 设置字体大小 */

        }
    </style>
</head>

<body>
    <div class="container">
        <!--导航栏-->
        <div class="navbar">
            <a href="#">首页</a>
            <a href="/topic_list">题库</a>
            <a href="#">面试</a>
            <a href="#">学习</a>
            <a href="#">求职</a>
            <a href="#">讨论区</a>
            <a href="#">发现</a>
            <a class="login" href="#">登录</a>
        </div>
        <!---网页内容-->
        <div class="content">
            <h1 class="_font">欢迎来的铁柱同学的OJ平台</h1>
            <p class="_font">点击下面的连接,获得题目列表,选择题目进行编程</p>
            <a class="_font" href="/topic_list">点我获得题目列表</a>
        </div>
    </div>
</body>
</html>

5.2题目列表页面

OJSever->Rendered_html->List.html

<!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>题目列表</title>
    <style>
        *{
            margin: 0px;/*消除网页默认外边距 */
            padding: 0px;/*消除网页默认内边距 */
        }

        html,
        body {
            width: 100%;
            height: 100%;
        }
        .container .navbar{
            width: 100%;
            height: 50px;
            background-color: rgb(0, 0, 0);
            /* 给父级别标签设置取消后续浮动带来的影响 */
            overflow: hidden;
        }
        .container .navbar a{
            /* 设置成行内块,允许设置宽度 */
            display: inline-block;
            /* 设置每个a标签的宽度 */
            width: 80px;
            /* 设置字体颜色 */
            color: rgb(255, 255, 255);
            /* 设置字体大小 */
            font-size: larger;
            /* 设置文字的高度 */
            line-height: 50px;
            /* 去掉下划线 */
            text-decoration: none;
            /* 设置文字居中 */
            text-align: center;
        }
        /* 添加鼠标事件 */
        .container .navbar a:hover{
            background-color: blueviolet;
        }
        .container .navbar .login{
            float: right;
        }
        .container .list{
            padding-top: 50px;
            width: 800px;
            height: 100%;
            margin: 0px auto;
            /* background-color: slategrey; */
            text-align: center;
        }
        /* 调整表格 */
        .container .list table{
            width: 100%;
            font-size: larger;
            font-family:'Times New Roman', Times, serif;
            margin-top: 50px;
            background-color: rgb(247, 248, 255);
        }
        .container .list h1{
            color:rgb(22, 42, 223);
        }
        .container .list table .item{
            width: 100px;
            height: 40px;
            color:rgb(169, 169, 169);
            padding-top: 5px;
            padding-bottom: 5px;
            font-size: large;
            font-family: Georgia, 'Times New Roman', Times, serif;
        }
        .container .list table .item a {
            text-decoration: none;
            color: black;
        }
        .container .list table .item a:hover{
            color: blue;
            text-decoration:underline;
        }
        .container .footer{
            width: 100%;
            height: 150px;
            text-align: center;
            background-color: black;
            line-height: 50px;
            color:darkgray;
            margin-top: 15px;
            
        }
    </style>
</head>

<body>
    <div class="container">
        <!--导航栏-->
        <div class="navbar">
            <a href="/">首页</a>
            <a href="/topic_list">题库</a>
            <a href="#">面试</a>
            <a href="#">学习</a>
            <a href="#">求职</a>
            <a href="#">讨论区</a>
            <a href="#">发现</a>
            <a class="login" href="#">登录</a>
        </div>
        <div class="list">
            <h1>题目列表</h1>
            <table>
                <tr>
                    <th class="item">编号</th>
                    <th class="item">标题</th>
                    <th class="item">难度</th>
                </tr>
                <!-- 循环式的形成多个内容 -->
                {{#topic_list}}
                <tr>
                    <td class="item">{{number}}</td>
                    <td class="item"><a href="/topic/{{number}}">{{title}}</a></td>
                    <td class="item">{{grade}}</td>
                </tr>
                {{/topic_list}}
            </table>
        </div>
        <div class="footer">
            <h4>@c铁柱同学</h4>
        </div>
    </div>

</body>

</html>

5.3指定题目的编写提交页面

OJSever->Rendered_html->One.html

<!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 CDN -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ace.js" type="text/javascript"
        charset="utf-8"></script>
    <!-- CDN的语言工具,识别语言,自动补齐 -->
    <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://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
    <style>
        * {
            margin: 0;
            padding: 0;
        }

        html,
        body {
            width: 100%;
            height: 100%;
        }

        div .ace_editor {
            height: 600px;
            width: 100%;
        }
        .container .navbar{
            width: 100%;
            height: 50px;
            background-color: rgb(0, 0, 0);
            /* 给父级别标签设置取消后续浮动带来的影响 */
            overflow: hidden;
        }
        .container .navbar a{
            /* 设置成行内块,允许设置宽度 */
            display: inline-block;
            /* 设置每个a标签的宽度 */
            width: 80px;
            /* 设置字体颜色 */
            color: rgb(255, 255, 255);
            /* 设置字体大小 */
            font-size: larger;
            /* 设置文字的高度 */
            line-height: 50px;
            /* 去掉下划线 */
            text-decoration: none;
            /* 设置文字居中 */
            text-align: center;
        }
        /* 添加鼠标事件 */
        .container .navbar a:hover{
            background-color: blueviolet;
        }
        .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 h4{
            padding-top: 15px;
            padding-left: 10px;
        }
        .container .part1 .left_desc pre{
            padding-top: 15px;
            padding-left: 10px;
            font-size: large;
        }
        .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: 8px;
            margin-right: 5px;

        }
        .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="/topic_list">题库</a>
            <a href="#">面试</a>
            <a href="#">学习</a>
            <a href="#">求职</a>
            <a href="#">讨论区</a>
            <a href="#">发现</a>
            <a class="login" href="#">登录</a>
        </div>
        <!-- 左右呈现 -->
        <div class="part1">
            <div class="left_desc">
                <span></span>
                <h4><span id="number">{{number}}</span>.{{title}}.{{grade}}</h4>
                <pre>{{desc}}</pre>
            </div>
            <div class="right_code">
                <!-- 代码编辑区 -->
                <!-- <textarea name="C++" id="" cols="30" rows="10">{{preset}}</textarea> -->
                <!-- ACE需要的标签 -->
                <pre id="code" class="ace_editor"><textarea class="ace_text-input">{{preset}}</textarea></pre>
                <!-- <button class="bt" οnclick="submit()">提交代码</button> -->

            </div>
        </div>
        <!-- 提交并显示结果 -->
        <div class="part2">
            <div class="result"></div>
            <button class="btn-submit" onclick="submit()">保存并提交</button>
        </div>
    </div>


    <script>
        //初始化对象 
        editor = ace.edit("code");
        //设置风格和语言(更多风格和语言,请到github上相应目录查看) 
        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.收集当前页面的数据:题号,代码
            var code=editor.getSession().getValue();//得到用户代码
            console.log(code);
            var number=$(".container .part1 .left_desc h4 #number").text();//拿到编号
            console.log(number);
            var judge_url="/judge/"+number;

            //2.构建json串给后端并发起请求
            // 通过ajax发起请求
            $.ajax({
                method:'Post',//请求的方法
                url:judge_url,//向指定的url发起请求
                dataType:'json',//告知服务端我需要什么格式
                contentType:'application/json;charset=utf-8',//告知服务端我是什么格式
                data:JSON.stringify({
                    'code':code,
                    'input':'',

                }),
                success: function(data){
                    //如果成功的到的结果
                    //console.log(data);
                    show_result(data);
                }
            });

            //3.得到结果,解析并显示到result
            function show_result(data)
            {
                // console.log(data.stat);
                // console.log(data.mean);
                //拿到结果标签
                var result_div=$(".container .part2 .result");
                result_div.empty();//清空上一次的结果
                //先拿到一定有的两部分
                var stat=data.stat;
                var mean=data.mean;

                var reason_lable=$("<p>",{
                        text:mean
                    });

                reason_lable.appendTo(result_div);
                if(stat==0)
                {
                    //编译运行是成功的
                    var out=data.out;
                    var rerr=data.rerr;

                    var out_lable=$("<pre>",{
                        text:out
                    });

                    var rerr_lable=$("<pre>",{
                        text:rerr
                    });

                    out_lable.appendTo(result_div);
                    rerr_lable.appendTo(result_div);
                }
                else
                {
                    //编译运行出错,显示出错原因
                    //把包含错误原因的p标签插入到result_div标签
                    
                }
            }
        }
    </script>
</body>

</html>

6 顶层项目部署Makefile

在顶层新建一个Makefile文件,该文件的功能就是可以make时可以同时编译CompilerServer服务和OJServer服务,当输入make submit时会自动形成一个output文件,里面包含了compiler_server和oj_server的应用程序和一些运行程序必须的文件,时间打包的功能。最后输入make clean不光会清理掉创建的可执行程序,还会清理掉output的内容。

# 编译功能
.PHONY:all
all:
	@cd CompilerServer;\
	make;\
	cd -;\
	cd OJServer;\
	make;\
	cd -;

# 部署功能
.PHONY:submit
submit:
	@mkdir -p output/CompilerServer;\
	mkdir -p output/OJServer;\
	cp -rf CompilerServer/compiler_server output/CompilerServer;\
	cp -rf CompilerServer/tempfile output/CompilerServer;\
	cp -rf OJServer/oj_server output/OJServer;\
	cp -rf OJServer/conf output/OJServer;\
	cp -rf OJServer/lib output/OJServer;\
	cp -rf OJServer/TopicBank output/OJServer;\
	cp -rf OJServer/Rendered_html output/OJServer;\
	cp -rf OJServer/wwwroot output/OJServer;\

# 清除功能
.PHONY:clean
clean:
	@cd CompilerServer;\
	make clean;\
	cd -;\
	cd OJServer;\
	make clean;\
	cd -;\
	rm -rf output;

7 项目组件的安装与使用

jsoncpp

安装json

sudo yum install -y jsoncpp-devel

使用jsoncpp要在编译时添加-ljsoncpp

序列化
把一个结构化的数据转化成字符串。

#include <jsoncpp/json/json.h>

int main()
{
    Json::Value root;//中间类型,以KV形式设置
    root["int"]=10;
    root["bool"]=true;
    root["string"]="string";
    
    Json::FastWriter fw;
    std::string fs=fw.write(root);
    
    Json::StyledWriter sw;
    std::string ss=sw.write(root);
	
	return 0;
}

反序列化

#include <jsoncpp/json/json.h>

int main()
{
	/*************************
	* jsonstr:
    * {"bool":true,"int":10,"str":"string"}
    * **********************/
    Json::Value root;
    Json::Reader rd;
    rd.parse(jsonstr,root);
    int id=root["int"].asInt();
    bool b=root["bool"].asBool();
    std::string str=root["str"].asString();
	return 0;
}

cpp-httplib

下载安装
在gitee搜索cpp-httplib,建议选择0.7.15版本,比较稳定

这个库是header-only的,解压出来之后里面只有一个httplib.h的头文件,把这个头文件拷贝到项目中就可以直接使用了。

注意,使用这个库必须使用高版本的gcc。

gcc升级方法
搜索scl gcc devsettool

安装scl工具集的yum源

sudo yum install centos-release-scl scl-utils-build

安装新版本的gcc(7以上的就可以,这里以7为例)

sudo yum install -y devtoolset-7-gcc devtoolset-7-gcc-c++

查看自己安装的版本

ls /opt/rh

启动高版本的gcc

scl enable devtoolset-7 bash 

启动的方式启动高版本只在当前会话有效,如果想要每次登录自动启动高版本,可以把启动命令添加到用户自己的~./bash_profile文件中,添加好保存退出,重新登录,之后每次登录就会自动执行启动高版本的命令。

使用方法
cpp-httplib是一个阻塞式多线程的网络http库,内部使用了原生线程库,所以编译的时候要加-lpthread

1.创建服务
创建Server对象就可以创建一个服务
通过set_base_dir方法设置首页信息,

inline bool httplib::Server::set_base_dir(const char *dir, const char *mount_point = (const char *)nullptr)

创建一个根目录,里面创建一个index.html网页,然后把参数dir设置成创建的根目录的地址就完成了设置。

通过Get方法可以处理客户端通过GET方法提交的http请求

inline httplib::Server &httplib::Server::Get(const char *pattern, httplib::Server::Handler handler)

pattern表示客户端请求的资源,后面是一个回调方法,表示如果用户请求pattern资源,就调用后面的回调方法,一般配合lambda表达式使用,表达式的两个参数分别是httplib::Request表示请求httplib::Response表示响应。

可以用Response类的set_content方法给客户端构建响应

inline void httplib::Response::set_content(const std::string &s, const char *content_type)

参数s表示响应的内容,参数content_type表示响应的格式。

然后调用Server对象的listen方法,启动服务

bool httplib::Server::listen(const char *host, int port, int socket_flags = 0)

host表示IP地址,port表示端口号

示例:

#include "httplib.h"
int main()
{
    httplib::Server svr;
    //设置首页
    svr.set_base_dir("./wwwroot");
    //设置回调,当请求hello资源时,调用lambda表达式,
    svr.Get("/hello",[](const httplib::Request& req,httplib::Response& resp){
        resp.set_content("你好httplib","text/plain;charset=utf-8");//设置相应的内容(body)
    });
	//启动服务
    svr.listen("0.0.0.0",8848);
}

2.创建客户端
首先创建Client对象用来请求

可以选择GET或POST方法请求,以POST方法为例

inline httplib::Result httplib::Client::Post(const char *path, const std::string &body, const char *content_type)

path:表示要请求的资源
body:表示发起请求的正文部分
content_type:表示请求正文的类型
返回值Result里面会包含一个指向Response的智能指针,所以可以通过返回值拿到请求得到的响应。指针如果为空则表示请求失败

boost库

安装boost库

sudo yum install -y boost-devel

分割字符串

#include <iostream>
#include <boost/algorithm/string.hpp>
#include <vector>

int main()
{
    const std::string src="1,title,困难,,,1,50000";
    std::vector<std::string> ret;
    boost::split(ret,src,boost::is_any_of(","),boost::algorithm::token_compress_on);
    for(auto it:ret)
    {
        std::cout<<it.c_str()<<std::endl;
    }
    return 0;
}

第一个参数表示要分割的字符串,该方法不会修改原字符串
第二个参数是分割完的结果,要保存在一个数组中
第三个参数表示分隔符,凡是在is_any_of括号里的字符串里的字符都会被当作分隔符
第四个参数表示是否压缩,如果是token_compress_on,当有多个分隔符连接在一起时,会把这些重复的分隔符当作一个进行分割。如果是token_compress_off,当有多个分隔符时,只会有一个被当作分隔符,其他的每一个重复的分隔符会当作空字符串存入到结果数组中。

ctemplate

是一个Google开源的C/C++的网页渲染库。

安装
需要源码安装,首先clone对于的库

 git clone https://hub.fastgit.xyz/OlafvdSpek/ctemplate.git

进入ctemplate目录下执行以下指令

./autogen.sh
./configure
make                     //编译注意使用高版本的gcc
sudo make install  //安装到系统

安装完成后,如果使用是报错说找不到动态库,到动态库目录下使用find -name命令找到动态库的目录,有两种方案
方法一:把动态库目录添加到LD_LIBRARY_PATH环境变量

export LD_LIBRARY_PATH=动态库路径

方法二:修改配置文件

cd /etc/ld.so.conf.d/

到该目录下以后touch一个.conf文件,然后把动态库的路径添加到新建的文件中,ldconfig更新一下缓存即可。该过程需要sudo权限。

注意:路径必须是/home打头的。

使用
需要两部分内容:一个能够承装数据的KV结构的数据字典,还有一个被渲染的网页内容。渲染功能就是把数据字典的数据替换进网页里

网页test.html:

<!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>测试ctemplate</title>
</head>
<body>
    <!-- 用双大括号阔起来的内容就表示要被替换的内容  -->
    <p>{{key}}</p>
    <p>{{key}}</p>
    <p>{{key}}</p>
    <p>{{key}}</p>
    <p>{{key}}</p>
</body>
</html>

渲染代码:

#include <iostream>
#include <string>
#include <ctemplate/template.h>

int main()
{
    std::string old_html="./test.html";//网页路径加文件名
    std::string value="你好";//要去替换的值

    ctemplate::TemplateDictionary d("test");//定义了一个名叫test的数据字典
    d.SetValue("key",value);//给数据字典插入数据,把网页中的key替换成value值

    //获取要被渲染的网页对象
    ctemplate::Template* tpl=ctemplate::Template::GetTemplate(old_html,ctemplate::DO_NOT_STRIP);
	//第一个参数是被渲染网页的路径加文件名
	//第二个参数是选项,选择是否去除空格等,DO_NOT_STRIP表示保持原貌

    //添加字典数据到网页,完成渲染
    std::string new_html;//保存渲染完成的结果
    tpl->Expand(&new_html,&d);

    //查看结果
    std::cout<<new_html<<std::endl;

    return 0;
}

编译时要添加-lctemlpate和-lpthread,因为库里面也用了原生线程库

MySQL workbench

下载
在这里插入图片描述
按照上面的步骤找到workbench,下载安装即可。

安装好以后需要连接一下数据库,点击Databases的Connect to Databases…
在这里插入图片描述
填写上对应的信息点击ok即可
在这里插入图片描述
输入你oj_client用户的密码即可完成登录,注意要登录服务器的3306端口必须开放

然后就可以正常使用该软件
在这里插入图片描述
输入sql语句,点击上面的按钮执行就可以看待我的表,这里面显示的是我已经插好的一道题。

要像插入或修改内容,可以选择某一行,然后点击Form Editor
在这里插入图片描述
就可以在框框里面编辑想要插入或修改的内容,完成只会点击apply,就可以把修改同步到数据库上,非常方便。

mysql-connector-c

下载
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
通过上面的步骤,选择对应的版本下载然后上传到云服务器上就可以了。

安装使用
拿到压缩包以后,先解压安装包

tar xzf 安装包名

然后在使用该库的项目路径下,建立对应的软连接

	ln -s ~/thirdpart/xxxxxx/include include
	ls -s ~/thirdpart/xxxxxx/lib lib

直接使用库可能在编译时会导致找不到库,和ctemplate一样,要么带修改环境变量,要么待添加配置文件。上面已经说明了,这里就不再重复了。

上面的工作都做完以后,还要注意编译时要添加三个选项:-I./include -L./lib -lmysqlclient

  • 3
    点赞
  • 4
    收藏
  • 打赏
    打赏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:游动-白 设计师:我叫白小胖 返回首页
评论 6

打赏作者

c铁柱同学

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值