通过在交题时抓包, 可以发现会发送一个POST请求包, 发给 /submit-problem-judge
api, 直接在源码中查找即可。
JudgeController 文件内是获取前端提交的代码以及是否远程提交的类型
/**
* @MethodName submitProblemJudge
* @Description 核心方法 判题就此开始
* @Return CommonResult
* @Since 2020/10/30
*/
@RequiresAuthentication
@RequiresPermissions("submit")
@RequestMapping(value = "/submit-problem-judge", method = RequestMethod.POST)
public CommonResult<Judge> submitProblemJudge(@RequestBody SubmitJudgeDTO judgeDto) {
return judgeService.submitProblemJudge(judgeDto);
}
从前端传进来的 json 文件:
{
"cid" : 0,
"code" : "#include ...",
"gid" : null,
"isRemote" : true,
"language" : "C++",
"pid" : "HDU-1002",
"tid" : null
}
最后api返回的 json 数据包:
{
"data" : {
"cid" : 0,
"code" : "#include ...",
"cpid" : 0,
"displayPid" : "HDU-1002",
"errorMessage" : null,
"gid" : null,
"gmtCreate" : "2023-06-02T14:42:45.557+0000",
"gmtModified" : "2023-06-02T14:42:45.557+0000",
"ip" : "218.28.152.180",
"isManual" : null,
"judger" : null,
"language" : "C++",
"length" : 1022,
"memory" : null,
"oiRankScore" : null,
"pid" : 1005,
"score" : null,
"share" : false,
"status" : 5,
"submitId" : 56,
"submitTime" : "2023-06-02T14:42:45.556+0000",
"time" : null,
"uid" : "0d20b3693c1a422ba6784e4964309b43",
"username" : "Aze",
"version" : 0,
"vjudgePassword" : null,
"vjudgeSubmitId" : null,
"vjudgeUsername" : null
},
"msg" : "success",
"status" : 200
}
这是调用的 judgeService.submitProblemJudge(judgeDto)
:
在 JudgeServicempl.java 中, 其中 JudgeServicempl 继承自 抽象类 JudgeService
@Override
public CommonResult<Judge> submitProblemJudge(SubmitJudgeDTO judgeDto) {
try {
return CommonResult.successResponse(judgeManager.submitProblemJudge(judgeDto));
} catch (StatusForbiddenException | AccessException e) {
return CommonResult.errorResponse(e.getMessage(), ResultStatus.FORBIDDEN);
} catch (StatusNotFoundException e) {
return CommonResult.errorResponse(e.getMessage(), ResultStatus.NOT_FOUND);
} catch (StatusAccessDeniedException e) {
return CommonResult.errorResponse(e.getMessage(), ResultStatus.ACCESS_DENIED);
} catch (StatusFailException e) {
return CommonResult.errorResponse(e.getMessage());
}
}
而该函数时封装后用来判断提交情况的, 实际执行还得再剥一层。
而这位更是重量级, JudgeManager.submitProblemJudge
就是实现代码, 咱们来慢慢分析一下:
/**
* @MethodName submitProblemJudge
* @Description 核心方法 判题通过openfeign调用判题系统服务
* @Since 2020/10/30
*/
public Judge submitProblemJudge(SubmitJudgeDTO judgeDto) throws StatusForbiddenException, StatusFailException, StatusNotFoundException, StatusAccessDeniedException, AccessException {
judgeValidator.validateSubmissionInfo(judgeDto);
// 需要获取一下该token对应用户的数据
AccountProfile userRolesVo = (AccountProfile) SecurityUtils.getSubject().getPrincipal();
boolean isContestSubmission = judgeDto.getCid() != null && judgeDto.getCid() != 0;
boolean isTrainingSubmission = judgeDto.getTid() != null && judgeDto.getTid() != 0;
SwitchConfig switchConfig = nacosSwitchConfig.getSwitchConfig();
if (!isContestSubmission && switchConfig.getDefaultSubmitInterval() > 0) { // 非比赛提交有限制限制
String lockKey = Constants.Account.SUBMIT_NON_CONTEST_LOCK.getCode() + userRolesVo.getUid();
long count = redisUtils.incr(lockKey, 1);
if (count > 1) {
throw new StatusForbiddenException("对不起,您的提交频率过快,请稍后再尝试!");
}
redisUtils.expire(lockKey, switchConfig.getDefaultSubmitInterval());
}
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
// 将提交先写入数据库,准备调用判题服务器
Judge judge = new Judge();
judge.setShare(false) // 默认设置代码为单独自己可见
.setCode(judgeDto.getCode())
.setCid(judgeDto.getCid())
.setGid(judgeDto.getGid())
.setLanguage(judgeDto.getLanguage())
.setLength(judgeDto.getCode().length())
.setUid(userRolesVo.getUid())
.setUsername(userRolesVo.getUsername())
.setStatus(Constants.Judge.STATUS_PENDING.getStatus()) // 开始进入判题队列
.setSubmitTime(new Date())
.setVersion(0)
.setIp(IpUtils.getUserIpAddr(request));
// 如果比赛id不等于0,则说明为比赛提交
if (isContestSubmission) {
beforeDispatchInitManager.initContestSubmission(judgeDto.getCid(), judgeDto.getPid(), userRolesVo, judge);
} else if (isTrainingSubmission) {
beforeDispatchInitManager.initTrainingSubmission(judgeDto.getTid(), judgeDto.getPid(), userRolesVo, judge);
} else { // 如果不是比赛提交和训练提交
beforeDispatchInitManager.initCommonSubmission(judgeDto.getPid(), judgeDto.getGid(), judge);
}
// 将提交加入任务队列
if (judgeDto.getIsRemote()) { // 如果是远程oj判题
remoteJudgeDispatcher.sendTask(judge.getSubmitId(),
judge.getPid(),
judge.getDisplayPid(),
isContestSubmission,
false);
} else {
judgeDispatcher.sendTask(judge.getSubmitId(),
judge.getPid(),
isContestSubmission);
}
return judge;
}
judge.java
是一个提交状态类, 也是之前的 api 返回结果 json。注意到从前端获取的 json 包只有代码和题号有用, 接下来应该是通过题号判断是否为远程评测。
而 judge.setshare()
只是获取用户信息, 接下来要看 beforeDispatchInitManager
类的东西, 会在里面更新 judge
对象的题目信息。直接看默认提交也就是 initCommonSubmission
的内容:
public void initCommonSubmission(String problemId, Long gid, Judge judge) throws StatusForbiddenException {
AccountProfile userRolesVo = (AccountProfile) SecurityUtils.getSubject().getPrincipal();
QueryWrapper<Problem> problemQueryWrapper = new QueryWrapper<>();
problemQueryWrapper.select("id", "problem_id", "auth", "is_group", "gid");
problemQueryWrapper.eq("problem_id", problemId);
Problem problem = problemEntityService.getOne(problemQueryWrapper, false);
if (problem == null){
throw new StatusForbiddenException("错误!当前题目已不存在,不可提交!");
}
if (problem.getAuth() == 2) {
throw new StatusForbiddenException("错误!当前题目不可提交!");
}
boolean isRoot = SecurityUtils.getSubject().hasRole("root");
if (problem.getIsGroup()) {
if (gid == null){
throw new StatusForbiddenException("提交失败,该题目为团队所属,请你前往指定团队内提交!");
}
if (!isRoot && !groupValidator.isGroupMember(userRolesVo.getUid(), problem.getGid())) {
throw new StatusForbiddenException("对不起,您并非该题目所属的团队内成员,无权进行提交!");
}
judge.setGid(problem.getGid());
}
judge.setCpid(0L)
.setPid(problem.getId())
.setDisplayPid(problem.getProblemId());
// 将新提交数据插入数据库
judgeEntityService.save(judge);
trainingManager.checkAndSyncTrainingRecord(problem.getId(), judge.getSubmitId(), judge.getUid());
}
映入眼帘的有一个 problemQueryWrapper
, 哥们直接猜测是与数据库进行交互, 虽然不知道干嘛的, 应该 .select
是查询返回字段, eq
大概是要查的 key
。而接受是用 Problem
类, 简单看一下应该是直接对应数据库里的problem表
列名 | 实体属性类型 | 键 | 备注 |
---|---|---|---|
id | long | primary key | auto_increment 1000开始 |
judge_mode | String | 默认为default、其他值有spj、interactive | |
problem_id | String | 题目展示id | |
title | String | 题目标题 | |
author | String | 默认可为无 | |
type | int | 题目类型 0为ACM,1为OI | |
time_limit | int | 时间限制(ms),默认为c/c++限制,其它语言为2倍 | |
memory_limit | int | 空间限制(mb),默认为c/c++限制,其它语言为2倍 | |
stack_limit | int | 栈限制(mb),默认为128 | |
description | String | 内容描述 | |
input | String | 输入描述 | |
output | String | 输出描述 | |
examples | Srting | 题面输入输出样例,不纳入评测数据 | |
source | int | 题目来源(比赛id),默认为hoj,可能为爬虫vj | |
difficulty | int | 题目难度,0简单,1中等,2困难 | |
hint | String | 备注 提醒 | |
auth | int | 默认为1公开,2为私有,3为比赛中。 | |
io_score | int | 当该题目为io题目时的分数 默认为100 | |
code_share | boolean | 该题目对应的相关提交代码,用户是否可用分享 | |
spj_code | String | 特判或交互程序代码 | |
spj_language | String | 特判或交互程序的语言 | |
user_extra_file | String | 选手程序的额外文件 json key:文件名 value:文件内容 | |
judge_extra_file | String | 特判或交互程序的额外文件 json key:文件名 value:文件内容 | |
is_remove_end_blank | boolean | 是否默认去除用户代码的文末空格 | |
open_case_result | boolean | 是否默认开启该题目的测试样例结果查看 | |
caseVersion | String | 题目测试数据的版本号 | |
is_upload_case | boolean | 是否是上传zip评测数据的 | |
modified_user | String | 最新修改题目的用户 | |
gmt_create | datetime | 创建时间 | |
gmt_modified | datetime | 修改时间 |
这里还只是获取信息阶段, 我们如果想导入新的远程提交网站应该得在提交远程题目那去看看。不过先继续看判题流程吧。
查询完之后给对应信息传入 judge
对象, 并将该对象同样记录到数据库中。
回到 beforeDispathInitManager
, 简单查看一下其他的 init 方法, 可以发现都是类似的。
接下来是重头戏:
// 将提交加入任务队列
if (judgeDto.getIsRemote()) { // 如果是远程oj判题
remoteJudgeDispatcher.sendTask(judge.getSubmitId(),
judge.getPid(),
judge.getDisplayPid(),
isContestSubmission,
false);
} else {...}
表明我们下一步的检索对象是 remoteJudgeDispatcher
。
remoteJudgeDispatcher 远程评测类
里面只有一个 sendTask
方法:
public void sendTask(Long judgeId, Long pid, String remoteJudgeProblem, Boolean isContest, Boolean isHasSubmitIdRemoteReJudge) {
JSONObject task = new JSONObject();
task.set("judgeId", judgeId);
task.set("remoteJudgeProblem", remoteJudgeProblem);
task.set("token", judgeToken);
task.set("isContest", isContest);
task.set("isHasSubmitIdRemoteReJudge", isHasSubmitIdRemoteReJudge);
try {
boolean isOk;
if (isContest) {
isOk = redisUtils.llPush(Constants.Queue.CONTEST_REMOTE_JUDGE_WAITING_HANDLE.getName(), JSONUtil.toJsonStr(task));
} else {
isOk = redisUtils.llPush(Constants.Queue.GENERAL_REMOTE_JUDGE_WAITING_HANDLE.getName(), JSONUtil.toJsonStr(task));
}
if (!isOk) {
judgeEntityService.updateById(new Judge()
.setSubmitId(judgeId)
.setStatus(Constants.Judge.STATUS_SUBMITTED_FAILED.getStatus())
.setErrorMessage("Call Redis to push task error. Please try to submit again!")
);
}
remoteJudgeReceiver.processWaitingTask();
} catch (Exception e) {
log.error("调用redis将判题纳入判题等待队列异常,此次判题任务判为系统错误--------------->", e);
judgeEntityService.failToUseRedisPublishJudge(judgeId, pid, isContest);
}
}
看起来是定义 JSONObject task
来传入任务到 redis 缓存中依次执行。而 isOK
是判断是否成功装入 redis 中, 如果 isOK = false
就执行 .updateById
操作, 这里咱先不看, 如果成果的话就会转入 remoteJudgeReceiver
类, 进行等待远程 OJ的返回。
但有点问题, 传入只有在 HOJ 上的提交ID, 题号如HDU-1100, 和HOJ展示题号,以及是否为比赛提交这些参数, 如何构造出一个远程提交的封包且检测实时结果呢?不知道是不是封装进 装入redis 的操作中了, 先看 remoteJudgeReceiver
类吧。
@Async("judgeTaskAsyncPool")
public void processWaitingTask() {
// 优先处理比赛的提交
// 其次处理普通提交的提交
handleWaitingTask(Constants.Queue.CONTEST_REMOTE_JUDGE_WAITING_HANDLE.getName(),
Constants.Queue.GENERAL_REMOTE_JUDGE_WAITING_HANDLE.getName());
}
不懂 redis, 什么玩意这是(
但也能看懂一些, 这部分是循环对队列中的评测任务进行处理, 其中 handleJudgeMsg
是主要的处理方法。
@Override
public void handleJudgeMsg(String taskStr, String queueName) {
JSONObject task = JSONUtil.parseObj(taskStr);
String token = task.getStr("token");
String remoteJudgeProblem = task.getStr("remoteJudgeProblem");
Boolean isHasSubmitIdRemoteReJudge = task.getBool("isHasSubmitIdRemoteReJudge");
String remoteOJName = remoteJudgeProblem.split("-")[0].toUpperCase();
Long judgeId = task.getLong("judgeId");
Judge judge = judgeEntityService.getById(judgeId);
if (judge != null) {
if (Objects.equals(judge.getStatus(), Constants.Judge.STATUS_CANCELLED.getStatus())) {
if (judge.getCid() != 0) {
UpdateWrapper<ContestRecord> updateWrapper = new UpdateWrapper<>();
// 取消评测,不罚时也不算得分
updateWrapper.set("status", Constants.Contest.RECORD_NOT_AC_NOT_PENALTY.getCode());
updateWrapper.eq("submit_id", judge.getSubmitId()); // submit_id一定只有一个
contestRecordEntityService.update(updateWrapper);
}
} else {
dispatchRemoteJudge(judge,
token,
remoteJudgeProblem,
isHasSubmitIdRemoteReJudge,
remoteOJName);
}
}
}
显然是将 task
拆包, 然后判断是否提交失败, 如果失败就更新数据库。
如果getbyid
能从数据库中获取到 评测对象 judge
, 说明已经在评测中,然后判断是否被取消评测, 是则更新数据库, 不是则正常提交, 转入 dispatchRemoteJudge
方法处理。
private void dispatchRemoteJudge(Judge judge, String token, String remoteJudgeProblem,
Boolean isHasSubmitIdRemoteReJudge, String remoteOJName) {
ToJudgeDTO toJudgeDTO = new ToJudgeDTO();
toJudgeDTO.setJudge(judge)
.setToken(token)
.setRemoteJudgeProblem(remoteJudgeProblem);
Constants.RemoteOJ remoteOJ = Constants.RemoteOJ.getRemoteOJ(remoteOJName);
if (!checkExistedAccountByOJ(remoteOJ)) {
judge.setStatus(Constants.Judge.STATUS_SYSTEM_ERROR.getStatus());
judge.setErrorMessage("System Error! Cause: The System does not have [" + remoteOJ + "] account configured. " +
"Please report the matter to the administrator!");
judgeEntityService.updateById(judge);
} else {
if (remoteOJName.equals(Constants.RemoteOJ.CODEFORCES.getName())
|| remoteOJName.equals(Constants.RemoteOJ.GYM.getName())) {
if (ChooseUtils.openCodeforcesFixServer) {
fixServerCFJudge(isHasSubmitIdRemoteReJudge, toJudgeDTO, judge);
} else {
commonJudge(Constants.RemoteOJ.CODEFORCES.getName(), isHasSubmitIdRemoteReJudge, toJudgeDTO, judge);
}
} else if (remoteOJName.equals(Constants.RemoteOJ.POJ.getName())) {
pojJudge(isHasSubmitIdRemoteReJudge, toJudgeDTO, judge);
} else {
commonJudge(remoteOJName, isHasSubmitIdRemoteReJudge, toJudgeDTO, judge);
}
}
// 如果队列中还有任务,则继续处理
processWaitingTask();
}
定义了一个 ToJudgeDTO
类, 去看看是什么成分, 也是评测的东西, 一个抽象类, 只存了有关远程评测账号啥的。
接着用 remoteOJ
存储对应的远程OJ名字, 看来如果想加OJ得在这修改一下 HOJtodo
public enum RemoteOJ {
HDU("HDU"),
CODEFORCES("CF"),
GYM("GYM"),
POJ("POJ"),
SPOJ("SPOJ"),
ATCODER("AC");
private final String name;
private RemoteOJ(String name) {
this.name = name;
}
public String getName() {
return name;
}
public static Boolean isRemoteOJ(String name) {
for (RemoteOJ remoteOJ : RemoteOJ.values()) {
if (remoteOJ.getName().equals(name)) {
return true;
}
}
return false;
}
public static RemoteOJ getRemoteOJ(String name) {
for (RemoteOJ remoteOJ : RemoteOJ.values()) {
if (remoteOJ.getName().equals(name)) {
return remoteOJ;
}
}
return null;
}
}
接下来是判断找到的 OJ 是否存在对应账号 checkExistedAccountByOJ
, 里面用 SwitchConfig
访问在后台管理中 “系统开关” 配置的账号密码, 这里估计得修改一下前端。 HOJtodo
如果存在就继续判断是哪个OJ, 如果是CF和POJ要有特殊处理。
对于CF, 会判断 ChooseUtils.openCodefrocesFixServer
, 但这里始终都是 False, 应该是用于调试的。
接下来看 commonJudge
。 困了, 睡觉去。