CGI, AJAX и распи два

Тема в разделе "Глядите, что я сделал", создана пользователем parovoZZ, 17 ноя 2019.

  1. parovoZZ

    parovoZZ Гуру

    Цель данной лабораторной работы - показать, как обновлять информацию на веб странице без перезагрузки самой страницы. Для этого нам потребуется веб-браузер и веб-сервер с поддержкой технологии CGI. Я буду использовать веб-сервер lighttpd, который будет установлен на одноплатный компьютер raspberry PI 2. Впрочем, можно использовать абсолютно любой другой.
    Про CGI можно почитать в интернетах, но вкратце - это технология межпроцессного обмена по каналам stdin и stdout. Применительно к нашему случаю, это означает, что по нашей просьбе веб-сервер запускает CGI программу, передаёт ей какие-то параметры и после выполнения этой программы получает готовые данные. Программой может быть любой исполняемый файл, способный выводить информацию в stdout и получать её из stdin. Такой программой может быть и скрипт при условии, что на компьютере установлен интерпретатор того языка, на котором написан этот скрипт. Это самый простой путь, т.к. не требует предварительной компиляции. Давайте с него и начнем. Будем писать в среде bash.
    Создадим в домашней директории новый файл. Я пользуюсь текстовым редактором nano. Вы можете использовать любой другой.
    Код (Bash):
    nano newfile
    При этом будет создан файл newfile в домашней папке пользователя и сразу же открыт в редакторе.
    Например, мы хотим узнать дату и время на нашей малинке. Так и запишем в наш файл:
    Код (Bash):
    date
    Сохраняем файл путем нажатия клавиш ctrl и o. Чтобы выйти из редактора, нажимаем ctrl и х. Теперь, если мы откроем этот файл в интерпретаторе
    Код (Bash):
    bash newfile
    нам будет выведена дата и время на малинке.
    Теперь запустим наш скрипт на исполнение:
    Код (Bash):
    ./newfile
    но вместо даты и времени мы получим:
    Код (Bash):
    -bash: ./newfile: Permission denied
     
    Почему? Да потому, что нет прав у нашего скрипта на исполнение.
    Код (Bash):
    ls -l newfile
    -rw-r--r-- 1 pi pi 5 Nov 17 17:34 newfile
    Давайте их добавим
    Код (Bash):
    chmod +x newfile
    И теперь
    Код (Bash):
    ./newfile
    Sun 17 Nov 18:08:28 MSK 2019
     
    Но на дату смотреть скучно, давайте посмотрим на температуру. Для этого запишем в наш файл:
    Код (Bash):
    cat /sys/class/thermal/thermal_zone0/temp
    Если запустим наш скрипт на исполнение, то увидим примерно такую цифру:
    Код (Bash):
    38470
    Чтобы получить градусы, эту цифру необходимо разделить на тысячу. Для этого воспользуемся командой let:
    Код (Bash):
    temp=`cat /sys/class/thermal/thermal_zone0/temp`
    let "temp =  temp / 1000"
    echo "Температура процессора $temp градусов"
     
    После запуска скрипт покажет следующее:
    Код (Bash):
    Температура процессора 37 градусов
    Ставим веб-сервер. Я буду использовать lighttpd:
    Код (Bash):
    sudo apt install lighttpd
    Также необходимо установить модуль CGI:
    Код (Bash):
    sudo lighty-enable-mod cgi
    Уже сейчас, если в веб-браузере набрать IP адрес малинки, увидим приветственную страницу веб-сервера. Если порт 80 занят или есть желание его сменить, то делается это в файле lighttpd.conf, который лежит в папке /etc/lighttpd
    Для поддержки веб-сервером CGI программ, необходимо подкорректировать файл 10-cgi.conf:
    Код (Bash):
    nano /etc/lighttpd/conf-enabled/10-cgi.conf
    У меня содержимое этого файла выглядит так:
    Код (Bash):

    server.modules += ( "mod_cgi" )

    alias.url += ( "/cgi-bin/" => "/usr/lib/cgi-bin/" )
    $HTTP["url"] =~ "^/cgi-bin/" {
            cgi.assign = ( "" => "/bin/bash" )
    }
     
    По умолчанию для raspbian (которая на базе debian), местом хранения CGI программ является путь /usr/lib/cgi-bin/. Менять его не будем.
    После изменения конфигурационного файла, необходимо перезапустить веб-сервер:
    Код (Bash):
     sudo /etc/init.d/lighttpd force-reload
    Давайте переместим наш скрипт в директорию cgi-bin (я использую файловый менеджер mc) и попробуем запустить наш скрипт в веб браузере по следующему адресу:
    . Если всё сделали правильно, то мы увидим следующее:
    Всё из-за того, что в нашем случае веб-сервер выступает в качестве шлюза между веб-браузером и CGI программой. Поэтому мы в нашем скрипте допишем необходимую информацию в соответствии с требованиями стандарта HTML5.
     
    Последнее редактирование: 17 ноя 2019
    Un_ka, alp69, ИгорьК и 2 другим нравится это.
  2. parovoZZ

    parovoZZ Гуру

    Первое, что необходимо указать в скрипте, это с каким интерпретатором его связать:
    Код (Bash):
    #!/bin/bash/
    На самом деле, мы уже указали путь к интерпретатору, когда составляли конфигурацию в файле 10-cgi.conf. Но по правилам хорошего тона лучше это указывать в файле скрипта, т.к. скрипт может и не быть CGI программой и окружение будет другое.
    Далее нам необходимо указать веб-серверу, какие данные мы будем отправлять:
    Код (Bash):
    echo "Content-type: text/html"
    echo ""
    Указатель типа контента отделяется от собственно контента пустой строкой.
    Формируем стандартный html заголовок:
    Код (Bash):
    echo "<!doctype html>"
    echo "<html lang='en-EN'>"
    echo "<head>"
    echo "<title>"
    echo "raspberry"
    echo "</title>"
    echo "<meta charset='utf-8'>"
    echo "</head>"
     
    Далее формируем тело страницы:
    Код (Bash):

    echo "<body>"

    echo "<pre>"
    date
    cat /sys/class/thermal/thermal_zone0/temp
    temp=`cat /sys/class/thermal/thermal_zone0/temp`
    let "temp =  temp / 1000"
    echo "Температура процессора $temp градусов"

    echo "</pre>"
    echo "</body>"
    echo "</html>"
    Сохраняем файл, обновляем страницу в браузере:
    Таким образом, мы научились создавать динамические страницы. Такие страницы нигде не хранятся - они формируются динамически, путем запроса через веб-сервер.
     
    Un_ka нравится это.
  3. Sun Nov 17 20:19:53 MSK 2019 и Температура процессора 37 градусов
    Как-то стабильности нет, две локали, и английская и русская. В одной понятнее было-бы, а тут не знаешь во что верить. Толи тебе по русски ответили, толи перед тобой враг :)
    А где страница то? Один единственный запрос на /cgi-bin/newfile который получает html контент, не данные о дате и температуре а полный контент для отображения.
    Если бы загружался контент (который может быть довольно тяжелым, стили скрипты изображения и т.п.) и отдельно подгружались данные о дате и температуре - было-бы очевидно, вот тут статика, которая может быть кэширована браузером, а вот тут данные.
    Тема "как обновлять информацию на веб странице без перезагрузки самой страницы" по моему не раскрыта.

    П.С. А ajax то где? Может опечатка и его там нет?
     
  4. lighttpd при выполнении cgi не контролирует соединение с клиентом.
    Скрипт может выполняться бесконечно долго, а клиент в это время может разорвать соединение.
    В этом случае lighttpd почему-то продолжает выполнять скрипт, несмотря на то что клиент уже сдулся.
    В пример добавил строчку cat /dev/zero, обратился по прежней ссылке, разорвал соединение и уже минут 30 для одного запроса lighttpd продолжает выполнять cgi, при этом употребляет много процессорного времени и памяти.

    Определяю Main PID сервиса
    Код (Text):
    pi@raspberrypi:~ $ systemctl status lighttpd
    ● lighttpd.service - Lighttpd Daemon
      Loaded: loaded (/lib/systemd/system/lighttpd.service; enabled; vendor preset: enabled)
      Active: active (running) since Mon 2019-11-18 06:55:05 GMT; 33min ago
      Process: 7333 ExecStartPre=/usr/sbin/lighttpd -tt -f /etc/lighttpd/lighttpd.conf (code=exited, status=0/SUCCESS)
    Main PID: 7339 (lighttpd)
      Tasks: 3 (limit: 2200)
      Memory: 788.9M
      CGroup: /system.slice/lighttpd.service
              ├─7339 /usr/sbin/lighttpd -D -f /etc/lighttpd/lighttpd.conf
              ├─7469 /bin/bash /usr/lib/cgi-bin/newfile
              └─7473 cat /dev/zero
    Смотрю на top для этого процесса
    Код (Text):
    pi@raspberrypi:~ $ top -p 7339
    top - 07:34:22 up 19:31,  2 users,  load average: 4.61, 4.75, 4.44
    Tasks:  1 total,  1 running,  0 sleeping,  0 stopped,  0 zombie
    %Cpu(s):  0.0 us, 13.0 sy,  0.0 ni, 70.5 id, 16.5 wa,  0.0 hi,  0.0 si,  0.0 st
    MiB Mem :  926.1 total,  34.4 free,  122.4 used,  769.2 buff/cache
    MiB Swap:  100.0 total,  83.2 free,  16.8 used.  738.9 avail Mem

    PID    USER    PR  NI  VIRT   RES     SHR S  %CPU  %MEM   TIME+   COMMAND
    7339 www-data  20  0   71712  65760  2052 R  29.9   6.9  3:08.83  lighttpd
     
    parovoZZ нравится это.
  5. parovoZZ

    parovoZZ Гуру

    Теперь у нас возникает закономерный вопрос - а если в верстке сайта появилось желание что-то изменить, добавить страничку или верстальщик, дизайнер - это совсем другие люди? И вот здесь мы подходим к технологиям, которые позволяют подгружать контент в созданный заранее шаблон. Одна из них - AJAX. AJAX - это Asynchronous Javascript and XML. Другими словами, происходит асинхронный запрос (синхронный не используется практически - он блокирует страницу полностью до получения ответа от сервера) с помощью объекта XMLHttpRequest, который поддерживается всеми современными браузерами. Экземпляр этого объекта и вызов его методов осуществляется с помощью JavaScript скриптов. Какую информацию мы можем запросить у сервера? Как следует из расшифровки аббревиатуры - XML. Но это не так. Информация может быть любой:
    cgi.png
    Извлечь информацию из ответа от сервера мы можем четырьмя методами:
    responseText
    responseType
    responseURL
    responseXML
    Как мы помним, наш скрипт newfile возвращает простой текст, поэтому мы будем использовать свойство responseText. Но прежде нам необходимо объявить экземпляр объекта XMLHttpRequest:
    Код (Javascript):
    var xhttp = new XMLHttpRequest();
    Далее, нам необходимо отследить получение браузером ответа от сервера. Для этого воспользуемся свойством onreadystatechange. Это свойство меняется каждый раз, когда меняется статус объекта XMLHttpRequest. Всего статусов может быть пять:
    0 — Объект не инициализирован.
    1 — Объект загружает данные.
    2 — Объект загрузил свои данные.
    3 — Объект не полностью загружен, но может взаимодействовать с пользователем.
    4 — Объект полностью инициализирован; получен ответ от сервера.
    Нам нужен последний статус под номером 4. Также нам нужен успех выполнения запроса сервером, т.е. ответ 200. В результате у нас получается такая конструкция:
    Код (Javascript):
    xhttp.onreadystatechange = function()
        {
            if (this.readyState == 4 && this.status == 200)
            {
                document.getElementById("main-text").innerHTML = this.responseText;
            }
        };
    Для того, чтобы вставить полученную информацию в веб страницу (в загруженный шаблон), мы попросим браузер отыскать для нас элемент на этой странице по его ID. У меня это "main-text". Чтобы вставить (с полной заменой) текст, воспользуемся методом innerHTML.
    Всё, что написано выше, произойдет лишь только в том случае, если мы сделаем запрос веб-серверу. Как правило, используют два типа запроса: GET и POST. POST используется в том случае, если нам надо передать на веб-сервер много данных. В нашем случае мы передаем строку URI, поэтому воспользуемся методом GET. Инициализируем соединение с веб-сервером:
    Код (Javascript):
    xhttp.open("GET", "cgi-bin/newfile", true);
    и отправляем запрос:
    Код (Javascript):
    xhttp.send();
    Вот, собственно, и всё. Далее нам осталось сверстать простую HTML страницу и научить браузер обновлять на ней информацию без её перезагрузки.
     
    Последнее редактирование: 19 ноя 2019
    Un_ka нравится это.
  6. Пытался понять можно ли использовать кроме метода get другой метод - post (put delete не использовал, не актуально) так и не понял.
    Где (точнее как) получить в скрипте заголовки запроса? Если post запрос выполнять можно, то как правильно направить поток из запроса в скрипт (в его stdin)?
     
  7. parovoZZ

    parovoZZ Гуру

    Давайте создадим простенькую HTML форму? которая будет принимать данные от нашей CGI программы. Говорю сразу: я ни разу не дизайнер, не верстальщик, но сделал как сделал. Код лежит здесь (только body (без тегов <body> !!!):
    https://codepen.io/dAshkova/pen/BaavLmz
    Вначале HTML файла необходимо вставить заголовок с ссылками на css файл и на файл скрипта (у меня он называется GetCGI.js). В результате получится следующее :
    HTML:
    <!doctype html>
    <html lang='en-EN'>

    <head>
        <title>
            GPIO
        </title>
        <meta charset='utf-8'>
        <script src="scripts/GetCGI.js"></script>
        <link rel="stylesheet" href="css/style.css">
    </head>

    <body>
        <header> <h2> CGI, AJAX и RASPI2 </h2></header>
        <div class="grid">
          <div class="menu"> Здесь будет меню </div>
          <div class="main">
          <h2>RASPBERRY PI</h2>
                    <div class="image">
                        <img
                           src="https://www.raspberrypi.org/homepage-9df4b/static/fb5ac6aeb0846cf50b466739c8e68327/bc3a8/162f80fba36c0749bc2d8e04f40e8b12c030c50d_raspberry-pi-2-1-1621x1080.jpg">
                        <div class="caption">
                            <span>
                                <div id="main-text" class="temp-text">
                                    Подождите...
                                </div>
                            </span>
                        </div>
       
                    </div>
          </div>
          <div> Здесь тоже что-то будет </div>
        </div>

    </body>


    </html>
    Менять содержимое мы будем в этом слое:
    HTML:
                           
       <div id="main-text" class="temp-text">
                    Подождите...
      </div>
    Т.е. JS скрипт будет искать элемент с id="main-text" и внедрять туда новое содержимое.



    JS скрипт у нас такой:
    Код (Javascript):
    function loadCGI()
    {
        var xhttp = new XMLHttpRequest();
        xhttp.onreadystatechange = function()
        {
            if (this.readyState == 4 && this.status == 200)
            {
                document.getElementById("main-text").innerHTML = this.responseText;
            }
        };

        xhttp.open("GET", "cgi-bin/newfile", true);
        xhttp.send();
    }
    Файл style.css кладем в папку css, скрипт в папку scripts. Картинку я сдернул прямой ссылкой с сайта распберри (я думаю, они не обидятся)) Обе эти папки и файл index.html кладем сюда:
    Код (Bash):
    /var/www/html/
    Теперь при попытке открыть домашнюю страницу мы увидим следующее:
    html.png

    И... ничего не работает. Почему? Потому что веб-браузер не знает, когда запускать нашу JS функцию. Поэтому ему надо чистаканкоетна указать в какой момент и какую функцию запускать на выполнение. А запускать функцию JS мы будем только после полной загрузки HTML страницы. Для этого необходимо
    HTML:
    заменить на
    HTML:
    <body onload="loadCGI;">
    Теперь мы увидим следующее
    html1.png
    Но так мы получим данные лишь однажды - после загрузки страницы. А где же обещанная динамическая подгрузка? А для этого браузер надо поставить на счетчик чистаканкретна. Открываем файлик с JS скриптом и допишем ещё одну функцию:
    Код (Javascript):
    function callCGI()
    {
        setInterval(
            () => {loadCGI();},
            5000
            );
    }
    Здесь мы включили таймер и он каждые 5 секунд будет запускать указанную функцию. А чтобы этот таймер запустить, необходимо всё в том же index.html заменить в body
    HTML:
    <body onload="callCGI();">
    Ну вот и всё.
     
    Un_ka, Алексей.А и ИгорьК нравится это.
  8. ИгорьК

    ИгорьК Гуру

    Друже, мне все нравится, но что это было? Зачем тренироваться писать веб-страницы?

    Все, что ли, через эту хрень проходят?
     
  9. Думаю, топик был создан для того, чтоб показать как использовать cgi работая через lighttpd, если у пользователя не возникает сложностей для сбора данных и вывода их в стандартный поток вывода, то использование cgi скриптов дает быстрый старт для получения собранных данных через веб интерфейс.
    А страницы как раз и позволяют это показать (если не тренироваться писать пример то что тогда использовать?). И без страниц всё получается, curl-ом выполняю запросы и получаю адекватные данные, но показывать как командной строкой получить данные - не очень красиво. А тут в примере показано и использование cgi скриптов и доступ к ним через ajax.
    Если это не так, думаю автор меня поправит.
    Если неожиданно оборудование перестанет отвечать на запросы, его просто выключат например, то очередной запрос от браузера повиснет в ожидании, тем временем каждые 5 секунд браузер будет отправлять новые запросы, которые тоже будут повисать.
    Если использовать не интервал, а setTimeout и в обработчике результата выполнения запроса снова выполнять setTimeout то множественных подвисших запросов можно избежать.
     
    Последнее редактирование: 20 ноя 2019
  10. parovoZZ

    parovoZZ Гуру

    А что предлагаешь?

    Использовать рекурсию?
     
  11. setTimeout он не блокирующий :):)
    Этим самым setTimeout говорим браузеру, через 5 секунд выполни ещё раз loadCGI которая тоже не блокирующая, вы явно указываете xhttp.open("GET", "cgi-bin/newfile", true); true - выполнять асинхронно

    П.С.
    Даже если запрос будет выполняться синхронно, то во время выполнения запроса браузер "замерзнет", а когда запрос будет выполнен (успешно или нет) дадим браузеру ещё раз задание выполнить тот же код через таймаут.
     
    Последнее редактирование: 20 ноя 2019
  12. ИгорьК

    ИгорьК Гуру

    От задачи зависит. Ибо здесь малина видна, а для нее туча готовых решений.
    Данные собирать? NodeRed, Mqtt.
     
  13. parovoZZ

    parovoZZ Гуру

    можно дальше продолжить ассоциативный ряд: зачем малина и все эти nodered, mqtt, если есть туча готовых решений - KNX, Z-Wave, Jablotron, AJAX, En-ocean...
     
  14. ИгорьК

    ИгорьК Гуру

    Так она
     
  15. SergeiL

    SergeiL Оракул Модератор

    Ну со временем придет просветление :)