• Уважаемые посетители сайта esp8266.ru!
    Мы отказались от размещения рекламы на страницах форума для большего комфорта пользователей.
    Вы можете оказать посильную поддержку администрации форума. Данные средства пойдут на оплату услуг облачных провайдеров для сайта esp8266.ru
  • Система автоматизации с открытым исходным кодом на базе 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 :)
 
Сверху Снизу