Личный веб-сервер на Wolfram Language

от автора

Иногда людям хочется быстро сделать веб-сервер, корневая логика которого будет на Wolfram Language. Существует правильный и долгий путь. Наградой будет красота решения и производительность. И существует второй путь, имени хлеба и троллейбуса, костылей и велосипедов. О нем мы и поговорим.

Я начал активно изучать Mathematica и Wolfram Language где-то полгода назад и сразу возникло желание использовать его как “повседневный” язык для разных бытовых и околорабочих задач. Знаете, у каждого есть язык, который первым приходит на ум, если нужно, скажем, проанализировать какую-то коллекцию данных или связать друг с другом несколько систем. Обычно это какой-то достаточно высокоуровневый скриптовый язык. В моем случае в этой роли выступал Python, но тут у него появился серьезный конкурент.

Однако не все можно решить, запустив блокнот Mathematica и разово выполнив код из него. Некоторые задачи требуют периодического исполнения либо запуска по какому-то событию. Нужен сервер. Для начала посмотрим, какие варианты развертывания и исполнения предлагает сама компания. Насколько я могу судить, опции следующие:
1) Старый добрый Mathematica Notebook. Иными словами, разовая рабочая сессия в GUI.
2) Wolfram Cloud. И это замечательная опция, которую использую в том числе и я. Однако есть масса причин, по которым вариант с облаком может не подойти. Назову лишь одну из них — каждый вызов стоит ненулевое количество денег. Для множества мелких периодических операций это может быть неоправданно затратно, особенно когда под рукой есть простаивающие мощности.
3) Wolfram Private Cloud. Звучит как какая-то грядущая возможность запустить собственное облако. Подробности мне неизвестны
4) Использовать Wolfram Symbolic Transfer Protocol. Выглядит как самый основательный и универсальный способ интеграции Wolfram Language в вашу систему. Сервер здесь — лишь один из частных случаев применения. Тот самый “правильный и долгий путь”.
5) Wolfram Script. Все просто — вызываем код на Wolfram Language как любой другой скрипт, без непосредственного участия графического интерфейса. Cron, pipeline и все остальные замечательные механизмы в нашем распоряжении. Этот способ мы и используем для быстрого создания сервера. Знай себе вызывай скрипт по HTTP-запросу и возвращай клиенту результат.

Непосредственно сервером мы можем выбрать что угодно, в моем случае это Tornado. Напишем простейший handler, который будет отправлять аргументы, заголовки и тело запроса в наш скрипт и считывать результаты его работы.

class MainHandler(tornado.web.RequestHandler):     def get(self):                      out = execute([WOLFRAM_EXECUTABLE,"-script", "server/main.m",                                           str(self.request.method),                                           str(json.dumps(self.request.arguments)),                                           str(json.dumps(self.request.headers)),                                           str(self.request.body)]                                           )             self.write(out) 

Собственно, “server/main.m” — это и есть наш скрипт на Wolfram Language. В нем нам нужно получить и интерпретировать переданные аргументы, а также вернуть результат.

method =     $CommandLine[[4]]  arguments =     Association @ ImportString[$CommandLine[[5]], "JSON"]  headers =     Association @ ImportString[$CommandLine[[6]], "JSON"]  body =     If[Length[$CommandLine] >= 7,$CommandLine[[7]], ""]  Print[“Hello world"] 

Наш скрипт выводит “Hello world”. Не то, чтобы для этого ему понадобились переданные в скрипт данные, но это лишь пример. Часть на питоне, в свою очередь, често возвращает эти данные клиенту.
В принципе, в этом вся суть метода. Преобразование данных в JSON и обратно, разумеется, не добавляет производительности.

В таком виде наш сервер сможет принимать и возвращать только строковые данные с кодом результата 200. Хочется немного больше гибкости. Для этого данные из скрипта должны передаваться не просто в виде строки, а в каком-то структурированном виде. Так у нас появляется еще одно преобразование в JSON и обратно. Формат его будет таким:

{      “code”: 200,      “reason”: OK,      “body”: “Hello world" } 

Теперь его нужно корректно обработать на другой стороне.

outJson =  json.loads(out)         self.set_status(outJson["code"], outJson["reason"])         if(outJson["body"] != None):             self.write(str(outJson["body"])) 

Следующим шагом будет добавление возможности возвращать не только текст, но и другие данные. Возможно, два двойных преобразования JSON казались кому-то недостаточно медленным решением… Добавим в наш JSON поля “file” и “contentType". Если поле “file" непустое, то вместо записи в поток вывода содержимого поля “body” мы считываем указанный файл.

outJson =  json.loads(out)         self.set_status(outJson["code"], outJson["reason"])         if(outJson["file"] != None):             self.add_header("Content-Type", outJson["contentType"])             with open(outJson["file"], 'rb') as f:                 while True:                     data = f.read(16384)                     if not data: break                     self.write(data)             self.finish()             os.remove(outJson["file"])         elif(outJson["body"] != None):             self.write(str(outJson["body"])) 

Взглянем на это все со стороны вызываемого скрипта. Пара методов для генерации ответа:

AsJson[input_] := ExportString[Normal @ input, "JSON"]  HTTPOut[code_, body_, reason_] :=     <|"code"->code, "body"->body, "reason"->reason, "file"->Null|>  HTTPOut[body_] :=     HTTPOut[200, body, Null]  HTTPOutFile[expression_, exportType_, contentType_] :=      Module[{filePath = FileNameJoin[{$TemporaryDirectory, "httpOutFile"}]},     Export[filePath, expression, exportType];     <|"code"->200,      "body"->Null,      "reason"->Null,      "file"->filePath,      "contentType"->contentType|> ] 

Наконец, напишем обработчики конкретных методов.

HTTPGet[arguments_, headers_] := AsJson[...]  Switch[method,      "GET", HTTPGet[arguments, headers],      "POST", HTTPPost[arguments, headers, body]] 

Таким образом, появляются методы HTTPGet, HTTPost и аналогичные. Настало время для создания бизнес-логики. Можно создать обработчики для различных путей (“/“, “/SomeEndpoint” и т.д.), но вместо этого мы добавим к вызову аргумент, который будет определять вызываемую функцию: “/?op=MyFunction”.
Осталось только добавить логику выбора и вызова этой функции в нашем скрипте. Можно написать большой Switch который и будет все делать, но вместо этого используем инструмент потоньше. Им будет ToExpression[].

HTTPGet[arguments_, headers_] :=     Module[{methodName = "GET"<>arguments["op"]},        AsJson[ToExpression[methodName][arguments, headers]]    ] 

Теперь можно просто добавить функцию GETMyFuction и первая единица бизнес-логики готова. Пусть эта функция выводит текущее время:

GETMyFuction[arguments_, headers_] :=     HTTPOut[ToString[Now]] 

Осталось привести пример вывода картинки. И раз уж мы не используем входные параметры, обозначим их безымянным паттерном, который соответствует любому количеству элементов.

GETTestGraph[___] :=     Module[{},       out = Graph[{a -> e, a -> c, b -> c, a -> d, b->d, c->a}];       HTTPOutFile[out, "PNG", "image/png"]    ] 

Теперь, при открытии в браузере “…/?op=TestGraph” можно увидеть вот такую картинку:

image

Wolfram Language помогает очень просто делать многие вещи. Например, мы с легкостью можем создать методы PUTSourceFile и DELETESourceFile, которые помогают расширять API нашего сервера, используя его же (без перезапуска сервера, разумеется). Это является первым шагом к созданию аналога CloudDeploy, но только работающего с нашим собственным неспешным личным сервером.

Однако, это выходит за рамки данной статьи. На этом всё и удачного дня!

ссылка на оригинал статьи http://habrahabr.ru/post/262983/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *