项目介绍
在线评测编程题目代码的系统,出题人预先设置题目的输入样例和输出样例,根据用户提交代码,进行编译代码,运行代码,判断代码执行结果是否正确。
后端服务
网关服务
接收前端请求,转发到对应的服务
用户服务
用户注册、用户登录、用户退出
题目服务
题目浏览,在线做题,题目提交、提交记录
判题服务
判题服务获取调用题目服务获取题目信息,测试用例,调用代码沙箱编译运行代码并对比结果
代码沙箱
编译运行代码,返回执行结果、运行时间、运行内存
公共模块
数据模型、统一异常处理、全局响应封装、工具类
judgeCase:判题用例,包括输入用例和输出用例
judgeInfo:判题信息,包括程序执行信息,运行时间,运行内存
judgeContext:判题上下文,包括题目信息,编程语言,输出用例,输出结果,判题信息
微服务架构优点
微服务架构是一种软件架构风格,把大而全的单体应用拆分为多个职责单一的模块,每个服务单元独立部署,运行和维护。服务单元之间通过网络通信进行交互,从而实现完整系统的功能。微服务架构可以提高系统的灵活性,可维护性,可扩展性,容错性。
本项目中,使用微服务架构将较重的服务(判题服务和代码沙箱)和核心服务(用户服务和题目服务)进行分离解耦,即使判题服务因为代码沙箱压力过大而阻塞,也不会影响核心业务运行。
用户服务
普通用户
注册、登录、获取登录用户、注销、修改个人信息
管理员
增删改 根据id获取用户(包装类) 分页获取用户(包装类)
题目服务
管理员
增删改、根据id获取题目、分页获取题目
普通用户
分页获取题目(包装类)、根据id获取题目(包装类)、分页获取题目提交列表、根据id获取提交题目详情
提交题目(限流器)
题目提交流程
向题目提交表记录题目id,用户id,编程语言和用户代码,设置判题状态为待判题。通过消息队列将questionSubmit对象传递给判题服务
限流器
// 1、 声明一个限流器
RRateLimiter rateLimiter = redissonClient.getRateLimiter(key);
// 2、 设置速率,5秒中产生3个令牌
rateLimiter.trySetRate(RateType.OVERALL, 3, 5, RateIntervalUnit.SECONDS);
// 3、试图获取一个令牌,获取到返回true
rateLimiter.tryAcquire(1)
RateType.OVERALL:所有实例共享令牌
RateType.PER_CLIENT�:单实例令牌数
redission分布式限流采用令牌桶思想和固定时间窗口,trySetRate方法设置桶的大小,利用redis key过期机制达到时间窗口目的,控制固定时间窗口内允许通过的请求量。
判题服务
判题模块作用
查询题目提交和题目信息,调用代码沙箱,把代码和输入用例交给代码沙箱去执行,收集输出结果并执行判题逻辑
判题流程
通过消息队列获得questionSubmit对象,获取题目信息,判断题目是否存在。存在提交数加1。获取用户代码,编程语言,输入用例,输出用例。更新判题状态为判题中,调用代码沙箱,获取程序运行结果,设置判题上下文对象,采用策略模式,调用不同判题方法进行判题(主要是比较不同语言的时间和内存是否超出限制以及输出用例和代码沙箱程序执行结果是否一致)设置判题状态(判题成功/判题失败)和通过数
多种代码沙箱
定义通用代码沙箱调用接口,提供多种代码沙箱的实现类
实例代码沙箱:跑通业务流程
远程代码沙箱:自主实现的代码编译、执行的沙箱接口
三方代码沙箱:开源的代码沙箱服务 judge0的代码沙箱
远程和三方代码沙箱通过http调用
沙箱类型配置化:application.yml中动态配置沙箱类型
沙箱实例工厂化:读取配置,适用工厂模式,创建对应的代码沙箱实现类
沙箱调用代理化:使用代理模式,在调用代码沙箱前后进行统一的日志操作
代码沙箱
实现方式
Java原生代码沙箱和Docker代码沙箱
实现思路
用户代码保存为文件
编译代码,得到class文件
String compileCmd = "javac -encoding utf-8 Main.java";
Process compileProcess = Runtime.getRuntime().exec(comoileCmd);
int exitCode = compileProcess.waitFor()
if(exitCode==0){
逐行读取输出;
}else{
逐行读取正常输出;
逐行读取异常输出;
}
执行java代码
String runCmd = "java Main.java"+inputArgs;
Process runProcess = Runtime.getRuntime().exec(runCmd);
收集整理输出结果
执行结果、执行时间、执行内存
文件清理,释放空间
错误处理,提升程序健壮性
异常情况
执行超时
占用内存
读文件
写文件
运行其他程序
执行高危操作
安全控制
超时控制
创建守护线程,超时自动中断Process
限制资源分配
java -Xmx256M
限制最大占用堆空间256M
限制代码–黑白名单
public static final List<String> balckList = Arrays.asList("Files","exec");
限制权限
Java安全管理器
public class DefaultSecurityManager extends SecurityManager{
public void checkPermission(Permission perm) {
throw new RuntimeException("权限异常"+perm.toString());
}
public void checkRead(String file) {
throw new RuntimeException("Read权限异常"+file);
}
public void checkWrite(String file) {
throw new RuntimeException("Write权限异常"+file);
}
public void checkExec(String file) {
throw new RuntimeException("Exec权限异常"+file);
}
public void checkConnect(String host,int port) {
throw new RuntimeException("Connect权限异常"+host+":"+port);
}
}
执行Java代码时
java -Dfile.encoding=UTF-8 -Djava.security.manager=DefaultSecurityManager -cp . Main
运行环境隔离
Java原生代码沙箱
通过Process.exec执行命令行操作来执行代码,并通过Process对象的流来获取输出结果,不够安全
Docker代码沙箱
创建隔离的Java容器并且通过exec命令在容器内执行Java代码和获取输出,更加安全。
用户代码保存为文件
编译为.class文件
拉取镜像、创建容器、运行容器,执行代码,得到输出结果
创建一个交互容器,创建容器时,指定文件路径,把本地文件同步到容器中
创建容器时,进行内存限制,网络限制,读写限制
设计模式
工厂模式
代理模式
策略模式
模版方法模式