Курс валют для самодельного табло я тянул с чужого API и упёрся в странное: скетч собран по примеру, WiFi подключён, адрес в браузере с ноутбука открывается — а плата получает 403, отказ в доступе. Полчаса я подозревал библиотеку, пока не поднял на компьютере заглушку — маленький сервер на порту 3333, который просто печатает всё, что к нему приходит, — и не посмотрел на запрос глазами сервера. Плата представлялась строкой User-Agent: ESP32HTTPClient, и сайт резал таких клиентов как ботов. Такая заглушка собирается за минуту из средств Serial-отладки и любого netcat: nc -l 3333 в терминале — и вы уже видите свои запросы на экране. Лечится одним вызовом addHeader, но по дороге я наконец понял, как устроены два этажа сетевой библиотеки ESP32 — и с тех пор сетевую отладку делаю в два раза быстрее.
В этом уроке соберём GET-запрос двумя способами: руками на сыром TCP-сокете WiFiClient — чтобы увидеть протокол как он есть — и одной строкой через HTTPClient, обёртку, которая делает ту же работу за нас. Разберём порты (почему 80 в адресе не пишут, а 8080 пишут), заголовки запроса и ответа и ту самую ловушку с User-Agent, из-за которой внешние API молча отказывают плате.
Два этажа: сокет и протокол
Сетевое общение на ESP32 — это два этажа. Нижний — WiFiClient, TCP-сокет: труба между платой и сервером, в которую можно писать байты и из которой читать ответ. Верхний — HTTPClient: обёртка, которая сама открывает трубу, пишет правильные строки запроса, разбирает ответ и выдаёт код. HTTPClient работает поверх WiFiClient — в исходниках библиотеки это видно буквально: begin(url) внутри вызывает client.connect(). Знать оба этажа полезно даже тому, кто всегда пользуется верхним: без нижнего непонятно, что именно происходит, когда сервер молчит.
Зачем WiFiClient, если HTTPClient всё делает сам? Затем, что TCP-труба не обязана говорить по-HTTP. Плата может постучаться на любой порт любого сервера: 80 — стандартный веб, 8080 — запасной веб на домашнем роутере, 3333 — ваша отладочная заглушка, 502 — Modbus TCP для промышленного железа. HTTPClient умеет только HTTP; WiFiClient — всё, что влезает в байты. На этом же объекте построен и веб-сервер: в уроке про мини-сервер на ESP32 мы принимали клиентов через server.available() и отвечали тем же клиентским объектом — зеркальная ситуация, только там платой были мы.
Аналогия, с которой я объясняю это новичкам на работе: сокет — курьер, протокол — формат писем. WiFiClient возит любые конверты куда угодно, а HTTPClient — это уже готовый шаблон письма «дай мне страницу» со всеми обязательными реквизитами. Курьер дешевле шаблона, но писать письма руками утомительно: в HTTP/1.0 после каждого ответа соединение закрывается, и следующий вопрос — снова новое connect и новые строки. Обёртка берёт рутину на себя, а вам оставляет только адрес и путь — поэтому в девяти задачах из десяти я добираюсь до HTTPClient и не жалею.
#include <WiFi.h> WiFiClient client;const char* host = "example.com"; void setup() { Serial.begin(115200); WiFi.begin("DomaSetka", "superParol"); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } if (!client.connect(host, 80)) { // труба в 80 порт Serial.println("Сервер не отвечает"); return; } // запрос HTTP собираем руками: строки запроса и пустая строка в конце client.print(String("GET / HTTP/1.0\r\n") + "Host: " + host + "\r\n" + "User-Agent: MoySensor/1.0\r\n" + "\r\n"); // читаем всё, что сервер отдаст, построчно while (client.connected() || client.available()) { if (client.available()) { String line = client.readStringUntil('\n'); Serial.println(line); } } client.stop(); // трубу закрыли} void loop() { // на живой плате можно повторять по кнопке или таймеру}В коде выше — весь HTTP в голом виде. Первая строка запроса GET / HTTP/1.0, затем обязательный заголовок Host: серверам с несколькими сайтами без него никак — они понимают по нему, к какому домену вы стучитесь на одном и том же адресе. Дальше ваши заголовки по желанию, и пустая строка — знак «запрос кончился». Сервер отвечает текстом: первая строка со статусом вида HTTP/1.0 200 OK, свои заголовки, снова пустая строка и тело. Читаем построчно через readStringUntil, как из Serial. Примерно так же просит страницы браузер, только у него заголовков наберётся с два десятка.
Две детали сырого сокета, которые выдают новичка. Первая: строки запроса заканчиваются парой CR-LF — возврат каретки и перевод строки, как в строке запроса из урока про мини-сервер, а не одиночным переводом строки; сервер, получивший неполный конец строки, обычно прощает, но RFC-требовательные перестают отвечать. Вторая: readStringUntil на живой плате ждёт символ до таймаута, который настраивается client.setTimeout(миллисекунды) — без него скетч виснет на каждой строке, если сервер молчит. И ещё: читая ответ построчно, я всегда ищу подстроку indexOf-ом, а не сравниваю строку целиком — хвостовой CR от сервера ломает точное сравнение, а на indexOf не влияет.
Теперь понятно, что делает HTTPClient в базовом уроке про HTTP-запросы: begin(url) — это connect на нужный порт плюс запомнить путь; GET() — напечатать запрос-строку с Host и парой заголовков, потом вычленить из первой строки ответа число; getString() — дочитать тело после пустой строки; end() — client.stop(). Никакой магии — та же труба, с которой снята рутина. Таблицу кодов 200, 404 и -1 мы там уже раскладывали, повторять не буду: напомню только, что -1 значит «до сервера не добрались вообще», а остальные три цифры — уже внятный ответ самого сервера.
| WiFiClient | HTTPClient | |
|---|---|---|
| Уровень | TCP-сокет, сырые байты | HTTP поверх сокета |
| Порты и протоколы | Любые: 80, 8080, 3333, 502 | Только HTTP(S), порт берётся из адреса |
| Разбор ответа | Вручную: строки и статусы | Сам: код ответа, тело, размер |
| Цена ошибки | Свой байт протокола — своя отладка | Код ответа сразу называет проблему |
| Когда беру | Свой протокол, отладка, не-HTTP сервисы | API и сайты — девять задач из десяти |
Заголовки: паспорт запроса
Заголовки — строки-паспорта, которые клиент приписывает к запросу. HTTPClient собирает минимальный набор сам: Host, User-Agent вида ESP32HTTPClient, Connection: close. Всё остальное добавляется вызовом http.addHeader(имя, значение) после begin и до GET. Самые ходовые в моих скетчах: User-Agent — назвать прибор человеческим именем; Authorization со значением Bearer и ключом — для API с авторизацией; Accept: application/json — попросить данные в JSON, а не в HTML-обёртке.
Ловушка, с которой начался урок: дефолтный User-Agent ESP32HTTPClient у части сервисов попадает под фильтр ботов — сервер отдаёт 403 при безупречном запросе, и в логах плате это никак не объясняется. Лечится представлением: http.addHeader("User-Agent", "DachaTemp/1.0"). Моё правило: в любом скетче, ходящем во внешние API, я ставлю свой User-Agent с именем устройства и версией прошивки — при разборе логов на сервере сразу видно, кто шлёт данные и с какой версии, а при смене поведения API быстро находится виноватый релиз.
Читать заголовки ответа тоже можно и нужно: http.header("Content-Type") вернёт тип тела, http.getSize() — длину из Content-Length, а если сервер шлёт кусками, вернёт -1. Это мой быстрый тест перед разбором ответа на живой плате: увидели Content-Type: application/json — данные тянут на ArduinoJson и его сериализацию; увидели text/html — сервер вернул страницу, и выгрызать из неё температуру indexOf-ом бесполезно, сначала разберитесь, почему API отвечает HTML. Один практический штрих: ключ для Authorization держите в отдельной вкладке скетча или в константе наверху — если когда-нибудь запубликуете код в репозиторий, менять ключ надо в одном месте, а не вылавливать по файлу.
С симулятором — честная оговорка. Нижний этаж движок не эмулирует: client.connect() в виртуальной сети не работает, WiFiClient.available() всегда возвращает ноль, поэтому сырой скетч выше запустится, но данных не увидит; для «прослушки» запроса нужна живая плата и заглушка на компьютере, как у меня в той истории с 403. Зато верхний этаж эмулируется полностью: сеть виртуальная, плата подключается и делает GET на тестовый URL, код ответа и тело печатаются в Serial. В скетче ниже добавлен и User-Agent — тот самый, который когда-то спас моё табло от 403. На живой плате этот же скетч я оставляю в проектах как контрольный тест сети: если он отвечает, проблема не в связи, а в логике выше.
- WiFiClient — TCP-сокет на любой порт; HTTPClient — обёртка над ним, умеющая только HTTP
- Сырой GET: connect(host, 80) → печать запрос-строки, Host и пустой строки → чтение построчно → stop
- http.addHeader() после begin: User-Agent, Authorization, Accept; дефолтный ESP32HTTPClient режут фильтры ботов
- Порядок чтения ответа: код от GET() → getString() → http.end(); после end тело уже не отдать
- http.header("Content-Type") и http.getSize() — быстрый тест, чем именно сервер ответил
- 1. Чем HTTPClient отличается от WiFiClient?
- 2. Плате нужно постучаться на отладочную заглушку на порту 3333. Чем это делается?
- 3. Внешний API отвечает 403 на безупречный запрос. Что добавить?
- 4. Где в сыром HTTP-запросе знак «запрос кончился»?
- 5. http.end() вызван, потом getString(). Что получите?
С одними GET-запросами плата вечно в роли просителя: спросить погоду, спросить курс, спросить статус. А показания собственного датчика хочется отдавать — серверу учёта, народному монитору, своей панели с графиками. Для этого существует POST: тело запроса формируете вы, сервер отвечает кодом 201 и подтверждением записи. В следующем уроке тройки соберём телеметрический скетч целиком: заголовок Content-Type, тело из переменных, коды 200 и 422 и ответ сервера, который не мешает дочитать. И заодно поймём, почему повторный POST — это дубликат записи в базе, а не обновление, как хотелось бы.
Частые вопросы по теме урока
- Чем HTTPClient отличается от WiFiClient?
- HTTPClient сам открывает WiFiClient, пишет строки запроса и разбирает ответ. Нижний этаж — сокет — остаётся WiFiClient. HTTPClient — обёртка над TCP-сокетом WiFiClient, заточенная под HTTP
- Плате нужно постучаться на отладочную заглушку на порту 3333. Чем это делается?
- WiFiClient — сырой TCP-сокет: любой порт, любой протокол. HTTPClient говорит только по HTTP и порт берёт из адреса. WiFiClient: client.connect(host, 3333)
- Внешний API отвечает 403 на безупречный запрос. Что добавить?
- Дефолтный User-Agent попадает под фильтры ботов: сервер отдаёт 403. http.addHeader("User-Agent", ...) решает вопрос. Заголовок User-Agent со своим именем вместо дефолтного ESP32HTTPClient
- Где в сыром HTTP-запросе знак «запрос кончился»?
- Запрос заканчивается пустой строкой после заголовков; после неё в POST ещё идёт тело, а в GET — ничего. Пустая строка после заголовков
- http.end() вызван, потом getString(). Что получите?
- Тело держится внутри клиента до end(). Правильный порядок: GET → проверка кода → getString → end. Пустую строку: тело ответа живёт только до end()