logo
+7 (495) 997-37-74
Москва, ул.Международная, 15

Веб-сервер на Processing. Часть 1 — Код сервера

Как мы уже успели понять из предыдущих статей, 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