• Система автоматизации с открытым исходным кодом на базе esp8266/esp32 микроконтроллеров и приложения IoT Manager. Наша группа в Telegram

Делюсь опытом Непонятное поведение ESP8266WebServer

Алексей.

Active member
Решил немножко проверить работу ESP8266WebServer.
Добавил только обработчик корневого url, в котором на запрос клиента отвечаю количеством обработанных запросов, просто вызываю server.send(200, "text/plain", message);
Со стороны ПК запустил скриптик (в двух экземплярах) в котором используя curl выполнил 100000 запросов, esp честно отсчитал 200К запросов и довольно быстро.
Немножко усложнил опыт, опять запускаю скрипт и пока он выполняется, соединяюсь с тем-же сервером, отправляю запрос получаю ответ и продолжаю оставаться в соединении до тех пор пока сервер сам не разорвет соединение, сервер через 2-е секунды разрывает соединение.
Почему то скрипт работающий рядом начал тормозить.
Заглянул в исходники, обнаружил потенциально опасное место в методе handleClient.
Код:
if (_currentClient.connected() || _currentClient.available()) {
    switch (_currentStatus) {
    case HC_NONE:
      // No-op to avoid C++ compiler warning
      break;
    case HC_WAIT_READ:
      // Wait for data from client to become available
      if (_currentClient.available()) {
        if (_parseRequest(_currentClient)) {
          _currentClient.setTimeout(HTTP_MAX_SEND_WAIT);
          _contentLength = CONTENT_LENGTH_NOT_SET;
          _handleRequest();

          if (_currentClient.connected()) {
            _currentStatus = HC_WAIT_CLOSE;
            _statusChange = millis();
            keepCurrentClient = true;
          }
        }
      } else { // !_currentClient.available()
        if (millis() - _statusChange <= HTTP_MAX_DATA_WAIT) {
          keepCurrentClient = true;
        }
        callYield = true;
      }
      break;
    case HC_WAIT_CLOSE:
      // Wait for client to close the connection
      if (millis() - _statusChange <= HTTP_MAX_CLOSE_WAIT) {
        keepCurrentClient = true;
        callYield = true;
      }
    }
  }
В пока запрос не принят полностью статус обработки имеет значение HC_WAIT_READ
если запрос принят полностью, вызывается обработчик этого запроса и если клиент ещё в соединении - статус переводится в HC_WAIT_CLOSE, как раз мой случай, клиент получил ответ и не торопится разрывать соединение с сервером.
Далее для значения статуса HC_WAIT_CLOSE очень интересный код
Код:
if (millis() - _statusChange <= HTTP_MAX_CLOSE_WAIT) {
  keepCurrentClient = true;
  callYield = true;
}
т.е. пока не прошло HTTP_MAX_CLOSE_WAIT (константа 2000) миллисекунд с последнего изменения статуса, продолжаем сохранять соединение.
В случае перехода millis через максимальное значение, соединение будет сохраняться очень долго и запросы других клиентов не будут обрабатываться.
Непонятно для чего ожидать разрыва соединения со стороны клиента.
Почему после обработки запроса сервер не разрывает соединение сразу, а ждет разрыв соединения со стороны клиента, клиент может вообще никогда не разрывать соединение.
 

pvvx

Активный участник сообщества
Значит опять ходим по кругу...

Закон TCP - Сервер никогда не должен разрывать соединение первым.
---
Алгоритм для кривых клиентов:
Посылается connect close.
Ждете разрыва соединения со стороны клиента 5 сек.
Клиент не разорвал соединение - рвет сервер и кидает TCP в TIME_WAIT на 120 сек.
Далее на 120 сек у сервера занят использованный порт с параметрами разорванного соединения.
Для Lwip - просто занят порт, т.к. он не проверяет внешние данные соединения у TCP в TIME_WAIT .
 

pvvx

Активный участник сообщества
Обрезание TIME_WAIT грозит большими проблемами в сети и красных полосках в Wireshark :p
Arduino не занимается TIME_WAIT, как итог - ESP не соединяется особенно после просыпания, т.к. комп посылает её - TCP стек компа дает разрыв соединения с тем-же запросом портов дурной ESP (IP то не менялись, а TCP метрики прошлого соединения ещё в TIME_WAIT) -> в Wireshark красные полоски.... Что твориться у NAT при работе ESP - это особый случай хулиганства...
Выкиньте ESP и забудьте. Оно работать в IP сетях не умеет.
 

Алексей.

Active member
Ждете разрыва соединения со стороны клиента 5 сек.
Прекрасно, только что там в коде творится, если в течении 2-х секунд переполнился millis и клиент не присылает fin, а сервер остается в состоянии established, остальные клиенты идут лесом и очень долго.
Закон TCP - Сервер никогда не должен разрывать соединение первым.
Протокол ftp
По каналу управления запрашиваю параметры канала данных для приема файла, сервер отвечает адресом и портом для канала данных, который может быть расположен в общем случае на другом ресурса.
Устанавливаю соединение для получения данных, принимаю данные, признаком того, что данные закончились является закрытие соединения со стороны сервера.
Код:
The server MUST close the data connection under the following conditions:
 1. The server has completed sending data in a transfer mode
    that requires a close to indicate EOF.
 

pvvx

Активный участник сообщества
т.е. пока не прошло HTTP_MAX_CLOSE_WAIT (константа 2000) миллисекунд с последнего изменения статуса, продолжаем сохранять соединение.
Все эти затычки никак не помогают ESP уложиться в элементарные спецификации IP/TCP.
Для множественных соединений с LwIP не очень-то всё хорошо. Должен исполняться только один тред. Иначе будут заскоки на чужие соединения...
Не поверю, что в Дурине всё с этим писано верно.
 

pvvx

Активный участник сообщества
Прекрасно, только что там в коде творится, если в течении 2-х секунд переполнился millis и клиент не присылает fin, а сервер остается в состоянии established, остальные клиенты идут лесом и очень долго.

Протокол ftp
По каналу управления запрашиваю параметры канала данных для приема файла, сервер отвечает адресом и портом для канала данных, который может быть расположен в общем случае на другом ресурса.
Устанавливаю соединение для получения данных, принимаю данные, признаком того, что данные закончились является закрытие соединения со стороны сервера.
Код:
The server MUST close the data connection under the following conditions:
 1. The server has completed sending data in a transfer mode
    that requires a close to indicate EOF.
И что не так? По данному делу TCP ушло в TIME_WAIT и более по данным IP : port сервера и клиента связи не должно быть в 120 сек.
 

pvvx

Активный участник сообщества
А т.к. ESP не имеет ресурсов удерживать массив TCP c таймерами (надо более 200 кило RAM), то её в помойку.
Ну или пишите так:
Соединились, отработали, sleep(120 сек).
 

pvvx

Активный участник сообщества
В RTL я менял (добавлял код) опрос отложенных TCP у LwIP для WEB. Там web отрабатывает к сотне одновременных соединений и обеспечивает открытие и закрытие десятков HTTP соединений в секунду… Ради этого пришлось повозиться…

Алго примерно такой – когда на запрос порта LwIP говорит, что он занят, то сканируете его массив TCP в состоянии TIME_WAIT и сравниваете метрику. Если хотя-бы один из IP и поров не совпадает, то можете открывать порт принудительно. Но у RTL 2 мегабайта RAM и в неё лезет…

Но самое главное – это сервер не должен закрывать соединение TCP. При реальной и правильной работе web сервера в TIME_WAIT попадает очень мало TCP. Это часто происходит при соединении с нерадивыми прокси. У них вечно не хватает портов и они рвут соединение не по правилам… Но это нарушение и нечего на него пенять.
 

pvvx

Активный участник сообщества
Вы уж там не переживайте с потерей ESP :)

В OpenWRT такое длилось многие годы. Пораженный детский web и прочие аналогичные приложения сменили только недавно и большую часть несоответствия спецификации TCP устранили. Но не везде. Летом потребовался месяц исключительно на устранение детских болезней в старой OpenWRT связанных с TCP…
ESP годится для начального изучения и первых опытов, но не для рабочих изделий. Так что жить ей в этой области нормально.
 

Алексей.

Active member
Да всё так, вы высказали свое мнение про tcp "Закон TCP - Сервер никогда не должен разрывать соединение первым." а я сослался на протокол, использующий именно tcp, в котором первой причиной по которой сервер разрывает соединение с клиентом - это информировать клиента, что передача завершена.
По данному делу TCP ушло в TIME_WAIT и более по данным IP : port сервера и клиента связи не должно быть в 120 сек.
А с таймаутом я лажанулся, разность двух без знаковых переменных отрицательной не станет, при переходе миллисов через 0, сервер просто меньше будет ждать когда клиент разорвет соединение.
 

Алексей.

Active member
Несмотря на запрет "Закон TCP - Сервер никогда не должен разрывать соединение первым." спецификация http секция 8.1.2.1
Код:
   An HTTP/1.1 server MAY assume that a HTTP/1.1 client intends to
   maintain a persistent connection unless a Connection header including
   the connection-token "close" was sent in the request. If the server
   chooses to close the connection immediately after sending the
   response, it SHOULD send a Connection header including the
   connection-token close.
предполагает следующее:
Сервер МОЖЕТ определить, что клиент желает сохранять соединение, если клиент на включил в запросе заголовок Connection со значением close.
Если сервер выбирает закрыть соединение немедленно после отправки ответа, он ДОЛЖЕН отправить заголовок Connection со значением close.

Клиента, которого я изображал, это полторы строчки кода.
Bash:
#!/bin/bash
exec 7<>/dev/tcp/192.168.4.1/80
echo -e "GET / HTTP/1.1\r\nHost: 192.168.4.1\r\nConnection: close\r\n\r\n" >&7
cat <&7
И клиентом я заголовок Connection: close в запросе отправлял и от сервера этот же заголовок в ответе получал, но несмотря на это, сервер не разрывал соединения немедленно после отправки ответа, хотя и включил нужный заголовок. Интересно, что он хотел этим сказать.
 

pvvx

Активный участник сообщества
Сервер МОЖЕТ определить, что клиент желает сохранять соединение, если клиент на включил в запросе заголовок Connection со значением close.
Если сервер выбирает закрыть соединение немедленно после отправки ответа, он ДОЛЖЕН отправить заголовок Connection со значением close.
Это уровень HTTP, а не TCP.
И клиентом я заголовок Connection: close в запросе отправлял и от сервера этот же заголовок в ответе получал, но несмотря на это, сервер не разрывал соединения немедленно после отправки ответа, хотя и включил нужный заголовок. Интересно, что он хотел этим сказать.
Что TCP стек ждет передачи конца TCP соединения от клиента, чтобы оставить порт свободным для новых запросов.
Разница у сервера и клиента в том, что сервер многопользовательский, а клиент - один для каждого пользователя. Кому в сети разбираться с TIME_WAIT? Естественно клиенту со своими соединениями, а не серверу. Иначе у сервера вмиг закончатся порты. Их всего-то 65536 и не все отданы под это дело. А сервер обслуживает более 65536 запросов за 120 сек. Иначе это DDOS, чего и так боятся.
 

pvvx

Активный участник сообщества
Со стороны ПК запустил скриптик (в двух экземплярах) в котором используя curl выполнил 100000 запросов, esp честно отсчитал 200К запросов и довольно быстро.
За сколько времени это произошло? Порт 80 у ESP один, клиент выходит на него с 0..65535 порта. IP одинаковы. За 120 сек, если ESP зарывал TCP соединение первым, 200К запросов пройти не может.
Проблемы с очередью TIME_WAIT — Alexander's Blog
 

pvvx

Активный участник сообщества
Проблема TIME_WAIT для исходящих соединений
Соединение в операционной системы идентифицируется четырьмя параметрами: локальный IP, локальный порт, удалённый IP, удаленный порт. Допустим, у нас есть клиент, который активно подключается/отключается к удаленной службе. Поскольку оба IP и удаленный порт остаются неизменными, то на каждое новое соединение выделяется новый локальный порт. Если клиент был активной стороной завершения TCP-сессии, то это соединение будет заблокировано какое-то время в состоянии TIME_WAIT. Если соединения устанавливаются быстрее чем порты выходят из карантина, то при очередной попытке соединения клиент получит ошибку EADDRNOTAVAIL (errno=99).
 

pvvx

Активный участник сообщества
@Алексей
Ещё раз - Оптимальным для сервера и является установить свой тайм-аут после посылки закрытия HTTP соединения. Т.к. сеть у нас может работать через GSM, то там и возникают самые большие задержки. Статистически и установлено, что если через 5 сек нет дисконекта TCP (FIN/ACK) от клиента, то придется закрывать TCP соединение HTTP серверу. Так-же инициировать первым закрытие TCP сервер может при ошибках (иногда это оправдано). Но это всё грозит ему исчерпанием портов и другие клиенты не получат доступа… Но на больших серверах, сравнение находящихся в карантине TCP соединений с TIME_WAIT идет по многим параметрам, а не как у LwIP (ESP) и исчерпать порты вы cможете только своим тестом с одного IP… С других IP доступ не будет нарушен. Но и там все орут и бегают, пытаются уменьшить период TIME_WAIT - весь инет завален этим... А причина - самопальные скрипты без учета сказанного выше...
 

pvvx

Активный участник сообщества
LwIP поддерживает TIME_WAIT, но ограниченно. Ограничение связано с проверкой IP и портов (входящих и исходящих) для анализа порта в карантине. В Arduino обычно стоит флаг reuse порт при открытии нового порта для TCP. Буфер для TIME_WAIT обрезан или вообще отключен, и/или по открытию/закрытию TCP стоит команда удалить TCP с состоянием TIME_WAIT. Причина – нет RAM для буфера с TCP в состоянии TIME_WAIT. От сюда и все последствия – ESP хулиган и кошмарит внешнюю сеть :) Ей всё равно до переполнения внешних NAT и прочих шлюзов, что приводит к падению скорости ваших-же соединений с внешним миром, до глюков.

т.е. пока не прошло HTTP_MAX_CLOSE_WAIT (константа 2000) миллисекунд с последнего изменения статуса, продолжаем сохранять соединение.
На этапе после посылки конца HTTP соединения* сервером никто не мешает вам бросить восвояси TCP соединение в LwIP. LwIP разорвет его сам хоть по общему тайм-ауту. Если в соединение придет ещё что-то от клиента, то LwIP обнаружив, что соединение не обслуживается так-же разорвет его. В большинстве случаев этого не произойдет, если клиент порядочный и сам закроет TCP соединение. Чем это грозит – затратами памяти на структуру TCP в LwIP и более ни чем. Но прослойка в виде socket() так не умеет и она жирная, с массой дублирующих переменных TCP структуры LwIP, плюс дублирующие буфера... В итоге, в малой памяти ESP socket-ов может существовать ограниченное кол-во. Детки по другому писать не умеют – как-же без socket()?

Вот и химичат в коде Arduino… то задержку вставят, то ещё чего, но спецификации TCP – тю-тю.


* The HTTP server sent a connection header, including a connection close token (HTTP-сервер отправил заголовок соединения, включая маркер закрытия соединения). Поведение HTTP сервера в случае желаний клиента продолжить не нормируется. Если сервер хочет закрыть HTTP соединение – клиент должен подчиниться. Можете послать и какой код ошибки... Клиент всегда прав - это из другой области :)
 

Алексей.

Active member
За сколько времени это произошло? Порт 80 у ESP один, клиент выходит на него с 0..65535 порта. IP одинаковы. За 120 сек, если ESP зарывал TCP соединение первым, 200К запросов пройти не может.
Вы на верно пропустили, я писал, что Со стороны ПК запустил скриптик (в двух экземплярах) в котором используя curl выполнил 100000 запросов
Bash:
#!/bin/bash
counter=1
while [ $counter -le 100000 ]; do
  echo ============= $counter =============
  ((counter++))
  curl http://192.168.4.1/
done[code]
Не закрывал esp соединение первым!
Что TCP стек ждет передачи конца TCP соединения от клиента, чтобы оставить порт свободным для новых запросов.
Да при чем тут это?
http сервер, включив в ответе заголовок Connection: close проинформировал клиента, что соединение будет немедленно закрыто, и не закрывает его в течении таймаута
Почему не закрыть соединение и отдать на откуп стеку сценарий разрыва соединения.
При загрузке данных по ftp, именно закрытием соединения сервер информирует клиента, что данным настал EOF.
Для http, сервер уже принял решение о немедленном закрытии соединения, чего он тогда его не закрывает?

Смотрю на диаграмму состояний для tcp в секции 3.2 картинка 6
И не вижу ограничений перехода из состояния established как в состояние ожидания закрытия соединения удаленного сокета (ожидание fin, правая сторона), так и самостоятельное закрытие соединения (отправка fin, левая сторона)
 

pvvx

Активный участник сообщества
Вы на верно пропустили, я писал, что Со стороны ПК запустил скриптик (в двух экземплярах) в котором используя curl выполнил 100000 запросов
Да, пропустил, что запросов, а не соединений.
http сервер, включив в ответе заголовок Connection: close проинформировал клиента, что соединение будет немедленно закрыто, и не закрывает его в течении таймаута
Почему не закрыть соединение и отдать на откуп стеку сценарий разрыва соединения.
Про это и писано - чтобы не занимать порты.
При загрузке данных по ftp, именно закрытием соединения сервер информирует клиента, что данным настал EOF.
Для http, сервер уже принял решение о немедленном закрытии соединения, чего он тогда его не закрывает?
Про это и писано - чтобы не занимать порты. Не включать в карантин с TIME_WAIT использованный порт.
С этим TIME_WAIT в TCP очень многое связано. Ранее, когда процы и сеть была медленная это не сказывалось - хватало пула портов...
Вставив задержку в Arduino пытаются обойти обрезание спецификации по TIME_WAIT.
Ещё в прошлом вашем "делюсь опытом" про получение случайного числа и описал проблемку у ESP. Случайное число ESP нужно для установки старта в пуле портов TCP. Таким кривым методом пытаются решить проблему TIME_WAIT. Но случайного числа там не выходит и после deep_sleep ESP лезет с тем-же IP и портом к серверу по тому-же IP и порту, который у сервера (при вашем подходе - что он инициировал закрытие соединение первым) ответ будет от TCP стека - закрыть соединение по ошибке. Но ESP не понимает и настаивает до "брызг крови" -> в WireShark это отмечается красными пакетами. Идет потеря времени и разряд батареи у ESP, а соединения так и нет и не будет... :) До HTTP или FTP это дело вообще не доходит.
 

pvvx

Активный участник сообщества
Запомнить последний индекс использованного порта в часах ESP видимо не способна... Хотя это не решает всю беду урезания TIME_WAIT, но всё-же поможет при местной связи без NAT.
Keep-Alive - это тоже борьба с TIME_WAIT, заодно и с пингом на открытие/закрытие TCP. Но ничего уже не решает. Всё современное оборудование это делает очень быстро...
 

pvvx

Активный участник сообщества
Перевод AJAX на Websocket - так-же из бед с TIME_WAIT :)
 
Сверху Снизу