http
и соответствующих стандартным
программным интерфейсам, чтобы идентифицировать
интернет-трафик.GET
или отправленные пароли.Как-то я рассказывал о своем pet-проекте. В нем контроллеры ESP8266 управляются через Интернет. Пользователь создает программу и сохраняет ее в облаке. Контроллеры периодически ходят в него и забирают новую программу, которую исполняют.
Недавно вышла статья Картинка, которая одновременно является кодом на Javascript где автор достает кролика из шляпы. Забавно, но в своем проекте я делал что-то отдаленно похожее. Учитывая то, что тема оказалась интересной, решил поделиться и своим опытом в фокусах.
Как выше упоминалось, контроллеры с определенным интервалом ходят в облако, чтобы забрать свежую программу. Выглядит транспорт примерно так:
Голубые стрелки демонстрируют путь передачи программы на контроллер.
Проблема такой схемы заключается в том, что есть заметные задержки между сохранением программы в облаке и непосредственным исполнением ее контроллером. Задержка зависит от настроек облака и обычно составляет около 10 секунд.
Это не криминально для загрузки программы долгосрочного исполнения. Но при настройке и калибровке контроллеров возникает необходимость менять программу гораздо чаще. В идеале, она должна меняться online.
Чтобы обойти это ограничение, была реализована такая схема:
Теперь контроллер передает свой локальный IP облаку (зеленые стрелки). Этот IP имеет смысл только в контексте локальной сети. Но, FrontEnd запускается как раз в ней. И теоретически, может выполнить http запрос непосредственно к контроллеру (розовая стрелка).
К сожалению, эта схема не работает без магии. Очередная проблема в том, что FrontEnd запущен под https а контроллеры в локальной сети не могут себе это позволить. Они работают по http. И возникает ошибка Mixed Content.
Познакомиться детально с проблемой можно тут и тут.
Но если бы это был тупик, статья бы не появилась. Обратите внимание, что загружаемый контент делится на два типа: активный и пассивный. К пассивному контенту отношение браузеров более лояльное. Например, можно встроить картинку с http сайта в https сайт. Проверено на Chrome (76.0.3809.132) и Firefox (80.0.1). Думаю, вы уже понимаете на что я намекаю.
Магия заключается в том, что вместо запросов через XMLHttpRequest, можно обойтись средствами пассивного контента. Для этого достаточно в тег img установить src с GET параметрами. В моем случае это выглядит так:
<img src=http://192.168.0.33/stdin?raw=$LTSTL,10,10,19840,80,100,70,40,20*5f>
И чудесным образом получилось достичь желаемого. FrontEnd смог в режиме реального времени отсылать на контроллеры в локальной сети команды управления. Более того, использование PWA позволило создать offline приложение, которое не требовало связи с Интернет. Условно, конечно.
Видео-демонстратор. Происходит управление спектром светильника в реальном времени:
Недостаток технологии заключалась в том, что ответ от контроллера не возвращается в классическом понимании. Но и тут есть выходы:
Во-первых, результатом ответа может быть успех или провал. Т.е. код ответа от контроллера выраженный в поведении img. Согласен, весьма посредственное решение. Но в моем случае этого достаточно необходимо просто знать был выполнен запрос или нет.
Во-вторых, картинка возвращается с характеристиками размером. В них можно передавать закодированный результат. Сложнее, но надежнее.
В-третьих, картинка может иметь графическое кодирование. Его может парсить FrontEnd. К сожалению, img не умеет отдавать бинарные данные самой картинки. Придется сначала вывести ее на canvas, а затем декодировать.
Последний вариант, который я рассматривал redirect на сайт облака с параметрами ответа. Т.е. картинка запрашивается с локального IP контроллера, тот получает параметры GET и выполняет программу. В ответ он перенаправляет запрос на сайт облака. В редиректе содержатся ответ контроллера как GET параметры. Облако парсит ответ и передает фронту.
Задача весьма специфичная. Надеюсь мой опыт окажется кому-то полезным.
Упомянутые выше параметры вызваны целевым назначением
устройства, пока абстрактного устройства.
А WEB интерфейс в данном случае предназначен для настройки
параметров и некоего управления в локальной но при этом
небезопасной сети со смартфонов или других мобильных гаджетов.
Платформа в виде платы должна иметь чип или модуль Wi-Fi, микроконтроллер, желательно SD карту или чип внешней памяти ёмкостью не менее нескольких мегабайт.
В проектируемом устройстве WEB сервер играет вторичную роль.
Кроме него будут работать ещё десятки задач. Организовать WEB
сервер так чтобы его присутствие не влияло на работу остальной
функциональности можно с помощью RTOS.
От middleware RTOS нам нужен развитый набор сервисов синхронизации,
вытесняющая многозадачность, надёжный менеджер динамической памяти,
быстрая файловая система для SD или eMMC карт, стек TCP/IP
протоколов и очень желательно сервисные инструменты отладки.
Все это есть в Azure RTOS. Эта RTOS имеет очень длинную и славную
историю с тех пор, когда ещё называлась ThreadX. Она нашла
применение в нескольких миллиардах устройств. Её отличает
надёжность, компактность и хорошая документация. Долгое
время была коммерческой и очень дорогой. Впервые в открытом виде
появилась в пакете ПО Synergy от Renesas для микроконтроллеров
Synergy на базе ARM Cortex-M4, и сразу получила широкую
популярность благодаря исключительно богатой палитре
предоставляемого middleware. Теперь портированное middleware Azure
RTOS доступно бесплатно и для STM32.
Верхним протоколом, на котором непосредственно базируется WEB
сервер является HTTP.
Azure RTOS уже имеет в своём составе HTTP сервер, работающий поверх
стека сетевых протоколов Azure RTOS NetX Duo. HTTP
Azure, согласно документации, требует всего от 3.0 KB до 9.5 KB
FLASH и от 0.5 KB до 2 KB ОЗУ. Ниже будет дана более реалистичная
оценка необходимого объема ОЗУ.
Поскольку NetX Duo обладает интерфейсом BSD, то есть возможность
достаточно легко адаптировать другие сторонние WEB сервера, хотя
тесная связь таких серверов с нижележащим API делает такой выбор
малоэффективным.
Свойства WEB сервера реализованного в Azure
RTOS:
- Поддержка 2-х типов авторизации: basic (передача пароля в
открытом виде) и digest (передача хэша пароля),
- Работа по протоколам HTTP и HTTPS (HTTP защищённый с помощью
TLS)
- Чтение файлов страниц с SD карты или другого носителя с FAT32
- Обработка запросов: GET, POST, HEAD, PUT, DELETE
- Поточная передача без указания размера данных: Content-Length
- Работа нескольких подключений одновременно.
Полностью документация на WEB сервер здесь.
Способ работы сервера очень простой. Устройство получает через
TCP соединение текстовую строку запроса от браузера пользователя
сформированную согласно спецификации HTTP.
Устройство также должно ответить строкой по спецификации HTTP.
Поэтому WEB сервер в Azure RTOS называется HTTP сервером. HTTP
сервер Azure RTOS сразу готов отдавать по запросам браузера файлы с
SD карты устройства. Но статические страницы мало интересны.
Самой распространённой технологией в WEB серверах встраиваемых
устройств является технология Server Side Includes (SSI).
В Azure WEB сервере такой технологии нет. Видимо она считается
устаревшей. Действительно, SSI позволял придать динамичность
страницам, когда в браузере была отключена возможность исполнять
JavaScript. Теперь же JavaScript включён повсеместно, без него
сложно добиться адаптируемости.
Более продвинутым считается способ взаимодействия браузера и
сервера с помощью технологии AJAX. Т.е.
браузер с помощью JavaScript после загрузки страницы в отдельном
потоке запрашивает дополнительные данные из устройства. Сервер в
устройстве на лету формирует блок запрашиваемых данных и отправляет
браузеру в таком же виде как он посылал страницы т.е. по
спецификации HTTP.
Никто, конечно, не мешает реализовать и SSI в сервере Azure. Это
делается очень просто и выльется в конечном итоге в длинную цепочку
операторов if else с проверками на вхождение строк-директив. Но
такой подход вызовет слишком тесную связность между страницами и
кодом в микроконтроллере.
Гораздо привлекательней выглядит AJAX, причём с передачей
файлов в JSON кодировке.
Дело в том, что JSON является форматом внутреннего представления
объектов JavaScript в WEB страницах. Нет ничего проще для WEB
разработчика чем передавать и принимать данные в формате JSON. В
JSON можно выполнить сериализацию буквально всего: параметров,
таблиц параметров, баз данных параметров, иерархических деревьев
параметров, представления параметров в виде виджетов и прочее.
JSON кодировка довольно простая. JSON в конечном счёте просто
строка. Внутри неё нельзя использовать байт 0, поэтому эта строка
легко интерпретируется как C-и строка. Одновременно это же
обстоятельство позволяет без перекодировки вставлять JSON в HTTP
строку ответа.
Немного сложнее дела обстоят с парсингом JSON. Парсинг JSON
необходим когда браузер клиента пришлёт устройству запрос например
с отредактированными настройками. Простые JSON строки можно парсить
и средствами языка C-и, но большие JSON строки уже требуют
серьёзных парсеров.
Здесь можно посоветовать проект Jansson. Надо только знать
что Jansson активно использует динамическую память, и если
передавать в JSON около сотни числовых и строковых параметров, то
для парсера может понадобиться около 50 Кбайт ОЗУ, зависит от длины
имён переменных и длины самих переменных.
MIME типы
Сервер Azure HTTP не
поддерживает HTTP pipelining, но
поддерживает передачу файлов один за другим в одном TCP соединении.
WEB страницы как правило содержат ссылки на файлы стилей, скриптов,
картинок и прочего. Все эти файлы скачиваются последовательно один
за другим браузером клиента. Чтобы получить файл браузер посылает
HTTP запрос. Сервер на старте отправки каждого файла отправляет
HTTP заголовок, например такой:
HTTP/1.1 200 OKContent-Type: text/htmlConnection: keep-aliveContent-Length: 13210Date: Sun, 17 May 2020 00:59:19 GMTCache-Control: max-age=1Last-Modified: Sun, 17 May 2021 00:59:19 GMT
Здесь имеет большое значение содержание поля Content-Type. Если его указать неправильно браузер может неправильно отобразить страницу. Сервер Azure содержание Content-Type устанавливает в соответствии с расширением передаваемого файла. Но список самих таких известных расширений у сервера небольшой, поэтому в файле nx_web_http_server.c дополняем массив _nx_web_http_server_mime_maps следующим образом:
/* Define basic MIME maps. */static NX_WEB_HTTP_SERVER_MIME_MAP _nx_web_http_server_mime_maps[] ={ {"html", "text/html"}, {"htm", "text/html"}, {"txt", "text/plain"}, {"css", "text/css"}, {"js", "application/javascript"}, {"gif", "image/gif"}, {"jpg", "image/jpeg"}, {"png", "image/png"}, {"ico", "image/x-icon"},};
Способы размещения контента. Для более удобного и быстрого размещения статического контента и сопутствующих файлов на WEB сервере устройства можно применить FTP сервер. FTP сервер имеется в поставке Azure RTOS. Такие среды разработки как Adobe Dreamweaver способны автоматически обновлять контент на целевом FTP сервере содержащем контент WEB сайта.
Ограничение видимости для WEB сервера
Azure.
По умолчанию корневой директорией HTTP сервера Azure является
корневая директория SD карты.
Чтобы сервер мог считывать файлы только из определенной
поддиректории ему надо установить функцией
fx_directory_local_path_set локальный путь для
задачи сервера. Это удобно делать при первом вызове в callback
функции перехватчика запросов сервера. Указатель на callback
функцию передаётся в задачу сервера при его создании с помощью
функции nx_web_http_server_create. Весь HTTP
сервер выполняется в одной задаче, поэтому такой метод
работает.
Сжатие контента. Современные браузеры
поддерживают компрессию передаваемых страниц и сопутствующих
файлов. Компрессия типичных HTML файлов, скриптов и стилей
позволяет уменьшит объем передаваемых данных в 3-4 раза. Сервер
HTTP Azure не поддерживает компрессию на лету, для
микроконтроллеров компрессия может оказаться более продолжительным
процессом чем передача несжатого контента. Есть возможность
выполнить компрессию статических файлов сразу же при подготовке
контента. И хранить файлы на SD карте уже сжатыми, однако HTTP
сервер Azure не поддерживает распознавание сжатых файлов и
соответствующую модификацию заголовков HTTP.
Проблема не так актуальна как может показаться. Например
одностраничные приложения выполненные во фреймворках скачивают
статичный контент всего один раз, и гораздо больший трафик может
создать динамический контент. Статический контент также можно
разместить на сторонних быстрых серверах способных сжимать. И можно
конечно же реализовать распознавание сжатых файлов в HTTP сервере
собственными силами если сжатие действительно сильно улучшит
отзывчивость сервера.
Неточность в исходниках Azure HTTP сервера при
базовой авторизации.
Исходные тексты содержат
неточность в файле nx_web_http_server.c после строки 3689.
В таком виде базовая авторизация затягивается на 10 сек., поскольку
не разрывается соединение после неавторизированного запроса. Туда
следует вставить вот такой фрагмент:
if (status == NX_WEB_HTTP_BASIC_AUTHENTICATE) { _nx_web_http_server_connection_reset(server_ptr,server_ptr -> nx_web_http_server_current_session_ptr , NX_WEB_HTTP_SERVER_TIMEOUT_SEND); return; }
После такого исправления пользователь сразу, а не через 10 сек.
увидит диалог с вводом имени и пароля.
Не забыть также вставить этот фрагмент для методов POST и
DELETE.
Выбор фреймворка очень важен. Фреймворки это такое сочетание JavaScript библиотек и файлов стилей CSS. Бывает просто одна библиотека, как например jQuery, бывает библиотека со стилями, как например jQuery UI. Фреймворк значительно облегчает создание адаптируемых, интуитивных и привлекательных WEB страниц, в противовес использованию голого HTML и JavaScript с архаичными стилями. С помощью фреймворков даже самый примитивный WEB сервер выполненный на Arduino, может предоставить удивительно стильный WEB интерфейс. Фреймворки автоматически адаптируют страницы так чтобы они оставались в приемлемом качестве на экранах любых размеров и в любых браузерах.
Но фреймворков очень много. В нашем случае прежде всего
интересует сколько файлов и какого размера требуется для
фреймворка, насколько удобен фреймворк в использовании
неспециалисту, насколько богат и гибок набор стилей и виджетов.
Надо помнить что файлы фреймворка скачиваются браузером вместе со
страницами WEB интерфейса из устройства. Слишком большой объем
фреймворка замедлит время открытия страниц.
Охватить анализом все фреймворки и библиотеки практически
невозможною Они возникают как грибы после дождя. Поэтому тут я
привёл таблицу как попытку оценить несколько фреймворков которые
есть на слуху:
Большинство фреймворков вводят новые элементы синтаксиса
разметки в страницы HTML, добиваясь таким образом новых способов
выражения дизайна. Это влечёт за собой необходимость дополнительно
изучать кроме HTML, JavaScript, CSS и DOM модели ещё и уникальный
язык и архитектурный подход фреймворка. Это ещё более усложняет
задачу выбора. Поэтому об оптимальном выборе речь не идёт.
Как локальный оптимум выбираем jQuery mobile. Он использует
одну из старейших и проверенных библиотек jQuery и интегрирован в WYSIWYG
среду разработки Adobe Dreamweaver, что
позволяет более удобно конструировать интерфейс по сравнению со
способами без WYSIWYG. jQuery mobile как следует из названия
предназначен в первую очередь для мобильных устройств, и это как
раз то что нужно, поскольку большинство пользователей скорее всего
захотят работать с WEB интерфейсом через мобильные устройства
Защищённый WEB сервер работает по протоколу HTTPS на порту 433.
В этом случае используется протокол шифрования и аутентификации
Transport Layer Security (TLS). В Azure RTOS реализован протокол
TLS версии 1.3 и также более ранние его версии.
По умолчанию сервер просит от клиента вот такой набор
алгоритмов:
Cipher Suite: TLS_RSA_WITH_AES_128_CBC_SHA (0x002f)
Для того чтобы WEB c TLS работал устройство должно ещё иметь в своих недрах сертификат сервера и секретный ключ сервера. Здесь предлагается их хранить во Flash памяти в виде массивов. Сертификат сервера должен содержать подпись доверенного центра (это стоит денег и времени), поэтому проще всего сгенерировать самоподписанный сертификат.
И ключ и сертификат должны быть представлены в двоичном формате DER.
Для примера: ключ занимает 1192 байта, один самоподписанный сертификат занимает 879 байт.
Чтобы сгенерировать самоподписанный сертификат и секретный ключ надо скачать файлы openssl.exe, libeay32.dll, ssleay32.dll и выполнить в их директории несколько команд:
1. Сгенерировать корневой секретный ключ ca.key
-
openssl genrsa -out ca.key 2048
2. Сгенерировать корневой сертификат CA.crt
-
openssl req -config CA.conf -new -x509 -sha256 -key ca.key
-days 3650 -out CA.crt
3. Сгенерировать секретный ключ сервера
srv.key
openssl genrsa -out srv.key 2048
4. Сгенерировать запрос сертификата сервера с использованием
корневого сертификата srv.csr -
openssl req -new -config Server.conf -out srv.csr -key
srv.key
5. Верифицировать и сгенерировать сертификат сервера
srv.crt -
openssl x509 -req -in srv.csr -CA ca.crt -CAkey ca.key
-CAcreateserial -out srv.crt -days 3650
6. Конвертировать сертификаты и ключ в формат DER -
openssl x509 -in CA.crt -out CA.der -outform
DER
openssl x509 -in srv.crt -out srv.der -outform
DER
openssl rsa -inform pem -in srv.key -outform der -out
srv_key.der
На HTTP сервер устанавливаем файлы srv.der и
srv_key.der предварительно сконвертировав их в
массивы. Причём у нас остаётся корневой сертификат, благодаря чему
даже если злоумышленники взломают устройство и похитят сертификат
сервера они не смогут сгенерировать новые сертификаты после того
как похищенный сертификат буде аннулирован.
Задача http сервера в Azure RTOS называется TCPSERVER
Thread.
Про этому названию её легко найти в списке запущенных задач в
отладчике IDE IAR Embedded Workbench for
Arm.
По наблюдениям на реальном проекте размер занятого
этой задачей стека не превышал 2500 байта.
Движок TLS требует нескольких объёмных структур данных:
Структура NX_SECURE_X509_CERT имеет размер 304
байта
Массив crypto_metadata должен иметь размер не
менее 17596 байта на каждую сессию. У нас выбрано до 2 сессий
одновременно. Следовательно надо 35192 байта
Массив tls_packet_buffer требует не менее 2000
байт и не более 64 Кбайт. Задаём ему 4000 байта. Его размер
определяется размером сертификата сервера. Сертификат может
сопровождаться цепочкой сертификатов поэтому надо определять этот
размер каждый раз по обстоятельствам.
Итого защищённый сервер потребует не менее 42 Кбайт. Это сравнительно немного. В целом на весь TCP стек с сервером потребуется около 100 Кбайт ОЗУ. Это с учётом буфера пакетов TCP/IP пакетов, JSON парсера для минимальных структур и прочих расходов на протоколы. Если иметь в виду ещё файловую систему, то размер возрастёт на 30-70 Кбайт в зависимости от того какое быстродействие хотим получить.
Используя фреймворк jQuery mobile сконструирована вот такая страница:
Если бы фреймворка не было, то эта же страница выглядела бы так:
Содержимое HTML страницы
<!doctype html><html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> <title>IoT Logger</title> <link href="jquery-mobile/jquery.mobile.theme-1.3.0.min.css" rel="stylesheet" type="text/css"> <link href="jquery-mobile/jquery.mobile.structure-1.3.0.min.css" rel="stylesheet" type="text/css"> <script src="jquery-mobile/jquery-1.11.1.min.js"> </script> <script src="jquery-mobile/jquery.mobile-1.3.0.min.js"> </script> <style> @media only screen and (device-width: 300px), only screen and (max-width:300px) { .css-element { yourcsscode:; } } input.cb_larger { width: 25px; height: 25px; margin: -12px 0 0 -10px; } .tbl_hdr { font-size: 14px; font-style: oblique; font-weight: normal; text-align: left; background-color: #DEDEDE } </style> </head> <body> <div align="center" data-role="page" id="page"> <div data-role="header" data-position="fixed"> <p id="page_header1" style="padding: 0px 0px 0px 0px; margin: 5px 0px 0px 0px ">IoT Logger</p> <p style="font-size: 10px; padding: 0px 0px 0px 0px; margin: 0px 0px 5px 5px; text-align: left "> <span>ID:</span> <em id="page_header2" style="color:deepskyblue">?</em> <span id="page_header3" style="margin-left: 10px">?</span> </p> <div data-role="navbar" style="width: 400px"> <ul> <li></li> <li></li> <li><li><button type="submit" id="reset_log" onclick="location.assign('device_log.html');" data-theme="b">Show log</button></li></li> </ul> </div> </div> <div data-role="content"> <div style="overflow:auto;"> <table id="ap_list" style="width:800px"> <caption style="font-size: 16px; font-weight: bold; text-align: left"> Available Access Points list for Station mode </caption> <tr> <th class="tbl_hdr"></th><th class="tbl_hdr">Enable</th> <th class="tbl_hdr">Access Point SSID</th> <th class="tbl_hdr">Password</th> <th class="tbl_hdr">Use DHCP</th> <th class="tbl_hdr">IP address</th> <th class="tbl_hdr">IP mask</th> <th class="tbl_hdr">IP gateway</th> </tr> <tr> <td>1</td> <td><input type="checkbox" class="cb_larger" id="ssiden1" name="SSIDEN1"></td> <td><input type="text" id="ssid1" name="SSID1" value=""></td> <td><input type="password" id="pass1" name="PASS1" value=""></td> <td><input type="checkbox" class="cb_larger" id="dhcp1" name="DHCP1"></td> <td><input type="text" id="ipaddr1" name="IPADDR1" value=""></td> <td><input type="text" id="ipmask1" name="IPMASK1" value=""></td> <td><input type="text" id="ipgateway1" name="IPGATEWAY1" value=""></td> </tr> <tr> <td>2</td> <td><input type="checkbox" class="cb_larger" id="ssiden2" name="SSIDEN2"></td> <td><input type="text" id="ssid2" name="SSID2" value=""></td> <td><input type="password" id="pass2" name="PASS2" value=""></td> <td><input type="checkbox" class="cb_larger" id="dhcp2" name="DHCP2"></td> <td><input type="text" id="ipaddr2" name="IPADDR2" value=""></td> <td><input type="text" id="ipmask2" name="IPMASK2" value=""></td> <td><input type="text" id="ipgateway2" name="IPGATEWAY2" value=""></td> </tr> <tr> <td>3</td> <td><input type="checkbox" class="cb_larger" id="ssiden3" name="SSIDEN3"></td><td> <input type="text" id="ssid3" name="SSID3" value=""></td> <td><input type="password" id="pass3" name="PASS3" value=""></td> <td><input type="checkbox" class="cb_larger" id="dhcp3" name="DHCP3"></td> <td><input type="text" id="ipaddr3" name="IPADDR3" value=""></td> <td><input type="text" id="ipmask3" name="IPMASK3" value=""></td> <td><input type="text" id="ipgateway3" name="IPGATEWAY3" value=""></td> </tr> <tr> <td>4</td> <td><input type="checkbox" class="cb_larger" id="ssiden4" name="SSIDEN4"></td> <td><input type="text" id="ssid4" name="SSID4" value=""></td> <td><input type="password" id="pass4" name="PASS4" value=""></td> <td><input type="checkbox" class="cb_larger" id="dhcp4" name="DHCP4"></td> <td><input type="text" id="ipaddr4" name="IPADDR4" value=""></td> <td><input type="text" id="ipmask4" name="IPMASK4" value=""></td> <td><input type="text" id="ipgateway4" name="IPGATEWAY4" value=""></td> </tr> <tr> <td>5</td> <td><input type="checkbox" class="cb_larger" id="ssiden5" name="SSIDEN5"></td> <td><input type="text" id="ssid5" name="SSID5" value=""></td> <td><input type="password" id="pass5" name="PASS5" value=""></td> <td><input type="checkbox" class="cb_larger" id="dhcp5" name="DHCP5"></td> <td><input type="text" id="ipaddr5" name="IPADDR5" value=""></td> <td><input type="text" id="ipmask5" name="IPMASK5" value=""></td> <td><input type="text" id="ipgateway5" name="IPGATEWAY5" value=""></td> </tr> </table> </div> <table id="ap_selector"> <tr> <th> <div style="text-align:left ; padding-top: 10px"> Selected operational mode </div> </th> </tr> <tr> <td> <input type="radio" id="md_sta" name="wifi_mode" value="0"> <label id="md_sta_lb" for="md_sta"> Station mode <br> <span style="font-weight: normal; font-size: 14px"> (The device will connect to the someone <br> of access points listed in the table above) </span> </label> </td> </tr> <tr> <td> <input type="radio" id="md_ap" name="wifi_mode" value="1"> <label id="md_ap_lb" for="md_ap"> Access Point mode<br> <span style="font-weight: normal; font-size: 14px">(The device will wait for someone to join with it)</span> </label> </td> </tr> </table> <fieldset class="ui-grid-a" style="width: 300px"> <div class="ui-block-a"> <button type="submit" id="submit_data" data-theme="a">Submit</button> </div> <div class="ui-block-b"> <button type="submit" id="reset_dev" data-theme="e">Reset device</button> </div> </fieldset> <div data-role="footer" data-position="fixed"> <p id="status_msg" style="font-weight:normal" hidden=true> </p> </div> </div> </div> </body> <script> // Глобальные переменные для хранения полученной из дивайса информации var params; var dev; var apl; var wrl; // Вывод строки статуса выполнения операции по нажатию кнопок function Show_cmd_status(data, status) { $("#status_msg").text("Data sending status: " + status); $("#status_msg").show(); setTimeout(function() { $("#status_msg").hide(); }, 2000); } // Функция извлечение интересующих параметров из объекта data и помещение их в поля ввода function Data_accept(data, status) { params = data; var v = params.find(function(item, i) { if (item["Client_AP_list"] != undefined) return true; }); apl = v["Client_AP_list"]; if (apl != undefined) { $("#ssid1").val(apl[0][1]); $("#pass1").val(apl[0][2]); $("#ipaddr1").val(apl[0][4]); $("#ipmask1").val(apl[0][5]); $("#ipgateway1").val(apl[0][6]); $("#ssid2").val(apl[1][1]); $("#pass2").val(apl[1][2]); $("#ipaddr2").val(apl[1][4]); $("#ipmask2").val(apl[1][5]); $("#ipgateway2").val(apl[1][6]); $("#ssid3").val(apl[2][1]); $("#pass3").val(apl[2][2]); $("#ipaddr3").val(apl[2][4]); $("#ipmask3").val(apl[2][5]); $("#ipgateway3").val(apl[2][6]); $("#ssid4").val(apl[3][1]); $("#pass4").val(apl[3][2]); $("#ipaddr4").val(apl[3][4]); $("#ipmask4").val(apl[3][5]); $("#ipgateway4").val(apl[3][6]); $("#ssid5").val(apl[4][1]); $("#pass5").val(apl[4][2]); $("#ipaddr5").val(apl[4][4]); $("#ipmask5").val(apl[4][5]); $("#ipgateway5").val(apl[4][6]); if (apl[0][0] == 1) $("#ssiden1").attr("checked", true); if (apl[1][0] == 1) $("#ssiden2").attr("checked", true); if (apl[2][0] == 1) $("#ssiden3").attr("checked", true); if (apl[3][0] == 1) $("#ssiden4").attr("checked", true); if (apl[4][0] == 1) $("#ssiden5").attr("checked", true); if (apl[0][3] == 1) $("#dhcp1").attr("checked", true); if (apl[1][3] == 1) $("#dhcp2").attr("checked", true); if (apl[2][3] == 1) $("#dhcp3").attr("checked", true); if (apl[3][3] == 1) $("#dhcp4").attr("checked", true); if (apl[4][3] == 1) $("#dhcp5").attr("checked", true); } v = params.find(function(item, i) { if (item["Parameters"] != undefined) return true; }); if (v != undefined) { wrl = v["Parameters"].find(function(item, i) { if (item[0] == "wifi_role") return true; }); if (wrl != undefined) { if (wrl[1] == 0) { $("#md_sta_lb").click(); } else { $("#md_ap_lb").click(); } } } v = params.find(function(item, i) { if (item["Device"] != undefined) return true; }); dev = v["Device"]; if (dev["HW_Ver"] != undefined) { $("#page_header1").html(dev["HW_Ver"]); $("#page_header2").html(dev["CPU_ID"]); $("#page_header3").html(dev["CompDate"] + " " + dev["CompTime"]); } } // Считываем параметры из полей ввода, записываем их в объект JSON // Сериализируем объект и отправляем устройству методом POST function Data_send() { apl[0][1] = $("#ssid1").val(); apl[0][2] = $("#pass1").val(); apl[0][4] = $("#ipaddr1").val(); apl[0][5] = $("#ipmask1").val(); apl[0][6] = $("#ipgateway1").val(); apl[1][1] = $("#ssid2").val(); apl[1][2] = $("#pass2").val(); apl[1][4] = $("#ipaddr2").val(); apl[1][5] = $("#ipmask2").val(); apl[1][6] = $("#ipgateway2").val(); apl[2][1] = $("#ssid3").val(); apl[2][2] = $("#pass3").val(); apl[2][4] = $("#ipaddr3").val(); apl[2][5] = $("#ipmask3").val(); apl[2][6] = $("#ipgateway3").val(); apl[3][1] = $("#ssid4").val(); apl[3][2] = $("#pass4").val(); apl[3][4] = $("#ipaddr4").val(); apl[3][5] = $("#ipmask4").val(); apl[3][6] = $("#ipgateway4").val(); apl[4][1] = $("#ssid5").val(); apl[4][2] = $("#pass5").val(); apl[4][4] = $("#ipaddr5").val(); apl[4][5] = $("#ipmask5").val(); apl[4][6] = $("#ipgateway5").val(); if ($("#ssiden1").prop("checked") == true) apl[0][0] = 1; else apl[0][0] = 0; if ($("#ssiden2").prop("checked") == true) apl[1][0] = 1; else apl[1][0] = 0; if ($("#ssiden3").prop("checked") == true) apl[2][0] = 1; else apl[2][0] = 0; if ($("#ssiden4").prop("checked") == true) apl[3][0] = 1; else apl[3][0] = 0; if ($("#ssiden5").prop("checked") == true) apl[4][0] = 1; else apl[4][0] = 0; if ($("#dhcp1").prop("checked") == true) apl[0][3] = 1; else apl[0][3] = 0; if ($("#dhcp2").prop("checked") == true) apl[1][3] = 1; else apl[1][3] = 0; if ($("#dhcp3").prop("checked") == true) apl[2][3] = 1; else apl[2][3] = 0; if ($("#dhcp4").prop("checked") == true) apl[3][3] = 1; else apl[3][3] = 0; if ($("#dhcp5").prop("checked") == true) apl[4][3] = 1; else apl[4][3] = 0; if ($("#md_sta").prop("checked") == true) wrl[1] = "0"; else wrl[1] = "1"; // Преобразуем объект JavaScript в строку JSON json_str = JSON.stringify(params); // Отправляем устройству методом POST строку JSON с отредактированными параметрами $.post("data.json", json_str, Show_cmd_status); } // Перейти на страницу отображения лога function Show_log() { location.assign("device_log.html"); } // По клику на кнопке с id = "submit_data" посылаем отредактированные данные обратно устройству $("#submit_data").click(Data_send); // По клику на кнопке с id = "reset_dev" посылаем команду сброса устройства $("#reset_dev").click(function() {$.post("reset", "", Show_cmd_status)}); // Здесь сразу запрашиваем у устройства по протоколу AJAX текущие настройки $.get("data.json", Data_accept); </script></html>
Как видно в заголовке страница вместе с файлом HTML будут скачаны еще 4-е файла принадлежащие фреймворку. На самом деле будет скачано больше, поскольку файлы фреймворка скачивают сами ещё несколько нужных им файлов.
Содержимое JSON файла c данными
[ { "Device": { "CPU_ID": "5301646835393735C86643535454227D", "SW_Ver": "V0.0.2", "HW_Ver": "IoT Logger 1.0.0", "CompDate": "Apr 16 2021", "CompTime": "13:03:54" } }, { "Parameters": [ [ "leds_mode", "1" ], [ "wifi_role", "1" ] ] }, { "Client_AP_list": [ [ 1, "SSID", "PASS", 0, "192.168.1.1", "255.255.255.0", "192.168.1.254" ], [ 1, "SSID", "PASS", 0, "192.168.1.1", "255.255.255.0", "192.168.1.254" ], [ 1, "SSID", "PASS", 0, "192.168.1.1", "255.255.255.0", "192.168.1.254" ], [ 1, "SSID", "PASS", 0, "192.168.1.1", "255.255.255.0", "192.168.1.254" ], [ 1, "SSID", "PASS", 0, "192.168.1.1", "255.255.255.0", "192.168.1.254" ] ] }]
И наконец посмотрим отзывчивость WEB интерфейса на примере платформы Sinergy
Диаграмма времени закачки страницы по протоколу HTTP Диаграмма времени закачки страницы по протоколу HTTPSдерЗдесь надо отметить что для Sinergy у Azure RTOS есть драйвер
аппаратного криптографического модуля, поэтому скорость HTTP и
HTTPS отличаются всего лишь в два раза.
В случае программной реализации отличие будет более
значительным.
Как обстоят дела с драйверами для криптографической периферии STM32
ещё предстоит выяснить.
Итак, загрузка адаптированной под мобильные гаджеты, зашифрованной с помощью TLS страницы вместе со всеми файлами фреймворка и файлом данных JSON в среднем займет не более 800 миллисекунд, что вполне приемлемо для интерфейса встраиваемого устройства.
Эта статья посвящена геттерам и сеттерам в C++. Приношу свои извинения, но речь пойдет не о корутинах. К слову, в ближайшее время появится вторая часть про пулы потоков.
TL;DR: геттеры и сеттеры не очень хорошо подходят для структуроподобных объектов.
В этой статье я лишь высказываю свое личное мнение, я не преследую цели кого-нибудь обидеть или задеть, я просто собираюсь объяснить, почему и когда стоит или не стоит, использовать геттеры и сеттеры. Буду очень рад любым дискуссиям в комментариях.
Следует сразу прояснить, что когда я говорю о геттере, я подразумеваю функцию, которая просто что-то возвращает, а когда я говорю о сеттере, я подразумеваю функцию, которая просто изменяет одно внутреннее значение, не выполняя никаких проверок или других дополнительных вычислений.
Допустим, у нас есть простая структура с обычными геттерами и сеттерами:
class PersonGettersSetters { public: std::string getLastName() const { return m_lastName; } std::string getFirstName() const { return m_firstName; } int getAge() const {return m_age; } void setLastName(std::string lastName) { m_lastName = std::move(lastName); } void setFirstName(std::string firstName) { m_firstName = std::move(firstName); } void setAge(int age) {m_age = age; } private: int m_age = 26; std::string m_firstName = "Antoine"; std::string m_lastName = "MORRIER"; };
Сравним эту версию с версией без геттеров и сеттеров.
struct Person { int age = 26; std::string firstName = "Antoine"; std::string lastName = "MORRIER";};
Она намного лаконичнее и надежнее. Здесь мы не можем, например, верну фамилию вместо имени.
Оба кода полностью функциональны. У нас есть класс Person с
именем (firstName
), фамилией (lastName
) и
возрастом (age
). Однако предположим, что нам нужна
функция, которая возвращает некоторую сводку по конкретному
человеку.
std::string getPresentation(const PersonGettersSetters &person) { return "Hello, my name is " + person.getFirstName() + " " + person.getLastName() + " and I am " + std::to_string(person.getAge());}std::string getPresentation(const Person &person) { return "Hello, my name is " + person.firstName + " " + person.lastName + " and I am " + std::to_string(person.age);}
Версия без геттеров выполняет эту задачу на 30% быстрее, чем
версия с геттерами. Почему? Из-за возврата по значению в геттере.
При возврате по значению создается копия, что снижает
производительность. Давайте сравним производительность
person.getFirstName()
; и
person.firstName
.
Как видите, прямой доступ к полю имени без геттера эквивалентен noop.
Однако можно использовать возврат не по значению, а по ссылке. Так образом мы получим такую же производительность, как и без использования геттеров. Обновленный код будет выглядеть так:
class PersonGettersSetters { public: const std::string &getLastName() const { return m_lastName; } const std::string &getFirstName() const { return m_firstName; } int getAge() const {return m_age; } void setLastName(std::string lastName) { m_lastName = std::move(lastName); } void setFirstName(std::string firstName) { m_firstName = std::move(firstName); } void setAge(int age) {m_age = age; } private: int m_age = 26; std::string m_firstName = "Antoine"; std::string m_lastName = "MORRIER"; };
Так как мы получаем ту же производительность, что и в лаконичной версии, мы можем на этом успокоиться, не так ли? Прежде чем отвечать на этот вопрос, попробуйте выполнить этот код.
PersonGettersSetters make() { return {}; }int main() { auto &x = make().getLastName(); std::cout << x << std::endl; for(auto x : make().getLastName()) { std::cout << x << ","; }}
Вы можете заметить некоторые странные символы, выведенные в
консоли. Но почему? Что произошло, когда мы сделали
make().getLastName()
?
Вы создаете экземпляр Person.
Вы получаете ссылку на фамилию.
Вы удаляете экземпляр Person.
И вот у нас есть висячая ссылка! Это может привести к крашам (в лучшем случае) или чему-то еще более худшему, чему-то, что можно найти только в фильмах ужасов.
Чтобы предупредить это, мы должны ввести
ref-qualified
функции.
class PersonGettersSetters { public: const std::string &getLastName() const & { return m_lastName; } const std::string &getFirstName() const & { return m_firstName; } std::string getLastName() && { return std::move(m_lastName); } std::string getFirstName() && { return std::move(m_firstName); } int getAge() const {return m_age; } void setLastName(std::string lastName) { m_lastName = std::move(lastName); } void setFirstName(std::string firstName) { m_firstName = std::move(firstName); } void setAge(int age) {m_age = age; } private: int m_age = 26; std::string m_firstName = "Antoine"; std::string m_lastName = "MORRIER"; };
Вот новое решение, которое будет работать везде. Вам нужно два
геттера. Один для lvalue
и один для
rvalue
(как xvalue
, так и для
prvalue
).
Тут особо нечего сказать. Если вы хотите добиться максимальной
производительности, вы должны написать один сеттер, который
принимает lvalue
, и один, который принимает
rvalue
. Однако, как правило, достаточно иметь всего
один сеттер, который принимает перемещаемое значение. Тем не менее,
вам придется расплатиться за это дополнительным move. Однако таким
образом у вас не получится производить небольшие изменения в
переменных. Вы должны заменять всю переменную целиком. Если вы
просто хотите заменить одну букву A в имени на D, то вы не сможете
сделать это с помощью сеттеров. Однако с помощью прямого доступа
так делать можно.
Кто-то может посоветовать вам просто сделать атрибут члена const. Однако меня это решение не устраивает. Создание константы предотвратит move-семантику и приведет к ненужному копированию.
У меня нет волшебного решения, которое я мог бы предложить вам
прямо сейчас. Тем не менее, мы можем написать обертку, которую мы
можем назвать immutable
<T>. Эта обертка должна
быть:
Constructible
Так как она immutable
, она не должна быть
assignable
Она может быть copy constructible
или move
constructible
Она должна быть конвертируемой в const T
&,
будучи lvalue
Она должна быть конвертируемой в T
, будучи
rvalue
Она должна использоваться, как и другие оболочки, с помощью
оператора *
или оператора ->
.
Получить адрес базового объекта должно быть легко.
Вот небольшая реализация:
#define FWD(x) ::std::forward<decltype(x)>(x)template <typename T>struct AsPointer { using underlying_type = T; AsPointer(T &&v) noexcept : v{std::move(v)} {} T &operator*() noexcept { return v; } T *operator->() noexcept { return std::addressof(v); } T v;};template <typename T>struct AsPointer<T &> { using underlying_type = T &; AsPointer(T &v) noexcept : v{std::addressof(v)} {} T &operator*() noexcept { return *v; } T *operator->() noexcept { return v; } T *v;};template<typename T>class immutable_t { public: template <typename _T> immutable_t(_T &&t) noexcept : m_object{FWD(t)} {} template <typename _T> immutable_t &operator=(_T &&) = delete; operator const T &() const &noexcept { return m_object; } const T &operator*() const &noexcept { return m_object; } AsPointer<const T &> operator->() const &noexcept { return m_object; } operator T() &&noexcept { return std::move(m_object); } T operator*() &&noexcept { return std::move(m_object); } AsPointer<T> operator->() &&noexcept { return std::move(m_object); } T *operator&() &&noexcept = delete; const T *operator&() const &noexcept { return std::addressof(m_object); } friend auto operator==(const immutable_t &a, const immutable_t &b) noexcept { return *a == *b; } friend auto operator<(const immutable_t &a, const immutable_t &b) noexcept { return *a < *b; } private: T m_object;};
Таким образом, для иммутабельного объекта Person вы можете просто написать:
struct ImmutablePerson { immutable_t<int> age = 26; immutable_t<std::string> firstName = "Antoine"; immutable_t<std::string> lastName = "MORRIER";};
Я бы не сказал, что геттеры и сеттеры - это зло. Однако, когда вам не нужно делать что-либо еще в геттере и сеттере, достижение максимальной производительности, безопасности и гибкости подводит вас к написанию:
3-х геттеров (или даже 4-х): const lvalue
,
rvalue
, const rvalue
и, по вашему
усмотрению, для неконстантного lvalue
(даже если это
уже просто очень странно звучит, так как проще использовать прямой
доступ)
1 сеттер (или 2, если вы хотите выжать максимальную производительность).
Это по большому счету шаблон, который подходит практически для всего.
Некоторые люди могут вам сказать, что геттеры и сеттеры обеспечивают инкапсуляцию, но это не так. Инкапсуляция - это не просто делать атрибуты приватными. Речь идет о сокрытии внутренностей от пользователей, а в структуроподобных объектах вы редко хотите что-либо скрывать.
Мой совет: когда у перед вами структуроподобный объект, просто не используйте геттеры и сеттеры, а используйте публичный/прямой доступ. Проще говоря, если вам не нужен сеттер для поддержания инвариантности, вам не нужен приватный атрибут.
PS: Для людей, которые используют библиотеки с поверхностным копированием, влияние на производительность менее важно. Однако вам все равно нужно написать 2 функции вместо 0. Не забывайте, что чем меньше кода вы напишете, тем меньше будет ошибок, проще поддерживать и легче читать этот самый код.
Ну а что думаете вы? Используете ли вы геттеры и сеттеры? И почему?
Перевод материала подготовлен в рамках курса "C++ Developer. Basic". Всех желающих приглашаем на двухдневный онлайн-интенсив HTTPS и треды в С++. От простого к прекрасному. В первый день интенсива мы настроим свой http-сервер и разберем его что называется от и до. Во второй день произведем все необходимые замеры и сделаем наш сервер супер быстрым, что поможет нам понять на примере, чем же все-таки язык С++ лучше других. Регистрация здесь
В этом кратком обзоре я хотел бы поделиться своим опытом, как отключить проверку SSL для тестовых сайтов, иначе говоря, как сделать HTTPS сайты доступными для тестирования на локальных машинах.
В современное время https протокол становится все популярней, у него масса плюсов и достоинств, что хорошо. Правда для разработчиков он может вызывать легкий дискомфорт в процессе тестирования.
Всем известно, что при посещении сайта у которого временно что-то случилось c сертификатом вы обнаружите предупреждение, которое показывается, если сертификат безопасности не является доверенным net::ERR_CERT_AUTHORITY_INVALID?
Привет онлайн-кинотеатрам
Все современные браузеры показывают сообщение об ошибке HSTS
Самый простой способ обхода данного запрета это, разумеется, нажатие на вкладку Дополнительные и согласиться с Небезопасным режимом.
Но не во всех браузерах как оказывается, есть данная возможность. Так я столкнулся с данной проблемой в Chrome на Mac OS
Разработчики данной операционной системы настолько обеспокоены безопасностью пользователей, что даже убрали доступ в Небезопасном режиме к сайту, несмотря на то, что это сайт владельца устройства.
Ну что ж, поскольку, вести разработку в других, более сговорчивых браузерах было не комфортно, вот способы как обойти эту проблему:
Все хромоподобные браузеры (Chrome, Opera, Edge ) могут открыть небезопасную веб страницу, если на английской раскладке клавиатуры набрать фразу:
thisisunsafe
прямо на данной веб странице. Это даст возможность работать с сайтом без оповещение об ошибке на момент текущей сессии браузера, пока вы не закроете вкладку Chrome.
Если же вам предстоит более длительная работа с сайтом, то рекомендую для этих нужд создать отдельного тестового пользователя на рабочем столе и указать ему необходимы флаги.
Для Windows
C:\Program Files
(x86)\Google\Chrome\Application\chrome.exe"
--ignore-certificate-errors
Для Mac OS
/Applications/Google\ Chrome.app/Contents/MacOS/Google\
Chrome --ignore-certificate-errors
--ignore-urlfetcher-cert-requests &> /dev/null
Achtung! Данные манипуляции необходимо выполнять с выключенным Chrome приложением, иначе чуда не произойдет.
Если вы оставите сертификат ненадежным, то некоторые вещи не будут работать. Например, кэширование полностью игнорируется для ненадежных сертификатов.
Браузер напомнит, что вы находитесь в небезопасном режиме. По этому крайне не рекомендуется шастать по злачным сайтам Интернета с такими правами доступами.
*Так же есть метод с добавлением сертификатов тестируемого сайта в конфиги браузера Настройки->Безопасность->Настроить сертификаты->Импорт но мне он показался не продуктивным и очень муторным, поэтому не привожу
Надеюсь моя краткая статья кому-то пригодится при разработке и тестировании сайтов =)
Это простая статическая тестовая страничка, абсолютно ничего интересного.
Когда вы создаете различные формы (например: регистрации или входа) на Flutter, вы не заморачиваетесь с кастомизацией компонентов, потому что вы можете изменить любое поле формы под свой стиль.
Помимо кастомизации, Flutter предоставляет возможность обработки ошибок и валидации полей формы.
И сегодня мы постараемся разобраться с этой темой на небольшом примере.
Ну что ж, погнали!
Наш планЧасть 1- введение в разработку, первое приложение, понятие состояния;
Часть 2- файл pubspec.yaml и использование flutter в командной строке;
Часть 3- BottomNavigationBar и Navigator;
Часть 4- MVC. Мы будем использовать именно этот паттерн, как один из самых простых;
Часть 5 - http пакет. Создание Repository класса, первые запросы, вывод списка постов;
Часть 6 (текущая статья) - работа с формами, текстовые поля и создание поста.
Часть 7 - работа с картинками, вывод картинок в виде сетки, получение картинок из сети, добавление своих в приложение;
Часть 8 - создание своей темы, добавление кастомных шрифтов и анимации;
Часть 9 - немного о тестировании;
Для начала добавим на нашу страницу HomePage
кнопку
по которой мы будем добавлять новый пост:
@overrideWidget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Post List Page"), ), body: _buildContent(), // в первой части мы уже рассматривали FloatingActionButton floatingActionButton: FloatingActionButton( child: Icon(Icons.add), onPressed: () { }, ), );}
Далее создадим новую страницу в файле
post_add_page.dart
:
import 'package:flutter/material.dart';class PostDetailPage extends StatefulWidget { @override _PostDetailPageState createState() => _PostDetailPageState();}class _PostDetailPageState extends State<PostDetailPage> { // TextEditingController'ы позволят нам получить текст из полей формы final TextEditingController titleController = TextEditingController(); final TextEditingController contentController = TextEditingController(); // _formKey пригодится нам для валидации final _formKey = GlobalKey<FormState>(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Post Add Page"), actions: [ // пункт меню в AppBar IconButton( icon: Icon(Icons.check), onPressed: () { // сначала запускаем валидацию формы if (_formKey.currentState!.validate()) { // здесь мы будем делать запроc на сервер } }, ) ], ), body: Padding( padding: EdgeInsets.all(15), child: _buildContent(), ), ); } Widget _buildContent() { // построение формы return Form( key: _formKey, // у нас будет два поля child: Column( children: [ // поля для ввода заголовка TextFormField( // указываем для поля границу, // иконку и подсказку (hint) decoration: InputDecoration( border: OutlineInputBorder(), prefixIcon: Icon(Icons.face), hintText: "Заголовок" ), // не забываем указать TextEditingController controller: titleController, // параметр validator - функция которая, // должна возвращать null при успешной проверки // или строку при неудачной validator: (value) { // здесь мы для наглядности добавили 2 проверки if (value == null || value.isEmpty) { return "Заголовок пустой"; } if (value.length < 3) { return "Заголовок должен быть не короче 3 символов"; } return null; }, ), // небольшой отступ между полями SizedBox(height: 10), // Expanded означает, что мы должны // расширить наше поле на все доступное пространство Expanded( child: TextFormField( // maxLines: null и expands: true // указаны для расширения поля на все доступное пространство maxLines: null, expands: true, textAlignVertical: TextAlignVertical.top, decoration: InputDecoration( border: OutlineInputBorder(), hintText: "Содержание", ), // не забываем указать TextEditingController controller: contentController, // также добавляем проверку поля validator: (value) { if (value == null || value.isEmpty) { return "Содержание пустое"; } return null; }, ), ) ], ), ); }}
Не забудьте добавить переход на страницу формы:
floatingActionButton: FloatingActionButton( child: Icon(Icons.add), onPressed: () { Navigator.push(context, MaterialPageRoute( builder: (context) => PostDetailPage() )); },),
Запускаем и нажимаем на кнопку:
Вуаля! Форма работает.
У новичков могут возникнуть проблемы даже с готовым кодом. И это не издевательство, такое бывает.
Поэтому для 100%-ной работы коды постарайтесь использовать схожие версии Flutter и Dart с моими:
Flutter 2.0.6
Dart SDK version: 2.12.3
Также в комментах я обратил внимание на null safety. Это очень важно, я позабыл об этом и это мой косяк.
Я уже добавил в приложение поддержку null safety. Вы наверно обратили внимание на восклицательный знак:
// ! указывает на то, что мы 100% уверены// что currentState не содержит null значение_formKey.currentState!.validate()
О null safety и о её поддержи в Dart можно сделать целый цикл статей, а возможно и написать целую книгу.
Мы задерживаться не будем и переходим к созданию POST запроса.
POST, как уже было отмечено, является одним из HTTP методов и служит для добавления новых данных на сервер.
Для начала добавим модель для нашего результата и изменим
немного класс Post
:
class Post { // все поля являются private // это сделано для инкапсуляции данных final int? _userId; final int? _id; final String? _title; final String? _body; // создаем getters для наших полей // дабы только мы могли читать их int? get userId => _userId; int? get id => _id; String? get title => _title; String? get body => _body; // добавим новый конструктор для поста Post(this._userId, this._id, this._title, this._body); // toJson() превращает Post в строку JSON String toJson() { return json.encode({ "title": _title, "content": _body }); } // Dart позволяет создавать конструкторы с разными именами // В данном случае Post.fromJson(json) - это конструктор // здесь мы принимаем объект поста и получаем его поля // обратите внимание, что dynamic переменная // может иметь разные типы: String, int, double и т.д. Post.fromJson(Map<String, dynamic> json) : this._userId = json["userId"], this._id = json["id"], this._title = json["title"], this._body = json["body"];}// у нас будут только два состоянияabstract class PostAdd {}// успешное добавлениеclass PostAddSuccess extends PostAdd {}// ошибкаclass PostAddFailure extends PostAdd {}
Затем создадим новый метод в нашем Repository
:
// добавление поста на серверFuture<PostAdd> addPost(Post post) async { final url = Uri.parse("$SERVER/posts"); // делаем POST запрос, в качестве тела // указываем JSON строку нового поста final response = await http.post(url, body: post.toJson()); // если пост был успешно добавлен if (response.statusCode == 201) { // говорим, что все ок return PostAddSuccess(); } else { // иначе ошибка return PostAddFailure(); }}
Далее добавим немного кода в PostController
:
// добавление поста// функция addPost будет принимать callback,// через который мы будет получать результатvoid addPost(Post post, void Function(PostAdd) callback) async { try { final result = await repo.addPost(post); // сервер вернул результат callback(result); } catch (error) { // произошла ошибка callback(PostAddFailure()); }}
Ну что ж пора нам вернуться к нашему представлению
PostAddPage
:
class PostDetailPage extends StatefulWidget { @override _PostDetailPageState createState() => _PostDetailPageState();}// не забываем поменять на StateMVCclass _PostDetailPageState extends StateMVC { // _controller может быть null PostController? _controller; // получаем PostController _PostDetailPageState() : super(PostController()) { _controller = controller as PostController; } // TextEditingController'ы позволят нам получить текст из полей формы final TextEditingController titleController = TextEditingController(); final TextEditingController contentController = TextEditingController(); // _formKey нужен для валидации формы final _formKey = GlobalKey<FormState>(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Post Add Page"), actions: [ // пункт меню в AppBar IconButton( icon: Icon(Icons.check), onPressed: () { // сначала запускаем валидацию формы if (_formKey.currentState!.validate()) { // создаем пост // получаем текст через TextEditingController'ы final post = Post( -1, -1, titleController.text, contentController.text ); // добавляем пост _controller!.addPost(post, (status) { if (status is PostAddSuccess) { // если все успешно то возвращаемя // на предыдущую страницу и возвращаем // результат Navigator.pop(context, status); } else { // в противном случае сообщаем об ошибке // SnackBar - всплывающее сообщение ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Произошла ошибка при добавлении поста")) ); } }); } }, ) ], ), body: Padding( padding: EdgeInsets.all(15), child: _buildContent(), ), ); } Widget _buildContent() { // построение формы return Form( key: _formKey, // у нас будет два поля child: Column( children: [ // поля для ввода заголовка TextFormField( // указываем для поля границу, // иконку и подсказку (hint) decoration: InputDecoration( border: OutlineInputBorder(), prefixIcon: Icon(Icons.face), hintText: "Заголовок" ), // указываем TextEditingController controller: titleController, // параметр validator - функция которая, // должна возвращать null при успешной проверки // и строку при неудачной validator: (value) { // здесь мы для наглядности добавили 2 проверки if (value == null || value.isEmpty) { return "Заголовок пустой"; } if (value.length < 3) { return "Заголовок должен быть не короче 3 символов"; } return null; }, ), // небольшой отступ между полями SizedBox(height: 10), // Expanded означает, что мы должны // расширить наше поле на все доступное пространство Expanded( child: TextFormField( // maxLines: null и expands: true // указаны для расширения поля maxLines: null, expands: true, textAlignVertical: TextAlignVertical.top, decoration: InputDecoration( border: OutlineInputBorder(), hintText: "Содержание", ), // указываем TextEditingController controller: contentController, // также добавляем проверку поля validator: (value) { if (value == null || value.isEmpty) { return "Содержание пустое"; } return null; }, ), ) ], ), ); }}
Логика работы следующая:
мы нажаем добавить новый пост
открывается окно с формой, вводим данные
если все ок, то возвращаемся на предыдущую страницу и сообщаем об этом иначе выводим сообщение об ошибке.
Заключительный момент, добавим обработку результата в
PostListPage
:
floatingActionButton: FloatingActionButton( child: Icon(Icons.add), onPressed: () { // then возвращает объект Future // на который мы подписываемся и ждем результата Navigator.push(context, MaterialPageRoute( builder: (context) => PostDetailPage() )).then((value) { if (value is PostAddSuccess) { // SnackBar - всплывающее сообщение ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Пост был успешно добавлен")) ); } }); },),
Теперь тестируем:
К сожалению JSONPlaceholder на самом деле не добавляет пост и поэтому мы не сможем его увидеть среди прочих постов.
Я надеюсь, что убедил вас в том, что работа с формами на Flutter очень проста и не требует почти никаких усилий.
Большая часть кода - это создание POST запроса на сервер и обработка ошибок.
Полезные ссылки
Всем хорошего кода)
Около 4 лет назад я сделал небольшую статью на тему невозможного в то время суверенного интернета. С того времени многое изменилось, появились законы и даже реализации этих законов, что ожидаемо вызвало много публикаций на эту тему. Однако, для обычного пользователя все эти движения оставались незаметными. Лично у меня тоже не было возможности и необходимости уделять внимание этим вопросам.
Совсем недавно, буквально "на днях", в новостях проскакивали сообщения о недоступности страницы публичного DNS от Cloudflare ( https://1.1.1.1 ) из сетей российских провайдеров, например, Ростелекома, что навело меня на мысль вернуться к изучению вопроса.
Быстрый поиск по профильным ресурсам связистов показал, что уже много месяцев происходит процесс осувереннивания российского сегмента сети. Например, Роскомнадзор строит свой аналог базы RIPE для российских провайдеров и пользователей интернета. И, внезапно, национальную систему доменных имён. На форуме НАГ попадался даже документ с инструкциями по перенастройке провайдерских DNS на суверенный манер, со страшным названием "Инструкция по подключению операторов связи и владельцев АС к Национальной системе доменных имен (НСДИ).
Беглое изучение этого документа приводит к следующим выводам: НСДИ уже активно применяется. В документе приводятся несколько вариантов использования НСДИ, в том числе с возможной подменой корневых DNS (см. статью в wikipedia). С большой вероятностью настройки DNS, которые выдает провайдер вашему устройству, уже используют НСДИ. Другой вывод: в настоящее время функционирование НСДИ обеспечивается мощностями MSK-IX , что следует из принадлежности IP адресов в "Инструкции".
На момент написания статьи сервера НСДИ отдают ту же информацию, что есть в файле root.hints в современных ОС (оригинал файла находится по адресу https://www.internic.net/domain/named.root ). У меня не сложилось однозначного понимания, как НСДИ поможет осуверенниванию и какие плюсы и минусы этого решения. Прошу прокомментировать тех, кто разобрался в вопросе глубже.