文章目录
- 一、演⽰项⽬
- 二、所⽤技术与开发环境
- 三、项⽬宏观结构
- 四、公共模块comm
- 五、编译运行模块compile_server
- [六、基于 MVC 结构的 OJ 模块](#六、基于 MVC 结构的 OJ 模块)
-
- [1、oj_model.hpp 文件版](#1、oj_model.hpp 文件版)
- [2、oj_model2.hpp MySQL版](#2、oj_model2.hpp MySQL版)
- 3、oj_view.hpp
- 4、oj_control.hpp
- 5、oj_server.cc
- 6、conf
- 7、questions
- 8、template_html
- 9、wwwroot
一、演⽰项⽬
项⽬源码链接:https://gitee.com/zhang-shiyang-h/l_code/tree/master/OnlineOJ



二、所⽤技术与开发环境
1、核心技术栈
后端开发
- C++基础 + STL标准库
- 多进程、多线程并发编程
- 负载均衡架构设计
第三方库
- Boost库(字符串切割等工具)
- cpp-httplib HTTP 网络库
- ctemplate 前端模板渲染库
- jsoncpp JSON 序列化 / 反序列库
数据库
- MySQL + C API 数据库连接
前端基础
- HTML/CSS/JS/jQuery/AJAX
- Ace 在线代码编辑器
2、开发环境
- 服务器:Ubuntu 20.04 云服务器
- 开发工具:VS Code
- 数据库管理:MySQL Workbench
三、项⽬宏观结构
我的项⽬核⼼由三个模块组成:
- comm: 公共模块
- compile_server: 编译运行模块
- oj_server: 题目管理、负载均衡及业务功能
代码结构:
c
comm:
httplib.h
Mutex.hpp
Log.hpp
Util.hpp
compile_server:
compile.hpp
runner.hpp
compile_run.hpp
compile_server.cc
temp 存放临时文件
oj_server
oj_model.hpp 文件版
oj_model2.hpp MySQL版
oj_view.hpp
oj_control.hpp
oj_server.cc
conf:
service_machine.conf
questions:
questions.list
1:
desc.txt
header.cpp
tail.hpp
template_html:
all_questions.html
one_question.html
wwwroot
index.html
makefile1
c
compile_server:compile_server.cc
g++ -o $@ $^ -std=c++17 -ljsoncpp -lpthread
.PHONY:clean
clean:
rm -rf compile_server
makefile2
c
oj_server:oj_server.cc
g++ -o $@ $^ -I./include -L./lib -std=c++17 -lpthread -lctemplate -ljsoncpp -lmysqlclient
.PHONY:clean
clean:
rm -rf oj_server
makefile3
c
.PHONY:all
all:
@cd compile_server; \
make;\
cd -;\
cd oj_server;\
make;\
cd -;
.PHONY:output
output:
@mkdir -p output/compile_server;\
mkdir -p output/oj_server;\
cp -rf compile_server/compile_server output/compile_server;\
cp -rf compile_server/temp output/compile_server;\
cp -rf oj_server/conf output/oj_server/;\
cp -rf oj_server/questions output/oj_server/;\
cp -rf oj_server/template_html output/oj_server/;\
cp -rf oj_server/wwwroot output/oj_server/;\
cp -rf oj_server/oj_server output/oj_server/;
.PHONY:clean
clean:
@cd compile_server;\
make clean;\
cd -;\
cd oj_server;\
make clean;\
cd -;\
rm -rf output;

四、公共模块comm
1、Mutex.hpp
cpp
#pragma once
#include <iostream>
#include <pthread.h>
namespace MutexModule
{
// 互斥锁封装类
class Mutex
{
public:
// 初始化锁
Mutex()
{
pthread_mutex_init(&_mutex, nullptr);
}
// 加锁
void Lock()
{
int n = pthread_mutex_lock(&_mutex);
}
// 解锁
void Unlock()
{
int n = pthread_mutex_unlock(&_mutex);
}
// 获取锁指针
pthread_mutex_t *Get()
{
return &_mutex;
}
// 销毁锁
~Mutex()
{
pthread_mutex_destroy(&_mutex);
}
private:
pthread_mutex_t _mutex; // 原生互斥锁对象
};
// RAII 自动锁
class LockGuard
{
public:
LockGuard(Mutex &mutex)
: _mutex(mutex)
{
_mutex.Lock();
}
~LockGuard()
{
_mutex.Unlock();
}
private:
Mutex &_mutex;
};
}
2、Log.hpp
cpp
#pragma once
#include <iostream>
#include <cstdio>
#include <string>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <memory>
#include <ctime>
#include <sys/types.h>
#include <unistd.h>
#include "Mutex.hpp"
namespace LogModule
{
using namespace MutexModule;
const std::string gsep = "\r\n";
// 日志策略基类(接口)
class LogStrategy
{
public:
~LogStrategy() = default;
virtual void SyncLog(const std::string &message) = 0;
};
// 控制台日志输出(线程安全)
class ConsoleLogStrategy : public LogStrategy
{
public:
void SyncLog(const std::string &message) override
{
LockGuard lockguard(_mutex);
std::cout << message << gsep;
}
~ConsoleLogStrategy() {}
private:
Mutex _mutex;
};
const std::string defaultpath = "/var/log";
const std::string defaultfile = "my.log";
// 文件日志输出(自动建目录、线程安全)
class FileLogStrategy : public LogStrategy
{
public:
FileLogStrategy(const std::string &path = defaultpath, const std::string &file = defaultfile)
: _path(path), _file(file)
{
LockGuard lockguard(_mutex);
if (std::filesystem::exists(_path))
return;
try
{
std::filesystem::create_directories(_path);
}
catch (const std::filesystem::filesystem_error &e)
{
std::cerr << e.what() << std::endl;
}
}
// 追加写入日志文件
void SyncLog(const std::string &message) override
{
LockGuard lockguard(_mutex);
std::string filename = _path + (_path.back() == '/' ? "" : "/") + _file;
std::ofstream out(filename, std::ios::app);
if (!out.is_open())
return;
out << message << gsep;
out.close();
}
private:
std::string _path;
std::string _file;
Mutex _mutex;
};
// 日志等级类
enum class LogLevel
{
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
};
// 日志等级转字符串
std::string LevelStr(LogLevel level)
{
switch (level)
{
case LogLevel::DEBUG:
return "DEBUG";
case LogLevel::INFO:
return "INFO";
case LogLevel::WARNING:
return "WARNING";
case LogLevel::ERROR:
return "ERROR";
case LogLevel::FATAL:
return "FATAL";
default:
return "UNKNOWN";
}
}
// 获取格式化时间字符串(线程安全)
std::string GetTimeStamp()
{
time_t curr = time(nullptr);
struct tm curr_tm; // 出参
localtime_r(&curr, &curr_tm);
char buf[128]; // 出参
snprintf(buf, sizeof(buf), "%4d-%02d-%02d %02d:%02d:%02d",
curr_tm.tm_year + 1900,
curr_tm.tm_mon + 1,
curr_tm.tm_mday,
curr_tm.tm_hour,
curr_tm.tm_min,
curr_tm.tm_sec);
return buf;
}
// 日志核心管理类
class Logger
{
public:
Logger()
{
EnableConsoleLogStrategy();
}
// 切换为文件输出
void EnableFileLogStrategy()
{
_fflush_strategy = std::make_unique<FileLogStrategy>();
}
// 切换为控制台输出
void EnableConsoleLogStrategy()
{
_fflush_strategy = std::make_unique<ConsoleLogStrategy>();
}
// 日志消息构造: 负责拼接内容, 析构时自动输出
class LogMessage
{
public:
// 构造日志头部(时间、等级、进程ID、文件名、行号)
LogMessage(LogLevel level, std::string src_name, int line_number, Logger &logger)
: _logger(logger)
{
std::stringstream ss;
ss << "[" << GetTimeStamp() << "] "
<< "[" << LevelStr(level) << "] "
<< "[" << getpid() << "] "
<< "[" << src_name << "] "
<< "[" << line_number << "] - ";
_loginfo = ss.str();
}
// 流方式拼接日志内容
template <typename T>
LogMessage &operator<<(const T &info)
{
std::stringstream ss;
ss << info;
_loginfo += ss.str();
return *this;
}
// 析构自动输出日志
~LogMessage()
{
if (_logger._fflush_strategy)
{
_logger._fflush_strategy->SyncLog(_loginfo);
}
}
private:
std::string _loginfo;
Logger &_logger;
};
// 仿函数接口, 创建日志消息
LogMessage operator()(LogLevel level, std::string name, int line)
{
return LogMessage(level, name, line, *this);
}
private:
std::unique_ptr<LogStrategy> _fflush_strategy;
};
Logger logger;
// 简化调用宏
#define LOG(level) logger(level, __FILE__, __LINE__)
#define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy()
#define Enable_File_Log_Strategy() logger.EnableFileLogStrategy()
}
3、Util.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <atomic>
#include <fstream>
#include <vector>
#include <cstdio>
#include <cstdlib>
#include <boost/algorithm/string.hpp>
const std::string temp_path = "./temp/";
namespace UtilModule
{
// 时间工具类
class TimeUtil
{
public:
// 获取秒级时间戳
static std::string GetTimeStamp()
{
struct timeval _time; // 出参
gettimeofday(&_time, nullptr);
return std::to_string(_time.tv_sec);
}
// 获取毫秒级时间戳
static std::string GetTimeMs()
{
struct timeval _time; // 出参
gettimeofday(&_time, nullptr);
return std::to_string(_time.tv_sec * 1000 + _time.tv_usec / 1000);
}
};
// 路径工具类: 拼接编译/运行所需临时文件路径
class PathUtil
{
public:
// 给文件名添加指定后缀
static std::string AddSuffix(const std::string &file_name, const std::string &suffix)
{
std::string path_name = temp_path;
path_name += file_name;
path_name += suffix;
return path_name;
}
// 源文件
static std::string Src(const std::string &file_name)
{
return AddSuffix(file_name, ".cpp");
}
// 可执行文件
static std::string Exe(const std::string &file_name)
{
return AddSuffix(file_name, ".exe");
}
// 编译错误文件
static std::string CompileError(const std::string &file_name)
{
return AddSuffix(file_name, ".compile_error");
}
// 标准输入输出错误文件
static std::string Stdin(const std::string &file_name)
{
return AddSuffix(file_name, ".stdin");
}
static std::string Stdout(const std::string &file_name)
{
return AddSuffix(file_name, ".stdout");
}
static std::string Stderr(const std::string &file_name)
{
return AddSuffix(file_name, ".stderr");
}
};
// 文件工具类: 文件判断、读写、唯一文件名生成
class FileUtil
{
public:
// 判断文件是否存在
static bool IsFileExists(const std::string &path_name)
{
struct stat st;
if (stat(path_name.c_str(), &st) == 0)
{
return true;
}
return false;
}
// 生成唯一文件名(时间戳+原子自增ID)
static std::string UniqFileName()
{
static std::atomic_uint id(0);
id++;
std::string ms = TimeUtil::GetTimeMs();
return ms + "_" + std::to_string(id);
}
// 写入文件
static bool WriteFile(const std::string &target, const std::string &content)
{
std::ofstream out(target);
if (!out.is_open())
return false;
out.write(content.c_str(), content.size());
out.close();
return true;
}
// 读取文件, keep控制是否保留换行符
static bool ReadFile(const std::string &target, std::string *content, bool keep = false)
{
(*content).clear();
std::ifstream in(target);
if (!in.is_open())
return false;
std::string line; // 出参
while (std::getline(in, line)) // 读取一行
{
(*content) += line;
(*content) += (keep ? "\n" : "");
}
in.close();
return true;
}
};
// 字符串工具类: 字符串分割
class StringUtil
{
public:
// 分隔字符串,合并连续分隔符 target出参
static void SplitString(const std::string &src, std::vector<std::string> *target, const std::string &sep)
{
boost::split(*target, src, boost::is_any_of(sep), boost::algorithm::token_compress_on);
}
};
}
五、编译运行模块compile_server
提供服务:代码编译运行,输出格式化执行结果

1、compiler.hpp
cpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <unistd.h>
#include "../comm/Log.hpp"
#include "../comm/Util.hpp"
namespace ns_compiler
{
using namespace LogModule;
using namespace UtilModule;
// 编译模块: 负责调用g++编译用户代码
class Compiler
{
public:
Compiler() = default;
~Compiler() = default;
// 编译指定源文件, 生成可执行程序
static bool Compile(const std::string &file_name)
{
pid_t pid = fork();
if (pid < 0)
{
LOG(LogLevel::ERROR) << "创建子进程失败";
return false;
}
else if (pid == 0)
{
// 子进程: 执行编译逻辑
umask(0);
int _stderr = open(PathUtil::CompileError(file_name).c_str(), O_CREAT | O_WRONLY, 0644);
if (_stderr < 0)
{
LOG(LogLevel::WARNING) << "打开stderr文件失败";
exit(1);
}
// 标准错误重定向到文件
dup2(_stderr, 2);
// 替换为g++执行编译
execlp("g++", "g++", "-o", PathUtil::Exe(file_name).c_str(), PathUtil::Src(file_name).c_str(),
"-D", "COMPILER_ONLINE", "-std=c++11", nullptr);
LOG(LogLevel::ERROR) << "启动编译器g++失败";
exit(2);
}
else
{
// 父进程: 等待子进程完成
waitpid(pid, nullptr, 0);
if (FileUtil::IsFileExists(PathUtil::Exe(file_name)))
{
LOG(LogLevel::INFO) << PathUtil::Src(file_name) << "编译成功";
return true;
}
}
LOG(LogLevel::ERROR) << "编译失败, 未生成可执行程序";
return false;
}
};
}
2、runner.hpp
cpp
#pragma once
#include <iostream>
#include <sys/time.h>
#include <sys/resource.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cstdlib>
#include "../comm/Log.hpp"
#include "../comm/Util.hpp"
namespace ns_runner
{
using namespace LogModule;
using namespace UtilModule;
// 运行模块: 执行用户程序, 限制资源, 重定向输入输出
class Runner
{
public:
Runner() = default;
~Runner() = default;
// 设置进程资源限制(CPU时间、内存大小)
static void SetProcLimit(int _cpu_limit, int _mem_limit)
{
// 设置CPU时长限制(秒)
struct rlimit cpu_rlimit;
cpu_rlimit.rlim_max = RLIM_INFINITY;
cpu_rlimit.rlim_cur = _cpu_limit;
setrlimit(RLIMIT_CPU, &cpu_rlimit);
// 设置内存大小限制(KB)
struct rlimit mem_rlimit;
mem_rlimit.rlim_max = RLIM_INFINITY;
mem_rlimit.rlim_cur = _mem_limit * 1024;
setrlimit(RLIMIT_AS, &mem_rlimit);
}
// 运行可执行程序, 返回运行状态码
static int Run(const std::string &file_name, int cpu_limit, int mem_limit)
{
std::string _execute = PathUtil::Exe(file_name);
std::string _stdin = PathUtil::Stdin(file_name);
std::string _stdout = PathUtil::Stdout(file_name);
std::string _stderr = PathUtil::Stderr(file_name);
umask(0);
int _stdin_fd = open(_stdin.c_str(), O_CREAT | O_RDONLY, 0644);
int _stdout_fd = open(_stdout.c_str(), O_CREAT | O_WRONLY, 0644);
int _stderr_fd = open(_stderr.c_str(), O_CREAT | O_WRONLY, 0644);
if (_stdin_fd < 0 || _stdout_fd < 0 || _stderr_fd < 0)
{
LOG(LogLevel::ERROR) << "运行时打开标准文件失败";
return -1;
}
pid_t pid = fork();
if (pid < 0)
{
LOG(LogLevel::ERROR) << "运行时创建子进程失败";
close(_stdin_fd);
close(_stdout_fd);
close(_stderr_fd);
return -2;
}
else if (pid == 0)
{
// 子进程: 重定向并执行程序
dup2(_stdin_fd, 0);
dup2(_stdout_fd, 1);
dup2(_stderr_fd, 2);
close(_stdin_fd);
close(_stdout_fd);
close(_stderr_fd);
SetProcLimit(cpu_limit, mem_limit);
execl(_execute.c_str(), _execute.c_str(), nullptr);
exit(1);
}
else
{
// 父进程: 等待子进程退出
close(_stdin_fd);
close(_stdout_fd);
close(_stderr_fd);
int status = 0; // 出参
waitpid(pid, &status, 0);
LOG(LogLevel::INFO) << "运行完毕, info: " << (status & 0x7F);
return status & 0x7F;
}
}
};
}
测试资源限制:
cpp
#include <iostream>
#include <sys/time.h>
#include <sys/resource.h>
#include <unistd.h>
#include <signal.h>
void handler(int signo)
{
std::cout << "signo: " << signo << std::endl;
exit(1);
}
int main()
{
for (int i = 1; i < 31; i++)
{
signal(i, handler);
}
// 测试CPU时长限制
struct rlimit r;
r.rlim_max = RLIM_INFINITY;
r.rlim_cur = 1;
setrlimit(RLIMIT_CPU, &r);
while (1)
;
// // 测试内存限制
// struct rlimit r;
// r.rlim_max = RLIM_INFINITY;
// r.rlim_cur = 1024 * 1024 * 10; // 10M
// setrlimit(RLIMIT_AS, &r);
// int count = 0;
// while (true)
// {
// int *p = new int[1024 * 1024];
// count++;
// std::cout << "size: " << count << std::endl;
// sleep(1);
// }
return 0;
}
运行结果:



3、compile_run.hpp
cpp
#pragma once
#include <signal.h>
#include <jsoncpp/json/json.h>
#include "../comm/Log.hpp"
#include "../comm/Util.hpp"
#include "compiler.hpp"
#include "runner.hpp"
namespace ns_compile_and_run
{
using namespace LogModule;
using namespace UtilModule;
using namespace ns_compiler;
using namespace ns_runner;
// 编译运行模块: 整合编译、运行、结果返回、临时文件清理
class CompileAndRun
{
public:
// 清理本次请求产生的所有临时文件
static void RemoveTempFile(const std::string &file_name)
{
unlink(PathUtil::Src(file_name).c_str());
unlink(PathUtil::Exe(file_name).c_str());
unlink(PathUtil::CompileError(file_name).c_str());
unlink(PathUtil::Stdin(file_name).c_str());
unlink(PathUtil::Stdout(file_name).c_str());
unlink(PathUtil::Stderr(file_name).c_str());
}
// 状态码转描述信息
static std::string CodeToDesc(int code, const std::string &file_name)
{
std::string desc; // 出参
switch (code)
{
case 0:
desc = "编译运行成功";
break;
case -1:
desc = "提交的代码为空";
break;
case -2:
desc = "未知错误";
break;
case -3:
FileUtil::ReadFile(PathUtil::CompileError(file_name), &desc, true);
break;
case SIGABRT:
desc = "内存超过限制";
break;
case SIGXCPU:
desc = "CPU使用超时";
break;
case SIGFPE:
desc = "浮点数溢出";
break;
default:
desc = "未知错误: " + std::to_string(code);
break;
}
return desc;
}
// 编译运行服务入口
// 输入: JSON字符串(代码、输入、资源限制)
// 输出: JSON字符串(状态、描述、运行结果)
static void Start(const std::string &in_json, std::string *out_json)
{
Json::Value in_value; // 出参
Json::Reader reader;
reader.parse(in_json, in_value);
std::string code = in_value["code"].asString();
std::string input = in_value["input"].asString();
int cpu_limit = in_value["cpu_limit"].asInt();
int mem_limit = in_value["mem_limit"].asInt();
int status_code = 0;
int run_result = 0;
Json::Value out_value; // 出参
std::string file_name;
// 代码为空
if (code.size() == 0)
{
status_code = -1;
goto END;
}
// 生成唯一文件名
file_name = FileUtil::UniqFileName();
// 写入源文件
if (!FileUtil::WriteFile(PathUtil::Src(file_name), code))
{
status_code = -2;
goto END;
}
// 编译
if (!Compiler::Compile(file_name))
{
status_code = -3;
goto END;
}
// 运行
run_result = Runner::Run(file_name, cpu_limit, mem_limit);
if (run_result < 0)
status_code = -2;
else if (run_result > 0)
status_code = run_result;
else
status_code = 0; // 运行成功
END:
out_value["status"] = status_code;
out_value["reason"] = CodeToDesc(status_code, file_name);
// 运行成功, 读取输出
if (status_code == 0)
{
std::string stdout_str; // 出参
FileUtil::ReadFile(PathUtil::Stdout(file_name), &stdout_str, true);
out_value["stdout"] = stdout_str;
std::string stderr_str;
FileUtil::ReadFile(PathUtil::Stderr(file_name), &stderr_str, true);
out_value["stderr"] = stderr_str;
}
// 构造返回JSON
Json::StyledWriter writer;
*out_json = writer.write(out_value);
// 清理临时文件
RemoveTempFile(file_name);
}
};
}
测试用例:
cpp
#include"compile_run.hpp"
using namespace ns_compile_and_run;
int main()
{
std::string in_json; // 出参
Json::Value in_value;
in_value["code"] = R"(
#include<iostream>
int main()
{
std::cout<<"你可以看见我了"<<std::endl;
return 0;
})";
in_value["input"] = "";
in_value["cpu_limit"] = 1;
in_value["mem_limit"] = 1024 * 30;
Json::FastWriter writer;
in_json = writer.write(in_value);
std::cout << in_json << std::endl;
std::string out_json; // 出参
CompileAndRun::Start(in_json, &out_json);
std::cout << out_json << std::endl;
return 0;
}
运行结果:


4、compile_server.cc
cpp
#include"compile_run.hpp"
#include"../comm/httplib.h"
using namespace ns_compile_and_run;
using namespace httplib;
// 帮助提示: 参数错误时打印用法
void Usage(std::string proc)
{
std::cerr << "Usage: " << "\n\t" << proc << " port" << std::endl;
}
// HTTP服务主入口: 提供在线编译运行接口
int main(int argc,char* argv[])
{
// 校验启动参数
if(argc!=2)
{
Usage(argv[0]);
return 1;
}
Server svr;
// 注册服务接口: 接收JSON, 处理后返回JSON
svr.Post("/compile_and_run",[](const Request& req, Response& resp)
{
std::string in_json = req.body;
std::string out_json; // 出参
if(!in_json.empty())
{
// 调用核心编译运行服务
CompileAndRun::Start(in_json,&out_json);
resp.set_content(out_json,"application/json;charset=utf-8");
}
});
// 启动服务器, 监听全部网卡
svr.listen("0.0.0.0",atoi(argv[1]));
return 0;
}
六、基于 MVC 结构的 OJ 模块
核心:搭建在线判题小型网站
功能
- 首页: 展示题目列表
- 编辑页: 在线编写代码
- **判题功能:**编译 + 运行
MVC 职责
- M(模型): 处理题库数据(文件 / MySQL 增删改查)
- V(视图): 渲染网页,展示给用户
- C(控制器): 处理核心业务逻辑
1、oj_model.hpp 文件版
cpp
#pragma once
// 文件版本
#include <iostream>
#include <unordered_map>
#include <fstream>
#include <cassert>
#include <fstream>
#include <vector>
#include <string>
#include "../comm/Log.hpp"
#include "../comm/Util.hpp"
namespace ns_model
{
using namespace LogModule;
using namespace UtilModule;
// 题目结构体: 单题完整信息
struct Question
{
std::string number; // 题目编号
std::string title; // 题目标题
std::string star; // 难度
int cpu_limit; // CPU时间限制
int mem_limit; // 内存限制
std::string desc; // 题目描述
std::string header; // 代码头模板
std::string tail; // 测试用例尾
};
const std::string questions_list = "./questions/questions.list";
const std::string questions_path = "./questions/";
// 数据模型层: 加载、管理、提供题目数据
class Model
{
private:
std::unordered_map<std::string, Question> questions;
public:
Model()
{
assert(LoadQuestionList(questions_list));
}
// 加载题库配置文件
bool LoadQuestionList(const std::string &questions_list)
{
std::ifstream in(questions_list);
if (!in.is_open())
{
LOG(LogLevel::FATAL) << "加载题库失败, 请检查题库文件";
return false;
}
std::string line; // 出参
while (std::getline(in, line))
{
std::vector<std::string> tokens; // 出参
StringUtil::SplitString(line, &tokens, " ");
if (tokens.size() != 5)
{
LOG(LogLevel::WARNING) << "加载题目格式错误";
continue;
}
Question q;
q.number = tokens[0];
q.title = tokens[1];
q.star = tokens[2];
q.cpu_limit = atoi(tokens[3].c_str());
q.mem_limit = atoi(tokens[4].c_str());
std::string path = questions_path + q.number + "/";
FileUtil::ReadFile(path + "desc.txt", &q.desc, true);
FileUtil::ReadFile(path + "header.cpp", &q.header, true);
FileUtil::ReadFile(path + "tail.cpp", &q.tail, true);
questions[q.number] = q;
}
LOG(LogLevel::INFO) << "加载题库成功";
in.close();
return true;
}
// 获取全部题目 出参out
bool GetAllQuestions(std::vector<Question> *out)
{
if (questions.empty())
{
LOG(LogLevel::ERROR) << "获取题库失败";
return false;
}
for (const auto &kv : questions)
{
out->push_back(kv.second);
}
return true;
}
// 根据题号获取单题 出参q
bool GetOneQuestion(const std::string &number, Question *q)
{
auto it = questions.find(number);
if (it == questions.end())
{
LOG(LogLevel::ERROR) << "题目不存在: " << number;
return false;
}
(*q) = it->second;
return true;
}
~Model() {}
};
}
2、oj_model2.hpp MySQL版
cpp
#pragma once
// MySQL版本
#include <iostream>
#include <unordered_map>
#include <fstream>
#include <cassert>
#include <fstream>
#include <vector>
#include <string>
#include <mysql/mysql.h>
#include "../comm/Log.hpp"
#include "../comm/Util.hpp"
namespace ns_model
{
using namespace LogModule;
using namespace UtilModule;
// 题目结构体:单题完整信息
struct Question
{
std::string number; // 题目编号
std::string title; // 题目标题
std::string star; // 难度
int cpu_limit; // CPU时间限制
int mem_limit; // 内存限制
std::string desc; // 题目描述
std::string header; // 代码模板
std::string tail; // 测试用例
};
// MySQL 配置
const std::string oj_questions = "oj_questions";
const std::string host = "127.0.0.1";
const std::string user = "oj_client";
const std::string passwd = "123456";
const std::string db = "oj";
const int port = 3306;
// MySQL 数据模型层
class Model
{
public:
Model() = default;
~Model() = default;
// 执行SQL查询, 返回题目列表 出参out
bool QueryMySql(const std::string &sql, std::vector<Question> *out)
{
// 初始化MySQL句柄
MYSQL *my = mysql_init(nullptr);
if (nullptr == mysql_real_connect(my, host.c_str(), user.c_str(), passwd.c_str(), db.c_str(), port, nullptr, 0))
{
LOG(LogLevel::FATAL) << "连接数据库失败!";
return false;
}
// 设置编码
mysql_set_character_set(my, "utf8mb4");
LOG(LogLevel::INFO) << "连接数据库成功!";
// 执行SQL
if (0 != mysql_query(my, sql.c_str()))
{
LOG(LogLevel::WARNING) << sql << " execute error!";
return false;
}
// 获取结果集
MYSQL_RES *res = mysql_store_result(my);
if (res == nullptr)
{
LOG(LogLevel::WARNING) << "结果集为空";
mysql_close(my);
return false;
}
// 解析结果
int rows = mysql_num_rows(res);
for (int i = 0; i < rows; i++)
{
// 获取一行数据
MYSQL_ROW row = mysql_fetch_row(res);
Question q;
q.number = row[0];
q.title = row[1];
q.star = row[2];
q.desc = row[3];
q.header = row[4];
q.tail = row[5];
q.cpu_limit = atoi(row[6]);
q.mem_limit = atoi(row[7]);
// 加入结果列表
out->push_back(q);
}
// 释放资源
mysql_free_result(res);
mysql_close(my);
return true;
}
// 获取所有题目
bool GetAllQuestions(std::vector<Question> *out)
{
std::string sql = "select * from " + oj_questions;
return QueryMySql(sql, out);
}
// 根据题号获取单题
bool GetOneQuestion(const std::string &number, Question *q)
{
std::string sql = "select * from " + oj_questions + " where number='" + number + "'";
std::vector<Question> result;
if (QueryMySql(sql, &result) && result.size() == 1)
{
*q = result[0];
return true;
}
return false;
}
};
}
3、oj_view.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <ctemplate/template.h>
#include"oj_model2.hpp"
namespace ns_view
{
using namespace ns_model;
// HTML模版文件所在目录
const std::string template_path = "./template_html/";
// 视图层: 将数据渲染成HTML页面
class View
{
public:
View() = default;
~View() = default;
// 渲染题目列表页面
void AllExpandHtml(const std::vector<ns_model::Question> &questions, std::string *html)
{
// 模板文件路径
std::string src_html = template_path + "all_questions.html";
// 渲染数据字典
ctemplate::TemplateDictionary root("all_questions");
// 循环填充题目数据
for (const auto &q : questions)
{
ctemplate::TemplateDictionary *sub = root.AddSectionDictionary("questions_list");
sub->SetValue("number", q.number);
sub->SetValue("title", q.title);
sub->SetValue("star", q.star);
}
// 加载模板并渲染
ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP);
tpl->Expand(html, &root);
}
// 渲染单题详情页面
void OneExpandHtml(const ns_model::Question &q, std::string *html)
{
// 模板文件路径
std::string src_html = template_path + "one_question.html";
// 渲染数据字典
ctemplate::TemplateDictionary root("one_question");
// 填充单题数据
root.SetValue("number", q.number);
root.SetValue("title", q.title);
root.SetValue("star", q.star);
root.SetValue("desc", q.desc);
root.SetValue("pre_code", q.header);
// 加载模板并渲染
ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP);
tpl->Expand(html, &root);
}
};
}
4、oj_control.hpp
cpp
#pragma once
#include <iostream>
#include <mutex>
#include <vector>
#include <fstream>
#include <jsoncpp/json/json.h>
#include <algorithm>
#include "../comm/Log.hpp"
#include "../comm/Util.hpp"
#include "../comm/httplib.h"
#include "oj_model2.hpp"
#include "oj_view.hpp"
namespace ns_control
{
using namespace LogModule;
using namespace UtilModule;
using namespace httplib;
using namespace ns_model;
using namespace ns_view;
// 编译服务器节点
class Machine
{
public:
std::string ip;
int port;
uint64_t load; // 负载
std::mutex *mtx;
public:
Machine() : ip(""), port(0), load(0), mtx(nullptr) {}
~Machine() {}
// 提升负载
void IncLoad()
{
if (mtx) mtx->lock();
++load;
if (mtx) mtx->unlock();
}
// 减少负载
void DecLoad()
{
if (mtx) mtx->lock();
--load;
if (mtx) mtx->unlock();
}
// 重置负载
void ResetLoad()
{
if (mtx) mtx->lock();
load = 0;
if (mtx) mtx->unlock();
}
// 获取负载
uint64_t Load()
{
uint64_t _load = 0;
if (mtx) mtx->lock();
_load = load;
if (mtx) mtx->unlock();
return _load;
}
};
const std::string service_machine = "./conf/service_machine.conf";
// 负载均衡管理器
class LoadBalance
{
private:
std::vector<Machine> machines;
std::vector<int> online;
std::vector<int> offline;
std::mutex mtx;
public:
LoadBalance()
{
assert(LoadConf(service_machine));
LOG(LogLevel::INFO) << "加载 " << service_machine << " 成功";
}
// 加载服务器列表
bool LoadConf(const std::string &machine_conf)
{
std::ifstream in(machine_conf);
if (!in.is_open())
{
LOG(LogLevel::FATAL) << "加载配置失败";
return false;
}
std::string line; // 出参
while (std::getline(in, line))
{
std::vector<std::string> tokens; // 出参
StringUtil::SplitString(line, &tokens, ":");
if (tokens.size() != 2)
continue;
Machine m;
m.ip = tokens[0];
m.port = atoi(tokens[1].c_str());
m.load = 0;
m.mtx = new std::mutex();
online.push_back(machines.size());
machines.push_back(m);
}
in.close();
return true;
}
// 选择负载最低的机器 轮询+hash
bool SmartChoice(int *id, Machine **m)
{
std::lock_guard<std::mutex> lock(mtx);
int online_num = online.size();
if (online_num == 0)
{
LOG(LogLevel::FATAL) << " 所有的后端编译主机已经离线, 请事尽快查看";
return false;
}
*id = online[0];
*m = &machines[online[0]];
uint64_t min_load = (*m)->Load();
for (int i = 1; i < online_num; i++)
{
int curr_id = online[i];
uint64_t curr_load = machines[curr_id].Load();
if (curr_load<min_load)
{
min_load = curr_load;
*id = curr_id;
*m = &machines[curr_id];
}
}
return true;
}
// 下线机器
void OfflineMachine(int which)
{
std::lock_guard<std::mutex> lock(mtx);
for (auto it = online.begin(); it != online.end(); it++)
{
// 找到要下线的机器id
if (*it == which)
{
machines[which].ResetLoad();
online.erase(it);
offline.push_back(which);
break;
}
}
}
// 全部上线
void OnlineMachine()
{
std::lock_guard<std::mutex> lock(mtx);
online.insert(online.end(), offline.begin(), offline.end());
offline.clear();
LOG(LogLevel::INFO) << "所有主机已重新上线";
}
void ShowMachines()
{
std::lock_guard<std::mutex> lock(mtx);
std::cout << "在线主机: ";
for (auto id : online)
{
std::cout << id << " ";
}
std::cout << "\n离线主机: ";
for (auto &id : offline)
{
std::cout << id << " ";
}
std::cout << std::endl;
}
};
// MVC 核心控制器
class Control
{
private:
Model _model;
View _view;
LoadBalance _load_balance;
public:
Control() = default;
~Control() = default;
public:
void RecoveryMachine()
{
_load_balance.OnlineMachine();
}
// 获取题目列表网页
bool AllQuestions(std::string *html)
{
std::vector<struct Question> all; // 出参
if (_model.GetAllQuestions(&all))
{
sort(all.begin(), all.end(), [](const struct Question &q1, const struct Question &q2)
{
return atoi(q1.number.c_str()) < atoi(q2.number.c_str()); // 升序
});
_view.AllExpandHtml(all, html);
return true;
}
*html = "获取题目列表失败";
return false;
}
// 获取单题页面
bool Question(const std::string &number, std::string *html)
{
struct Question q; // 出参
if (_model.GetOneQuestion(number, &q))
{
_view.OneExpandHtml(q, html);
return true;
}
*html = "题目 " + number + " 不存在";
return false;
}
// 判题逻辑
void Judge(const std::string &number, const std::string& in_json, std::string *out_json)
{
struct Question q;
if (!_model.GetOneQuestion(number, &q))
return;
Json::Value in_value; // 出参
Json::Reader reader;
reader.parse(in_json, in_value);
std::string code = in_value["code"].asString();
Json::Value compile_value; // 出参
compile_value["input"] = in_value["input"].asString();
compile_value["code"] = code + "\n" + q.tail;
compile_value["cpu_limit"] = q.cpu_limit;
compile_value["mem_limit"] = q.mem_limit;
Json::FastWriter writer;
std::string req_json = writer.write(compile_value);
while (true)
{
int id = 0; // 出参
Machine *m = nullptr; // 出参
if (!_load_balance.SmartChoice(&id, &m))
break;
Client cli(m->ip, m->port);
m->IncLoad();
LOG(LogLevel::INFO) << " 选择主机成功, 主机id: " << id << " 详情: " << m->ip << ":"
<< m->port << " 当前主机的负载是: " << m->Load();
if (auto res = cli.Post("/compile_and_run", req_json, "application/json;charset=utf-8"))
{
if (res->status == 200)
{
*out_json = res->body;
m->DecLoad();
LOG(LogLevel::INFO) << "请求编译和运行服务成功...";
break;
}
m->DecLoad();
}
else
{
LOG(LogLevel::ERROR) << " 当前请求的主机id: " << id << " 详情: " << m->ip << ":" << m->port
<< " 可能已经离线";
_load_balance.OfflineMachine(id);
_load_balance.ShowMachines();
}
}
}
};
}
5、oj_server.cc
cpp
#include <iostream>
#include <signal.h>
#include "../comm/httplib.h"
#include "oj_control.hpp"
using namespace httplib;
using namespace ns_control;
static Control *ctrl_ptr = nullptr;
// 信号处理: 恢复所有离线机器
void Recovery(int signo)
{
ctrl_ptr->RecoveryMachine();
}
int main()
{
// 注册信号: Ctrl+\ 触发机器上线
signal(SIGQUIT, Recovery);
Server svr;
Control ctrl;
ctrl_ptr = &ctrl;
// 获取所有题目列表
svr.Get("/all_questions", [&ctrl](const Request &req, Response &resp)
{
std::string html; // 出参
ctrl.AllQuestions(&html);
resp.set_content(html, "text/html; charset=utf-8");
});
// 获取单题详情
svr.Get(R"(/question/(\d+))", [&ctrl](const Request &req, Response &resp)
{
std::string number = req.matches[1];
std::string html;
ctrl.Question(number,&html);
resp.set_content(html,"text/html; charset=utf-8");
});
// 提交判题
svr.Post(R"(/judge/(\d+))", [&ctrl](const Request &req, Response &resp)
{
std::string number=req.matches[1];
std::string result_json; // 出参
ctrl.Judge(number, req.body, &result_json);
resp.set_content(result_json, "application/json;charset=utf-8");
});
// 静态资源路径
svr.set_base_dir("./wwwroot");
// 启动服务
svr.listen("0.0.0.0", 8080);
return 0;
}
6、conf
1)service_machine.conf
c
127.0.0.1:8081
127.0.0.1:8082
127.0.0.1:8083
7、questions
1)questions.list
c
1 判断回文数 简单 1 30000
2 求最大值 简单 1 30000
3 求最大值 简单 1 30000
4 求最大值 简单 1 30000
5 求最大值 简单 1 30000
6 求最大值 简单 1 30000
7 求最大值 简单 1 30000
8 求最大值 简单 1 30000
9 求最大值 简单 1 30000
10 求最大值 简单 1 30000
11 求最大值 简单 1 30000
12 求最大值 简单 1 30000
13 求最大值 简单 1 30000
14 求最大值 简单 1 30000
15 求最大值 简单 1 30000
2)1
a)desc.txt
c
判断一个整数是否是回文数。回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。
示例 1:
输入: 121
输出: true
示例 2:
输入: -121
输出: false
解释: 从左向右读, 为 -121 。 从右向左读, 为 121- 。因此它不是一个回文数。
示例 3:
输入: 10
输出: false
解释: 从右向左读, 为 01 。因此它不是一个回文数。
进阶:
你能不将整数转为字符串来解决这个问题吗?
b)header.cpp
c
#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <algorithm>
using namespace std;
class Solution{
public:
bool isPalindrome(int x)
{
//将你的代码写在下面
return true;
}
};
c)tail.cpp
c
#ifndef COMPILER_ONLINE
#include "header.cpp"
#endif
void Test1()
{
// 通过定义临时对象,来完成方法的调用
bool ret = Solution().isPalindrome(121);
if(ret){
std::cout << "通过用例1, 测试121通过 ... OK!" << std::endl;
}
else{
std::cout << "没有通过用例1, 测试的值是: 121" << std::endl;
}
}
void Test2()
{
// 通过定义临时对象,来完成方法的调用
bool ret = Solution().isPalindrome(-10);
if(!ret){
std::cout << "通过用例2, 测试-10通过 ... OK!" << std::endl;
}
else{
std::cout << "没有通过用例2, 测试的值是: -10" << std::endl;
}
}
int main()
{
Test1();
Test2();
return 0;
}
8、template_html
1)all_questions.html
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: black;
overflow: hidden;
}
/* 导航栏链接样式 */
.container .navbar a {
display: inline-block;
width: 80px;
color: white;
font-size: large;
line-height: 50px;
text-decoration: none;
text-align: center;
}
/* 导航栏链接悬浮效果 */
.container .navbar a:hover {
background-color: green;
}
/* 登录按钮右浮 */
.container .navbar .login {
float: right;
}
/* 题目列表区域样式 */
.container .question_list {
padding-top: 50px;
width: 800px;
height: 100%;
margin: 0px auto;
/* background-color: #ccc; */
text-align: center;
}
/* 列表标题样式 */
.container .question_list table {
width: 100%;
font-size: large;
font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
margin-top: 50px;
background-color: rgb(243, 248, 246);
}
/* 题目表格样式 */
.container .question_list h1 {
color: green;
}
/* 表格单元格样式 */
.container .question_list table .item {
width: 100px;
height: 40px;
font-size: large;
font-family:'Times New Roman', Times, serif;
}
/* 表格链接样式 */
.container .question_list table .item a {
text-decoration: none;
color: black;
}
/* 表格链接悬浮效果 */
.container .question_list table .item a:hover {
color: blue;
text-decoration:underline;
}
/* 页脚样式 */
.container .footer {
width: 100%;
height: 50px;
text-align: center;
line-height: 50px;
color: #ccc;
margin-top: 15px;
}
</style>
</head>
<body>
<div class="container">
<!-- 导航栏 -->
<div class="navbar">
<a href="/">首页</a>
<a href="/all_questions">题库</a>
<a href="#">竞赛</a>
<a href="#">讨论</a>
<a href="#">求职</a>
<a class="login" href="#">登录</a>
</div>
<!-- 题目列表 -->
<div class="question_list">
<h1>OnlineJuge题目列表</h1>
<table>
<tr>
<th class="item">编号</th>
<th class="item">标题</th>
<th class="item">难度</th>
</tr>
{{#questions_list}}
<tr>
<td class="item">{{number}}</td>
<td class="item"><a href="/question/{{number}}">{{title}}</a></td>
<td class="item">{{star}}</td>
</tr>
{{/questions_list}}
</table>
</div>
<!-- 页脚 -->
<div class="footer">
<!-- <hr> -->
<h4>怀旧</h4>
</div>
</div>
</body>
</html>
2)one_question.html
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 编辑器 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ace.js" type="text/javascript"
charset="utf-8"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ext-language_tools.js" type="text/javascript"
charset="utf-8"></script>
<!-- 引入 jQuery -->
<script src="http://code.jquery.com/jquery-2.1.1.min.js"></script>
<style>
/* 全局样式重置 */
* {
margin: 0;
padding: 0;
}
html,
body {
width: 100%;
height: 100%;
}
/* 导航栏样式 */
.container .navbar {
width: 100%;
height: 50px;
background-color: black;
overflow: hidden;
}
/* 导航栏链接 */
.container .navbar a {
display: inline-block;
width: 80px;
color: white;
font-size: large;
line-height: 50px;
text-decoration: none;
text-align: center;
}
/* 导航栏悬浮效果 */
.container .navbar a:hover {
background-color: green;
}
/* 登录按钮右浮动 */
.container .navbar .login {
float: right;
}
/* 主体内容区域 */
.container .part1 {
width: 100%;
height: 600px;
overflow: hidden;
}
/* 左侧题目描述 */
.container .part1 .left_desc {
width: 50%;
height: 600px;
float: left;
overflow: scroll;
}
.container .part1 .left_desc h3 {
padding-top: 10px;
padding-left: 10px;
}
.container .part1 .left_desc pre {
padding-top: 10px;
padding-left: 10px;
font-size: 16px;
font-family: "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", sans-serif;
line-height: 1.7;
color: #333;
}
/* 右侧代码编辑器 */
.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: 0px;
margin-top: 10px;
margin-right: 10px;
}
/* 按钮悬浮效果 */
.container .part2 button:hover {
color:green;
}
/* 结果显示区域 */
.container .part2 .result {
margin-top: 15px;
margin-left: 15px;
}
.container .part2 .result pre {
font-size: large;
}
</style>
</head>
<body>
<div class="container">
<!-- 导航栏 -->
<div class="navbar">
<a href="/">首页</a>
<a href="/all_questions">题库</a>
<a href="#">竞赛</a>
<a href="#">讨论</a>
<a href="#">求职</a>
<a class="login" href="#">登录</a>
</div>
<!-- 题目描述 + 代码编辑区 -->
<div class="part1">
<div class="left_desc">
<h3><span id="number">{{number}}</span>.{{title}}_{{star}}</h3>
<pre>{{desc}}</pre>
</div>
<div class="right_code">
<pre id="code" class="ace_editor"><textarea class="ace_text-input">{{pre_code}}</textarea></pre>
</div>
</div>
<!-- 提交按钮 + 结果区 -->
<div class="part2">
<div class="result"></div>
<button class="btn-submit" onclick="submit()">提交代码</button>
</div>
</div>
<script>
// 初始化代码编辑器
editor = ace.edit("code");
editor.setTheme("ace/theme/monokai");
editor.session.setMode("ace/mode/c_cpp");
editor.setFontSize(16);
editor.getSession().setTabSize(4);
editor.setReadOnly(false);
// 开启代码提示
ace.require("ace/ext/language_tools");
editor.setOptions({
enableBasicAutocompletion: true,
enableSnippets: true,
enableLiveAutocompletion: true
});
editor.setValue(`{{pre_code}}`, 1);
// 提交代码
function submit(){
var code = editor.getSession().getValue();
var number = $(".container .part1 .left_desc h3 #number").text();
var judge_url = "/judge/" + number;
// AJAX 提交判题
$.ajax({
method: 'Post',
url: judge_url,
dataType: 'json',
contentType: 'application/json;charset=utf-8',
data: JSON.stringify({
'code':code,
'input': ''
}),
success: function(data){
show_result(data);
}
});
// 显示运行结果
function show_result(data)
{
var result_div = $(".container .part2 .result");
result_div.empty();
var _status = data.status;
var _reason = data.reason;
var reason_lable = $( "<p>",{
text: _reason
});
reason_lable.appendTo(result_div);
if(_status == 0){
var _stdout = data.stdout;
var _stderr = data.stderr;
var stdout_lable = $("<pre>", {
text: _stdout
});
var stderr_lable = $("<pre>", {
text: _stderr
})
stdout_lable.appendTo(result_div);
stderr_lable.appendTo(result_div);
}
}
}
</script>
</body>
</html>
9、wwwroot
1)index.html
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: black;
overflow: hidden;
}
/* 导航栏链接 */
.container .navbar a {
display: inline-block;
width: 80px;
color: white;
font-size: large;
line-height: 50px;
text-decoration: none;
text-align: center;
}
/* 鼠标悬浮效果 */
.container .navbar a:hover {
background-color: green;
}
.container .navbar .login {
float: right;
}
/* 内容区域居中 */
.container .content {
width: 800px;
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="/all_questions">题库</a>
<a href="#">竞赛</a>
<a href="#">讨论</a>
<a href="#">求职</a>
<a class="login" href="#">登录</a>
</div>
<!-- 主页内容 -->
<div class="content">
<h1 class="font_">欢迎来到我的OnlineJudge平台</h1>
<p class="font_">这个我个人独立开发的在线OJ平台</p>
<a class="font_" href="/all_questions">点击我开始编程啦!</a>
</div>
</div>
</body>
</html>