Java 课程设计:LWZ - Online Judge学生端(裁判机部分)
裁判机服务器
学生完成题目并提交之后,本项目还提供了裁判机进行判题,可以对学生的答案进行批改(主观题由老师在教师端批改),并且将批改结果汇总到存储介质中,裁判机的工作流程如下。
裁判机的存在是必要的,如果没有裁判机服务器而由客户端来判题,程序可能会面临安全问题。例如用户可以利用反编译的方式修改判题方式,使得无论填入什么答案都正确。或者客户端判题时会先获取答案,用户可以通过某种攻击手段导致答案甚至是其他数据泄露。因此学生答题和裁判机应当启用客户机——服务器模式,使用三层架构进行工作。
Socket 编程
数据封装
客户端发送的有效载荷为一个字符串,该字符串由操作码、用户名、班级名、题目集 id 和提交内容 5 个部分组成,5 个部分之间用 “ ” 连接。
服务器接收到数据之后,将数据按照分隔符 “ ” 进行分割。
//分割明文,执行对应的操作
String result_set[] = result_decode.split(" ");
通过操作码执行对应的操作,操作码和用户的请求的关系如下:
用户请求 | 数据库操作 |
---|---|
1 | 提交编程题 |
2 | 提交选择题 |
3 | 提交判断题 |
- 如果用户需要提交编程题,需要在提交内容部分附带上编程题的 id,同时由于代码中间可能存在大量 “ ”,因此对后续的分割字符串需要用 StringBuilder 重构。
客户端
客户端需要将数据按照正确的形式封装好数据,通过 IP 地址和端口号发送给服务器,并且等待并接收回显信息。此处客户端将封装某个用户在某个班级的某个题目集中的作答情况,发送后接收服务器返回的提交状态或批改结果。
import java.net.*;
import java.io.*;
/**
* 基于请求响应模型的客户端套接字,只有发送报文一个方法
* @author 林智凯
* @version 1.1
*/
public class JudgeClient
{
/**
* 这个方法用于向服务器发送请求
* @param actionCode 用户的操作行为码,String
* @param username 用户名(非加密),String
* @param classId 班级id,Integer
* @param collectionId 题目集id,Integer
* @param text 提交内容,String,选择题和判断题为逗号分隔的答案字符串,编程题为“题目id + 空格 + 代码”
* @return 该请求是否成功,Boolean
*/
public static String sendAnswer(String actionCode, String username, String classId, Integer collectionId, String text)
{
String serverName = "127.0.0.1";
int port = 12001;
try
{
@SuppressWarnings("resource")
Socket client = new Socket(serverName, port);
OutputStream outToServer = client.getOutputStream();
DataOutputStream out = new DataOutputStream(outToServer);
//明文写入操作码,跟着传入的参数
String plainText = actionCode + " " + username + " " + classId.toString() + " " + collectionId.toString() + " " + text;
//向套接字传输密文
out.writeUTF(plainText);
InputStream inFromServer = client.getInputStream();
//接收回显信息
DataInputStream in = new DataInputStream(inFromServer);
String resultDecode = new String(in.readUTF());
return resultDecode;
}catch(IOException e)
{
e.printStackTrace();
}
return null;
}
服务器
服务器则需要接收客户端发送的作答数据,进行批改后将数据正确提交到数据库中,并发送反馈信息给客户端。Server 类的主要方法如下表描述,和其他类的关系如 UML 类图描述。
方法 | 功能 |
---|---|
run() | 覆盖Thread的run()方法,运行服务器的套接字 |
judgProgram() | 编程题判题 |
judgChoice() | 选择题判题 |
judgJudgment() | 判断题判题 |
main() | 启动套接字,接收客户端的信息 |
核心代码如下:
Socket server = serverSocket.accept();
DataInputStream in = new DataInputStream(server.getInputStream());
//接收客户端传入的数据,并写入日志
String resultDecode = new String(in.readUTF());
//分割明文
String[] resultSet = resultDecode.split(" ");
//根据操作码,执行不同的操作
switch(resultSet[0]) {
//编程题
case "1":{
result = Server.judgProgram(resultDecode);
break;
}
//选择题
case "2":{
result = Server.judgChoice(resultDecode);
break;
}
//判断题
case "3":{
result = Server.judgJudgment(resultDecode);
break;
}
default :{
break;
}
}
//将响应结果发回客户端并写入日志
DataOutputStream out = new DataOutputStream(server.getOutputStream());
out.writeUTF(result);
server.close();
编程题判题
Judge 类
Judge 类负责判题编程题,通过调用其他类协同工作.
核心代码如下:
/**
* 该方法用于生成一个名为“main”的cpp文件,并且用测试点进行测试,返回作答情况
* @param String code:需要被写入cpp文件的C语言代码
* @param Integer num:题目编号
* @return String:返回判题的结果
*/
public static String judgeProgram(Integer num, String code) throws SQLException {
String result = "error";
//将代码写入cpp文件
if(Program.writeFile(code)) {
//实例化CommandDAOWindowsImpl对象
CommandDaoWindowsImpl command = new CommandDaoWindowsImpl();
//编译C语言代码
String compilerResult = command.compileProgram();
//编译成功
if("".equals(compilerResult)) {
//实例化PointRepositoryJDBCImpl对象
PointRepositoryDaoJdbcImpl pointRepository = new PointRepositoryDaoJdbcImpl();
//获取num题目的测试点
List<Testpoint> points = pointRepository.getTestpoint(num);
//只有输出的测试点
if(points.size() == 1 && points.get(0).getInput() == null) {
//答案正确
if(command.runOutput().equals(points.get(0).getOutput())) {
result = "答案正确";
}
else {
result = "答案错误";
}
}
//既有输入也有输出的多个测试点
else {
boolean flag = true;
for (int i = 0; i < points.size(); i++) {
//生成测试点文件
Program.writeTestPoint("text", points.get(i).getInput());
//获得测试点的输出
String textResult = command.runTestPoint();
//答案错误
if(!textResult.equals(points.get(i).getOutput())) {
result = "答案错误";
flag = false;
break;
}
}
//答案都正确
if(flag) {
result = "答案正确";
}
}
}
else { //编译错误
result = "编译错误";
}
}
else { //写入文件发送错误
result = "error";
}
return result;
}
CommandDAO 接口
CommandDAO 接口指定了 Java 执行 cmd 命令的行为,是进行编程题判题的核心所在。
CommandDaoWindowsImpl 类 CommandDAO 接口基于 Window 系统的实现类,核心代码如下。
/**
* 该方法用于将可执行文件用测试点测试
* @return String:程序用测试点测试后返回的结果
*/
public String runTestPoint(){
BufferedReader br = null;
StringBuilder outPutResult = new StringBuilder();
try{
ProcessBuilder pb = new ProcessBuilder().command("cmd.exe", "/c", CODE_PATH + "main.exe < " + CODE_PATH + "text.txt");
pb.redirectErrorStream(true);
Process process = pb.start();
process.waitFor();
InputStream inputStream = process.getInputStream();
br = new BufferedReader(new InputStreamReader(inputStream, "GBK"));
String line;
while ((line = br.readLine()) != null) {
outPutResult.append(line).append("\n");
}
br.close();
br = null;
} catch (Exception e) {
e.printStackTrace();
return null;
}
return outPutResult.toString();
}
代码查重
Repetition 类用于完成代码查重,基于 LCS 算法。LCS算法使用动态规划实现,设 X 和 Y 是两个字符串,i 和 j 分别是字符串 X 和 Y 的下标,则两个字符串的 LCS 的动态规划转换方程为:
通过最长公共子序列的长度和被查重代码的长度做除法运算即可得到相似度,核心代码如下:
/**
* 该方法用于计算2个字符串的最长公共子序列(LCS)的长度
* @param code1 被查重代码,String
* @param code2 代码样本,String
* @return Integer:2个字符创的LCS长度
*/
public static Integer getLcs(String code1, String code2) {
Integer n = code1.length();
Integer m = code2.length();
//初始化动态规划矩阵
int[][] dp = new int[n + 1][m + 1];
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= m; j++) {
dp[i][j] = 0;
}
}
//实现动态规划转换方程
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (code1.charAt(i - 1) == code2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else if(dp[i - 1][j] > dp[i][j - 1]){
dp[i][j] = dp[i - 1][j];
}
else {
dp[i][j] = dp[i][j - 1];
}
}
}
return dp[n][m];
}
日志
裁判机部署了 log4j 日志框架,用于将服务器运行的状态和异常进行记录,核心代码如下。
log.info("等待远程连接,端口号为:" + serverSocket.getLocalPort() + "...");
log.info("远程主机地址:" + server.getRemoteSocketAddress());
log.info("从" + server.getRemoteSocketAddress() + "接收" + resultDecode);
log.info("向" + server.getRemoteSocketAddress() + "发送" + out);
log.error(e);
日志框架的运行效果如下所示。
编码规范
使用阿里代码扫描工具修改错误,编码规范修改前的情况如下。
修改后裁判机服务器和用户服务器均没有编码规范问题。
样例一
没有覆盖的方法没有添加,更正前:
更正后:
样例二
变量名没有驼峰命名法,更正前:
改正后
样例三
变量可能为 null 导致错误,更正前:
更正后: