# moj-backtend **Repository Path**: godM675/moj-backtend ## Basic Information - **Project Name**: moj-backtend - **Description**: moj判题系统后端代码 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 0 - **Created**: 2024-02-26 - **Last Updated**: 2024-05-25 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # MOJ在线判题系统 ## 项目介绍 MOJ在线判题系统是一个能够在线进行创建题目,提交题目并进行判题的判题系统,系统当前支持`Java`、`Python`、`C`、`C++`、`JavaScript`五种主流的编程语言进行做题。 系统前端实现较为简单,通过登录用户角色区分管理员与普通用户显示和影藏管理页面。再管理页面,管理员可以创建、管理题目;用户可以自由搜索题目、阅读题目、编写并提交代码。 再系统后端,能够根据管理员设定的题目测试用例,在**代码沙箱**中对代码进行编译、运行、判断输出是否正确。 其中,代码沙箱是独立的一个服务,可以提供给其他服务使用。 - 前端代码:https://gitee.com/godM675/moj-fronted.git - 后端代码 - 单体架构:https://gitee.com/godM675/moj-backtend.git - 微服务架构:https://gitee.com/godM675/moj-backend-mircoservice.git ## 项目特点 不同于泛滥的管理系统、博客、商城,不同于只有增删改查的业务系统,本项目基于**Docker虚拟化技术+多种设计模式**,实现了一个可复用、安全性较高的**代码沙箱**,并通过微服务架构,实现了对该系统的扩展。 - 使用到了Docker-Java,通过程序调用docker的方式实现对docker容器的操作,并且能够通过对docker容器的**内存限制,时间限制**等安全性控制保证服务的安全性。 - 使用设计模式中的**模板方法模式**,在代码沙箱中对多种语言实现的核心业务流程是是相同的,且有一些过程可以复用,所以使用模板方法模式,优化代码,提高代码的可复用性。 - 使用了设计模式中的**策略模式**,在判题服务中,有关于判题时,不同语言的判题逻辑是不同的,例如,使用Java语言进行编程其内存限制和时间限制应该适当增加,如果将各种语言的判断逻辑都写在一处,这样代码会十分臃肿,因此使用策略模式,通过对传入的语言类型进行判断,调用不同类型语言的具体实现类从而简化了service的调用,极大程度上美化了代码。 - 使用了**工厂模式**,为了提高系统灵活性,对于代码沙箱的调用,没有使用指定的单一的代码沙箱,而是定义了一共通用的代码沙箱接口,并且提供了多种代码沙箱的实现类:**本地代码沙箱(本地编译运行)、远程代码沙箱(远程调用Docker)、第三方代码沙箱(移花接木)。**为了简化代码沙箱实例的获取,通过工厂模式动态的获取application.yaml配置文件中配置的沙箱类型灵活的获取沙箱实现类。 ## 技术栈 - **前端** - **Vue3** - **Axious** - **Monaco-editor** - **ByteMd** - **Arco Design** - **后端** - **Spring Cloud Alibaba** - **RabbitMq** - **MySql** - **Mybatis-Plus** - **Docker-java** - **Redis** - **其他** - **Docker** ## 项目预览 - **用户登录页面** ![image-20240314234824388](https://godm-typora.oss-cn-shenzhen.aliyuncs.com/2024/202403142348722.png) - **浏览题目页面** ![image-20240314234902710](https://godm-typora.oss-cn-shenzhen.aliyuncs.com/2024/202403142349936.png) - **在线做题页面** ![image-20240314235236952](https://godm-typora.oss-cn-shenzhen.aliyuncs.com/2024/202403142352190.png) - **题目提交页面** ![image-20240314235259157](https://godm-typora.oss-cn-shenzhen.aliyuncs.com/2024/202403142352318.png) - 创建题目页面 ![image-20240314235348586](https://godm-typora.oss-cn-shenzhen.aliyuncs.com/2024/202403142353771.png) ![image-20240314235405523](https://godm-typora.oss-cn-shenzhen.aliyuncs.com/2024/202403142354704.png) - **修改题目页面** ![image-20240314235434142](https://godm-typora.oss-cn-shenzhen.aliyuncs.com/2024/202403142354367.png) - **管理题目页面** ![image-20240314235454610](https://godm-typora.oss-cn-shenzhen.aliyuncs.com/2024/202403142354788.png) - **做题详情页面** ![image-20240314235542313](https://godm-typora.oss-cn-shenzhen.aliyuncs.com/2024/202403142355581.png) ## 项目部署 项目我将采用宝塔面板工具进行部署,使用docker部署前端项目以及后端单体项目(由于我的服务器内存不够所以不部署微服务的)和代码沙箱。 - **安装docker** > 以下命令基于CentOS环境。 1. 下载工具 ```shell yum install -y yum-utils ``` 2. 设置镜像的仓库 ```shell yum-config-manager \ --add-repo \ https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo #配置阿里云的镜像 ``` 3. 更新yum软件包索引 ```shell yum makecache fast ``` 4. 安装docker相关配置 ```shell yum install docker-ce docker-ce-cli containerd.io ``` 5. 启动docker ```shell systemctl start docker # 查看当前版本号,是否启动成功 docker version # 设置开机自启动 systemctl enable docker ``` **注意,如果需要远程调用docker则需要配置docker远程连接** ```shell 打开docker.service文件 sudo vi /lib/systemd/system/docker.service 找到ExecStart 开头的配置,注释原配置 进行备份 插入以下内容 ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:2375 -H unix://var/run/docker.sock 保存退出 sudo systemctl daemon-reload sudo systemctl restart docker ``` 配置成功后访问**ip**:2375/version,如果显示版本信息,则表示配置成功。 之后在代码沙箱配置文件中的host文件配置`tcp://ip:2375`即可 - **安装mysql** 方式1: 无挂载模式 > 这种方式直接运行mysql之后,所有关于mysql的内容都在容器中,后续如果需要修改mysql的内容,需要手动进入容器内进行操作。且在宿主机上无备份,一旦容器被删除,数据也会被删除。 ```shell docker pull mysql //下载MySQL镜像 docker run --name mysql --restart=always -p 3306:3306 -e MYSQL_ROOT_PASSWORD=密码 -d mysql //启动MySQL ``` 方式2: 数据卷挂载模式 > 和**无挂载模式相对**,通过数据卷挂载的方式运行容器,将容器内的部分重要文件映射到宿主机上。直接操作宿主机对应的映射文件就能和容器内作同步,方便操作的同时还能保证容器内的数据在宿主机上有一个备份。 > > 下面的命令分别对mysql的日志文件、配置文件、数据文件进行了映射,你也可以自己修改。 ```shell docker run --name mysql --restart=always -p 3306:3306 -v /mydata/mysql/log:/var/log/mysql -v /mydata/mysql/data:/var/lib/mysql -v /mydata/mysql/conf:/etc/mysql/conf.d -e MYSQL_ROOT_PASSWORD=root -d mysql ``` **安装完成后,创建数据库名称为:moj、并且导入后端项目中的sql文件** - **打包并上传** 使用Idea和webStream等工具对代码进行打包,将**前端项目、后端项目、代码沙箱项目**打包完成后的**dist目录**和jar包以及项目根目录下的**dockerfile文件**和后端项目的docker-compose.yaml文件一起上传到服务器的任意地址。 1. **前端配置文件** ```dockerfile # 使用官方nginx作为父镜像 FROM nginx:stable-alpine # 复制前端构建产物到nginx的默认web根目录 COPY ./dist /usr/share/nginx/html # 暴露端口 EXPOSE 80 # 设置容器启动时执行nginx CMD ["nginx", "-g", "daemon off;"] ``` 2. **后端配置文件** ```dockerfile # 使用官方Java运行时作为父镜像 FROM openjdk:8-jre-alpine # 将JAR文件复制到容器内 COPY ./moj-backend-0.0.1-SNAPSHOT.jar /app/ # 设置工作目录 WORKDIR /app # 暴露端口 根据实际端口调整 EXPOSE 8201 # 启动应用 CMD ["java", "-jar", "moj-backend-0.0.1-SNAPSHOT.jar"] ``` 3. **代码沙箱配置文件** ```dockerfile # 使用官方Java运行时作为父镜像 FROM openjdk:8-jre-alpine # 将JAR文件复制到容器内 COPY ./moj-code-sandbox-0.0.1-SNAPSHOT.jar /app/ # 设置工作目录 WORKDIR /app # 暴露端口根据实际端口调整 EXPOSE 8202 # 启动应用 CMD ["java", "-jar", "moj-code-sandbox-0.0.1-SNAPSHOT.jar"] ``` 4. **docker-compose配置文件** ```yaml version: '3' services: moj-fronted: build: context: . dockerfile: Dockerfile-moj-fronted ports: - "80:80" # 将容器的80端口映射到主机的80端口 depends_on: - moj-backend # 确保后端服务先启动 volumes: - /mydata/nginx/conf:/etc/nginx/conf.d # 挂载nginx配置文件 moj-backend: build: context: . dockerfile: Dockerfile-moj-backend ports: - "8201:8201" # 将容器的8201端口映射到主机的8201端口 depends_on: - moj-code-sandbox # 确保代码沙箱服务先启动 moj-code-sandbox: build: context: . dockerfile: Dockerfile-moj-code-sandbox ports: - "8202:8202" # 将容器的8202端口映射到主机的8202端口 volumes: - /var/run/docker.sock:/var/run/docker.sock # 挂载docker.sock 连接docker的必要配置 ``` - **部署运行** 1. 生成镜像 将所有打包好的文件以及配置文件上传到服务器的同一目录,并且在该目录下,执行下列命令。**一共有6个文件和一个文件夹,运行之前确保都已经上传。** ```shell docker-compose build ``` 2. 创建容器并运行 镜像生成后,创建容器,并启动 ```shell docker-compose up -d ``` 3. 修改网络连接 由于我们的mysql与后端项目不是同一个docker-compose而是单独启动的,所以并不在同一个网络,这里我是为了后续mysql能提供给别的服务,所以单独拎了出来,也可以将mysql也写到docker-compose配置里面一起启动,如果是像我这的话需要**将mysql容器加入到docker-compose生成的网络中。** - **修改nginx配置文件** 加开我们之前挂载到本地的nginx映射文件目录,在里面创建一个名为moj.conf的配置文件,配置如下: ```nginx server { listen 80; server_name www.moj.ltgodm.cn moj.ltgodm.cn; location / { root /usr/share/nginx/html; index index.html index.htm; try_files $uri $uri/ /index.html; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } } server { listen 80; server_name api.moj.ltgodm.cn; location / { proxy_pass http://moj-backend:8201; # 将请求转发到后端服务器 proxy_set_header Host $host; # 设置请求头中的Host字段 proxy_set_header X-Real-IP $remote_addr; # 设置请求头中的真实IP字段 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 设置请求头中的代理链IP字段 proxy_set_header X-Forwarded-Proto $scheme; # 设置请求头中的原始协议字段 } } ``` ## 踩坑记录 ### Docker的网络连接 注意docker-compose生成的的网络配置是独立的,不是通过同一个docker-compose启动的服务默认是无法相互访问的。需要去配置容器间的网络。 如果是在docker容器中调用宿主机本地的Mysql服务,则不能用127.0.0.1 或者localhost 去调用mysql,**因为在docker容器中指向的是容器本地的地址,所以指向宿主机的ip。**可以通过`ip addr`命令查看docker0 网卡的ip地址,配置该网卡的ip。 ### resourse目录下的文件访问 在正常情况下,我们如果需要访问resouece目录下的文件是有许多种方式的一开始我是使用下面这种方式访问shell脚本并将其上传到docker容器中。 ```java /** * 上传shell脚本到容器中 * * @param dockerClient dockerClient连接 * @param containerId 容器id */ protected void uploadShellFile(DockerClient dockerClient, String containerId) { //将shell脚本上传到docker容器中 try { // 获取resources目录下的脚本文件 File saveUserFile = ResourceUtils.getFile("classpath:shell/saveUserFile.sh"); File removeUserFile = ResourceUtils.getFile("classpath:shell/removeUserFile.sh"); // 将脚本复制到容器内 CopyArchiveToContainerCmd copyCmd = dockerClient.copyArchiveToContainerCmd(containerId).withHostResource(saveUserFile.getAbsolutePath()).withRemotePath("/".concat(globalCodePath)); copyCmd.exec(); copyCmd.withHostResource(removeUserFile.getAbsolutePath()).withRemotePath("/".concat(globalCodePath)); copyCmd.exec(); log.info("shell脚本复制成功"); copyCmd.close(); } catch (FileNotFoundException e) { throw new ExecuteException("找不到shell脚本文件"); } } ``` 但是由于打包后,resource下的所有文件会被 BOOT-INF/classes 目录下,但是上述方式查找文件仍去原本的resource目录下寻找,自然就找不到文件。后续修改采用如下方式进行上传。完美解决。 ```java /** * 上传shell脚本到容器中 * * @param dockerClient dockerClient连接 * @param containerId 容器id */ protected void uploadShellFile(DockerClient dockerClient, String containerId) { // 获取resources目录下的脚本文件 InputStream saveFileStream = getClass().getResourceAsStream("/shell/saveUserFile.sh"); InputStream removeFileStream = getClass().getResourceAsStream("/shell/removeUserFile.sh"); //创建临时文件 Path saveUserFilePath = Paths.get(System.getProperty("java.io.tmpdir"),"saveUserFile.sh"); File saveUserFile = saveUserFilePath.toFile(); Path removeUserfilePath = Paths.get(System.getProperty("java.io.tmpdir"), "removeUserFile.sh"); File removeUserFile = removeUserfilePath.toFile(); //将shell脚本上传到docker容器中 try { Files.copy(saveFileStream,saveUserFilePath, StandardCopyOption.REPLACE_EXISTING); Files.copy(removeFileStream,removeUserfilePath, StandardCopyOption.REPLACE_EXISTING); // 将脚本复制到容器内 dockerClient.copyArchiveToContainerCmd(containerId).withHostResource(saveUserFile.getAbsolutePath()).withRemotePath(StrUtil.format("/{}/",globalCodePath)).exec(); dockerClient.copyArchiveToContainerCmd(containerId).withHostResource(removeUserFile.getAbsolutePath()).withRemotePath(StrUtil.format("/{}/",globalCodePath)).exec(); log.info("shell脚本复制成功"); } catch (Exception e) { log.error("复制shell脚本失败",e); }finally { try { saveFileStream.close(); removeFileStream.close(); } catch (IOException e) { throw new RuntimeException(e); } saveUserFile.deleteOnExit(); removeUserFile.deleteOnExit(); } } ``` ### docker-java 由于docker-java默认是支持远程连接的,所以需要去docker中配置远程连接,所以需要在docker的配置文件中做如下配置。 ```shell 打开docker.service文件 sudo vi /lib/systemd/system/docker.service 找到ExecStart 开头的配置,注释原配置 进行备份 插入以下内容 ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:2375 -H unix://var/run/docker.sock 保存退出 sudo systemctl daemon-reload sudo systemctl restart docker ``` 但是,当项目部署后,我们需要在docker容器内部访问docker并操作docker,显然,远程访问是非常不优雅的,那么如何进行本地访问呢? 我们需要将docker中的/var/run/docker.sock目录同步到docker容器中的/var/run/docker.sock目录,也就是挂载过去,这样我们就能够通过一种比较安全稳定的方式去访问docker。但是,值得注意的是:**远程访问时使用的是tcp://ip:2375,而本地访问时则需要使用unix:///var/run/docker.sock来进行访问**。 ### dokcer镜像 由于我们的项目是需要执行用户提交上来的代码,因此我们需要操作docker容器,将用户的代码隔离在一个安全的地方执行,所以我们就需要为编程语言找到合适的镜像进行编译运行,一开始找的openjdk、gcc、python等镜像都过于臃肿,服务器完全带不动,后续找到了像openjdk-alpine、alpine-gcc等轻量化的镜像极大减少了服务器的开销。