В этом руководстве мы рассмотрим обзор того, как создать простой и эффективный онлайн-компилятор кода для конкурентного программирования и собеседований по кодированию, используя Java (Весенняя загрузка) и Docker .
Что ж, для java это личный выбор, мне действительно нравится работать с этим языком, но вы можете выбрать любой другой язык программирования, который вам больше нравится.
Чтобы отделить среды выполнения от разных исходных кодов (чтобы избежать вредоносного кода, влияющего на вашу машину) и ограничить ресурсы, нам нужно использовать виртуальные машины или контейнеры, но в чем разница между этими двумя вариантами?
Виртуальная машина против контейнера
Виртуальная машина (VM) – это эмуляция компьютерной системы. Проще говоря, это позволяет запускать то, что кажется множеством отдельных компьютеров, на оборудовании, которое на самом деле является одним компьютером.
С помощью контейнеров вместо виртуализации базового компьютера, такого как виртуальная машина (VM), виртуализируется только операционная система.
Контейнеры располагаются поверх физического сервера и его основной операционной системы — обычно Linux или Windows. Каждый контейнер совместно использует ядро хост-ОС, а также, как правило, двоичные файлы и библиотеки. Общие компоненты доступны только для чтения.
Итак, теперь ясно, что использование контейнеров – лучший выбор для нас, поскольку они легче виртуальных машин, а docker – наиболее используемое решение для контейнеризации, вот почему я выбрал docker для этого проекта.
API-интерфейс
Во-первых, нам нужно разработать API, допустим, мы хотим предоставить онлайн-компилятор для 4 языков программирования (Java, C, C++ и Python). Таким образом, API должен выглядеть примерно так: четыре контроллера, один для Java, один для C, один для C++ и еще один для Python. Вызов этих контроллеров осуществляется через POST-запросы к следующим URL-адресам:
localhost:8080/компилятор/ java
локальный хост:8080/компилятор/ c
localhost:8080/компилятор/ cpp
localhost:8080/компилятор/ python
В качестве входных данных мы ожидаем 5 полей:
результат : ожидаемый результат.
Исходный код : исходный код на java, c, c++ или python.
ограничение по времени : ограничение по времени в секундах, которое исходный код не должен превышать во время его выполнения (должно составлять от 0 до 15 секунд).
ограничение памяти :: ограничение памяти в Мб, которое исходный код не должен превышать во время его выполнения (должно быть от 0 до 1000 МБ).
входной файл : входные данные, записанные в отдельные строки (необязательно).
// Python Compiler
@RequestMapping(
value = "python",
method = RequestMethod.POST
)
public ResponseEntity
Какого типа ответа следует ожидать пользователю?
Что ж, давайте уделим этому вопросу минутку. Если вы занимаетесь конкурентным программированием на таких платформах, как Codeforces, Leetcode или других, вы можете увидеть, что существует 6 типов вердиктов ( Принято , Неправильный ответ , Ошибка компиляции , Ошибка времени выполнения , Ошибка нехватки памяти , и Превышен лимит времени/| ).
Компиляция исходного кода внутри контейнера docker
Идея здесь состоит в том, чтобы взять исходный код, предоставленный пользователем, и создать образ docker в зависимости от языка, а затем запустить контейнер этого образа для компиляции и выполнения исходного кода. В зависимости от возвращаемого кода контейнера мы можем вынести вердикт, который мы обсуждали ранее, но нам нужно убедиться, что контейнер не превышает лимит времени, чтобы избежать бесконечных выполнений, ни лимит памяти (чтобы избежать вредоносного кода).
// Compile method
private ResponseEntity 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));
}
Точка входа в контейнер
Чтобы настроить точку входа образа docker, мы создаем во время выполнения файл точки входа, который содержит ограничение по времени, ограничение по памяти и команду выполнения.
Это пример того, как сгенерировать entrypoint.sh файл для выполнения на java: