C++项目——负载均衡在线OJ

前言

学习了这么久的C/C++与Linux,终于到了做项目的时候,想想还是有点小激动,哈哈哈哈哈。我们的目标是做一个跟leetcode、牛客类似的在线OJ系统,功能阉割了一些,比如说登录、论坛、求职等等。主要实现了提交题目与判定对错的功能,其中,搭载了负载均衡模块,能让客户端提交的代码交给N台服务器(或者一台服务器的N个进程)去处理。

代码地址负载均衡在线oj系统  ,建议配合代码进行观看学习

一、所用技术与开发环境

所用技术:

  • C++ STL 标准库
  • Boost 准标准库(字符串切割)
  • cpp-httplib 第三方开源网络库
  • ctemplate 第三方开源前端网⻚渲染库
  • jsoncpp 第三方开源序列化、反序列化库
  • 负载均衡设计
  • 多进程、多线程
  • Ace前端在线编辑器(了解)
  • html/css/js/jquery/ajax (了解)

开发环境: 

  • Centos 7 云服务器
  • vscode

二、项目宏观结构

项目核心是三个模块

  1. comm: 公共模块
  2. compile_server: 编译与运行模块
  3. oj_server: 获取题目列表,查看题目与编写题目界面,负载均衡...

编写思路

  1. 先编写compile_server
  2. oj_server
  3. version 基于文件版的在线OJ
  4. 前端的⻚面设计(了解)

三、日志服务设计

日志的本质就是打印字符串,只是看是输入在文件中还是显示器中,我们默认选择输入在屏幕中,这里选择了可变参数包进行日志的打印。

#pragma once#include <iostream>
#include <cstdarg>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
using namespace std;namespace ns_log
{enum{Debug = 0,Info,Warning,Error,Fatal};enum{Screen = 10,OneFile,ClassFile};string LevelToString(int level){switch (level){case Debug:return "Debug";case Info:return "Info";case Warning:return "Warning";case Error:return "Error";case Fatal:return "Fatal";default:return "Unknown";}}const int default_style = Screen;const string default_filename = "Log.";const string logdir = "log";class Log{public:Log(int style = default_style, string filename = default_filename): _style(style), _filename(filename){if (_style != Screen)mkdir(logdir.c_str(), 0775);}// 更改打印方式void Enable(int style){_style = style;if (_style != Screen)mkdir(logdir.c_str(), 0775);}// 时间戳转化为年月日时分秒string GetTime(){time_t currtime = time(nullptr);struct tm *curr = localtime(&currtime);char time_buffer[128];snprintf(time_buffer, sizeof(time_buffer), "%d-%d-%d %d:%d:%d",curr->tm_year + 1900, curr->tm_mon + 1, curr->tm_mday, curr->tm_hour, curr->tm_min, curr->tm_sec);return time_buffer;}// 写入到文件中void WriteLogToOneFile(const string &logname, const string &message){FILE *fp = fopen(logname.c_str(), "a");if (fp == nullptr){perror("fopen failed");exit(-1);}fprintf(fp, "%s\n", message.c_str());fclose(fp);}// 打印日志void WriteLogToClassFile(const string &levelstr, const string &message){string logname = logdir;logname += "/";logname += _filename;logname += levelstr;WriteLogToOneFile(logname, message);}void WriteLog(const string &levelstr, const string &message){switch (_style){case Screen:cout << message << endl; // 打印到屏幕中break;case OneFile:WriteLogToClassFile("all", message); // 给定all,直接写到all里break;case ClassFile:WriteLogToClassFile(levelstr, message); // 写入levelstr里break;default:break;}}// 打印日志void LogMessage(int level, const char *file, int line, const char *format, ...){char rightbuffer[1024]; // 处理消息va_list args;           // va_list 是指针va_start(args, format); // 初始化va_list对象,format是最后一个确定的参数// 现在args指向了可变参数部分vsnprintf(rightbuffer, sizeof(rightbuffer), format, args); // 写入到rightbuffer中va_end(args);char leftbuffer[1024]; // 处理日志等级、pid、时间、文件名和行号string levelstr = LevelToString(level);string currtime = GetTime();string idstr = to_string(getpid());snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%s][%s][%s:%d]", levelstr.c_str(), currtime.c_str(), idstr.c_str(), file, line);string loginfo = leftbuffer;loginfo += rightbuffer;WriteLog(levelstr, loginfo);}// 提供接口给运算符重载使用void _LogMessage(int level, const char *file, int line, char *rightbuffer){char leftbuffer[1024];string levelstr = LevelToString(level);string currtime = GetTime();string idstr = to_string(getpid());snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%s][%s][%s:%d]", levelstr.c_str(), currtime.c_str(), idstr.c_str(), file, line);string messages = leftbuffer;messages += rightbuffer;WriteLog(levelstr, messages);}// 运算符重载void operator()(int level, const char *file, int line, const char *format, ...){char rightbuffer[1024];va_list args;                                              // va_list 是指针va_start(args, format);                                    // 初始化va_list对象,format是最后一个确定的参数vsnprintf(rightbuffer, sizeof(rightbuffer), format, args); // 写入到rightbuffer中va_end(args);_LogMessage(level, file, line, rightbuffer);}~Log(){}private:int _style;string _filename;};Log lg;class Conf{public:Conf(){lg.Enable(Screen);}~Conf(){}};Conf conf;
}// 辅助宏
#define lg(level, format, ...) ns_log::lg(level, __FILE__, __LINE__, format, ##__VA_ARGS__)

四、compiler 编译服务设计

对于在线OJ平台,我们先来处理编译服务。

1.编译服务

当远端提交代码的时候,我们就要对该代码提供编译服务,我们可以把提交的代码打包,使用程序替换(替换为g++)的方式进行编译,但是,如果远端代码是程序错误的代码或者恶意代码,我们贸然替换可能会导致服务程序崩溃,因此需要fork创建子进程,让子进程去进行程序替换执行代码

如下是编译代码,首先编译的时候,我们传入的参数只有文件名,利用统一的PathUtil接口,将文件名转化为相对路径,同时给文件添加上后缀以方便将运行输出、编译错误内容、标准输入、标准输出、标准错误分类的写入文件中。 

    //路径拼接功能const std::string tmp_path = "./temp/";class PathUtil{private:// 代码复用static std::string AddSuffix(const std::string &file_name, const std::string &suffix){std::string path_name = tmp_path;path_name += file_name;path_name += suffix;return path_name;}public:// 如下三个编译时需要的临时文件// 构建源文件完整文件名 -> 路径+后缀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 CompilerError(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 Compiler{public:Compiler(){}~Compiler(){}// 是否编译成功// file_name只是文件名,没有后缀  我们需要自己拼接// test -> ./temp/test.cpp// test -> ./temp/test.exe// test -> ./temp/test.stderrstatic bool Compile(const std::string &file_name){pid_t pid = fork();if (pid < 0){lg(ns_log::Error,"编译错误,创建子进程失败");return false;}else if (pid == 0){// 子进程umask(0);int _stderr = open(PathUtil::CompilerError(file_name).c_str(), O_CREAT | O_WRONLY, 0644);if (_stderr < 0){lg(Warning,"没有成功形成stderr文件");exit(1);}// 重定向  编译出错重定向到file_name.stderr文件中dup2(_stderr, 2);// 子进程  调用编译器进行编译execlp("g++","g++", "-o", PathUtil::Exe(file_name).c_str(),PathUtil::Src(file_name).c_str(), "-std=c++11","-D", "COMPILER_ONLINE", nullptr); // 最后传nullptr代表参数传递完毕//程序替换成功,后面的代码都不会执行了,会编程替换的代码lg(ns_log::Error,"启动编译器g++失败,可能是参数错误");exit(2);}else{// 父进程waitpid(pid, nullptr, 0);// 编译是否成功,看是否有exe文件if (FileUtil::IsFileExists(PathUtil::Exe(file_name))){lg(Info,"%s编译成功!!",PathUtil::Src(file_name).c_str());return true;}}lg(ns_log::Error,"编译失败,没有形成可执行程序");return false;}};

 测试结果如下,能够正常编译code程序,并且生成可执行与错误信息。

2.运行服务

我们想让编译模块提供编译服务,编译完成也要能自动运行,因此我们还需要运行服务,也是需要fork创建子进程去完成运行的(如果当前进程去运行发生错误会导致程序崩溃),其中运行我们并不关心程序运行的结果是否正确,因为这要配合测试用例,我们只关心程序运行是否正常运行完成,有没有收到信号(使用进程等待的方式查看)。同时将标准输入、标准输出、标准错误分门别类的写入到文件中,方便后续处理。

同时使用了setrlimit()函数去限制运行时间与内存占用空间。

#pragma once#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <sys/resource.h>#include "../comm/log.hpp"
#include "../comm/util.hpp"namespace ns_runner
{using namespace ns_log;using namespace ns_util;class Runner{public:Runner(){}~Runner(){}// 设置进程资源占用大小static void SetProcLimit(int cpu_limit, int mem_limit){// CPU时长struct rlimit _cpu_rlimit;_cpu_rlimit.rlim_cur = cpu_limit;_cpu_rlimit.rlim_max = RLIM_INFINITY; // 无穷大setrlimit(RLIMIT_CPU, &_cpu_rlimit);// 内存大小struct rlimit _mem_rlimit;_mem_rlimit.rlim_cur = mem_limit * 1024;_mem_rlimit.rlim_max = RLIM_INFINITY;setrlimit(RLIMIT_AS, &_mem_rlimit);}// 指明文件名即可,有comm/util.hpp里的PathUtil帮我们拼接// 返回值 > 0 : 程序异常了,返回值为退出收到了信号编号,// 返回值 == 0 : 正常运行完毕,结果保存到了临时文件中// 返回值 < 0 : 内部错误// cpu_limit:该程序运行时,可以使用的最大cpu资源上限// mem_limit:该程序运行时,可以使用的最大内存大小(KB)static int Run(const std::string &file_name, int cpu_limit, int mem_limit){/*********************************************** 程序运行:* 1.代码跑完,结果正确* 2.代码跑完,结果不正确* 3.代码没跑完,结果异常* Run不用考虑结果是否正确,交给测试用例解决* 因此我们只考虑是否跑完* ********************************************/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 _in_fd = open(_stdin.c_str(), O_CREAT | O_RDONLY, 0664);int _out_fd = open(_stdout.c_str(), O_CREAT | O_WRONLY, 0664);int _err_fd = open(_stderr.c_str(), O_CREAT | O_WRONLY, 0664);if (_in_fd < 0 || _out_fd < 0 || _err_fd < 0){lg(ns_log::Error, "运行时打开标准文件失败!!");return -1; // 文件打开失败}pid_t pid = fork();if (pid < 0){lg(ns_log::Error, "运行时创建子进程失败!!");close(_in_fd);close(_out_fd);close(_err_fd);return -2; // 创建子进程失败}else if (pid == 0){// 子进程dup2(_in_fd, 0);dup2(_out_fd, 1);dup2(_err_fd, 2);SetProcLimit(cpu_limit, mem_limit);//第一个参数为我要执行谁  第二个参数为如何执行execl(_execute.c_str(), _execute.c_str(), nullptr);perror("execl failed");exit(1);}else{// 父进程close(_in_fd);close(_out_fd);close(_err_fd);int status = 0;waitpid(pid, &status, 0);// 程序运行异常,一定是因为收到了信号!lg(Info, "This is a test log message with value:%d", (status & 0x7F));return status & 0x7F;}}};
}

测试结果

3.编译并运行服务 

前面我们实现了编译与运行功能,但是远端传递过来的肯定不是直接的code,而是按照双方的通信协议进行传输数据,因此编译并运行(complie_and_run)他还得提供定制通信协议字段的功能,并正确调用编译服务和运行服务,同时文件id必须要有唯一性,不然多个用户之间会相互影响。

我们采用JSONcpp 库来帮助我们进行序列化与反序列化,如下命令安装jsoncpp

sudo yum install jsoncpp-devel

 如下就是json的初步使用,需要注意编译时需要添加 -ljsoncpp 来链接jsoncpp库

现在我们了解了json的使用,知道了json是按照 {key,value} 格式进行序列化,那么远端进行提交的代码我们就可以先利用json处理成相应的格式。如下,代码是一个key,输入是一个key,cpu限制与内存限制又是一个key

如下三个函数是PathUtil中compile_run需要用到的工具函数

  1. UniqFileName为唯一的文件名,使用了毫秒级时间错+atomic原子递增的唯一值
  2. WriteFile是往文件中写入函数,因为客户端传递的json里面的code需要写入到文件中,以便后续进行编译、运行。
  3. ReadFile为从文件中读取函数,因为编译运行是否发生错误、结果是什么都放在文件中,从文件中读取原因和结果赋值到json中再给客户端
        // 如下三个函数都是在PathUtil类中的//生成随机名static std::string UniqFileName(){// 毫秒级时间戳 + "_" + 原子性递增唯一值static std::atomic_uint id(0);id++;std::string ms = TimeUtil::GetTimeMs();std::string uniq_id = std::to_string(id);return ms + "_" + uniq_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;}//往文件中读取static bool ReadFile(const std::string &target, std::string *content, bool keep = false){std::ifstream in(target);if (!in.is_open()){return false;}std::string line;// getline 按行读取,但不保存分隔符,有时候需要保留\n// getline 返回值ifstream会隐式类型转化为boolif (!keep)while (std::getline(in, line))*content += line;elsewhile (std::getline(in, line))*content += line + '\n';in.close();return true;}

如下是compile_run的代码,主要函数为Start(const std::string &in_json, std::string *out_json),传入in_json格式字符串,进行处理,处理后将结果返回给到out_json。

#pragma once#include "compiler.hpp"
#include "runner.hpp"
#include "../comm/log.hpp"
#include "../comm/util.hpp"#include <signal.h>
#include <unistd.h>
#include <jsoncpp/json/json.h>
namespace ns_compile_and_run
{using namespace ns_util;using namespace ns_compiler;using namespace ns_runner;using namespace ns_log;class CompileAndRun{public:static void RemoveTempFile(const std::string &file_name){// 清理文件的个数是不确定的,但有哪些我们知道std::string _src = PathUtil::Src(file_name);if (FileUtil::IsFileExists(_src))unlink(_src.c_str());std::string _compiler_error = PathUtil::CompilerError(file_name);if (FileUtil::IsFileExists(_compiler_error))unlink(_compiler_error.c_str());std::string _execute = PathUtil::Exe(file_name);if (FileUtil::IsFileExists(_execute))unlink(_execute.c_str());std::string _stdin = PathUtil::Stdin(file_name);if (FileUtil::IsFileExists(_stdin))unlink(_stdin.c_str());std::string _stdout = PathUtil::Stdout(file_name);if (FileUtil::IsFileExists(_stdout))unlink(_stdout.c_str());std::string _stderr = PathUtil::Stderr(file_name);if (FileUtil::IsFileExists(_stderr))unlink(_stderr.c_str());}// 状态码转化为状态描述  根据Start()函数对code的设置来定/**************************** code > 0 : 进程收到信号导致异常崩溃* code < 0 : 整个过程非运行报错(代码为空,编译报错等)* code = 0 :整个过程全部正常运行* **************************/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:// desc = "编译时发生了错误";// 编译时错误直接从错误哦文件中读取错误原因FileUtil::ReadFile(PathUtil::CompilerError(file_name), &desc, true);break;case SIGABRT: // 6desc = "内存超过范围";break;case SIGXCPU: // 24desc = "运行超时";break;case SIGFPE: // 8desc = "浮点数溢出";break;default:desc = "未知:" + to_string(code);break;}return desc;}/******************************* 输入:* code:用户提交的代码* input:用户给给自己提交的代码对应的输入,不做处理  方便后面扩展* cpu_limit:时间要求* mem_limit:空间要求** 输出:* 1.必填:*      status:状态码*      reason:请求结果* 2.选填:*      stdout:我的程序运行完的结果*      stderr:我的程序运行完的错误结果** 参数:* in_json: {"code":"#include ....","input":"....","cpu_limit":1,"mem_limit":"10240"}* out_json:{"status":"0","reason":"...","stdout":"...","stderr":"..."}* ****************************/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();Json::Value out_value;int status_code = 0;int run_result_code = 0;std::string file_name; // 需要内部形成的唯一文件名if (code.size() == 0){status_code = -1;// 差错处理goto END;}// 获取具有唯一性的名字,防止编译冲突  不带后缀file_name = FileUtil::UniqFileName();// 形成临时src文件,并且写入用户传递的代码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_code = Runner::Run(file_name, cpu_limit, mem_limit);if (run_result_code < 0){// 打开文件描述符或fork失败status_code = -2;}else if (run_result_code > 0){// 程序运行崩溃status_code = run_result_code;}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;FileUtil::ReadFile(PathUtil::Stdout(file_name), &_stdout, true);out_value["stdout"] = _stdout;std::string _stderr;FileUtil::ReadFile(PathUtil::Stderr(file_name), &_stderr, true);out_value["stderr"] = _stderr;}Json::StyledWriter writer;*out_json = writer.write(out_value);//删除编译、运行生成的临时文件//RemoveTempFile(file_name);}};
}

 我们自定义一个json,然后代码提交看看效果

#include "compile_run.hpp"using namespace ns_compile_and_run;int main()
{// in_json : {"code" : "#include ....", "input" : "....", "cpu_limit" : 1, "mem_limit" : "10240"};// out_json:{"status" : "0", "reason" : "...", "stdout" : "...", "stderr" : "..."} ;// 测试-------------》》》》》》》》》》》充当客户端请求过来的json串std::string in_json;Json::Value in_value;in_value["code"] = R"(#include <iostream>int main(){std::cout<<"hello code"<<std::endl;return 0;})";in_value["input"] = "";in_value["cpu_limit"] = 1;in_value["mem_limit"] = 1024 * 30;Json::FastWriter write; in_json = write.write(in_value);std::cout<<in_json<<std::endl;string out_value;CompileAndRun::Start(in_json,&out_value);std::cout<<out_value<<std::endl;return 0;
}

运行结果如下,运行成功 

如果嫌每次都要生成这么多文件比较烦,可以打开compile_run.hpp下的这个接口,可以删除temp目录下生成的文件。因为我们的目的并不是获取这些文件,而是将这些文件的结果返回到json当中,读取完比这些文件就没有作用了。 

4.引入cpp-httplib库

4.1升级gcc版本

要使用cpp-httplib库,版本低的gcc运行会报错的,最好先将gcc升级成新版本,输入如下指令查看gcc版本

gcc -v

 如果发现版本是9.几的就可以跳过该步骤,直接去4.2引入cpp-httplib

 使用如下指令让scl gcc devsettool帮我们升级gcc

sudo yum install centos-release-scl scl-utils-build
sudo yum install -y devtoolset-9-gcc devtoolset-9-gcc-c++
ls /opt/rh/

当看到devtoolset-9存在的时候,就证明我们安装好了 

然后我们要进行配置,命令行输入如下指令打开bash_profile文件

vim ~/.bash_profile

 把下面两行代码添加到 bash_profile文件中(输入 i 就可以插入,再esc退出插入模式,输入:wq保存)

scl enable devtoolset-9 bashsource /opt/rh/devtoolset-9/enable 

如下就是配置好了 

启一下xshell输入gcc -v 就可以看到变为了新版本

4.2cpp-httplib

大家点击如下链接进行下载,git clone需要用户名和密码,用ZIP下载不需要

cpp-httplibv0.7.15

然后拖拽可以直接拷贝到Linux系统,或者输入指令rz 进行选择拷贝。随后输入如下指令进行解压

unzip cpp-httplib-v0.7.15.zip 

解压之后我们需要将其中的httplib.h给拷贝到项目中

如下,拷贝到了我的项目中,这里右边的内容你需要换成自己的路径 

cp cpp-httplib-v0.7.15/httplib.h   109/ONLINE_JUDGE_2/comm/ 

拷贝完成,comm下多了一个httplib

4.3httplib的使用 

运用也十分简洁,调用Server对象的Get方法,第一个参数为路径,第二个参数为回调函数,函数参数类型第一个是Request请求、第二个是Response响应,我们给响应set_content添加正文内容,就可以在网页端看到效果了。这些是我们处理的回调对象,最后要记得进行listen监听,“0.0.0.0”代表可以接受来自所有网络接口的连接。

在编译时,我们一定要添加-lpthread,因为httplib使用的pthread库,需要指定链接 

学会了httplib库的基本使用,我们得将http与编译运行模块链接起来。

通过html前后端的交互,用户Post请求的Request正文正好就是我们想要的json string。于是就可以提取出req.body,然后就让这个body去进行编译并运行,最后返回给页面。

由于目前我们html网站还没写,因此使用Postman进行数据发送,如下,发送的原始Json,收到返回的是compile_run 模块返回的内容,成功完成http的编译与运行结果反馈。

日志也成功打印出来消息 

文件也成功生成 

同时,我们想要负载均衡的在线OJ系统,那么compile_server服务就不能只支持一个,得支持多个,因此port端口不能固定的传入,可以通过命令行参数agrv运行时再传入端口,修改成如下即可。

五、oj_server服务设计

我们想做一个基于MVC结构的oj服务设计,他的本质就是建立一个小型网站,能从网站上进行代码提交并返回给编译服务进行编译,再将结构返回到前端网页

MVC介绍

  • M:Model,是和数据交互的模块,比如对题库的增删查改
  • V :View,拿到数据后,要进行构建网页,渲染网页内容,展示给用户
  • C:control,控制器,核心业务逻辑

oj_server的主要功能 

  1. 获取首页,用题目列表充当
  2. 编辑区域页面
  3. 提交判题功能(编译并运行)

1. http路由选择

一样的,使用http进行路由选择,主要分为了三个路由,获取所有题目(从文件中读取所有题目并返回)、获取某一个具体题目内容、提交代码,目前网站随便放一点信息能体现路由成功就好,后面再添加功能

2.文件版题目设计 

题目的主要内容

  1. 题目的编号
  2. 题目的标题
  3. 题目的难度
  4. 题目的描述
  5. 时间要求(内部处理)
  6. 空间要求(内部处理)

由两批文件构成

  1. questions.list:题目列表
  2. 题目描述,题目的预设值代码(header.cpp)、测试用例代码(tail.cpp)
  • 先从questions.list题目描述中获取题目信息,根据题目编号去找到对应的文件夹
  • 再找到文件夹下的desc.txt描述,将描述反馈到网站,在header编辑器中进行编译(html实现,先理解逻辑就好),提交时与tail.cpp(测试用例)进行拼接
  • 再交给编译运行服务进行处理
  • 最后将结果再返回到网站中。

3.model——数据交互模块

我们需要将文件中的数据读取出来,MVC模块中model就是用来干这个活的,我们可以定义一个Question类,里面有如下内容,可以完美的将题目的所有信息获取到,再创建一个vector<Question>容器,从文件中一行一行的将Question的信息读取出来

    struct Question{string number; // 题目唯一编号string title;  // 题目标题string star;   // 题目难度int cpu_limit; // 题目的时间要求(s)int mem_limit; // 题目的空间要求(KB)string desc;   // 题目的描述string header; // 题目预设给用户在线编译器的代码string tail;   // 题目的测试用例,与header拼接形成完整代码};

 代码逻辑并不复杂,其中拆分逻辑用到了boost库

安装如下

sudo yum install -y boost-devel

 oj_model.hpp代码如下

#pragma once#include "../comm/log.hpp"
#include "../comm/util.hpp"
#include <cassert>
#include <vector>
#include <unordered_map>
// 根据题目list文件,加载所有的题目信息到内存中
// model:主要用来和数据进行交互,对外提供访问的接口namespace ns_model
{using namespace std;using namespace ns_log;using namespace ns_util;struct Question{string number; // 题目唯一编号string title;  // 题目标题string star;   // 题目难度int cpu_limit; // 题目的时间要求(s)int mem_limit; // 题目的空间要求(KB)string desc;   // 题目的描述string header; // 题目预设给用户在线编译器的代码string tail;   // 题目的测试用例,与header拼接形成完整代码};const std::string question_list = "./questions/questions.list";const std::string question_path = "./questions/";class Model{public:Model(){assert(LoadQuestionList(question_list));}bool LoadQuestionList(const string &question_list){ifstream in(question_list);if (!in.is_open()){lg(Fatal, "加载题库失败,请检查题库文件");return false;}std::string line;while (getline(in, line)){vector<string> tokens;StringUtil::SplitString(line, &tokens, " ");// 1 判断回文数 简单 1 30000if (tokens.size() != 5){lg(Warning,"加载部分题目失败,请检查文件格式");continue;}Question q;q.number = tokens[0];q.title = tokens[1];q.star = tokens[2];q.cpu_limit = stoi(tokens[3]);q.mem_limit = stoi(tokens[4]);// 题目路径拼接string path = question_path;path += q.number;path += "/";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.insert({q.number, q});}lg(Info,"加载题库成功!!!");in.close();return true;}bool GetAllQuestions(vector<Question> *out){if (_questions.size() == 0){lg(Error,"用户获取题库失败");return false;}for (auto &q : _questions){// first:string, second:Questionout->push_back(q.second);}return true;}bool GetOneQuestions(const string &number, Question *out){const unordered_map<string, Question>::const_iterator &iter = _questions.find(number);if (iter == _questions.end()){lg(Error,"用户题目失败,题目编号:%s",number.c_str());return false;}*out = iter->second;return true;}private:// first为题目编号  second为题目结构体unordered_map<string, Question> _questions;};
}

4.view构建并渲染网页模块

前面的model模块让我们可以获取到构建网页所需要的数据了,现在view模块需要利用获取到的数据去构建网页。

4.1 ctemplate安装

这里会用到ctemplate库,我们先用如下指令clone一下

git clone https://github.com/OlafvdSpek/ctemplate.git

再执行如下指令,就安装成功了  

cd ctemplate/
./autogen.sh
./configure
make
sudo make install

4.2 ctemplate的使用

  • ctemplate的主要作用是渲染html,本质上就是替换,将形成的数据字典添加上key:value的键值对,在html里将这些被双括号括起来的  {{键}}  都替换为值
  • 为什么要这么做呢?因为我们要将Question结构体的数据自动的填充到网页中,形成了数据字典,填充好字段,后续html也设置好key,后续ctemplate就可以自动的帮我们实现了。

测试代码如下 

#include <iostream>
#include <string>
#include <ctemplate/template.h>using namespace std;
int main()
{string in_html = "./test.html";string value = "能力越小责任越小";//形成数据字典  类似于形成了一个unordered_map <key,value> testctemplate::TemplateDictionary root("test"); root.SetValue("key",value);//获取被渲染网页对象     DO_NOT_STRIP是不要剥离(保持字典原貌)ctemplate::Template *tpl = ctemplate::Template::GetTemplate(in_html,ctemplate::DO_NOT_STRIP);//添加字典数据到网页中string out_html;tpl->Expand(&out_html,&root);cout<<out_html<<endl;
}

4.3 view 渲染与html 

学会了刚刚的用法,这里我们使用了循环渲染html,因为题目有很多,是放在vector<Question> questions里面的,因此html我们得让成功的输出每一行,而我们并不清楚具体有多少行,因为我们可能随时会添加题目,于是用循环渲染的方式,让代码更有健壮性。

效果如下,这是all_questions网站(获取所有的题目大致信息),有questions里面有几道题就会循环渲染几次 

 

如下是one_question网站(获取某一个题目的具体信息),只需要一道题,因此不需要循环渲染 

现在html就能成功获取后端数据了

具体代码如下 

#pragma once#include <ctemplate/template.h>#include "oj_model.hpp"namespace ns_view
{using namespace ns_model;const std::string template_path = "./template_html/";class View{public:View(){}void AllExpandHtml(const std::vector<Question> &questions, std::string *html){// 题目编号  标题  难度// 推荐使用表格显示// 形成路径std::string src_html = template_path + "all_questions.html";// 形成template字典ctemplate::TemplateDictionary root("all_questions");for (const auto &q : questions){// 形成子字典ctemplate::TemplateDictionary *sub = root.AddSectionDictionary("question_list");sub->SetValue("number", q.number);sub->SetValue("title", q.title);sub->SetValue("star", q.star);}// 获取被渲染的htmlctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP);// 开始渲染tpl->Expand(html, &root);}void OneExpandHtml(const Question &question, std::string *html){// 形成路径std::string src_html = template_path + "one_question.html";//形成数据字典ctemplate::TemplateDictionary root("one_question");root.SetValue("number",question.number);root.SetValue("desc",question.desc);root.SetValue("title",question.title);root.SetValue("star",question.star);root.SetValue("pre_code",question.header);// 获取被渲染的htmlctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html,ctemplate::DO_NOT_STRIP);tpl->Expand(html,&root);}~View(){}};
}

5.control 控制

5.1结合model模块与view模块进行获取html网页信息

这里是我们只是调用了之前写的model与view模块接口

#pragma once
#include "oj_model.hpp"
#include "oj_view.hpp"
namespace ns_contrl
{using namespace ns_model;using namespace ns_log;using namespace ns_util;using namespace ns_view;class Control{public:Control(){}//根据题目数据构建网页bool AllQuestions(string* html){vector<Question> questions;if(_model.GetAllQuestions(&questions)){// 给题目编号进行排序sort(questions.begin(), questions.end(), [](const Question &q1, const Question q2){ return stoi(q1.number) < stoi(q2.number); });// 获取题目信息成功,将所有的题目数据构建成网页_view.AllExpandHtml(questions, html);}else{*html = "获取题目失败, 形成题目列表失败";return false;}return true;}bool OneQuestion(const string& number,string *html){Question q;if (_model.GetOneQuestion(number, &q)){// 获取指定题目信息成功,将题目数据构建成网页_view.OneExpandHtml(q, html);}else{*html = "获取指定题目: " + number + " 失败, 不存在!";return false;}return true;}~Control(){}private:Model _model;View _view;};
}

5.2 Machine 模块

到这里,我们已经完成了从文件中获取数据,并将数据展示在html网页中,但是仅仅是展示还没够,我们还得将写好的数据能够提交到后端进行判题。

其中,我们是负载均衡的在线OJ系统,那我们应该要提交给哪一台机器呢?

因此我们得还有一个类来负责机器管理。此时我们得对负载因子进行加锁,因为同一时间可能有很多人进行提交代码到机器上进行编译处理,因此加锁可以预防多线程下变量的安全问题。

class Machine{friend class LoadBlance;public:Machine(): _ip(string()), _port(0), _load(0), _mtx(nullptr){}Machine(string ip,uint16_t port): _ip(ip), _port(port), _load(0), _mtx(new std::mutex()){}//提升主机负载void IncLoad(){if(_mtx)_mtx->lock();++_load;if(_mtx)_mtx->unlock();}//直接使用参数_load也能获取,这样写是为了统一接口uint64_t Load(){uint64_t load = 0;if(_mtx)_mtx->lock();load = _load;if(_mtx)_mtx->unlock();return _load;}//减少主机负载void DecLoad(){if(_mtx)_mtx->lock();--_load;if(_mtx)_mtx->unlock();}//清零负载  主机离线后需要立刻将负载清零,防止下次主机上线时负载不为0void ResetLoad(){if (_mtx)_mtx->lock();_load = 0;if (_mtx)_mtx->unlock();}~Machine(){}private:std::string _ip;  // ipuint16_t _port;        // portuint64_t _load;   // 负载std::mutex *_mtx; // 锁};

5.3 LoadBlance负载均衡模块 

目前有了machine类,可以对机器进行管理,还需要负载均衡模块帮我们遍历查询哪一台主机负载少,就让哪个主机进行编译。

其中参数部分有 _online在线主机、_offline离线主机、_machines所有主机,首先从service_machine.conf进行主机的配置,按照ip+port进行配置,这里我们只有一台服务器,因此使用多进程进行模拟负载均衡。

机器的上线与离线通过_online 与 _offline的互相erase和insert实现。

const std::string service_machine = "./conf/service_machine.conf";class LoadBlance{public:LoadBlance(){assert(LoadConf(service_machine));lg(Info,"加载 %s 成果",service_machine.c_str());}bool LoadConf(const std::string &machine_conf){std::ifstream in(machine_conf);if(!in.is_open()){lg(Fatal,"加载: %s 失败",machine_conf.c_str());return false;}std::string line;while(std::getline(in,line)){std::vector<std::string> tokens;StringUtil::SplitString(line,&tokens,":");if(tokens.size()!=2){lg(Warning,"切分: %s 失败",line.c_str());continue;}Machine m(tokens[0],stoi(tokens[1]));_machines.push_back(m);_online.push_back(_machines.size());}in.close();return true;}bool SmartChoice(int *id,Machine** m){// 1.选择好的主机(负载少的主机)并更新负载// 2.可能需要离线该主机 lock_guard<mutex> lck(_mtx);// 负载均衡的算法 ——> 轮询 + hashint online_num = _online.size();if (online_num == 0){lg(Fatal, "所有的编译主机全部离线,需要赶紧上线!!!");return false;}// 通过遍历的方式找到所有负载最小的机器*id = _online[0];*m = &_machines[_online[0]];uint64_t min_load = _machines[_online[0]].Load();for (int i = 1; i < online_num; i++){uint64_t cur_load = _machines[_online[i]].Load();if (min_load > cur_load){min_load = cur_load;*id = _online[i];*m = &_machines[_online[i]];}}return true;}//选择进行下线 某个id 的主机void OfflineMachine(int which_id){lock_guard<mutex> lock(_mtx);for (auto iter = _online.begin(); iter != _online.end(); iter++){if (*iter == which_id){// 找到了要离线的主机  需要先给负载清0_machines[which_id].ResetLoad();_offline.push_back(*iter);_online.erase(iter);return;}}}// 统一上线主机void OnlineMachine(){lock_guard<mutex> lock(_mtx);{_online.insert(_online.end(), _offline.begin(), _offline.end());_offline.erase(_offline.begin(), _offline.end());}lg(Info, "所有的主机又上线啦!");}//仅供调试查看信息void showMachines(){lock_guard<mutex> lock(_mtx);std::cout << "当前在线主机列表:";for (auto &id : _online){std::cout << id << " ";}std::cout << endl;std::cout << "当前离线主机列表:";for (auto &id : _offline){std::cout << id << " ";}std::cout << endl;}~LoadBlance(){}private:// 可以给我们提供编译服务所有的主机// 主机下标充当idstd::vector<Machine> _machines;// 所有在线的主机idstd::vector<int> _online;// 所有离线的主机idstd::vector<int> _offline;// 保证loadBlance安全的锁std::mutex _mtx;};

5.4 control 控制模块

MVC模式中Model和View已经完成了,目前可以从文件中读取数据,View可以获取数据渲染的htm。现在我们要使用control模块对他们进行控制了。同时还提供了Judge功能,能够将用户提交的代码进行判题。

  1. 根据题目编号,拿到题目
  2. 对in_json进行反序列化,得到题目code、input
  3. 重新拼接用户代码+测试用例,得到新的代码
  4. 选择负载最低的主机
  5. 对负载最低的主机发起http请求得到结果
  6. 结果赋值给输出参数out_json
class Control{public:Control(){}// 根据题目数据构建网页bool AllQuestions(string *html){vector<Question> questions;if (_model.GetAllQuestions(&questions)){// 给题目编号进行排序sort(questions.begin(), questions.end(), [](const Question &q1, const Question q2){ return stoi(q1.number) < stoi(q2.number); });// 获取题目信息成功,将所有的题目数据构建成网页_view.AllExpandHtml(questions, html);}else{*html = "获取题目失败, 形成题目列表失败";return false;}return true;}bool OneQuestion(const string &number, string *html){Question q;if (_model.GetOneQuestion(number, &q)){// 获取指定题目信息成功,将题目数据构建成网页_view.OneExpandHtml(q, html);}else{*html = "获取指定题目: " + number + " 失败, 不存在!";return false;}return true;}// 第一个参数in_json// {//      "code" : "#include ....",//      "input" : "...",// }void Judge(const std::string &number, const std::string in_json, std::string *out_json){// 1. 根据题目编号,拿到题目struct Question q;_model.GetOneQuestion(number, &q);// 2. 对in_json进行反序列化,得到题目id、code、inputJson::Reader reader;Json::Value in_value;reader.parse(in_json, in_value);std::string code = in_value["code"].asString();// 3. 重新拼接用户代码+测试用例,得到新的代码Json::Value compile_value;compile_value["input"] = in_value["input"].asString();compile_value["code"] = code + q.tail;// Json::Value既可以接受字符串又可以接受整数compile_value["cpu_limit"] = q.cpu_limit;compile_value["mem_limit"] = q.mem_limit;Json::StyledWriter writer;std::string compile_string = writer.write(compile_value);// 4. 选择负载最低的主机(做差错处理)// 选择规则: 一直选择,直到主机可用,否则就是全部挂掉while (true){int id = 0;Machine *m = nullptr;if (!_load_blance.SmartChoice(&id, &m)){break;}lg(Info, "选择主机成功,主机id: %d,详情: %s:%d,当前主机负载:%d", id, m->_ip.c_str(), m->_port,m->_load);// 5. 发起http请求得到结果Client cli(m->_ip, m->_port);m->IncLoad();if (auto res = cli.Post("/compile_and_run", compile_string, "application/json;charset=utf-8")){// 请求成功// 6. 结果赋值给out_josnif (res->status == 200){*out_json = res->body;m->DecLoad();lg(Info,"请求编译和运行服务成果...");break;}m->DecLoad();}else{// 请求失败lg(ns_log::Error, "当前请求的主机id: %d,详情: %s:%d 可能已经离线", id, m->_ip.c_str(), m->_port);_load_blance.OfflineMachine(id);}}}~Control(){}private:Model _model;            // 提供后台数据View _view;              // 提供网页渲染功能LoadBlance _load_blance; // 核心负载均衡器};

注意这里我们使用了之前导入的jsoncpp库,因此编译时需要添加 -ljsoncpp 指明动态库。

测试结果如下,成功进行编译服务。

六、html 

html设计这里就不多展开了,大家看源码即可,但是前后端交互的页面我们还是得有所了解的。他主要在我们进行代码提交,后端服务给我们将编译结果返回的时候。

点击提交代码时,我们要获取题号与code,因为后端要得到json并做处理,由于使用的是ACE (Ajax.org Cloud9 Editor)编辑器。

他有方法可以直接获取编辑框里的代码,因此直接定义code变量为获取的代码就可以。

获取到了题号和代码,我们就要构建json向后端发起请求,这里使用了ajax发送json请求,当请求成功,就回去执行show_result函数

这个函数的目的就是将状态码和原因先显示到标签位置,如果状态码为0,证明至少编译时没问题的,结果对不对还得看测试用例,于是我们就可以将之前写入到stdout和stderr的运行结果再展示到标签位置。

七、总结

首先完成了编译运行模块,通过fork创建子进程,让子进程去调用程序替换,父进程等待子进程,同时获取到子进程的退出码。同时使用了httplib库来帮助我们进行Get、Post请求。

再完成了在线oj网站与服务器的搭建,从文件中读取机器的ip地址与端口,构建出负载均衡的所有机器。

然后智能选择负载较少的机器,给这个机器发送编译请求。

最后使用ajax进行前后端的交互,前端进行提交,将数据提交到后台,后台对数据做出响应,再将结果返回到前端上。

如下是某个题的答题界面

代码地址负载均衡在线oj系统

谢谢大家观看!

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

旅游文化管理平台

摘 要 乡村振兴战略典型村落——战旗村&#xff0c;依靠自身优秀的资源迅猛发展。新冠肺炎疫情的影响下&#xff0c;我国旅游业受到巨大冲击。2020年在短暂缓冲后国内旅游业呈现缓慢恢复的态势。新型冠状病毒爆发&#xff0c;第三产业尤其是旅游业发展逐渐走向低靡&#xff0c…

go语言对接S3存储的SDK(支持minio和OSS)

背景 在某个项目中&#xff0c;客户要求支持S3协议的存储&#xff0c;因为之前的项目是go来开发的支持的oss和minio 。 但并不一定支持S3的协议&#xff0c;而且使用了二种SDK&#xff0c;感觉比较麻烦。 既然客户提出来了要求。那我们改一下就是了。 操作 引入 go语言中有对…

AI时代下的自动化代码审计工具

代码审计工具分享 吉祥学安全知识星球&#x1f517;除了包含技术干货&#xff1a;Java代码审计、web安全、应急响应等&#xff0c;还包含了安全中常见的售前护网案例、售前方案、ppt等&#xff0c;同时也有面向学生的网络安全面试、护网面试等。 这两年一直都在提“安全左移”&…

DAY8-力扣刷题

1.全排列 给定一个不含重复数字的数组 nums &#xff0c;返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。 通过对之前习题的了解&#xff0c;一看到该题就想到了用回溯法 46. 全排列 - 力扣&#xff08;LeetCode&#xff09; 方法一&#xff1a;回溯法 class Solu…

如何编写基本的Java程序

安装Java Development Kit&#xff08;JDK&#xff09;是开发Java应用程序的第一步。 1.下载JDK Java程序必须运行在JVM上&#xff0c;我们第一件事情就是安装JDK。 1.下载地址&#xff1a;Java Downloads | Oraclehttps://www.oracle.com/java/technologies/downloads/#jdk…

10W+人都在看的年度技术精选、游戏行业安全、私域、AI实践指南报告整合,码住!

在网易工作了十多年&#xff0c;不说别的&#xff0c;小智在这里光学习就学习到很多干货&#xff0c;今天将这些干货内容统一分享给同仁&#xff01;真的是集齐精华&#xff0c;大家先点赞收藏关注&#x1f44d; 往年&#xff0c;基于网易数智在娱乐社交、游戏、泛零售、政务、…

二开的精美UI站长源码分享论坛网站源码 可切换皮肤界面

二开的精美UI站长源码分享论坛网站源码 可切换皮肤界面 二开的精美UI站长源码分享论坛网站源码 可切换皮肤界面

智慧物流:实现供应链的技术升级

智慧物流不仅是物流管理系统的智能化升级&#xff0c;更是以物联网、大数据分析等技术手段为基础的一种高效、智能的供应链解决方案。通过实时监控、数据分析和智能优化&#xff0c;智慧物流将传统物流的各个环节有效连接起来&#xff0c;实现信息流、资金流和物流的无缝对接。…

AMD模块的使用案例-基于普通htm

AMD模块的使用案例-基于普通html 实现过程结果 实现过程 AMD 是一种适用于浏览器环境的模块化规范&#xff0c;主要用于异步加载模块。RequireJS 是实现 AMD 的一个流行库。 ​ 使用案例&#xff1a; 文件目录&#xff1a; index.html&#xff0c;使用<script data-main&…

【每天学会一个渗透测试工具】AppScan安装及使用指南

&#x1f31d;博客主页&#xff1a;泥菩萨 &#x1f496;专栏&#xff1a;Linux探索之旅 | 网络安全的神秘世界 | 专接本 | 每天学会一个渗透测试工具 https://www.hcl-software.com/appscan AppScan是一种综合型漏洞扫描工具&#xff0c;采用SaaS解决方案&#xff0c;它将所以…

打造完美启动页:关键策略与设计技巧

启动页&#xff08;Splash Screen&#xff09;设计是指在应用程序启动时&#xff0c;首先展示给用户的界面设计。这个界面通常在应用加载或初始化期间显示&#xff0c;其主要目的是为用户提供一个视觉缓冲&#xff0c;展示品牌标识&#xff0c;并减少用户在等待过程中的焦虑感。…

【锐捷】VSU环境下部署VAC

配置要求 1.两台核心交换机部署VSU&#xff0c;Domain ID为1&#xff0c;S1的Switch ID为1&#xff0c;优先级为150&#xff0c;设备描述为VSU-S1&#xff1b;S2的Switch ID为2&#xff0c;优先级为120&#xff0c;设备描述为VSU-S2&#xff1b;两台设备的G0/48口用于BFD双机检…

【Golang - 90天从新手到大师】Day06 - 数组

系列文章合集 Golang - 90天从新手到大师 数组是golang中最常用的一种数据结构,数组就是同一类型数据的有序集合 定义一个数组 格式: var name [n]type n为数组长度,n>0 且无法修改,type为数组的元素类型如: var a [2]int上面的例子定义了一个长度为2,元素类型为int的数组…

PostgreSQL源码分析——缓冲区管理器

这里我们分析一下PG数据库缓冲区的代码。缓冲区是十分重要的&#xff0c;对数据库的性能和稳定性有着直接的影响。缓冲区是数据库SQL计算层与外部存储&#xff08;磁盘&#xff09;交互的关键。数据页的落盘与读取&#xff0c;都要经过缓冲区。 README src/backend/storage/…

解决企业微信内嵌H5页面导航栏返回上一级是空白页面问题

在项目中,产品要求返回上一级不能空白页,可以是工作台,所以要引入企业微信的返回按钮的用法,以下是详细步骤: 1.引入企业微信的版本内容 <script src"https://res.wx.qq.com/wwopen/js/jsapi/jweixin-1.0.0.js"></script> 在public底下的index.html底…

pdf只要前几页,pdf怎么只要前几页

在现代办公和学习环境中&#xff0c;PDF文件已成为我们日常处理信息的重要工具。然而&#xff0c;有时我们并不需要整个PDF文件的内容&#xff0c;而只是其中的几页。那么&#xff0c;如何高效地提取PDF文件中的特定页面呢&#xff1f;本文将为您介绍几种实用的方法。 打开 “ …

法大大亮相国家级期刊,助力数字政务有实“例”!

近日&#xff0c;在最新发布的国家级学术期刊《市场监督管理》中&#xff0c;法大大作为国内领先的电子签厂商亮相&#xff0c;这也是电子签行业的“第一次”。 截自《市场监督管理》2024年第12期 《市场监督管理》杂志于1953年创刊&#xff0c;是中国工商出版社主办的一本学术…

【Vue-Vben-Admin】1、初次运行和介绍

【Vue-Vben-Admin】1、初次运行和介绍 Vben-Admin 初次运行和介绍 小小的介绍规定版本文件树安装依赖运行项目 小小的介绍 一款 Vue3 Typescript4 Vite2 后台管理项目&#xff0c;功能挺多的&#xff0c;还有组件库 规定版本 此个人文档规定版本为 2.8.0&#xff0c;可能版本…

如何查看公网IP?

什么是公网IP&#xff1f; 公网IP&#xff08;Internet Protocol&#xff09;是指分配给互联网上的计算机设备的唯一标识符。公网IP地址是由互联网服务提供商&#xff08;ISP&#xff09;分配给用户设备&#xff0c;使其可以与全球范围内的其他设备进行通信。公网IP地址通常采…

【OpenVINO™】使用 OpenVINO™ C# 异步推理接口部署YOLOv8 ——在Intel IGPU 上速度依旧飞起!!

OpenVINO Runtime支持同步或异步模式下的推理。Async API的主要优点是&#xff0c;当设备忙于推理时&#xff0c;应用程序可以并行执行其他任务&#xff08;例如&#xff0c;填充输入或调度其他请求&#xff09;&#xff0c;而不是等待当前推理首先完成。 当我们使用异步API时&…