Как мы уже успели понять из предыдущих статей, Processing — это чрезвычайно развитый инструмент со множеством возможностей по созданию визуальных, мультимедийных, аналитических, сетевых и прочих программ. В этой статье мы начнём знакомство с одной из интересных возможностей Processing — созданием настоящего веб-сервера.
Это сама по себе интересная и полезная в плане обучения современным веб-технологиям тема, но научившись создавать собственные веб-сервера на Processing, вы сможете применить эти знания и для своих практических нужд, например, создав сервер с уникальными и нужными вам функциями в своей локальной сети.
В процессе рассмотрения этой задачи мы коснёмся таких вопросов, как программирование на HTML, CSS, структура директорий и файлов веб-сервера, протоколы передачи данных (HTTP) и множества других интересных тем.
Скетч веб-сервера
Рассмотрение нашей темы мы начнём с разбора работы скетча веб-сервера на Processing. В качестве исходного кода мы возьмём пример с Github, который немного переработаем и в который внесём некоторые изменения и улучшения.
Основная часть кода веб-сервера на Processing:
/* Processing Web Server */ import processing.net.*; Server webServer; Request req; Response resp; String path = ""; void setup() { size(200, 200); webServer = new Server(this, 8080); } void draw() { Client cl = webServer.available(); if (cl != null && cl.active()) { String request_string = cl.readString(); req = new Request(request_string); resp = new Response(); req.parse(); if (req.valid && req.method.equals("GET")) { // Remove double dots to prevent the client from accessing files outside of the public directory String sanitized_route = req.route.replaceAll("\\.\\.", ""); path = sketchPath("") + "public" + sanitized_route; println("yes"); // If the file exists read it into the body of the request File f = new File(path); if (f.exists() && !f.isDirectory()) { resp.body = loadBytes(path); if (match(path, "\\.html$") != null) {resp.type = "text/html";} else if (match(path, "\\.css$") != null) {resp.type = "text/css";} else if (match(path, "\\.png$") != null) {resp.type = "image/png";} } else if (f.isDirectory() && new File(path + "index.html").exists()) { // If a directory is accessed load it's index.html file by default resp.body = loadBytes(path + "index.html"); resp.type = "text/html"; } else { resp.http_status = 404; resp.body = "Not found.".getBytes(); } } else { // if (req.valid && req.method.equals("GET")) resp.http_status = 500; } String head = resp.makeMeta(); println(" <= " + join(split(head, "\r\n"), " ")); cl.write(head); cl.write(resp.body); cl.stop(); } // if (cl != null && cl.active()) } // draw
Теперь подробно разберём его работу. Вначале подключаем специализированную сетевую библиотеку.
import processing.net.*;
Объявляем объект webServer.
Server webServer;
И создаём объекты для обработки запросов клиентов (req) и ответов нашего сервера (resp).
Request req; Response resp;
И создаём строковую переменную path для хранения пути к файлам нашего веб-сервера
String path = "";
Далее в функции setup() создаём стандартное для скетчей на Processing окно и создаём объект Server, передавая ему в качестве параметра номер порта 8080.
void setup() { size(200, 200); webServer = new Server(this, 8080); }
Стандартное окно скетча на Processing:
Затем в циклической функции draw() начинаем работу нашего веб-сервера. В начале каждого цикла «получаем» клиента при помощи функции available().
void draw() { Client cl = webServer.available();
Далее проверяем состояние клиента и если он не равен null и активен, то производим обработку запроса. Обратите внимание, что стиль обработки запросов и сам код Processing сервера очень похож на код аналогичного сервера на Wiring для микроконтроллеров Arduino. Различие здесь состоит в том, что Processing работает на «больших компьютерах», поэтому имеет в своём распоряжении больше ресурсов и, в связи с этим, имеет более «мощные» и простые (лаконичные) функции.
Также нужно отметить, что веб-сервер на Processing не имеет ограничений, свойственных реализации веб-серверов на Ардуино (например, на количество сетевых сокетов).
if (cl != null && cl.active()) {
Затем мы получаем запрос клиента и присваиваем его строковой переменной request_string, с которой далее будем проводить различные манипуляции по анализу и обработке её содержимого.
String request_string = cl.readString();
И создаём экземпляр объекта Request, передавая ему в качестве параметра значение переменной request_string.
req = new Request(request_string);
Теперь разберём подробнее код класса обработчика запросов Request.
Класс Request
Код класса Request:
/* Request */ class Request { HashMap<String,String> headers = new HashMap<String,String>(); String request; String[] requestArray; String[] firstLine; String method; String route; Boolean valid = false; Request(String request_) { request = request_; } void parse() { requestArray = split(request, "\r\n"); firstLine = split(requestArray[0], ' '); method = firstLine[0]; route = firstLine[1]; for (int i = 1; i < requestArray.length; i++) { String keyval[] = split(requestArray[i], ": "); if (keyval.length == 2) { // This catches blank lines, etc headers.put(keyval[0], keyval[1]); valid = true; } } } } //class Request
Здесь мы создаём объект headers как экземпляр HashMap (ассоциативного массива «ключ-значение») для последующего хранения стандартных значений GET-запроса клиента. В качестве и ключа и значения здесь выступают строки (String).
HashMap<String,String> headers = new HashMap<String,String>();
Пример пар ключ-значение GET-запроса, которые затем будут помещаться в массив headers:
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 Accept-Language: ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate DNT: 1 Connection: keep-alive
Далее создаются вспомогательные переменные, необходимые для обработки запроса.
String request; String[] requestArray; String[] firstLine; String method; String route; Boolean valid = false;
Конструктор Request с присвоением соответствующего параметра (содержимого запроса).
Request(String request_) { request = request_; }
И непосредственно сама функция обработки (парсинга) запроса.
void parse() { requestArray = split(request, "\r\n"); firstLine = split(requestArray[0], ' '); method = firstLine[0]; route = firstLine[1]; for (int i = 1; i < requestArray.length; i++) { String keyval[] = split(requestArray[i], ": "); if (keyval.length == 2) { // This catches blank lines, etc headers.put(keyval[0], keyval[1]); valid = true; } } }
Здесь мы разделяем запрос на отдельные строки и помещаем их в соответствующий массив.
requestArray = split(request, "\r\n");
Выделяем первую строку запроса.
firstLine = split(requestArray[0], ' ');
И присваиваем соответствующем переменным значения «метода» (в нашем случае GET) и пути (route) соответствующей страницы (файла).
method = firstLine[0]; route = firstLine[1];
Далее перебираем все строки запроса.
for (int i = 1; i < requestArray.length; i++) {
Выделяем из них пары ключ-значение.
String keyval[] = split(requestArray[i], ": ");
И помещаем их в массив headers.
if (keyval.length == 2) { // This catches blank lines, etc headers.put(keyval[0], keyval[1]); valid = true; }
Причём отбраковываем пустые «технические» строки без пар ключ-значение.
if (keyval.length == 2) { // This catches blank lines, etc
И в случае нахождения хотя бы одной такой пары делаем вывод о «валидности» запроса.
valid = true;
Тут обращает на себя внимание лаконичность и предельная простота и выразительность кода — довольно сложная логика (обработки запроса) реализуется всего несколькими функциями.
Теперь возвращаемся к коду основного скетча и создаём объект resp как экземпляр класса Response.
resp = new Response();
Класс Response
Код класса Response:
/* Response */ class Response { // Didn't use a hashmap, so the type is slightly different int http_status; String status = "HTTP/1.1 "; String response; String type; byte body[]; Response() { http_status = 200; response = "HTTP/1.1 "; type = "text/html"; } String makeMeta() { response += str(http_status); if (http_status == 200) {response += " OK";} else if (http_status == 404) {response += " Not Found";} else if (http_status == 500) {response += " Server Error";} response += "\r\n"; response += "Content-Type: " + type + "\r\n\r\n"; return response; } } // class Response
В классе Response вначале мы создаём вспомогательные переменные, необходимые для формирования ответа нашего сервера.
int http_status; String status = "HTTP/1.1 "; String response; String type; byte body[];
В конструкторе класса Response мы задаём переменным стандартные (default) значения. Эти значения подходят в большинстве случаев, но при необходимости могут быть изменены в скетче.
Response() { http_status = 200; response = "HTTP/1.1 "; type = "text/html"; }
Непосредственным формированием (стандартного) ответа сервера на запросы клиента занимается функция makeMeta().
String makeMeta() { response += str(http_status); if (http_status == 200) {response += " OK";} else if (http_status == 404) {response += " Not Found";} else if (http_status == 500) {response += " Server Error";} response += "\r\n"; response += "Content-Type: " + type + "\r\n\r\n"; return response; }
Здесь к значению переменной response («HTTP/1.1») добавляется цифровой код статуса ответа сервера (при успешном ответе это код 200).
response += str(http_status);
Пример сформированного ответа:
HTTP/1.1 200 OK Content-Type: text/html
И затем вычисляется и добавляется строковое значение результата операции.
if (http_status == 200) {response += " OK";} else if (http_status == 404) {response += " Not Found";} else if (http_status == 500) {response += " Server Error";}
Далее формируется значение Content-Type для ответа сервера.
response += "\r\n"; response += "Content-Type: " + type + "\r\n\r\n";
Таким образом осуществляется формирование стандартной части ответа нашего сервера.
Теперь переходим к дальнейшему разбору работы кода основного скетча.
Основной скетч
Далее осуществляем парсинг запроса по вышеописанной схеме при помощи функции req.parse().
req.parse();
Затем определяем пригодность запроса для обработки нашим сервером.
if (req.valid && req.method.equals("GET")) {
И в случае положительного решения, начинаем обработку удаляя из запроса все точки идущие подряд (для исключения доступа к файлам вне стандартной «публичной» директории нашего сервера).
String sanitized_route = req.route.replaceAll("\\.\\.", "");
И формируем путь к странице (файлу) «склеивая» его из пути к нашему серверу на диске, директории «public» и «санированного» пути к странице.
path = sketchPath("") + "public" + sanitized_route;
Тут же выводим сообщение в консоль об успешной обработке запроса.
println("yes");
Затем производим дальнейший разбор запроса и загружаем в тело ответа содержимое необходимого файла.
// If the file exists read it into the body of the request File f = new File(path); if (f.exists() && !f.isDirectory()) { resp.body = loadBytes(path);
И присваиваем соответствующий тип переменной resp.type.
if (match(path, "\\.html$") != null) {resp.type = "text/html";} else if (match(path, "\\.css$") != null) {resp.type = "text/css";} else if (match(path, "\\.png$") != null) {resp.type = "image/png";}
Если в запросе указана директория или файл index.html, то выдаём главную страницу.
else if (f.isDirectory() && new File(path + "index.html").exists()) { // If a directory is accessed load it's index.html file by default resp.body = loadBytes(path + "index.html"); resp.type = "text/html";
А если не выполняется ни первое, ни второе условие, то выводим сообщение с ошибкой 404 и уведомлением, что требуемый файл не найден.
} else { resp.http_status = 404; resp.body = "Not found.".getBytes(); }
Ну и в случае, когда ни одно условие не выполняется, выводим ошибку 500, означающую, что наш сервер не может корректно обработать запрос и не может уточнить причину этой ошибки.
} else { // if (req.valid && req.method.equals("GET")) resp.http_status = 500; }
Далее мы формируем стандартную мета-информацию для ответа сервера (этот процесс мы подробно разобрали выше, при анализе работы класса Responce).
String head = resp.makeMeta();
И выводим тестовую информацию об ответе сервера в консоль.
println(" <= " + join(split(head, "\r\n"), " "));
Пример тестового вывода нашего скетча в консоль:
Ну и в завершение, выдаём сформированный ответ (страницу) клиенту.
cl.write(head); cl.write(resp.body); cl.stop();
Пример работы
В результате работы нашего скетча любой клиент, обратившийся к нашему серверу с корректным запросом по HTTP протоколу, получит в ответ нужную веб-страницу, точно так же, как это происходит с «большими» веб-серверами и сайтами в интернете.
Поскольку наш сервер и наш веб-клиент (браузер) находятся на одном и том же компьютере, то в качестве адреса в строке браузера нужно задавать localhost и соответствующий порт (8080).
http://localhost:8080/
Вот пример подобного обращения к нашему серверу:
И код HTML страницы, формирующей его:
Заключение
В этой статье мы рассмотрели работу «движка» веб-сервера на Processing, в следующей статье мы рассмотрим работу с файлами и директориями нашего сервера и программирование внешнего вида веб-страниц, выдаваемых им, на HTML и CSS, а также прочие нюансы работы его работы.
Ссылки по теме
Система программирования Processing
Processing и Ардуино. Работа по Serial
Сетевые возможности Processing
Работа Processing в браузере и на сайте
Веб-сервер на Processing. Часть 1 — Код сервера
Веб-сервер на Processing. Часть 2 — Файлы и код на HTML и CSS