【项目】均衡代码评测

@TOC

目录

项目介绍

开发环境

主要技术

项目实现

公共模块

日志

工具类

编译运行模块

介绍

编译

运行

编译和运行结合起来

业务逻辑模块

介绍

MVC模式框架

模型(Model)

视图(View)

控制器(Controller)

负载均衡设计

判题

会话模块

项目总结 


项目介绍


该项目是基于负载均衡的在线oj,模拟我们平时刷题网站(leetcode和牛客)写的一个在线判题系统。

项目主要分为五个模块:

  • 编译运行模块:基于httplib库搭建的编译运行服务器,对用户提交的代码进行测试
  • 业务逻辑模块:基于httplib库并结合MVC模式框架搭建oj服务器,负责题目获取,网页渲染以及负载均衡地将用户提交代码发送给编译服务器进行处理
  • 数据管理模块:基于MySQL数据库对用户的数据、题目数据进行管理
  • 会话模块:基于cookie和session针对登录用户创建唯一的会话ID,通过cookie返回给浏览器
  • 公共模块:包含整个项目需要用到的第三方库以及自己编写的工具类的函数

开发环境

Centos7.6、C/C++、vim、g++、MySQL Workbench、Postman

主要技术

  • C++ STL 标准库
  • cpp-httplib 第三方开源网络库
  • ctemplate google第三方开源前端网页渲染库
  • jsoncpp 第三方开源序列化、反序列化库
  • 负载均衡设计
  • MVC模式框架
  • ajax
  • MySQL

项目实现

公共模块

日志

为了方便后期编码调试和项目演示,这里设计了一个日志打印函数,日志打印的格式如下:

日志的五个级别:

  • INFO:正常信息
  • DEBUG:调试信息
  • WARNING:警告信息
  • ERROR:错误信息
  • FATAL:致命信息

实现如下:

#include <iostream>
#include <string>
#include <ctime>

#define INFO 1
#define DEBUG 2
#define WARNING 3
#define ERROR 4
#define FATAL 5

#define LOG(level, msg) Log(#level, msg, __FILE__, __LINE__)

void Log(const std::string level, const std::string msg, std::string filename, int line)
{
    std::cout << "[" + level + "][" + msg + "][" << time(nullptr) << "][" + filename + "][" << line << "]" << std::endl;
}

工具类

工具类模块中存放着四个工具类:

  • 时间工具类:包含了毫秒级时间戳的获取的方法
  • 路径工具类:包含了对不同文件添加后缀和拼接临时文件路径的方法
  • 文件工具类:包含了对文件读写、判断文件是否存在等文件操作方法
  • 字符串工具类:包含了对字符串进行切割等操作字符串的方法

编译运行模块

介绍

该模块负责编译运行oj_server上传过来的代码,并将结果返回给oj_server。oj_server会向编译服务器发送json串,格式如下:

code:用户代码
input:用户自己提交的代码的输入
cpu_limit:时间限制
mem_limit:内存限制

{
    "code":"xxx",
    "input":"xxx",
    "cpu_limit":"xxx",
    "mem_limit":"xxx"
}

编译服务器需要将代码提取出来,并进行编译,结果以json串格式返回,如下:

status:代码运行状态码
reason:原因
stderr:代码运行完报错信息
stdout:代码运行完的结果

{
    "status":"xxx",
    "reason":"xxx",
    "stderr":"xxx",
    "stdout":"xxx"
}

        编译服务器是基于第三方库cpp-httplib进行搭建的,需要注意的是,编译此库需要用安装新版本gcc,需要是7以上即可。compile_server注册了两种请求方式——/check_net和/compile_run,oj_server可以通过请求/check_net,根据响应来判断compile_server是否上线,可以给上线主机发起/compile_run请求对代码进行编译,并将结果响应给oj_server

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cerr << "Usage:\n\t" << argv[0] << " port" << std::endl;
        return -1;
    }
    Server svr;
    // 用来给oj_server检测网络是否通畅
    svr.Get("/check_net", [](const Request &req, Response &rep)
            { rep.set_content("ok", "text/html;charset=utf-8"); });
    // 注册POST方法
    svr.Post("/compile_run", [](const Request &req, Response &rep)
             {
        std::string in_json = req.body;
        std::string out_json;
        CompileRun::Start(in_json, out_json);
        rep.set_content(out_json, "application/json;charset=utf-8"); });

    LOG(INFO, "begin listen");
    svr.listen("0.0.0.0", atoi(argv[1]));
}

简单的描述框图:

编译

用户的代码可以写入到文件中,并保存在我们项目设置的temp目录下。对应每一个用户的代码的文件,我们都需要给它设置一个唯一的文件名,这个文件名我们通过毫秒级时间戳+原子性递增id生成唯一的一个文件名

毫秒级时间戳获取方法:可以通过gettimeofday这个函数先获取到当前时间信息,从struct timeval这个结构体中提取,如下

int gettimeofday(struct timeval *tv, struct timezone *tz);

struct timeval {
    time_t      tv_sec;     /* seconds */
    suseconds_t tv_usec;    /* microseconds */
};

我们可以将tv_sec除以1000,tv_usec乘以1000,二者都转为毫秒,再相加,这样就可以得到当前的毫秒级时间戳

单单靠一个毫秒级时间戳还不能够完全保证唯一性,所以这里再拼接一个原子性递增id,这里使用atomic_uint,从0开始递增,这样即便时间戳相同,id也不是相同的,这样就保证了文件名的唯一性,实现如下:

static std::string GetMsTimeStamp()
{
    struct timeval tv;
    gettimeofday(&tv, nullptr);

    return std::to_string(tv.tv_sec * 1000 + (int)(tv.tv_usec / 1000));
}

static std::string UniqueFilename()
{
    // 毫秒级时间戳+原子性递增得出唯一文件名
    static std::atomic_uint id(0);
    ++id;

    return TimeUtil::GetMsTimeStamp() + "_" + std::to_string(id);
}

获取到了唯一的文件名之后,就可以给不同的文件添加不同的后缀,我们的项目有这么几个文件:

// 编译 如果编译成功,会生成可执行,编译失败,错误信息会被记录到xxx_x.compile_err文件中
xxx_x.cc
xxx_x.compile_err
xxx_x.exe
// 运行
xxx_x.stdin
xxx_x.stdout
xxx_x.stderr
        static bool Compile(const std::string &filename)
        {
            // 要编译的文件放在了temp目录下
            // filename.exe filename.cc filename.err
            umask(0);  //重置文件描述符的权限
            int err_fd = open(PathUtil::CompileError(filename).c_str(), O_CREAT | O_WRONLY, 0644);
            if (err_fd < 0)
            {
                LOG(ERROR, "open error file fail");
                exit(1);
            }
            pid_t id = fork();
            if (id == 0)
            {
                // child
                // 1. 打开错误文件,没有就创建
                dup2(err_fd, 2);
                // 程序替换,编译代码 g++ -o target.ext src.cc -std=c++11
                execlp("g++", "g++", "-o", PathUtil::Exe(filename).c_str(), PathUtil::Src(filename).c_str(), "-std=c++11", "-D", "COMPILE_RUN", nullptr);
                // 失败才会走到这一步
                LOG(ERROR, "compile execlp fail");
                exit(2);
            }
            else if (id < 0)
            {
                // error
                close(err_fd);
                LOG(ERROR, "compile fork fail");
                return false;
            }
            // parent
            waitpid(id, nullptr, 0);
            close(err_fd);
            // 判断exe文件是否存在 是否编译成功
            if (FileUtil::FileIsExists(PathUtil::Exe(filename)))
            {
                LOG(INFO, "file: " + filename + " 编译成功");
                return true;
            }
            LOG(ERROR, "file: " + filename + " 编译失败");
            return false;
        }

编译开始,我们可以打开xxx_x.compile_err,并对标准错误进行重定向,如果编译错误,那么错误信息会被写入到该文件中,编译成功,该文件将为空。这个项目我们通过创建子进程并进行程序替换的方式来编译源文件,编译完成之后,我们只需要让父进程检查temp目录下是否存在可执行程序文件,如果有则说明编译成功,否则编译失败。

运行

编译成功后,就要开始对可执行程序进行执行了,执行之前,需要打开三个文件,也就是上面谈到的xxx_x.stdin、xxx_x.stdout和
xxx_x.stderr三个文件,并将标准输入、标准输出和标准错误分别重定向到三个文件中。执行可执行程序的方式和上面的一样,也是通过创建子进程并进行程序替换的方式运行可执行程序,通过退出码分析出运行结果。

我们这个项目对每道题题目的代码运行时间和内存大小都有限制,所以我们执行可执行程序之前我们需要对内存和时间进行限制,这里使用setrlimit系统函数来进行设置,接口如下:

int setrlimit(int resource, const struct rlimit *rlim);

struct rlimit结构体(描述软硬限制),原型如下:

struct rlimit {
  rlim_t rlim_cur;
  rlim_t rlim_max;
};

这里我们需要设置的两个参数分别是RLIMIT_ASRLIMIT_CPU,如下:

RLIMIT_AS  // 进程的最大虚内存空间,字节为单位。
RLIMIT_CPU // 最大允许的CPU使用时间,秒为单位。当进程达到软限制,内核将给其发送SIGXCPU信号,这            
              一信号的默认行为是终止进程的
              执行。然而,可以捕捉信号,处理句柄可将控制返回给主程序。如果进程继续耗费CPU时间,            
              核心会以每秒一次的频率给其发送SIGXCPU信号,
              直到达到硬限制,那时将给进程发送 SIGKILL信号终止其执行。

这里我们将二者的硬限制都设置为无穷大RLIM_INFINITY,软限制设置为题目要求的,具体代码如下:

static void SetProcLimit(int cpu_limit, int mem_limit)
{
    struct rlimit climit;
    climit.rlim_cur = cpu_limit;
    climit.rlim_max = RLIM_INFINITY;

    setrlimit(RLIMIT_CPU, &climit);

    struct rlimit mlimit;
    mlimit.rlim_cur = mem_limit * 1024; // 转为kb
    mlimit.rlim_max = RLIM_INFINITY;

    setrlimit(RLIMIT_AS, &mlimit);
}

父进程需要分析运行结果,如果waitpid的返回值小于0,说明父进程等待失败,也是运行错误,否则分析status,如果是正常退出,我们可以提取出退出码分析,如果是异常退出,此时我们能够知道子进程是被信号所杀,这时,我们只需要提取出信号即可

         static int Run(const std::string &filename, int cpu_limit, int mem_limit)
         {            
            std::string execute = PathUtil::Exe(filename);
            std::string stdin = PathUtil::Stdin(filename);
            std::string stdout = PathUtil::Stdout(filename);
            std::string stderr = PathUtil::Stderr(filename);
            //生成对应的文件用来存储对应的数据

            // 打开三个文件
            umask(0);
            int in_fd = open(stdin.c_str(), O_CREAT | O_WRONLY, 0644);
            int out_fd = open(stdout.c_str(), O_CREAT | O_WRONLY, 0644);
            int err_fd = open(stderr.c_str(), O_CREAT | O_WRONLY, 0644);

            if (in_fd < 0 || out_fd < 0 || err_fd < 0)//万一失败,就得报错
            {
                LOG(ERROR, "open std file error");
                return -1;
            }

            pid_t id = fork();
            if (id < 0)
            {
                close(in_fd);
                close(out_fd);
                close(err_fd);
                LOG(ERROR, "run fork error");
                return -1;
            }
            else if (id == 0)
            {
                // child
                // 进行文件描述符的重定向
                dup2(in_fd, 0);
                dup2(out_fd, 1);
                dup2(err_fd, 2);
                // 对cpu和内存资源进行限制
                SetProcLimit(cpu_limit, mem_limit);
                execl(execute.c_str(), execute.c_str(), nullptr);
                // 程序替换失败
                exit(1);
            }
            // father
            int status = 0;
            int ret = waitpid(id, &status, 0);
            close(in_fd);
            close(out_fd);
            close(err_fd);
            int sig = 0; // 检验是否是被信号所杀
            if (ret > 0)
            {
                if (WIFEXITED(status)) //检测进程的终止状态,判断子进程是否正常终止
                {
                    // 正常退出
                    int exit_code = WEXITSTATUS(status);
                    if (exit_code == 0)
                    {
                        LOG(INFO, "run success");
                    }
                    else if (exit_code == 1)
                    {
                        LOG(ERROR, "run execlp fail");
                        return -1;
                    }
                    else
                    {
                        LOG(ERROR, "unknow status code:" + std::to_string(exit_code));
                        return -1;
                    }
                }
                else
                {
                    // 异常退出
                    sig = status & 0x7f;
                    LOG(WARNING, "sig: " + std::to_string(sig));
                }
            }
            else
            {
                // 等待失败
                LOG(ERROR, "run wait fail");
                return -1;
            }
            return sig; // 返回收到的信号 正常是0 异常时一个信号
        }

综合编译和运行结果进行分析,对返回json串进行设置:

  1. 如果编译失败,或编译成功运行失败,我只需要设置status、reason两个个字段
  2. 如果编译运行成功,我们还需要设置stdout和stderr两个字段

编译和运行结合起来

因为编译加上运行可能会有多种情况,如果把这些在编译模块的主函数里面进行结合的话,代码会相当的冗长,难分,为了方便,创建一个compile_run函数来进行进行多种情况的判断。

 static void Start(const std::string &in_json, std::string &out_json)
        {
            Json::Value in_value;
            Json::Reader reader;
            // 对json串进行反序列化
            reader.parse(in_json, in_value); // 将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();    //空间限制
            //从in_value中获取我们需要的相关数据

            Json::Value out_value;
            std::string filename; // 生成的唯一文件名
            int status_code = 0;  // 状态码
            int res = 0;

            if (code.size() == 0) 
            {
                // 提交空代码
                status_code = -1;
                goto END;
            }
            filename = FileUtil::UniqueFilename();
            // 将代码写入文件中
            if (!FileUtil::WriteFile(PathUtil::Src(filename), code)) //将代码写进filename.cc文件中
            {
                // 未知错误:写文件失败
                status_code = -2;
                goto END;
            }
            // 编译代码
            if (!Compiler::Compile(filename))
            {
                // 代码编译失败
                status_code = -3;
                goto END;
            }
            // 执行代码
            res = Runner::Run(filename, cpu_limit, mem_limit);
            if (res < 0)
            {
                // 运行时未知错误
                status_code = -2;
            }
            else
            {
                //负数  代码有问题
                // 0    正常运行
                // 1-31 被信号终止
                status_code = res;
            }

        END:
            out_value["status"] = status_code;
            out_value["reason"] = CodeToDesc(filename, status_code);

            if (status_code == 0)
            {
                out_value["stdout"] = FileUtil::ReadFile(PathUtil::Stdout(filename));
                out_value["stderr"] = FileUtil::ReadFile(PathUtil::Stderr(filename));
            }
            else if (status_code > 0)
            {
                out_value["stderr"] = FileUtil::ReadFile(PathUtil::Stderr(filename));
            }
            Json::FastWriter writer;
            out_json = writer.write(out_value);

            // 清除临时文件
            RemoveTempFile(filename);
            LOG(INFO, "临时文件已清除");
        }

编译运行完之后记得需要将相关文件进行删除,不然后台会保留过多无意义的临时文件。

        static void RemoveTempFile(const std::string &filename)
        {
            std::string src = PathUtil::Src(filename);
            if (FileUtil::FileIsExists(src))
                unlink(src.c_str());

            std::string exe = PathUtil::Exe(filename);
            if (FileUtil::FileIsExists(exe))
                unlink(exe.c_str());

            std::string compile_err = PathUtil::CompileError(filename);
            if (FileUtil::FileIsExists(compile_err))
                unlink(compile_err.c_str());

            std::string in = PathUtil::Stdin(filename);
            if (FileUtil::FileIsExists(in))
                unlink(in.c_str());

            std::string out = PathUtil::Stdout(filename);
            if (FileUtil::FileIsExists(out))
                unlink(out.c_str());

            std::string err = PathUtil::Stderr(filename);
            if (FileUtil::FileIsExists(err))
                unlink(err.c_str());
        }

上面就是整体后端代码的实现,

业务逻辑模块

介绍

        该模块是整个项目业务逻辑的核心,包括用户登录注册、题目获取、与数据库进行数据交互、网页渲染以及协调编译服务器的负载均衡,同时该模块也会用到会话模块和数据库模块,进行用户会话管理、数据管理。综合这些利用第三方库cpp-httplib结合MVC模式框架搭建一个oj服务器,该服务器注册了很多Get和Post请求方法,供前端页面发起ajax请求进行前后端数据交互,及时更新前端页面


int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cerr << "Usage:\n\t" << argv[0] << " port" << std::endl;
        return -1;
    }
    // 控制器
    Control ctrl;
    pthread_t tid;
    pthread_create(&tid, nullptr, check, &ctrl);
    // 会话
    std::shared_ptr<AllSessionInfo> all_sess(new AllSessionInfo);
    std::shared_ptr<Server> svr(new Server);
    std::shared_ptr<UserManage> manager(new UserManage);

    svr->Get(R"(/all_questions)", [&ctrl](const Request &req, Response &rep)
             {
                //LOG(INFO, "get questions request");
                std::string html;
                ctrl.GetAllQuestionsListHtml(html);
                rep.set_content(html, "text/html;charset=utf-8"); });
    svr->Get(R"(/question/(\d+))", [&ctrl](const Request &req, Response &rep)
             { 
                //LOG(INFO, "get one question request");
                std::string number = req.matches[1];
                std::string html;
                ctrl.GetOneQuestionByNumberHtml(number, html);
                rep.set_content(html, "text/html;charset=utf-8"); });
    svr->Post(R"(/judge/(\d+))", [&ctrl](const Request &req, Response &rep)
              { 
                //LOG(INFO, "get one judge request");
                std::string number = req.matches[1];
                std::string out_json;
                ctrl.Judge(number, req.body, out_json); 
                rep.set_content(out_json, "application/json;charset=utf-8"); });
    // 注册
    /******
     * json
     * user 账户名
     * password 密码
     * ******/
    svr->Post("/register", [&ctrl](const Request &req, Response &rep)
              {
                LOG(INFO, "get a register request");
                std::string out_json;
                ctrl.Register(req.body, out_json);
                rep.set_content(out_json, "application/json;charset=utf-8"); });
    // 登录
    svr->Post("/sign", [&ctrl, &all_sess, &manager](const Request &req, Response &rep)
              {
                LOG(INFO, "get a sign request");
                std::string out_json;
                int user_id = ctrl.SignIn(req.body, "user", out_json);
                std::string tmp;
                if (user_id > 0)
                {
                    Session sess(req.body, user_id, "user");
                    std::string session_id = sess.GetSessionId();
                    tmp = "JSESSION=" + session_id;
                    all_sess->SetSessionInfo(session_id, sess);
                    // 将用户添加到管理
                    manager->AddUserToSet(user_id, 1);
                }

                rep.set_header("Set-Cookie", tmp.c_str());// 返回cookie
                rep.set_content(out_json, "application/json;charset=utf-8"); });
    svr->Get("/GetUserId", [&all_sess](const Request &req, Response &rep)
             {
                    //1.会话校验
                    Json::Value resp_json;
                    resp_json["id"] = all_sess->CheckSessionInfo(req);

                    std::string out_json;
                    Json::FastWriter writer;
                    out_json = writer.write(resp_json); 
                    rep.set_content(out_json, "application/json;charset=utf-8"); });
                    
    svr->Post("/GetUserName", [&all_sess, &ctrl](const Request &req, Response &rep)
              {
                    LOG(INFO, "get a get username request");
                    Json::Value in_json;
                    Json::Reader reader;
                    reader.parse(req.body, in_json);
                    std::string out_json;
                    int id = in_json["id"].asInt();
                    std::string strId = in_json["strId"].asString();
                    std::string table = in_json["table"].asString();
           
                    Json::Value out_value;
                    out_value["username"] = ctrl.GetUserName(id, strId, table);
                    Json::FastWriter writer;
                    out_json = writer.write(out_value); 
                    rep.set_content(out_json, "application/json;charset=utf-8"); });
    // 修改密码
    svr->Post("/forget", [&ctrl](const Request &req, Response &rep)
              {
                LOG(INFO, "get a forget password request");
                std::string out_json;
                ctrl.Forget(req.body, out_json);
                rep.set_content(out_json, "application/json;charset=utf-8"); });
    // 管理员登录
    svr->Post("/administrator", [&ctrl, &all_sess, &manager](const Request &req, Response &rep)
              {
                LOG(INFO, "get a administrator sign request");
                std::string out_json;
                int administrator_id = ctrl.SignIn(req.body, "administrators", out_json);
                std::string tmp;
                if (administrator_id > 0)
                {
                    Session sess(req.body, administrator_id, "administrator");
                    std::string session_id = sess.GetSessionId();
                    tmp = "JSESSION=" + session_id;
                    all_sess->SetSessionInfo(session_id, sess);
                    // 将用户添加到管理
                    manager->AddUserToSet(administrator_id);
                }
                rep.set_header("Set-Cookie", tmp.c_str());// 返回cookie
                rep.set_content(out_json, "application/json;charset=utf-8"); });
    svr->Post("/add_question", [&ctrl](const Request &req, Response &rep)
              {
                LOG(INFO, "get a add question request");
                std::string out_json;
                ctrl.AddQuestion(req.body, out_json);
                rep.set_content(out_json, "application/json;charset=utf-8"); });
    svr->set_base_dir("wwwroot");
    svr->listen("0.0.0.0", atoi(argv[1]));

    return 0;
}

MVC模式框架

模型(Model)

Model负责与数据库进行交互,听取控制器的调用,往数据库中插入数据或从数据库中获取数据并让View将请求结果返回给用户

插入数据的几种情形:

  • 用户注册
  • 管理员添加题目

查询数据的几种情形:

  • 用户获取题目信息
  • 获取用户信息

题目设计:

  1. 我们项目题目的属性有这么几个:编号、标题、难度、时间限制、内存限制、题目描述、头文件、用户显示代码、测试代码,后序我们需要对header、body和tail这三个部分进行拼接,形成一份新的代码,再提交给编译服务器
  2. 形成的数据库表结构如下:

struct Question
{
    std::string show_num; // 显示编号
    std::string num;      // 题目编号
    std::string title;    // 题目标题
    std::string level;    // 题目难度等级
    int cpu_limit;        // 题目时间限制 单位 s
    int mem_limit;        // 题目内存限制 单板 byte
    std::string desc;     // 题目描述
    std::string header;   // 用户需要用到的头文件
    std::string body;     // 显示给用户的代码
    std::string tail;     // 用来测试用户的代码
};

接口设计:

  1. 接口主要包括加载配置、单个题目的获取、全部题目的获取、添加题目和用户数据相关操作
  2. 加载配置主要是将show_num和num建立起映射关系,show_num这个属性是给用户页面显示的,不直接用number显示给用户的原因是:number在数据库中是自增长的,且每次添加题目,其number是从最大的number+1开始增长,在不删除题目的情况下,number是连续的,如果中途删除了某个题目,后序number这个序列就不会是连续的,中间会断开,如:1、2、3、4,删除了3,再增加一个题目,number是5,这时number序列就是1、2、4、5,这样就不是连续的,所以用number显示给用户不太好,设计一个show_num是连续的,并与number建立好映射关系,这样就比较友好
  3. 单个题目的获取和多个题目的获取,主要是查询数据库,获取到的数据可以交付给View
  4. 添加题目需要往数据库中插入数据,同时更新show_num和number的映射关系
  5. 用户数据操作结合数据库操作一起完成
视图(View)

View负责将Model提供的数据以某种方式呈现给用户,这个项目主要是网页界面。View会使用google的开源库ctemplate进行网页渲染,以这种方式将数据呈现给用户

ctemplate的获取

可以在GitHub的国内镜像网站中获取,速度会比较快,链接如下(里面有详细的安装方法说明):

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

ctemplate的简单用法

  1. {{变量名}}:把它放入我们的网页中,该部分会被替换成我们字典中添加的值,使用如:{{number}}、{{show_num}}
  2. {{#片断名}}:片断在数据字典中表现为一个子字典,字典是可以分级的,根字典下面有多级子字典。片断可以处理条件判断和循环,循环的结束{{/片段名}}
  3. TemplateDictionary:可以创建字典
  4. SetValue:可以往字典中添加模板
  5. AddSectionDictionary:可以往字典中添加子字典
  6. GetTemplate和Expand:两个接口可以获取到扩展之后的模板

接口设计:

这里对题目列表和单个题目两张网页进行渲染,将数据添加到网页中,实现如下:

namespace ns_view
{
    using namespace ns_model;
    using namespace ns_view;
    using namespace ctemplate;

    const std::string template_path = "wwwroot/template_html/";
    const std::string user_path = "wwwroot/";

    class View
    {
    public:
        void ExpandAllQuestionsHtml(const std::vector<Question> qs, std::string &outhtml)
        {
            std::string src_html = template_path + "all_questions.html";
            // 创建数据字典
            TemplateDictionary root("all_questions");
            for (auto &q : qs)
            {
                // 往root添加子字典
                TemplateDictionary *sub = root.AddSectionDictionary("questions_list");
                sub->SetValue("number", q.num);
                sub->SetValue("show_num", q.show_num);
                sub->SetValue("title", q.title);
                sub->SetValue("level", q.level);
            }
            // 获取要渲染的网页  不做删除任何符号的动作
            Template *tpl = Template::GetTemplate(src_html, DO_NOT_STRIP);
            // 开始渲染
            tpl->Expand(&outhtml, &root);
        }
        void ExpandOneQuestioHtml(Question &q, std::string &outhtml)
        {
            std::string src_html = template_path + "question.html";
            // 创建数据字典
            TemplateDictionary root("question");

            root.SetValue("show_num", q.show_num);
            root.SetValue("title", q.title);
            root.SetValue("level", q.level);
            root.SetValue("desc", q.desc);
            root.SetValue("pre_code", q.body);

            // 获取要渲染的网页  不做删除任何符号的动作
            Template *tpl = Template::GetTemplate(src_html, DO_NOT_STRIP);
            // 开始渲染
            tpl->Expand(&outhtml, &root);
        }

       
    };
}
控制器(Controller)

Controller是整个项目业务逻辑的控制器负责协调model和view一起完成业务。

负载均衡设计

控制器的核心还包括了一个负载均衡的小模块,帮助控制器根据主机负载选择编译服务器,负载均衡的设计框架如下:

    const std::string service = "./conf/service_machine.conf";

    // 负载均衡模块
    class LoadBlance
    {
    public:
        LoadBlance()
        {
            assert(LoadConf()); // 加载配置文件
        }
        bool LoadConf()
        {
            std::ifstream in(service, std::ifstream::in);
            if (!in.is_open())
            {
                LOG(FATAL, "加载主机配置文件失败");
                return false;
            }
            std::string line;
            while (getline(in, line))
            {
                std::vector<std::string> tokens;
                StringUtil::Spilit(line, tokens, ":");
                if (tokens.size() != 2)
                {
                    LOG(WARNING, "切分" + line + "失败");
                    return false;
                }
                Machine m;
                m.ip = tokens[0];
                m.port = std::stoi(tokens[1]);
                m.status = OFFLINE;     // 默认都是下线
                m.mtx = new std::mutex; // 记得释放
                m.id = _machines.size();
                _machines.push_back(std::move(m));
            }
            in.close();
            LOG(INFO, "加载主机配置文件成功");
            return true;
        }
        int AutoChoose(Machine *&machine)
        {
            _mtx.lock();
            if (_count == 0)
            {
                LOG(FATAL, "所有主机全部下线,请及时核查原因");
                _mtx.unlock();
                return -1;
            }
            int id = 0;
            int min_load = INT_MAX;
            for (int i = 0; i < _machines.size(); ++i)
            {
                // 当前主机下线就选择另一台主机
                if (_machines[i].status == OFFLINE)
                    continue;
                int load = _machines[i].GetLoad();
                std::cout << "load " << i << ": " << load <<  std::endl;
                if (load < min_load)
                {
                    min_load = load;
                    id = i;
                }
            }
            _mtx.unlock();
            machine = &_machines[id];
            return id;
        }
        void Online(int id)
        {
            _mtx.lock();
            _machines[id].status = ONLINE;
            LOG(INFO, "主机" + std::to_string(id) + "已经上线 详情" + _machines[id].ip + ":" + std::to_string(_machines[id].port));
            _count++;
            _mtx.unlock();
        }
        void OfflineMachine(int id)
        {
            _mtx.lock();
            _machines[id].status = OFFLINE;
            _count--;
            _machines[id].ResetLoad();
            _mtx.unlock();
        }
        void ShowMachines()
        {
            _mtx.lock();
            std::cout << "-------------------online-------------------" << std::endl;
            for (auto &m : _machines)
            {
                if (m.status == ONLINE)
                    std::cout << m.id << " ";
            }
            std::cout << std::endl;
            std::cout << "-------------------offline-------------------" << std::endl;
            for (auto &m : _machines)
            {
                if (m.status == OFFLINE)
                    std::cout << m.id << " ";
            }
            std::cout << std::endl;
            _mtx.unlock();
        }

    public:
        std::vector<Machine> _machines; // 可以提供服务的所有主机,下标充当主机id
        int _count = 0;                 // 在线主机数
        std::mutex _mtx;
    };

主机设计: 主机的属性应该有状态(上线、下线),当前负载,主机id,主机ip,主机绑定端口号,如下:

    enum status
    {
        ONLINE,
        OFFLINE
    };

    class Machine
    {
    public:
        // 增加主机负载
        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 GetLoad()
        {
            uint64_t curload;
            if (mtx)
                mtx->lock();
            curload = load;
            if (mtx)
                mtx->unlock();
            return curload;
        }
        ~Machine()
        {
            // ...
        }

    public:
        int id; // 主机id
        std::string ip;
        int port;
        enum status status = OFFLINE;
        uint64_t load = 0; // 负载
        std::mutex *mtx = nullptr;
    };

加载配置文件: 我们的配置文件中存放着我们需要用到的主机信息,每行存放一台主机的信息,格式如:ip:port,如下:

127.0.0.1:8082
127.0.0.1:8083
127.0.0.1:8084

通过读取文件,并对每一行进行分析,将主机信息存放到vector容器中

根据负载选择主机: 我们需要遍历所有上线的主机,选出负载最小的那一台主机,如果当前上线主机上为0,则打印出提示信息,发起警告,如果选择主机成功,我们可以返回主机id,具体操作如下:

        int AutoChoose(Machine *&machine)
        {
            _mtx.lock();
            if (_count == 0)
            {
                LOG(FATAL, "所有主机全部下线,请及时核查原因");
                _mtx.unlock();
                return -1;
            }
            int id = 0;
            int min_load = INT_MAX;
            for (int i = 0; i < _machines.size(); ++i)
            {
                // 当前主机下线就选择另一台主机
                if (_machines[i].status == OFFLINE)
                    continue;
                int load = _machines[i].GetLoad();
                std::cout << "load " << i << ": " << load <<  std::endl;
                if (load < min_load)
                {
                    min_load = load;
                    id = i;
                }
            }
            _mtx.unlock();
            machine = &_machines[id];
            return id;
        }

主机上线: 我们需要能够设计一个接口来更改status这个字段,如果主机上线了,我们就把status字段改为ONLINE,否则改为OFFLINE,如何检测主机是否上线呢?还记得我们前面在编译服务器中注册的一个Post /chech_net用来检测网络通畅请求方法吗,我们可以在oj_sever启动时开辟一个线程,该线程会不停地给所有状态为OFFLINE的主机发起请求,如果得到了响应,那么说明,该主机已经上线,我们就可以把该主机的status字段改成ONLINE,表示主机已经上线,检测方法如下:

void *check(void *arg)
{
    sleep(1);
    pthread_detach(pthread_self());
    Control *ctrl = (Control *)arg;
    LoadBlance *loadblance = &ctrl->_lb;

    while (1)
    {
        std::vector<Machine> machines = loadblance->_machines;
        for (auto &machine : machines)
        {
            if (machine.status == OFFLINE)
            {
                Client clt(machine.ip, machine.port);
                if (auto res = clt.Post("/check_net", "check", "text/plain;charset=utf-8"))
                {
                    // 得到响应,证明对端主机上线
                    loadblance->Online(machine.id);
                }
            }
        }
    }
}

主机上线了。我们需要对负载均衡模块中的上线主机上count进行加1的操作:

        void Online(int id)
        {
            _mtx.lock();
            _machines[id].status = ONLINE;
            LOG(INFO, "主机" + std::to_string(id) + "已经上线 详情" + _machines[id].ip + ":" + std::to_string(_machines[id].port));
            _count++;
            _mtx.unlock();
        }

主机下线: 如果自己下线,我们只需要对count进行减1的操作,并对主机的负载进行重置:

        void OfflineMachine(int id)
        {
            _mtx.lock();
            _machines[id].status = OFFLINE;
            _count--;
            _machines[id].ResetLoad();
            _mtx.unlock();
        }
判题

步骤:

对传入json串进行反序列化,获取题目id

Json::Value in_value;
Json::Reader reader;
reader.parse(in_json, in_value);
std::string code = in_value["code"].asString();

根据题目id获取测试代码,并拼接上用户提交的代码,填充code,code需要用header、body和tail三种进行拼接

Question q;
_model.GetOneQuestionByNumber(number, q);
in_value["code"] = q.header + code + q.tail;
in_value["cpu_limit"] = q.cpu_limit;
in_value["mem_limit"] = q.mem_limit;

Json::FastWriter writer;
std::string compile_json = writer.write(in_value);

选择一台负载最小的主机,调用负载均衡里面的AutoChoose接口完成

选择主机成功,向该主机发起http请求,将json传过去,响应得到json之后再返回给前端

会话模块

用户登录成功之后,服务器会针对该用户创建一个会话,并保存在服务器这一端,同时服务器会根据用户的身份、账号和密码再利用MD5哈希算法生成唯一的Session ID,并通过Cookie返回给浏览器。

class Session
{
public:
    Session(){}
    Session(const std::string &in_json, int id, const std::string &identity){}
    bool SumMd5(){}
    //获取会话id
    std::string &GetSessionId(){}

public:
    std::string _session_id; //当前会话的会话id
    std::string _real_str;   //用来生成会话id的原生字符串
    std::string _identity;
    int _id;
};

MD5接口如下:

int MD5_Init(MD5_CTX *c);// 初始化MD5码
int MD5_Update(MD5_CTX *c, const void *data, unsigned long len);// 更新获取MD5码
int MD5_Final(unsigned char *md, MD5_CTX *c);// 生成的16字节MD5码放在md中

实现如下: 其中_str是由用户的账号名+密码+身份组成

bool SumMd5()
{
    MD5_CTX ctx;
    // 1.初始化
    MD5_Init(&ctx);
	// 2.更新MD5
    if (MD5_Update(&ctx, _str.c_str(), _str.size()) != 1)
    {
        return false;
    }
	// 3.取出MD5
    unsigned char md5[16] = {0};
    if (MD5_Final(md5, &ctx) != 1)
    {
        return false;
    }

    char tmp[3] = {0};
    char buf[32] = {0};
    
    // 将md5码转为16进制,进行输出
    for (int i = 0; i < 16; i++)
    {
        snprintf(tmp, sizeof(tmp) - 1, "%02x", md5[i]);
        strncat(buf, tmp, 2);
    }

    _session_id = buf;
    return true;
}

其中Session ID会返回给浏览器,单个会话会保存在服务器中。用户在获取每一张网页页面,前端页面会发起异步ajax请求,请求获取用户id,后端收到请求需要进行会话校验,如果校验失败,则返回一个小于0的用户ID,不允许用户获取页面,同时提示用户进行登录,如果成功则给用户返回一个大于0的用户ID,并给用户显示页面,且可以如下:

var user_id = -1;
var user_name = "";

function CheckUser() {
    console.log(user_id);
    if (user_id > 0) {
        $(".nav_bar .last_li").text(user_name);
        return;
    }
    // 发起ajax请求获取用户id
    $.ajax({
        url: "/GetUserId",
        method: "Get",
        dataType: 'json',
        contentType: 'application/json;charset=utf-8',

        success: function (data) {
            console.log(data);
            if (data.id > 0) {
                user_id = data.id;
                GetUsername();
                console.log(user_name);
                $(".nav_bar .last_li").text(user_name);
                // $(".nav_bar .last_li").attr("href", "#");
            } else {
                alert("请先进行登录");
                window.location.href = "/signin.html";
            }
        },
    });
}

数据库模块
数据库模块主要与MVC模式框架中的Model进行数据交互。该模块模块主要使用MySQL C Connect连接数据库,对项目数据进行存放,该项目主要有三张表:oj_questions、user和administrators,分别存放题目信息,用户信息和管理员信息,如下:

数据库模块代码框架:

namespace ns_database
{

    class DataBase
    {
    public:
        DataBase()
        {
            //初始化mysql操作句柄
            _mfp = mysql_init(nullptr);
            assert(ConnectMysql());
            pthread_mutex_init(&_mtx, nullptr);
        }
        ~DataBase()
        {
            //关闭连接
            mysql_close(_mfp);
            pthread_mutex_destroy(&_mtx);
        }
        bool ConnectMysql()
        {
            if (nullptr == mysql_real_connect(_mfp, "127.0.0.1", "oj_client", "125000", "oj", 3306, nullptr, 0))
            {
                LOG(FATAL, "连接数据库失败");
                return false; // 给开发人员看
            }
            mysql_set_character_set(_mfp, "utf8");
            LOG(INFO, "连接数据库成功");
            return true;
        }
        std::string GetUserName(int id, const std::string &strId, const std::string &table)
        {
            std::string sql = "select user from " + table + " where " + strId + " = ";
            sql += std::to_string(id) + ";";
            // std::cout << sql << std::endl;
            int ret = mysql_query(_mfp, sql.c_str());
            if (ret != 0)
            {
                LOG(WARNING, "SQL执行失败: " + std::string(mysql_error(_mfp)));


                LOG(WARINNG, sql + " 执行失败");
                return ""; // 给开发人员看
            }
            // 提取结果
            MYSQL_RES *result = mysql_store_result(_mfp);
            // 分析结果
            int row = mysql_num_rows(result);
            if (row == 0)
            {
                LOG(ERROR, "查询结果为空,SQL语句: " + sql);
                LOG(ERROR, "无结果");
                return "";
            }
            MYSQL_ROW line = mysql_fetch_row(result);
            std::cout << "line[0]" << " " << line[0] << std::endl;
            std::cout << "line[1]" << " " << line[1] << std::endl;
            std::cout << "line[2]" << " " << line[2] << std::endl;
            free(result);
            return line[2];
        }
        void AddQuestion(const std::string &in_json, std::string &out_json)
        {
            // 1.获取用户信息
            Json::Value in_value;
            Json::Reader reader;
            reader.parse(in_json, in_value);
            std::string title = in_value["title"].asString();
            std::string level = in_value["level"].asString();
            std::string desc = in_value["desc"].asString();
            std::string header = in_value["header"].asString();
            std::string body = in_value["body"].asString();
            std::string tail = in_value["tail"].asString();
            std::string cpu_limit = in_value["cpu"].asString();
            std::string mem_limit = in_value["mem"].asString();
            std::string sql = "insert into oj_questions(title, level, `desc`, `header`, `body`, `tail`, cpu_limit, mem_limit) values('";
            sql += title + "', '";
            sql += level + "', '";
            sql += desc + "', '";
            sql += header + "', '";
            sql += body + "', '";
            sql += tail + "', ";
            sql += cpu_limit + ", ";
            sql += mem_limit + ");";
            // LOG(INFO, sql);
            Json::Value out_value;
            if (Insert(sql))
            {
                LOG(INFO, "题目添加成功");
                out_value["result"] = "success";
            }
            else
            {
                LOG(INFO, "题目添加失败");
                out_value["result"] = "fail";
            }
            Json::FastWriter writer;
            out_json = writer.write(out_value);
        }
        bool QueryMysql(const std::string &sql, MYSQL_RES *&result)
        {
            pthread_mutex_lock(&_mtx);
            int ret = mysql_query(_mfp, sql.c_str());
            if (ret != 0)
            {
                LOG(WARINNG, sql + " 执行失败");
                perror("执行失败");
                return false;
            }
            // 提取结果
            result = mysql_store_result(_mfp);
            pthread_mutex_unlock(&_mtx);
            return true;
        }
        bool Insert(const std::string &sql)
        {
            int ret = mysql_query(_mfp, sql.c_str());
            if (ret != 0)
            {
                LOG(WARINNG, sql + " 执行失败");
                LOG(WARINNG, "MySQL 错误: " + std::string(mysql_error(_mfp)));
                return false; // 给开发人员看
            }
            return true;
        }
        int Select(const std::string &sql, const std::string &password, std::string &out)
        {
            // std::cout << sql << std::endl;
            int ret = mysql_query(_mfp, sql.c_str());
            if (ret != 0)
            {
                LOG(WARINNG, sql + " 执行失败");
                return -1; // 给开发人员看
            }
            // 提取结果
            MYSQL_RES *result = mysql_store_result(_mfp);
            // 分析结果
            int row = mysql_num_rows(result);
            if (row == 0)
            {
                out = "1";
                return -2;
            }
            MYSQL_ROW line = mysql_fetch_row(result);
            if (line[1] != password)
            {
                out = "2";
                return -3;
            }
            out = "0";
            free(result);
            return atoi(line[0]);
        }

        bool Update(const std::string &sql, std::string &out)
        {
            // std::cout << sql << std::endl;
            int ret = mysql_query(_mfp, sql.c_str());
            if (ret != 0)
            {
                LOG(WARINNG, sql + " 执行失败");
                return false; // 给开发人员看
            }
            out = "0";
            return true;
        }

    private:
        MYSQL *_mfp;
        pthread_mutex_t _mtx;
    };
}

项目总结 


问题与解决

如何检测编译主机是否上线?通过给编译主机注册一个检测网络畅通的请求方法,oj_server可以单独开一个线程不停地给状态为OFFLINE的主机发起/check_net请求,以此判断网络是否畅通,然后修改主机状态
用户代码提交过快,会导致后端数据库频繁请求,导致数据库报错:Operation now in progress。解决:前端界面通过js控制按钮点击事件,每1s才能够点击一次,后端数据库模块对该sql执行进行加锁,二者结合有效解决问题

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/486579.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Linux】文件属性信息、文件目录权限修改

Linux文件属性信息 在 Linux 中&#xff0c;ls命令用于列出目录内容&#xff0c;并提供了许多参数以定制输出和显示不同类型的信息。以下是一些常用的ls命令参数 -a显示所有文件和目录&#xff0c;包括以.开头的隐藏文件。-l使用长格式列出文件和目录的详细信息&#xff0c;包…

基于 C++ STL 的图书管理系统213行

定制魏&#xff1a;QTWZPW&#xff0c;获取更多源码等 目录 一、实践项目名称 二、实践目的 三、实践要求 四、实践内容 五、代码框架参考 六、代码效果展示 七、完整代码主函数展示 一、实践项目名称 基于 C STL 的图书管理系统 二、实践目的 通过设计和实现一个基于…

Linux中的常用基础操作

ls 列出当前目录下的子目录和文件 ls -a 列出当前目录下的所有内容&#xff08;包括以.开头的隐藏文件&#xff09; ls [目录名] 列出指定目录下的子目录和文件 ls -l 或 ll 以列表的形式列出当前目录下子目录和文件的详细信息 pwd 显示当前所在目录的路径 ctrll 清屏 cd…

专业135+总分400+重庆邮电大学801信号与系统考研经验重邮电子信息与通信工程,真题,大纲,参考书。

今年分数出来还是比较满意&#xff0c;专业801信号与系统135&#xff0c;总分400&#xff0c;没想到自己也可以考出400以上的分数&#xff0c;一年的努力付出都是值得的&#xff0c;总结一下自己的复习心得&#xff0c;希望对大家复习有所帮助。专业课&#xff1a;&#xff08;…

C/C++语言相关常见面试题总结

目录 const关键字的作用 volatile 关键字 #define和const有什么区别 decltype和auto的区别 extern 关键字的作用 如何避免野指针 C/C中的类型转换以及使用场景 什么是RTTI&#xff1f;其原理是什么&#xff1f; RTTI 的原理&#xff1a; C中引用和指针的区别 C11用过…

亮剑AIGC,紫光云能否胜人一筹?

【全球云观察 &#xff5c; 科技热点关注】 扎实创新每一步&#xff0c; 先人一步快人一步。 2023年全球科技行业最火的莫过于生成式AI&#xff0c;即Artificial Intelligence Generated Content。在迈向生成式AI的道路上&#xff0c;虽然说不上千军万马&#xff0c;但是国内…

Redis - hash 哈希表

前言 ⼏乎所有的主流编程语⾔都提供了哈希&#xff08;hash&#xff09;类型&#xff0c;它们的叫法可能是哈希、字典、关联数组、映射。在 Redis 中&#xff0c;哈希类型是指 value 本⾝⼜是⼀个键值对结构&#xff0c;形如 key "key"&#xff0c;value { { fiel…

java设计模式(2)---六大原则

设计模式之六大原则 这篇博客非常有意义&#xff0c;希望自己能够理解的基础上&#xff0c;在实际开发中融入这些思想&#xff0c;运用里面的精髓。 先列出六大原则&#xff1a;单一职责原则、里氏替换原则、接口隔离原则、依赖倒置原则、迪米特原则、开闭原则。 一、单一职…

Linux环境基础开发工具yum,vim使用

目录 1.Linux 软件包管理器 yum1.1什么是软件包1.2关于 rzsz1.3注意事项1.4查看软件包1.5如何安装软件1.6如何卸载软件 2.Linux开发工具2.1Linux编辑器-vim使用2.1.1vim的基本概念2.1.2vim的基本操作2.1.3vim正常模式命令集2.1.4vim末行模式命令集2.1.5vim操作总结 2.2简单vim配…

成功案例|全基因组测序+GWAS联合分析揭示不同种族帕金森病的遗传同质性和异质性

发表期刊&#xff1a;npj Parkinson’s Disease 影响因子&#xff1a;8.7 测序方式&#xff1a;WGS 研究对象&#xff1a;人 1 研究背景 帕金森病&#xff08;PD&#xff09;是一种常见的与年龄相关的神经退行性疾病&#xff0c;其特征是运动迟缓、姿势不稳定、僵硬和静息…

面试八股——redis——缓存——缓存穿透、击穿、雪崩

HR&#xff1a;你在项目中的那些场景用到了redis&#xff1f; 1. 缓存穿透问题 &#xff08;项目中使用的方法&#xff09; 2. 缓存击穿 解决办法1&#xff1a;加互斥锁。大量并发时&#xff0c;先让一个人去查&#xff0c;其他人等着。这样剩下人就可在缓存直接获取值。&#…

软件应用,麻将馆棋牌室计时计费管理系统软件,在计时的时候可以使用灯控器智能控灯

软件应用&#xff0c;麻将馆棋牌室计时计费管理系统软件&#xff0c;在计时的时候可以使用灯控器智能控灯 一、前言 以下软件操作教程以佳易王棋牌计时计费软件V18.0为例说明&#xff0c;其他版本可以参考 件文件下载可以点击最下方官网卡片——软件下载——试用版软件下载 …

Perfetto Trace抓取

1. Perfetto简介 Perfetto 是一个用于 Android 系统的性能跟踪工具&#xff0c;可以帮助开发者分析系统性能和调试问题。 Perfetto 是 Android 10 中引入的全新平台级跟踪工具。这是适用于 Android、Linux 和 Chrome 的更加通用和复杂的开源跟踪项目。 在低于Android R的版本上…

高性价比不入耳运动耳机哪个牌子好?精心筛选五大高赞好物推荐

在现在的时代运动耳机已成为我们不可或缺的伴侣&#xff0c;而在众多的运动耳机品牌中&#xff0c;选择一款高性价比、佩戴舒适且不入耳的款式&#xff0c;往往能让我们在运动过程中享受更加纯粹的音乐体验&#xff0c;接下来&#xff0c;就让我们一起探索那些备受好评的不入耳…

RobotFrameWorkRIDE失败重试

一、方法一&#xff1a;修改源码 【方式】https://blog.csdn.net/qq_15158911/article/details/119077562 二、方式二&#xff1a;使用插件 【需要环境】robotframework>4.1Python>3.8RobotFrameWorkRIDE2.X 【操作】 1、安装robotframework-retryfailed pip insta…

CleanMyMac X 4.15.1 for Mac 最新中文破解版 系统优化垃圾清理工具

CleanMyMac X for Mac 是一款功能更加强大的系统优化清理工具&#xff0c;相比于 CleanMyMac 4.15.1来说&#xff0c;功能增加了不少&#xff0c;此版本为4.15.1官方最新中英文正式破解版本&#xff0c;永久使用&#xff0c;解决了打开软件崩溃问题&#xff0c;最新版4.15.1版本…

Java后端项目性能优化实战-群发通知

背景 公司群发通知模块性能存在问题&#xff0c;我进行全面的系统调优&#xff0c;系统处理能力大幅提升。 原发送流程 优化后的发送流程 优化的点 说明&#xff1a;以下问题基本都是压测过程遇到的&#xff0c;有些问题普通的功能测试暴露不了。优化目标&#xff1a;保证高…

Transformer的前世今生 day07(Masked Self-Attention

Masked Self-Attention 掩码自注意力 由于NLP中的生成模型&#xff0c;是一个一个的生成单词&#xff0c; 所以为了让自注意力也实现这个过程&#xff0c;就设计了掩码自注意力 掩码&#xff1a;在自注意力机制中&#xff0c;每个输入位置都会与其他位置进行注意力计算&#x…

Maven从零到放弃:文档学习

学习背景&#xff1a; Maven一直在使用&#xff0c;自己也构建过相应的项目。也对常用的配置和命令有一些了解&#xff0c;也处理过一些相关的问题。例如&#xff1a;项目缺少依赖&#xff1b;版本冲突或其他构建失败的问题。但是一直没有进行系统性的学习。 链接&#xff1a;…

【暴刷力扣】11. 盛最多水的容器

11. 盛最多水的容器 题目 给定一个长度为 n 的整数数组 height 。有 n 条垂线&#xff0c;第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。 找出其中的两条线&#xff0c;使得它们与 x 轴共同构成的容器可以容纳最多的水。 返回容器可以储存的最大水量。 说明&#xf…