bot执行的实现
目前我只实现了java代码的执行,其他的应该类似。
我自己也没接触过像leetcode这些在线编译器等业内的成熟的解决方案,这里只是贴出我的一个方案,仅供参考。
大体思路是:
每一个bot的执行对应一个容器的启动,在游戏对局结束以后,删除容器以及游戏过程中产生的文件,容器的启动只会在游戏刚开始时,紧接着会将用户代码进行封装,编译成字节码文件,再传给docker容器内部,之后只需每回合向容器内部传入对局信息input即可,我的想法是尽量避免磁盘IO的,能用内存传递数据尽量用内存,要不然直接通过文件来达到读取文件,执行代码相对容易点但是同时速度可能会慢点。
这个过程中有很多问题
1.如何封装用户代码?
我只实现了java,然后要求类名必须是class Main
,且必须包含方法:public int nextStep(String input)
,这样一来,封装代码就是简简单单的字符串拼接问题了,值得注意的是,拼接过程中还要注意用户导入的包信息需要注意,以及实际拼接过程中换行其实没那么重要,只要用户每一行代码写好分号即可。
2.启动容器后如何让容器不退出
因为我们要使用的是java自带的exec函数,这个函数会自己启动一个终端来执行指令,但是执行完了,终端就没了,启动的容器就没了,所以我们启动的时候需要加上额外参数-idt,具体是:
sudo docker run -dit botrun:v1 /bin/bash
这里参数d指后台运行,后面跟了/bin/bash,也就是后台运行这个终端,那这样docker就不会退出了。这里的botrun:v1镜像是我自己写的一个镜像,很简单,只装了一个jdk8.
3.封装用户代码之后,copy到容器内部,将其编译为字节码文件,那么执行时如何传入input?
这里是一个困扰我很久的问题之一,执行字节码文件很简单,只需java 文件
即可,但是如何传入参数呢?一个想法是将用户代码封装到一个Bot类中,这个类中调用用户编写的nextStep方法,然后通过Scanner输入即可,例如我是这样拼接的:
UUID uuid = UUID.randomUUID();
String uid = uuid.toString().substring(0,4);
String name = "Bot"+uid+".java"; //定义类文件名字
String head = "public class "+name.substring(0,7)+"{"+
" public static void main(String[] args) {" +
" Scanner in = new Scanner(System.in);" +
" Main m = new Main();"+
" String input = in.next();" +
" System.out.println(m.nextStep(input));" +
" }";
String tail = "}";
int k = bot.getBotCode().indexOf("class Main");
//获取用户代码的导入的包信息
String codeImport = bot.getBotCode().substring(0,k);
//获取用户的源代码
String codeSource = "static "+bot.getBotCode().substring(k);
//最终的代码
String code = "import java.util.Scanner;"+codeImport+head+codeSource+tail;
我们是在容器里面执行这个字节码文件,所以也需要在这个容器里面的终端去进行输入,但是java怎么实现呢?java的exec函数他只会启动一个终端,貌似是可以在这个终端里面进行输入的,但是我没有深究,大家可以尝试,有个方法可以返回一个输出流。
我是这样解决的,在传入字节码文件到容器时同时传入一个shell脚本,脚本里面填入内容:
echo $1 | java 字节码文件
,这里$1是linux中shell脚本参数站位符,那么执行代码只需要
bash 脚本文件.sh 传入的数据
即可
这里贴上我写的两个函数,分别是创建文件,传文件到容器内:
private String firstExcute(){ //一开始执行时,先创建字节码文件,返回类文件名字,创建运行脚本,脚本和类文件名相同
UUID uuid = UUID.randomUUID();
String uid = uuid.toString().substring(0,4);
String name = "Bot"+uid+".java"; //定义类文件名字
String head = "public class "+name.substring(0,7)+"{"+
" public static void main(String[] args) {" +
" Scanner in = new Scanner(System.in);" +
" Main m = new Main();"+
" String input = in.next();" +
" System.out.println(m.nextStep(input));" +
" }";
String tail = "}"; //最后一行
int k = bot.getBotCode().indexOf("class Main");
String codeImport = bot.getBotCode().substring(0,k); //获取用户代码的导入的包信息
String codeSource = "static "+bot.getBotCode().substring(k); //获取用户的源代码
String code = "import java.util.Scanner;"+codeImport+head+codeSource+tail; //最终的代码
String shell = "echo $1 | java "+name.substring(0,7);
String sourcePath = "/home/lighthouse/docker/docker_botrunning/codes/";
try {
OutputStream buildFile = new FileOutputStream(sourcePath+name); //创建类文件
buildFile.write(code.getBytes(StandardCharsets.UTF_8));
buildFile.close();
OutputStream buildShell = new FileOutputStream(sourcePath+name.substring(0,7)+".sh"); //创建脚本
buildShell.write(shell.getBytes(StandardCharsets.UTF_8));
buildShell.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return name;
}
private void copyCode(String id,String name){ //复制文件到容器内部,并编译
String[] cmd1 = new String[]{"sudo","docker","cp",name.substring(0,7)+".java",id+":/usr/src/myapp"};
String[] cmd2 = new String[]{"sudo","docker","cp",name.substring(0,7)+".sh",id+":/usr/src/myapp"};
String[] cmd3 = new String[]{"sudo","docker","exec",id,"javac",name}; //编译
try {
Runtime.getRuntime().exec(cmd1,null,new File("/home/lighthouse/docker/docker_botrunning/codes"));
Runtime.getRuntime().exec(cmd2,null,new File("/home/lighthouse/docker/docker_botrunning/codes"));
Process p = Runtime.getRuntime().exec(cmd3,null,new File("/home/lighthouse/docker/docker_botrunning/codes"));
p.waitFor();
// System.out.println("复制文件到容器");
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
4.传入input,获取方向
接上面所说,我们只需要执行那个脚本然后传入参数即可。但是,tmd蛋疼的地方来了,我一开始美美的觉得我只需要写如下代码即可:
String[] cmd = new String[]{"sudo","docker","exec","-it","容器ID","bash","脚本文件",input};
Runtime.getRuntime().exec(cmd);
但是这里是不行的,分析了下原因可能是因为它只会默认把这个bash命令当作是在docker里面的执行的命令,但是后面的部分认为是当前终端的内容,网上查了下解决方法,就是从bash之后加上双引号,作为一个整体供docker使用。
但是,最后还是没成功,报错是什么什么”not TTY”来着,字面意思就是输入设备找不到(*),这里我就懒得写出的我的寻找解决方案过程了,我最后是又写了个shell脚本,脚本内容是:
sudo docker exec -i $1 /bin/bash -c "bash $2 $3"
那么这里一共有三个参数位置,我们只需传入$1容器ID,$2容器内的脚本名字,$3input字符串
完成时已经可以正常运行代码了,然后我回头来看看代码时,发现个问题,之所以一开始用下面这代码(*处):
String[] cmd = new String[]{"sudo","docker","exec","-it","容器ID","bash","脚本文件",input};
Runtime.getRuntime().exec(cmd);
这个没法执行的原因可能是因为我加了参数“t”,t的作用是在容器内启动一个伪终端,是不是这就导致了报那个啥“TTY”的错误?或许是这样的,但是我也没有尝试了,因为我觉得通过这个botrun脚本运行还挺方便的
5.销毁相关资源
这里就是写了个函数,用于销毁资源,删除本机文件就是使用java自带的File类完成,关闭容器也只是一条命令的事
private void end(Player player){ //关闭容器,删除本机相关文件
String id = player.getContainerId();
String name = player.getJavaName();
File file1 = new File("/home/lighthouse/docker/docker_botrunning/codes/"+name);
File file2 = new File("/home/lighthouse/docker/docker_botrunning/codes/"+name.substring(0,7)+".sh");
if(file1.exists()) file1.delete();
if(file2.exists()) file2.delete(); //删除本机文件
String[] cmd2 = new String[]{"sudo","docker","stop",id}; //停止容器
try {
Process p2 = Runtime.getRuntime().exec(cmd2);
p2.waitFor();
this.join(2000);
System.out.println("end执行");
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
PlayerPool.players.remove(player); //将这个player移除
}
讨论群里面也有人说了这样经常启动,关闭容器会不会很耗资源,确实,这点我是考虑到的,但是也懒得管了,有同学也说可以将所有bot集中在一个容器里面执行,但是这样我觉得是不方便控制bot执行的资源控制的,毕竟若需要控制资源例如内存,我们只需在启动时加上参数 -memory 200M
即可,还有就是集中在一个容器内部,那么就有可能有危险了,例如用户提交了一个代码:
String cmd = "sudo rm -f 文件名"
就有可能删除其他文件,从而对其他bot的执行带来影响
6.其他
在botrunningsystem系统中,写过一个类叫做Bot,为了能够根据传来的Bot找到对应的容器,需要保存额外的容器ID信息,所以我新加了一个类叫Player,里面内容如下:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Player {
Integer userId;
String containerId; //容器ID
String javaName; //类文件名
}
然后再创建一个Player池:
public static final Map<Integer,Player> players = new HashMap<>();
用于快速根据userId找到player实例。
另外为了区分对局是刚开始还是进行中还是结束了,我对bot新加了一个成员变量:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Bot {
Integer userId;
String botCode;
String input;
Integer status;
}
后续根据这个status来判断即可,当然,backend服务内的相关代码也需要做更改
@Override
public void run() {
Integer direction = 0;
if(bot.getStatus() == 0){ //刚刚开始
String name = firstExcute(); //一开始执行时,先创建类文件,返回类文件名字
String id = startContainer(name); //启动容器,最后返回容器ID
copyCode(id,name); //复制到容器里面
Player player = new Player(bot.getUserId(),id,name);
PlayerPool.players.put(bot.getUserId(),player);
direction = getDirection(player); //获取下一步
} else if(bot.getStatus() == 1){
Player player = PlayerPool.players.get(bot.getUserId());
direction = getDirection(player); //获取下一步
} else if(bot.getStatus() == 2){ //结束了
Player player = PlayerPool.players.get(bot.getUserId());
end(player); //关闭容器,删除本机java和字节码文件
}
if(bot.getStatus() != 2){
MultiValueMap<String,String> data = new LinkedMultiValueMap<>();
data.add("user_id",bot.getUserId().toString());
data.add("direction",direction.toString());
restTemplate.postForObject(receiveBotMoveUrl,data,String.class);
}
}
😍
😍😍😍
😘😘😘😘😘😘😘😘😘
佬可以分享下这个botrun:v1镜像吗,我不会做镜像😭😭
docker stop换成docker kill会不会有帮助呢?后者关闭容器速度更快些。
https://blog.csdn.net/succing/article/details/122393565
nice!我测试了下kill确实有点慢
ok~
佬贴个效果截图就更好了~
效果一样的呀,就和课上使用joor反射类库实现是一样的效果,要说差别,还是有点,就是在开局时会延迟1s多,可能那个时间还在编译文件吧
好的好的~ qwq
同一个容器可以随机文件名,bot代码执行完删除,但同时也会有大量的开文件删文件操作.
主要是恶意代码也有可能包括了删除全部文件的指令