Табло погоды жило по наивной схеме: спросил, получил не то — подожди десять секунд, спроси снова. Через полгода такого режима сервис-поставщик внезапно начал отдавать мне 429, а потом и вовсе на час перестал отвечать на любые мои запросы. Я добросовестно долбил перегруженный сервер каждые десять секунд, и его защита приняла табло за бота. Час тишины в приложении, неловкое объяснение жене, почему на табло опять одни нули, — и понимание, что «повторять упорно» и «повторять вежливо» — разные вещи, с которыми даже самодельное устройство ходит в чужую сеть.
Вежливый повтор строится на двух вещах: на разборе кода — что именно сказал сервер — и на нарастающей паузе между попытками, экспоненциальном backoff. Разбор кодов мы начинали в базовом уроке про HTTP и продолжали в уроке про WiFiClient и HTTPClient — там 301 вёл в заголовок Location, а -1 значил «до сервера не добрались». Здесь соберём полный автомат: switch по коду, попытки с паузами 1-2-4-8 секунд, потолок в пять штук, светодиод статуса и сброс счётчика при успехе.
Коды: пять цифр, пять реакций
Коды раскладываются на классы по первой цифре, и от класса зависит реакция. 200 — счастье: читаем тело и работаем. 301 — сервер говорит «переехали»: на живой плате новый адрес лежит в http.header("Location"), и запрос повторяется уже туда, потому что тело у переадресации пустое. 404 — моя опечатка: повторять бессмысленно хоть сто раз, чинить адрес в скетче. 500 — сервер получил запрос и споткнулся внутри себя: повтор разрешён, но с паузой — перегруженному серверу лавина повторов только мешает. 503 — «перегрузка или обслуживание»: та же пауза, дольше; у 503 бывает подсказка Retry-After с числом секунд, которую вежливый клиент читает.
Отдельного разговора стоит -1: это не ответ сервера, а вердикт «соединение не открылось» — сеть, адрес, порт или таймаут. Отличие от 500 принципиальное: там мы до сервера доехали, и он споткнулся; здесь дорога оборвалась раньше. Отсюда разный порядок действий. При -1 я сначала смотрю WiFi.status() и адрес, и только потом думаю о повторах; выручает и http.setConnectTimeout(3000) — с ним GET вернёт -1 через три секунды вместо дефолтных пяти, и цикл не провисает. При 5xx — наоборот: сеть в порядке, повторяю с нарастающей паузой, а серверу даю выдохнуть.
Класс 4xx, кроме показанного 404, стоит запомнить отдельно: 429 — «слишком часто». Именно его мне показал сервис перед часом блокировки, и это единственный код четвёртой сотни, который лечится повтором — но именно вежливым, с паузой длиннее обычной. Лимиты у бесплатных тарифов бывают разные: у одних — десятки запросов в час, у других — секунды тишины между запросами. Я ставлю штатный интервал так, чтобы часовой бюджет не выбирался больше чем наполовину: запас живёт на случай ретраев, которые умножают число запросов ровно тогда, когда сервер и так болен.
| Код | Что сказал сервер | Что делаю |
|---|---|---|
| 200 | Вот данные | Читаю getString(), сбрасываю счётчик попыток |
| 301 | Страница переехала | Повтор по адресу из http.header("Location") |
| 404 | Такого адреса нет | Повтор бесполезен: чиню адрес в скетче |
| 500 | Споткнулся внутри себя | Повтор с паузой 1-2-4-8 с, потолок — пять попыток |
| 503 | Перегрузка или обслуживание | Пауза дольше; на живой плате смотрю Retry-After |
#include <WiFi.h>#include <HTTPClient.h> const char* ssid = "DomaSetka";const char* password = "superParol";const char* url = "http://api.example.com/v1/weather"; int popytka = 0; // какая попытка текущего циклаint pauza = 1000; // пауза перед следующим запросом, мсunsigned long nextAt = 0; // когда можно снова спрашивать void setup() { Serial.begin(115200); pinMode(12, OUTPUT); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println();} void loop() { if (millis() < nextAt) return; // молчим, пока не вышла пауза popytka++; HTTPClient http; http.begin(url); http.setConnectTimeout(3000); int kod = http.GET(); switch (kod) { case 200: { Serial.print("200: "); Serial.println(http.getString()); digitalWrite(12, HIGH); // успех — горит ровно popytka = 0; pauza = 1000; nextAt = millis() + 15000; // штатный опрос раз в 15 с break; } case 301: Serial.println("301: adres smenilsya, smotri Location"); nextAt = millis() + 60000; // чинить адрес, не долбить break; case 404: Serial.println("404: adres krivoj, povtoryat bessmyslenno"); digitalWrite(12, LOW); nextAt = millis() + 60000; break; case 500: case 503: { if (popytka >= 5) { Serial.print("sdalsya posle 5 popytok, kod="); Serial.println(kod); popytka = 0; pauza = 1000; nextAt = millis() + 60000; // минута тишины после сдачи } else { Serial.print("popytka "); Serial.print(popytka); Serial.print(": kod="); Serial.print(kod); Serial.print(", pauza "); Serial.print(pauza / 1000); Serial.println(" s"); digitalWrite(12, LOW); nextAt = millis() + pauza; pauza = pauza * 2; // 1-2-4-8-8: экспонента с потолком if (pauza > 8000) pauza = 8000; } break; } default: Serial.print("kod="); Serial.println(kod); // -1 и прочее: смотрим сеть и адрес nextAt = millis() + 10000; break; } http.end();}Арифметика пауз: попытка первая — секунда, вторая — две, третья — четыре, четвёртая — восемь, пятая — снова восемь, потолок. Весь цикл сдачи занимает 23 секунды, после которых скетч молчит минуту и начинает заново: в журнале виден ритм «пять попыток — тишина», а не бесконечный пулемёт запросов. Удвоение я пишу умножением pauza = pauza * 2, а не побитовым сдвигом — читается легче, а главное — рядом стоит потолок if (pauza > 8000). Без него пауза после двадцатой попытки выросла бы до семнадцати минут, а после тридцатой переменная int переполнилась бы, пауза стала бы отрицательной, и запросы били бы немедленно и вечно.
Почему потолок именно пять попыток, а не три или десять? Три — мало для всплеска нагрузки: перегруженный сервер часто оживает за 4-6 секунд, и вы успеете спросить трижды, прежде чем он задышает. Десять — много: десять попыток с паузами держат канал занятым полминуты и больше, а выигрыш после восьмой секунды почти нулевой. Пять попыток на 23 секунды — число, подобранное руками на моём тарифе: ни разу не увидел штрафа за частоту, и в девяти случаях из десяти живой сервер успевал ответить до четвёртой попытки.
Сброс счётчика — только на успехе: popytka = 0 и pauza = 1000 возвращают автомат в исходное состояние, и следующий отказ снова получит свои пять попыток. Штатный опрос после успеха я ставлю раз в 15 секунд — millis-расписание то же, что и в планировщиках прошлых уроков. Светодиод дублирует журнал: горит ровно — всё хорошо, тухнет — идёт цикл попыток. Живое место этой схемы: она не лечит 404 и 301. Автомат их не повторяет в принципе — адрес чинится в скетче руками, потому что никакая экспонента не сделает верным неверный путь.
Отдельная забота — не превратить backoff в детектор, который молчит до последнего. Мой порог тревоги: два цикла сдачи подряд — пять попыток, тишина, снова пять попыток; после этого устройство обязано поднять вопрос громче, чем строка в Serial: на дачном табло это особый ритм мигания светодиода, в проектах посерьёзнее — сигнал по запасному каналу. Смысл разделения простой: retry защищает чужой сервер от вас, а монитор доступности защищает вас от сети; вместе они закрывают оба конца провода.
Учебный сервер на Serial-командах
Движок симулятора отвечает кодом 200 на любой GET — настоящих 404 и 500 в виртуальной сети не бывает, и честнее сказать это прямо, чем притворяться. Поэтому в симе роль сервера играет заглушка: функция, возвращающая код по сценарию, а сценарий вы переключаете из Serial — введите 1, и «сервер» отвечает 200; 2 — 404; 3 — 500. Механику ввода чисел с порта я разбирал в уроке про parseInt. Сам автомат кодов и backoff при этом настоящий: те же попытки, те же паузы 1-2-4-8, тот же потолок в пять, какие уйдут на живую плату с http.begin/GET/end вместо заглушки. Введите 3 и смотрите ритм: пять попыток с удваивающейся паузой, сдача, тишина; потом введите 1 — и следующий запрос вспыхивает зелёным.
План пробы по шагам. Введите 3 — автомат покажет пять попыток с удваивающейся паузой: следите за строками pauza — одна, две, четыре, восемь секунд, потом сдача и тишина. Введите 1 — «сервер» оживает: светодиод вспыхивает ровно, счётчик сброшен, штатный опрос идёт дальше. Введите 2 — автомат отказывается долбить: одна попытка, сообщение про кривой адрес, светодиод погашен. Каждая смена сценария сбрасывает попытки — так же и на живой плате: успех возвращает полный бюджет попыток.
- Класс кода диктует реакцию: 2xx читать, 3xx идти по Location, 404 чинить адрес, 5xx повторять с паузой
- Backoff 1-2-4-8 с с потолком в пять попыток: 23 секунды на цикл сдачи — вежливо и предсказуемо
- Умножение pauza * 2 без потолка уползает в минуты, а после переполнения int — в вечный шторм запросов
- -1 — дорога оборвалась до сервера: сначала WiFi.status и адрес; 5xx — доехали, сервер споткнулся: пауза и повтор
- Сброс popytka и pauza — только на успехе; у 503 на живой плате читают Retry-After
- 1. Сервер отвечает 404 на каждый запрос. Сколько попыток сделает автомат из урока?
- 2. Паузы при 500 идут 1-2-4-8 с. Сколько секунд занимает цикл из пяти попыток до сдачи?
- 3. Чем -1 отличается от 500 при отладке?
- 4. Что случится с pauza = pauza * 2 без потолка при затяжном сбое?
- 5. Сервер отдал 503 с заголовком Retry-After: 30. Что делает вежливый клиент?
Теперь у табло полный набор: два источника по тайм-слотам, монитор роутера с журналом и автомат повтора, который не ссорится с чужими серверами. Скелет везде один — millis-расписание и разбор кода, и на этом скелете держится остальное: телеметрия из урока про POST и погодные сценарии итоговой метеостанции. Если из всей тройки брать одну мысль — пусть будет эта: сеть отвечает цифрами, и устройство, которое умеет читать эти цифры и держать паузу, живёт в сети дольше, чем устройство, которое просто стучит. На живой плате прогоните худший случай нарочно: выключите роутер и посмотрите, как скетч сдаётся и молчит по расписанию, а не штурмует мёртвый порт без остановки.
Частые вопросы по теме урока
- Сервер отвечает 404 на каждый запрос. Сколько попыток сделает автомат из урока?
- 404 — адрес кривой в скетче, повтор его не лечит: одна попытка, чинить адрес. Пять попыток с паузами — удел 5xx. Одну — повторять бессмысленно
- Паузы при 500 идут 1-2-4-8 с. Сколько секунд занимает цикл из пяти попыток до сдачи?
- 1 + 2 + 4 + 8 + 8 = 23 секунды: пятая попытка ждёт уже на потолке. После сдачи автомат молчит минуту и начинает заново. 23
- Чем -1 отличается от 500 при отладке?
- При -1 проверяют сеть, адрес и порт (и WiFi.status), при 5xx сеть в порядке — пауза и повтор, сервер споткнулся сам. -1 — соединение не открылось; 500 — доехали, сервер споткнулся
- Что случится с pauza = pauza * 2 без потолка при затяжном сбое?
- Экспонента без потолка уходит в минуты и часы; переполнение int делает паузу отрицательной — запросы бьют немедленно и вечно. Вырастет до минут, а после переполнения int станет отрицательной
- Сервер отдал 503 с заголовком Retry-After: 30. Что делает вежливый клиент?
- Retry-After — подсказка сервера, через сколько секунд можно вернуться. На живой плате её читают http.header("Retry-After"). Ждёт 30 секунд и повторяет