Java 및 Docker를 사용하여 온라인 코드 컴파일러를 개발하는 방법

이 자습서에서는 Java(Spring Boot) 및 Docker를 사용하여 경쟁력 있는 프로그래밍 및 코딩 인터뷰를 위한 간단하고 효율적인 온라인 코드 컴파일러를 만드는 방법에 대한 개요를 볼 것입니다.

시작하기 전에 다음 링크https://github.com/zakariamaaraki/RemoteCodeCompiler에서 프로젝트의 소스 코드를 찾을 수 있습니다.

왜 자바와 도커인가?



Java의 경우 개인적인 선택입니다. 저는 이 언어로 작업하는 것을 정말 좋아하지만 선호하는 다른 프로그래밍 언어를 선택할 수 있습니다.

다른 소스 코드에서 실행 환경을 분리하고(악성 코드가 시스템에 영향을 주지 않도록) 리소스를 제한하려면 가상 시스템 또는 컨테이너를 사용해야 하는데 이 두 가지 선택의 차이점은 무엇입니까?

가상 머신 대 컨테이너





VM(가상 머신)은 컴퓨터 시스템의 에뮬레이션입니다. 간단히 말해서 실제로는 한 대의 컴퓨터인 하드웨어에서 여러 대의 개별 컴퓨터처럼 보이는 것을 실행할 수 있습니다.

컨테이너를 사용하면 가상 머신(VM)과 같은 기본 컴퓨터를 가상화하는 대신 OS만 가상화됩니다.

컨테이너는 물리적 서버와 해당 호스트 OS(일반적으로 Linux 또는 Windows) 위에 위치합니다. 각 컨테이너는 호스트 OS 커널과 일반적으로 바이너리 및 라이브러리도 공유합니다. 공유 구성 요소는 읽기 전용입니다.

이제 컨테이너를 사용하는 것이 Vms보다 가볍고 도커가 가장 많이 사용되는 컨테이너화 솔루션이므로 이 프로젝트에서 도커를 선택한 이유가 분명합니다.

API





먼저 API를 설계해야 합니다. 4가지 프로그래밍 언어(Java, C, C++ 및 Python)용 온라인 컴파일러를 제공하고 싶다고 가정해 보겠습니다.
따라서 API는 다음과 같아야 합니다.
4개의 컨트롤러는 Java용, C용, C++용, Python용입니다.
이러한 컨트롤러에 대한 호출은 다음 URL에 대한 POST 요청을 통해 수행됩니다.
  • 로컬 호스트:8080/컴파일러/자바
  • 로컬 호스트:8080/컴파일러/c
  • 로컬 호스트:8080/컴파일러/cpp
  • 로컬 호스트:8080/컴파일러/파이썬

  • 입력으로 5개의 필드가 필요합니다.

  • output : 예상 출력.

  • sourceCode : java, c, c++ 또는 python의 소스 코드입니다.

  • timeLimit : 실행 중에 소스 코드가 초과하지 않아야 하는 시간 제한(초)입니다(0~15초 사이여야 함).

  • memoryLimit : 실행 중에 소스 코드가 초과하지 않아야 하는 Mb 단위의 메모리 제한(0에서 1000Mb 사이여야 함).

  • inputFile : 별도의 줄에 작성된 입력(선택 사항).

  • // Python Compiler
        @RequestMapping(
                value = "python",
                method = RequestMethod.POST
        )
        public ResponseEntity<Object> compile_python(@RequestPart(value = "output", required = true) MultipartFile output,
                                                @RequestPart(value = "sourceCode", required = true) MultipartFile sourceCode,
                                                @RequestParam(value = "inputFile", required = false) MultipartFile inputFile,
                                                @RequestParam(value = "timeLimit", required = true) int timeLimit,
                                                @RequestParam(value = "memoryLimit", required = true) int memoryLimit
        ) throws Exception {
            return compiler(output, sourceCode, inputFile, timeLimit, memoryLimit, Langage.Python);
        }
    
        // C Compiler
        @RequestMapping(
                value = "c",
                method = RequestMethod.POST
        )
        public ResponseEntity<Object> compile_c(@RequestPart(value = "output", required = true) MultipartFile output,
                                              @RequestPart(value = "sourceCode", required = true) MultipartFile sourceCode,
                                              @RequestParam(value = "inputFile", required = false) MultipartFile inputFile,
                                              @RequestParam(value = "timeLimit", required = true) int timeLimit,
                                              @RequestParam(value = "memoryLimit", required = true) int memoryLimit
        ) throws Exception {
            return compiler(output, sourceCode, inputFile, timeLimit, memoryLimit, Langage.C);
        }
    
        // C++ Compiler
        @RequestMapping(
                value = "cpp",
                method = RequestMethod.POST
        )
        public ResponseEntity<Object> compile_cpp(@RequestPart(value = "output", required = true) MultipartFile output,
                                                @RequestPart(value = "sourceCode", required = true) MultipartFile sourceCode,
                                                @RequestParam(value = "inputFile", required = false) MultipartFile inputFile,
                                                @RequestParam(value = "timeLimit", required = true) int timeLimit,
                                                @RequestParam(value = "memoryLimit", required = true) int memoryLimit
        ) throws Exception {
            return compiler(output, sourceCode, inputFile, timeLimit, memoryLimit, Langage.Cpp);
        }
    
        // Java Compiler
        @RequestMapping(
                value = "java",
                method = RequestMethod.POST
        )
        public ResponseEntity<Object> compile_java(@RequestPart(value = "output", required = true) MultipartFile output,
                                              @RequestPart(value = "sourceCode", required = true) MultipartFile sourceCode,
                                              @RequestParam(value = "inputFile", required = false) MultipartFile inputFile,
                                              @RequestParam(value = "timeLimit", required = true) int timeLimit,
                                              @RequestParam(value = "memoryLimit", required = true) int memoryLimit
        ) throws Exception {
            return compiler(output, sourceCode, inputFile, timeLimit, memoryLimit, Langage.Java);
        }
    

    사용자는 어떤 유형의 응답을 기대해야 합니까?



    자, 이 점에 대해 잠시 살펴보겠습니다. Codeforces, Leetcode 등과 같은 플랫폼에서 경쟁 프로그래밍을 수행하는 경우 6가지 유형의 평결(허용됨, 오답, 컴파일 오류, 런타임 오류, 메모리 부족 오류 및 시간 제한 초과)이 있음을 알 수 있습니다.

    도커 컨테이너 내에서 소스 코드 컴파일



    여기서 아이디어는 사용자가 제공한 소스 코드를 가져와 언어에 따라 도커 이미지를 만든 다음 이 이미지의 컨테이너를 실행하여 소스 코드를 컴파일하고 실행하는 것입니다. 컨테이너의 반환 코드에 따라 이전에 논의한 판정을 결정할 수 있지만 컨테이너가 무한 실행을 피하기 위한 시간 제한이나 메모리 제한(악성 코드를 방지하기 위한)을 초과하지 않도록 해야 합니다.

    // Compile method
        private ResponseEntity<Object> compiler(
                MultipartFile output,
                MultipartFile sourceCode,
                MultipartFile inputFile,
                int timeLimit,
                int memoryLimit,
                Langage langage
        ) throws Exception {
    
            String folder = "utility";
            String file = "main";
            if(langage == Langage.C) {
                folder += "_c";
                file += ".c";
            } else if(langage == Langage.Java) {
                file += ".java";
            } else if(langage == Langage.Cpp) {
                folder += "_cpp";
                file += ".cpp";
            } else {
                folder += "_py";
                file += ".py";
            }
    
            if(memoryLimit < 0 || memoryLimit > 1000)
                return ResponseEntity
                        .badRequest()
                        .body("Error memoryLimit must be between 0Mb and 1000Mb");
    
            if(timeLimit < 0 || timeLimit > 15)
                return ResponseEntity
                        .badRequest()
                        .body("Error timeLimit must be between 0 Sec and 15 Sec");
    
            LocalDateTime date = LocalDateTime.now();
    
            createEntrypointFile(sourceCode, inputFile, timeLimit, memoryLimit, langage);
    
            logger.info("entrypoint.sh file has been created");
    
            saveUploadedFiles(sourceCode, folder + "/" + file);
            saveUploadedFiles(output, folder + "/" + output.getOriginalFilename());
            if(inputFile != null)
                saveUploadedFiles(inputFile, folder + "/" + inputFile.getOriginalFilename());
            logger.info("Files have been uploaded");
    
            String imageName = "compile" + new Date().getTime();
    
            logger.info("Building the docker image");
            String[] dockerCommand = new String[] {"docker", "image", "build", folder, "-t", imageName};
            ProcessBuilder probuilder = new ProcessBuilder(dockerCommand);
            Process process = probuilder.start();
            int status = process.waitFor();
            if(status == 0)
                logger.info("Docker image has been built");
            else
                logger.info("Error while building image");
    
            logger.info("Running the container");
            dockerCommand = new String[] {"docker", "run", "--rm", imageName};
            probuilder = new ProcessBuilder(dockerCommand);
            process = probuilder.start();
            status = process.waitFor();
            logger.info("End of the execution of the container");
    
            BufferedReader outputReader = new BufferedReader(new InputStreamReader(output.getInputStream()));
            StringBuilder outputBuilder = new StringBuilder();
            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
            StringBuilder builder = new StringBuilder();
    
            boolean ans = runCode(outputReader, outputBuilder, reader, builder);
    
            String result = builder.toString();
    
            // delete files
            deleteFile(folder, file);
            new File(folder + "/" + output.getOriginalFilename()).delete();
            if(inputFile != null)
                new File(folder + "/" + inputFile.getOriginalFilename()).delete();
            logger.info("files have been deleted");
    
            String statusResponse = statusResponse(status, ans);
    
            logger.info("Status response is " + statusResponse);
    
            return ResponseEntity
                    .status(HttpStatus.OK)
                    .body(new Response(builder.toString(), outputBuilder.toString(), statusResponse, ans, date));
        }
    


    컨테이너의 진입점



    도커 이미지의 진입점을 구성하기 위해 실행 중에 시간 제한, 메모리 제한 및 실행 명령을 포함하는 진입점 파일을 생성합니다.

    다음은 Java 실행을 위해 entrypoint.sh 파일을 생성하는 방법의 예입니다.

    // create Java entrypoint.sh file
        private void createJavaEntrypointFile(String fileName, int timeLimit, int memoryLimit, MultipartFile inputFile) {
            String executionCommand = inputFile == null
                    ? "timeout --signal=SIGTERM " + timeLimit + " java " + fileName.substring(0,fileName.length() - 5) + "\n"
                    : "timeout --signal=SIGTERM " + timeLimit + " java " + fileName.substring(0,fileName.length() - 5) + " < " + inputFile.getOriginalFilename() + "\n";
            String content = "#!/usr/bin/env bash\n" +
                    "mv main.java " + fileName+ "\n" +
                    "javac " + fileName + "\n" +
                    "ret=$?\n" +
                    "if [ $ret -ne 0 ]\n" +
                    "then\n" +
                    "  exit 2\n" +
                    "fi\n" +
                    "ulimit -s " + memoryLimit + "\n" +
                     executionCommand +
                    "exit $?\n";
            OutputStream os = null;
            try {
                os = new FileOutputStream(new File("utility/entrypoint.sh"));
                os.write(content.getBytes(), 0, content.length());
            } catch (IOException e) {
                e.printStackTrace();
            }finally{
                try {
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    


    톡 싸요 코드 보여주세요 😃



    이것은 단지 개요일 뿐이며 이 모든 것을 코딩하는 방법을 설명하는 대신 내 Github 저장소https://github.com/zakariamaaraki/RemoteCodeCompiler의 링크를 방문하는 것이 좋습니다.

    질문이 있으시면 언제든지 질문해 주세요!

    좋은 웹페이지 즐겨찾기