写在前面

今天算是阳康了,没精力整那个思维导图,凑合一下水水。

另外这篇文章也是网上抄的。

这次是实现贪吃蛇的自动化操作。具体原理就是把用户前端输入的代码存储到后端,经过一系列的通信走到一个微服务里,该微服务的功能就是编译并执行这段代码然后将值返回。再经过一系列的通信返回到前端。

有一大堆在配置和项目重构的准备也懒得搞了。

目前只能使用Java代码。

对于 Bot 的服务,整体处理在 BotRunningSystem 中。在 BotRunningSystem 中有一个单独的线程 Botpool,这个线程会不断的执行代码,每次执行一次代码就去把执行的结果 (移动的过程) 返回给处理移动的 nextstep,如果在等待时间之内 nextstep 函数没有能够接收到处理的指令,那么游戏就结束,如果两名 Bot 的下一步操作都有获取到,那么就进入到 Judge 来判断合法性,直到结束
在这里面用到了一个依赖,依赖为 joor 用它来接收一段代码,将代码插入到队列当中,每次从队列里取出代码运行,运行结束后将代码返还给用户

消费者线程

消费者线程中需要处理 Bot 的代码,因此要求有任务立即执行,这样的做法以保证用户体验,要想实现这样的效果,会用到 conditionvirable
创建的 Bootpool 需要继承自 Thread 里面需要一个 run 函数,在这个 run 函数里,首先需要的是定义一些锁在这个循环与其他的不同的地方在于。如果队列为空将其堵塞,如果有消息则需要唤醒
在 BotPool 中定义一个队列用来存储信息。由于队列里存的都是 Bot,因此需要存一个辅助的类为 Bot

1.1Bot 类

依据存什么用什么的原则,在 Bot 类里需要存下 userId、bot 的代码、bot 的当前的局面

1.1.1 关于 BootPool 中的 run 里面的线程

首先先来解读一下原理:具体来说,这是一个死循环,每次操作队列,队列会在两个线程里面操作,一个是生产者不断的加任务,一个是消费者不断的消耗任务。两个线程会出现读写冲突,因此需要加锁,当当前队列为空的时候就需要阻塞,
阻塞线程用 condtion 的 await 的 api,唤醒线程用 signnal 或者 signnall
当队列不空的时候,则需要取出队头,如果队列为空则需要阻塞。当队列不空并且解锁的话就需要去消费下任务。

消费一下队列需要用到 consumer 函数

关于 consumer 函数
对于这个函数的调用一定要在 Lock 锁的后面,因为这个函数比较耗时,可能会执行几秒钟。其中的整个编译过程是很慢的,因此在执行这个代码之前的操作一定是先解锁,如果不解锁未来往队列里加代码的操作时会被阻塞掉,但是没有必要阻塞掉,只有涉及到读写冲突的时候才需要堵塞。一旦取完队头,进程与队列就没有关系了,这样就不会产生读写冲突,因此锁在执行之前一定要提前释放。

1.1.2 定义函数 addBot 以实现在队列里插入一个 bot

这个函数主要是向队列添加一个新的 bot
这个函数会在 BotRunningServiceimpl 中调用
在 impl 中调用的时候,需要单开一个线程,因此需要开一个静态变量把所有线程都存下来

1.2 实现线程的启动

线程的启动仿照之前的匹配系统 matchingSystem 对于启动的入口中调用匹配池的 botPool 函数启动线程。

综上所述:消息队列就是这么实现的,首先有一个地方可以不断的往里加任务,加任务的时候会有锁,获取到锁则增加任务,如果锁被另外线程拿住的话,会阻塞在 lock.lock() 中,当获取完这个锁之后,会将 Bot 加到队列里面,在加完之后一定要唤醒另一个线程

再说明 Signal: 唤醒任意线程 singnalll 唤醒所有的线程。

实现消费者函数

声明:当前实现的仅仅是 Java 代码的编译,如果想能够编译其他的代码,前往搜索引擎搜索:Java 如何执行 docker 代码
在一开始已经说过,实现编译用到的是 Joor 以实现动态的编译。
为了能够保证整个过程的时间可控,把它放进线程中,以支持当超时的时候,断掉运行程序。

2.1 首先先定义一个进程

定义一个类为 Consumer,Consumer 类继承自 Thread
在类里面重载一个 run 函数
为了能够执行进程操作,定义函数为 startTimeOut()
在这个函数中,重要的是控制线程的执行时间,因为超时会断掉运行程序。

this.join(tiemout)
解读整个流程:线程启动,去开一个新的线程执行 run 函数,当前线程执行 join。当前线程在 join 函数下等待 tiemout/ 新的线程执行完毕,之后再去执行后面的线程操作。如果在最多 timeout 的时间间隔里还没有执行结束,则中断掉。
中断的 API 为 this.interrupt()
为何不能选择 sleep:sleep 函数具有限制性,尽管时间结束,但是依旧要跑完时间,join 操作是在时间结束的时候立刻去执行下一步,相比之下 join 执行更具有立即性。

2.2 在 run 函数里编译代码

实现编译 bot 的代码,需要定义辅助接口 BotInterface,来用来实现前端用户编写 AI 的接口,在这里面定义接口函数 nextMove(),用来获取下一步的方向。

在 run 函数调用这个辅助接口,以实现编译。

BotInterface botInterface =Reflect
其中这个 Reflect 是添加的依赖,可以动态的实现编译代码。

2.2.1 模拟前端传入编译

模拟整个从前端提取代码然后把 Bot 代码编译的过程。
定义 Bot,继承 BotInterface
这样在 Reflect 对象里使用的名称为 com.kob.newbotrunningsystem.utils.bot,代码就是 Bot 的代码
对于编译有一个问题,就是说,同名的类仅仅会编译一次,由于每个用户的代码都不一样
解决这个问题需要用到随机字符串为 UUID uuid =UUID.random(UUID)
返回前 8 位,在名称的后面添加这个随机字符串。
不仅如此,代码的类名也需要添加随机字符串,添加这样的字符串就需要用到一个逻辑去加。
逻辑为,新开一个字符串,匹配某一行代码的后面的字符串,匹配完成后在字符串的前面 + 随机字符串。

2.3 实现最后的进程运行 —BotPool 中的 Consumer 函数

在这个函数里,调用之前的 startTimeOut() 函数,来定义 Bot 的执行时间

对于 Bot 的运行时间,两个 Bot 要求一共等待 5 秒,一个 Bot 一共等待或者执行 2s, 拿出一秒来做冗余是一种比较合理的运行状态

2.4 Server 端接收代码的操作

在 BotPool 模块中编译完成代码之后,需要把编译出来的移动方向的 direction 在 server 端取出来,以映射到前端页面。
实现这个接口就需要用到三件套 server impl controller
分别为

ReceiveBotMoveService
ReceiveBotMoveServiceImpl
ReceiveBotMoveController
在 controller 中,用 post 方法进行链接。链接方法为 post 方法,链接为 /pk/receive/bot/move/,然后把这个 链接进行放行。
下一步,就需要把编译的代码的得到的移动结果传递给 nextstep
在 WebSocketServer 中的 move 函数的逻辑是来执行人的操作,执行机器的操作传递给 Game 类的方法与之是类似的。

2.5 将代码编译出来的移动信息返还给后端 server

首先要做的是给 Bot 的代码模块:BotRunningSystem 来创建 restTemplateConfig 以注入 restTemplate, 这样就可以去 cousumer 类中的 run 函数把整个的移动路径给返回。然后定义全局 urlreceiveBotMoveUrl 把数据信息加载到链接上,最终通过 restTemplate 将数据返回。