Простий http-сервер на Wolfram Language

Преамбула

Принципова схема роботи сервера
Ідея написання цієї статті виникла у мене після прочитання схожою статті на Хабрахабр, де розповідається про те, як створити власний сервер на localhost з використанням Mathematica. Сам веб-сервер був написаний з використанням Python і розширюваного сервера Tornado. Він обробляв запити і відправляв відповіді у форматі json, а логіка була реалізована в Mathematica. При цьому спілкування між Python і Mathematica відбувалося за допомогою командного рядка, а кожен запит на сервер перезапускал ядро Математики. Інші подробиці можна прочитати в самій статті автора @Nilis. Тут я хотів би показати як написати простий код, який буде виконувати аналогічні функції — тобто створити http-сервер для обробки запитів і надіслання відповідей. Плюс хотілося б показати деякі цікаві можливості Wolfram Language і його синтаксису.
Порожня реалізація сервера
Найпростіше почати реалізацію основної функції, а потім вже просуватися вглиб. Для того, щоб її написати — треба завантажити контекст SocketLink`, виконавши наступну команду:
Needs["SocketLink`"]; 

Після цього стають доступні функції цього контексту. Подивитися які функції він містить можна так:
Information["SocketLink`*"]; 





SocketLink` CreateAsynchronousServer CreateServerSocket CreateClientSocket OpenSocketStreams
Пояснимо, що кожна представляє:
  1. CreateClientSocket[port] — створює сокет з використанням порту — port;
  2. CreateServerSocket[host, port] — створює клієнтський сокет підключається до хосту — host і використовує порт — port;
  3. OpenSocketStreams[socket] — відкриває потік вводу і виводу у сокета — socket;
  4. CreateAsynchronousServer[socket, handler] — створює "сервер" з зазначеним сокетом — socket і обробником — handler.
У найбільш простому випадку нам знадобиться всього дві функції з цього контексту:
CreateAsynchronousServer і CreateServerSocket. Звичайно на перший погляд здається, що вже все готово і ніякого сенсу в "написанні" сервера немає. Але це не так. CreateAsynchronousServer — не вміє робити нічого крім прослуховування зазначеного сокета. Значить реалізовувати все інше доведеться нам. Для початку непогано було б створити допоміжну функцію, яка буде мати обмеження на вхідні аргументи:
MathematicaSimpleServer[socket_Socket, handler_Handler] := 
CreateAsynchronousServer[socket, handler]; 

Пояснення для користувачів Mathematica. Ліворуч від знака ":=" SetDelayed[]) знаходиться шаблон, який після виконання збережеться в пам'яті. Справа знаходиться правило, яке буде виконуватися при зустрічі шаблону. Вказівка у вигляді socket_Socket — говорить про те, що перший аргумент повинен бути сокетом. Шаблон handler_Handler — говорить про те, що другий аргумент повинен мати тип Handler. Цього типу в даний момент не існує. Однак Mathematica не звертає на це увагу і дозволяє в будь-якому випадку створити функцію, яка буде приймати в якості одного з вхідних параметрів неіснуючий тип.
Якщо цього типу не існує — значить його потрібно створити. Займемося цим. Наступний код показує, яким чином в Mathematica можна створити власний простий тип даних з використанням TagSetDelayed[]:
(* getters for Handler *)
Handler /: 
GetRequestParser[Handler[parser_RequestParser, _]] := parser; 

Handler /: 
GetResponseGenerator[Handler[_, generator_ResponseGenerator]] := generator; 

(* setters for Handler *)
Handler /: 
SetRequestParser[handler_Handler, parser_RequestParser] := 
Handler[handler, GetResponseGenerator[handler]]; 

Handler /: 
SetResponseGenerator[handler_Handler, generator_ResponseGenerator] := 
Handler[GetRequestParser[handler], generator]; 

Пояснення синтаксису для користувачів Mathematica. Знак "/:" TagSetDelayed[]) означає, що тільки для типу даних (Handler), який знаходиться зліва буде перезаписати робота функції, визначення якої цілком знаходиться праворуч. Правило, яке буде виконуватися при виклику функції знаходиться як зазвичай праворуч від знака ":=". Цей спосіб буде працювати в тому числі і для захищених системних функцій. Так як в цьому випадку відбувається зміна не пов'язане з ім'ям самої функції, а з ім'ям типу. Деяка особливість полягає в тому, що усередині шаблону між знаками "/:" і ":=" повинен обов'язково в явному вигляді де перебуває тип Handler (але не на верхньому рівні). Знову ж варто зауважити, що чотири функції вище були визначені з використанням ще не існуючих типів даних: RequestParser, ResponseGenerator. Тепер залишився останній штрих у визначенні обробника — це створити шаблон, який буде виконуватися при виклику обробника всередині сервера. Він зобов'язаний приймати на вхід список із двох елементів — потоків вводу та виводу. Дії з цими потоками можна виконувати практично будь-які. Як було сказано вище — обробник повинен читати потік вводу і записувати в потік виводу. Реалізуємо це наступним чином:
handler_Handler[{input_InputStream, output_OutputStream}] := 
Module[{
requestParser = GetRequestParser[handler], 
responseGenerator = GetResponseGenerator[handler], 
request = "", response = ""
}, 

(* read data from input stream of the socket *)
{request} = 
If[# != {}, FromCharacterCode[#], 
Print[DateString[], "\nERROR"]; Close[input]; Close[output]; Return[]]& @ 
Last[Reap[While[True, TimeConstrained[
Sow[BinaryRead[input]], 0.01, 
(Close[input]; Break[])
];];]]; 

(* logging *)
Print[DateString[], "\nREQUEST:\n\n", request]; 

(* processing request *)
response = responseGenerator[requestParser[request]]; 

(* logging and writing data to the output stream *)
Switch[response, 
_String, 
Print[DateString[], "\nRESPONSE:\n\n", response];
BinaryWrite[output, ToCharacterCode[response]], 
{__Integer}, 
Print[DateString[], "\nRESPONSE:\n\n", FromCharacterCode[response]];
BinaryWrite[output, response]; 
]; 
Close[output]; 
]; 

Як не дивно — але для Mathematica зовсім не обов'язково створювати якесь одне унікальне ім'я в лівій частині шаблону. Це може бути складний вираз із заголовком і внутрішнім вмістом. Такий спосіб створення визначень функцій досить рідко використовується, але в нашому випадку він буде дуже корисний. Внутрішній вміст обробника визначає всю логигу роботи сервера. Тепер порожня реалізація сервера готова. Вона буде робочою, але нічого корисного робити не буде. Адже вся обробка запиту лежить на неіснуючих функціях: requestParser, responseGenerator. В нашому випадку на їх вхід передається рядок, і результатом повинна бути рядок (або список байт, на що натякає другий варіант вибору перемикача Swich[]). Хоча ніхто не заважає повертати після читання запиту все що завгодно, тільки за умови, що це "що завгодно" буде коректно оброблятися функцією для створення відповідей.
Обробка запиту
Тепер, коли сервер готовий, необхідно подбати про реалізацію типу RequestParser. Саме він буде використовуватися першим. Точно таким же чином, як це було зроблено вище для Handler, створимо найпростіше визначення:
requestParser_RequestParser[request_String] := request; 

Згідно з цим визначенням функція буде повертати просто саму рядок запиту. Для початку цього вистачить. Тепер все теж саме, але для генератора відповідей:
responseGenerator_ResponseGenerator[parsed_String] := 
"HTTP/1.1 200 OK
Content-Length: 1024
Connection: close

<!DOCTYPE html>
<html>
<head>
< meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />
<title>MathematicaSimpleServer</title>
</head>
<body>
Hello from mathematica simple server!
</body>
</html>"

Визначення генератора теж найпростіше. Це просто рядок відповіді поєднана з html-розміткою відображуваної сторінки. Тепер точно все готово! Можна спробувати запустити сервер і перевірити, як він буде працювати. Зробити це можна виконавши наступний код:
socket = CreateServerSocket[8888]; 
handler = Handler[RequestParser[], ResponseGenerator[]]; 
server = MathematicaSimpleServer[socket, handler]; 

Тепер відкриємо браузер і перейдемо за адресою http://localhost:8888/. У браузері відкриється сторінка наступного виду:

При цьому у вікно Messages надрукується лог запиту:
Thu 19 Jan 2017 01:22:00
REQUEST:

GET / HTTP/1.1

Accept: text/html, application/xhtml+xml, image/jxr, */*

Accept-Language: ru-RU

User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393

Accept-Encoding: gzip, deflate

Host: localhost:8888

Connection: Keep-Alive

лог відповіді:
Wed 18 Jan 2017 14:56:45
RESPONSE:

HTTP/1.1 200 OK
Content-Length: 1024
Connection: close

<!DOCTYPE html>
<html>
<head>
< meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>MathematicaSimpleServer</title>
</head>
<body>
Hello from mathematica simple server!
</body>
</html>

Ура! Наш сервер все таки працює. Цікаво, а що буде якщо ми не будемо його зупиняти і спробуємо змінити код відображається html-сторінки, який повертається під час виклику ResponseGenerator[]? Зробимо це — просто визначимо функцію ще раз:
responseGenerator_ResponseGenerator[parsed_String] := 
("HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: " <> ToString[StringLength[#]] <> "
Connection: close

" <> #)& @ "<!DOCTYPE html>
<html>
<head>
< meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />
<title>MathematicaSimpleServer</title>
</head>
<body>
Hello from mathematica simple server! <br/>
we changed the server logic without stopping ;)
</body>
</html>"


Після виконання коду вище і оновлення сторінки в браузері відображається змінений контент. Виходить, що можна не зупиняти роботу веб-сервера і продовжувати додавати нові визначення ResponseGenerator і RequestParser. Але тим не менш корисно знати, яким чином можна його зупинити. Досить виконати код:
SocketLink'Sockets'CloseSocket[socket]; 
StopAsynchronousTask[server]; 

Розширення можливостей сервера
Невелика підготовка. Щоб надалі не виникло жодних проблем, відразу встановимо в якості робочої директорію розташування цього документа:
SetDirectory[NotebookDirectory[]]; 

Очевидно, що відображення двох рядків у вікні браузера — не дуже хороша демонстрація можливостей Mathematica. Заради тестових цілей створимо просту html-сторінку index. Код сторінки представлений нижче. Тут вже є кілька посилань на інші адреси, які повинен повертати сервер. Так само додана функція, яка створює сам відповідь цілком.
IndexPage[] := 
"<!DOCTYPE html>
<html>
<head>
< meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />
<title>MathematicaSimpleServer</title>
<link rel=\"icon\" type=\"image/png\" href=\"/favicon.png\"/>
</head>
<body>
Index page for the mathematica simple server

<ul>
<li><a href="/graphic?Sin\" >graphic</a></li>
<li><a href="/page.html\" >page</a></li>
<li><a href="notebook.nb\" >notebook</a></li>
</ul>
</body>
</html>"; 

ResponseString[page_String] := 
StringTemplate["HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: `length`
Connection: close

`page`"][<|"length" -> StringLength[page], "page" -> page|>]; 

ResponseString[page_String, length_Integer] := 
StringTemplate["HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: `length`
Connection: close

`page`"][<|"length" -> length, "page" -> page|>]; 

Функція, що з'єднує рядок відповіді та код розмітки сторінки ResponseString[] має два визначення: перше обчислює розмір сторінки і замінює значення заголовка Content-Length на отриманий результат. У другому визначенні можна самостійно вказати розмір тіла відповіді. Як вже було сказано вище — на головній сторінці є декілька посилань. Передбачається, що ведуть посилання на одну з адрес, де виконується якась своя серверна логіка. Всього різних випадків чотири — це відображення самої головної сторінки, посилання на довільний графік, на html-сторінку та сторінку, утворену з готового блокнота Математики. Кожен з випадків потрібно розглядати окремо. Перший випадок, завантаження основної сторінки. Вона виконується тільки якщо був отриманий GET запит за адресою "/" або "/index". Як перевірити це адреса? Вступити можна різними способами. Нижче показаний не самий популярний, але цікавий. Спочатку переопределим функції Keys і Part на тип даних RequestParser наступним чином:
RequestParser /: 
Keys[RequestParser[rules___Rule]] := {rules}[[All, 1]]; 

RequestParser /: 
Part[RequestParser[___, "Address" -> function: (_Symbol|_Function), ___], "Address"] := 
function; 

Варто пояснити синтаксис виразів вище. rules___Rule — являє собою шаблон з довільним (в тому числі і нуль) кількістю правил заміни. Це означає, що RequestParser можна створювати використовуючи будь-яку кількість обробних функцій, а всі імена цих функцій можна буде отримати просто використавши функцію Keys. Цікаво, що Keys є системною функцій з атрибутом Protected, який забороняє змінювати цю функцію, але вказівка типу за допомогою TagSetDelayed дозволяє це зробити. Точно так само було перевизначено вбудована функція Part. Як вже було сказано вище, передбачається, що RequestParser — це складне вираз, яке всередині себе має містити набір правил. Кожне правило — це ключ і значення (функція для обробки). Навіщо ж все це знадобилося? Писати велику кількість умов і перевірок рядка запиту досить важко. Тим більше легко помилитися в порядку ухвал, так як якщо погано підібрати регулярний вираз або шаблон для перевірки запиту, то деякі ділянки коду виявляться недосяжними. Набагато простіше обробляти один запит відразу декількома функціями і повертати результат у вигляді асоціації з парами: ім'я функції і результат. Нижче представлена реалізація цього способу:
requestParser_RequestParser[request_String] /; 
MatchQ[requestParser, RequestParser[_String -> (_Symbol|_Function)]] := 
Association[Map[Function[key, key -> (requestParser[[key]])[request]], Keys[requestParser]]]; 

Тепер необхідно створити ті самі функції. У нас буде всього одна така функція для отримання адреси з методу запиту, тому що фантазія автора всю варіативність роботи сервера зуміла укласти тільки в відмінність адрес в першому рядку запиту.
TakeAddress[request_String] := 
First[StringCases["GET " ~~ address___ ~~ " HTTP/" -> address][request]]; 

Для тих хто не знайомий з Mathematica — для рядкових виразів у мові є безліч цікавих конструкцій. Конструкція вище інтуїтивно зрозуміла, вона просто вибирає весь текст, що знаходиться між HTTP GET і в першому рядку. У попередньої реалізації сервера генератор відповідей обробляв рядок, який повертав обробник запиту. Але тепер на вхід цієї функції буде передаватися асоціація з пар ключ-значення. Значить необхідно для генератора створити нове визначення, яке зможе перетворити отриману асоціацію у відповідь.
responseGenerator_ResponseGenerator[parsed_Association] /; 
parsed[["Address"]] == "/" || parsed[["Address"]] == "/index" := 
ResponseString[IndexPage[]]; 

Перезапустим сервер з новим обробником. Тепер усередині RequestParser знаходиться вказівка імені функції — "Address" — і самої функції TakeAddress — з допомогою якої обробляються всі відповіді.
socket = CreateServerSocket[8888]; 
handler = Handler[RequestParser["Address" -> TakeAddress], ResponseGenerator[]]; 
server = MathematicaSimpleServer[socket, handler]; 


Головна сторінка працює. Додамо правильну обробку запитів до інших ресурсів. Перший з них — це спроба отримання графіка зазначеної функції. Спочатку додамо визначення генератора відповідей. Ще одна цікава особливість Mathematica. Перевизначити функцію можна не тільки вказавши інші типи аргументів або інше їх число. Так само можна встановити довільне складну умову виконання функції з допомогою знака "/;" Condition[]). Умова найзручніше писати тим самим шаблоном (ім'я та аргументи/сигнатура функції) після знаку "/;" і до знака ":=".
responseGenerator_ResponseGenerator[parsed_Association] /; 
StringMatchQ[parsed[["Address"]], "/graphic?" ~~ ___] := 
Module[{function = ToExpression[StringSplit[parsed[["Address"]], "?"][[-1]]]}, 
ResponseString[ExportString[Plot[function[x], {x, -5, 5}], "SVG"]]
];

Можна перевірити як це буде працювати — перейти по першій посилання на сторінку http://localhost:8888/index або просто за адресою http://localhost:8888/graphic?Sin. Тепер там відображається наступне:

Якщо ж замість ../graphic?Sin написати ../graphic?Cos або навіть Log/Exp/Sqrt/Function[x,x^3]/т. д., то на сторінці відобразиться відповідний графік. Тепер додамо в обробник можливість відображати довільну html-сторінку, яка розташовується в робочій директорії або в одній з піддиректорій. Для початку створимо цю сторінку використавши код:
Export["page.html", TableForm[Table[{ToString[i] <> "!", i!}, {i, 1, 9}]], "HTML"]; 

На жаль, будь-який виконав код рядка вище відразу ж помітить, що експорт виразів Mathematica у формат html займає досить багато часу. Виходить, що для Математики не складе труднощів створити безліч сторінок або інших елементів (картинок/таблиць), але експорт всіх цих даних кожен раз буде займати значно більше часу ніж створення стандартними засобами. Що ж. Будемо припускати, що всі сторінки вже існують і знаходяться на диску в робочій директорії сервера — там, куди ми тільки що зберегли тестову сторінку. Тепер спробуємо відобразити її. У цьому випадку сторінка і будь-які її елементи імпортуються у вигляді списку байт і з'єднуються зі списком байт, які відповідають рядку заголовків відповіді.
responseGenerator_ResponseGenerator[parsed_Association] /; 
FileExistsQ[FileNameJoin[{Directory[], parsed[["Address"]]}]] && 
StringTake[parsed[["Address"]], -3] != ".nb" := 
Module[{path = FileNameJoin[{Directory[], parsed[["Address"]]}], data}, 

data = Import[path, "Byte"]; 
Join[ToCharacterCode[ResponseString["", Length[data]]], data]
];


Як і очікувалося, сервер повернув браузеру таблицю значень факторіала цілих чисел. Останній випадок. Відображення в браузері збереженого блокнота. У робочій директорії створимо новий блокнот notebook.nb за допомогою коду:
notebook = CreateDocument[{TextCell["Bubble sort","Section"], 
ExpressionCell[Defer[list = RandomInteger[{0, 9}, 20]], "Input"], 
ExpressionCell[Defer[list //. {firsts___, prev_, next_, lasts___} :> 
{firsts, next, prev, lasts} /; 
next < prev], "Input"]
}]; 

NotebookEvaluate[notebook, InsertResults -> True]; 
NotebookSave[notebook, FileNameJoin[{Directory[], "notebook.nb"}]]
NotebookClose[notebook]; 

Тепер виконуємо дії схожі на ті, що відбувалися при запиті html-сторінки. Але перед поверненням html-коду спочатку блокнот конвертується в html.
responseGenerator_ResponseGenerator[parsed_Association] /; 
FileExistsQ[FileNameJoin[{Directory[], parsed[["Address"]]}]] && 
StringTake[parsed[["Address"]], -3] == ".nb" := 
Module[{path, data, notebook}, 

path = FileNameJoin[{Directory[], parsed[["Address"]]}]; 
notebook = Import[path, "NB"]; 
Export[path <> ".html", notebook, "HTML"]; 

data = Import[path <> ".html", "Byte"]; 
Join[ToCharacterCode[ResponseString["", Length[data]]], data]
];


Точного відображення блокнот в браузері звичайно ж не буде. Все залежить від способу експорту Математикою блокнота в html. Також для нашого сервера варто було б додати сторінку помилки, яку буде відображатись користувачу у разі відсутності ресурсу.
responseGenerator_ResponseGenerator[parsed_Association] /; 
!FileExistsQ[FileNameJoin[{Directory[], parsed[["Address"]]}]] := 
Join[ToCharacterCode[StringReplace[ResponseString["Page not found"], "200 OK" -> "404 !OK"]]];

Очевидно, що показана у цій статті реалізація веб-сервера навіть близько не є повною. Більше того, вона володіє великими недоліками. Як приклад можна навести хоча б те, що сервер в змозі повертати лише два коду: 200 і 404. Тому представлений код найкраще розглядати як експериментальний/демонстративний.
Юридичний аспект
Як стало відомо автору — стандартна ліцензія на Wolfram Mathematica не дозволяє використовувати ядро Математики всередині серверних додатків і для комерційних і для особистих цілей. Це обмеження не поширюється запланованих завдань, які не повинні бути пов'язані з вебом. У Wolfram Research є власна дуже хороша платформа для реалізації великих (і не дуже) серверних додатків — це webMathematica. Саме ліцензія на webMathematica дає можливість уникнути проблем з законом і використовувати Wolfram Language на сервері, причому не важливо буде програма розроблена на ній (webMathematica) чи ні, ліцензію придбати все одно доведеться. Автор дотримується думки, що його код ніяких ліцензійних угод не порушує, тому тут показаний просто текст програми дозволяє вбудованими засобами Mathematica запустити веб-сервер на localhost. Адже детектив, в якому описується злочин сам по собі не є злочином.
Висновок
У цій статті мені хотілося продемонструвати в першу чергу цікаві синтаксичні можливості Wolfram Language. Різні способи створення функцій, правил і умов при вирішенні конкретної задачі нестандартним способом. А також я сподіваюся, що цей посібник дозволить ентузіастам попрактикуватися в веб-розробці комбінуючи це з вивченням Mathematica і її можливостей. У свою чергу чекаю усіляких рад щодо поліпшення коду, можливі ідеї для реалізації в рамках цієї задачі, а так само критику і зауваження. Скачати блокнот містить дану роботу можна за наступною посилання. Всім дякую за увагу!
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.