Под данным катом описано как я создал свой аналог The GO Playground. Это было сделано исключительно по собственному желанию в образовательных целях. Ссылка на репозиторий.
Кодовая база проекта традиционно состоит из фронта и бэка. В качестве языка бэка используется Go, на фронте JS + jQuery.
В моей реализации реактивная отправка данных на фронт осуществляется с помощью протокола WS, а получение исходного кода по ссылке с помощью HTTP. Для роутинга в приложении я использовал httprouter. Изначально я использовал html/template и автообновление блоков HTML c помощью функции load(), но потом отошел от этой идеи ввиду некоторых сложностей реализации.
На самом деле оригинальная платформа сделана очень хитрым образом с точки зрения использования ресурсов системы. Если в пользовательской программе используются конструкции задержки по типу time.Sleep, программа на самом деле не спит, а выполняет свой код непрерывно, минуя задержки. А на выходе генерируется последовательность байт с учетом задержки, которую фронт отрабатывает таким образом, что пользователь не видит разницы. Создается ощущение, что программа запущена локально. Для безопасной работы с сетью и файловой системой используются некоторые трюки, про все подробности, используемые в The GO Playground можно почитать по ссылке. Оригинальный код платформы по ссылке.
Домашняя страница выглядит весьма минималистично, на выбор представлено пару тестовых программ. Блок ввода кода, блок результата выполнения программы.
В оригинальной платформе файл с пользовательским кодом один и называется prog.go. В моей реализации для хранения пользовательского кода используются обычные файлы .go, в которые происходит запись и чтение в случае получение кода по ссылке. Как вариант можно было использовать БД, в которой ключом будет ссылка, а значением пользовательский код.
Все хэндлеры вызываются на структуре Store, хранящей указатели на структуру логера, конфига приложения, интерфейс с базовыми методами платформы и структуру Upgrader для преобразования входящего HTTP соединения в протокол WS.
type Store struct {
Log *logrus.Logger
Config *config.PlayConfig
Coder internal.Coder
Upgrader websocket.Upgrader
}
Определим базовый интерфейс работы сервера.
type Coder interface {
GetCode(link string) ([]byte, error)
ShareCode(conn *websocket.Conn) error
Run(conn *websocket.Conn) (io.ReadCloser, error)
}
Метод GetCode получает ссылку на код и записывает в ответ файл с пользовательским кодом в формате JSON.
Метод ShareCode получает WS соединение по которому получает пользовательский код с фронта и в ответ отправляет ссылку.
Метод Run получает WS соединение по которому получает пользовательский код с фронта и возвращает pipe, который удовлетворяет интерфейсу io.ReadCloser.
В проекте используются следующие хэндлеры.
package api
import "github.com/julienschmidt/httprouter"
func (s *Store) Register(router *httprouter.Router) {
//HTTP handlers
router.GET("/", s.playGo)
router.GET("/p/:link", s.playGo)
router.GET("/pp/:link", s.getCode)
//Websocket handlers
router.GET("/run", s.run)
router.GET("/share", s.share)
}
Все стандартно: домашняя страница по пути / и по пути /p/:link, хэндлер для получения пользовательского кода по ссылке, что касается HTTP. Остальные хэндлеры нужны для работы по WS.
Для отправки данных на фронт используется pipe, который при получении данных отправляет их по WS, вызывая метод WriteMessage, когда все сообщения вычитаны соединение закрывается методом Close.
func (s Store) pipe(pipe io.ReadCloser, conn *websocket.Conn, w http.ResponseWriter, r *http.Request) {
buf := bufio.NewReader(pipe)
for {
line, _, err := buf.ReadLine()
if err == io.EOF {
conn.Close()
break
}
err = conn.WriteMessage(websocket.TextMessage, line)
if err != nil {
s.Log.Error(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
}
}
На фронте по нажатию на кнопку Run создается WS соединение. В момент его открытия код копируется из поля ввода текста и отправляется на сервер. Далее по мере поступления сообщений они записываются в textarea в новую строчку. Когда на сервере
все сообщения из pipe вычитаны соединение закрывается, в textarea печатается текст Program exited.
function Run() {
var ws = new WebSocket("ws://localhost" + port + "/run");
var cnt = 0
ws.onopen = function() {
cnt = 0
var myTextArea = $('#code_out')[0];
var copyCode = $('#code_in').val();
ws.send(copyCode);
myTextArea.value = "";
};
ws.onmessage = function (evt) {
var myTextArea = $('#code_out')[0];
if (cnt==0) {
myTextArea.value = myTextArea.value + evt.data;
cnt++
return
}
myTextArea.value = myTextArea.value + "\n" + evt.data;
};
ws.onclose = function() {
var myTextArea = $('#code_out')[0];
myTextArea.value = myTextArea.value + "\n" + "\n" + "Program exited.";
};
}
Когда пользователь переходит по ссылке для получения кода, фронт отлавливает это и делает get запрос на бэк для получения пользовательского кода. После получения ответ парсится и заполняет поле исходного кода.
function Fetch(path) {
fetch("http://localhost" + port + "/pp/" + path)
.then((response) => {
return response.json();
})
.then((data) => {
$("#code_in").val(data)
});
}
window.onload = function () {
var pr = $("select#programs").val();
Fetch(pr)
var path = document.location.pathname;
if (path.includes("/p/")) {
Fetch(path.split("/")[2])
}
}
В целом рассказал все, о чем хотел рассказать. Жду ваши хорошие и плохие комментарии :)
Комментарии (6)
rozhnev
13.09.2022 09:58+1Из своего опыта написания своей площадки PHPize.online советую сразу перейти к хранению кода в базе данных
QuAzI
Чел проделал классную работу, а ему уже влепили -1 и без каких либо комментариев вообще. Хабр превращается в помойку, где какие-то токсы просто безнаказанно сливают людям карму и им за это не летит вообще никакого пенальти
DeniSix
Я хоть минусы и не ставлю, но на мой вкус статья ни о чём. Самое интересное это
и оно не раскрыто.
А вот такое — совсем не дело:
Если не хочется иметь дело с докером, можно было завернуть в wasm, например, и обрисовать плюсы-минусы такого решения.
Alekstet Автор
Почему не раскрыто, я написал далее по тексту что у них сделано. Но и это же обзор моего решения, а не оригинального.
DeniSix
Может это моё старческое брюзжание, но без подобного исследования это статья уровня "Todo list REST API на <language_name>", ценность которой для технического сообщества стремится к нулю.
В общем, если вы так изучаете язык — это прекрасно и не имею ничего против, но от статьи на хабр хотелось бы чего-то более глубокого (в контексте статьи — sandboxing).