В этой статье я опишу ситуацию с которой столкнулся во время разработки своего проекта Arduino Mega Server. Суть дела заключается в том, что существует такая библиотека Arduino Ethernet Library, написанная для поддержки сетевой платы Ethernet Shield на чипе W5100. Это стандартная плата и стандартная библиотека, которая многие годы поставляется в комплекте со средой разработки Arduino.
И эта библиотека является основой для всех проектов, использующих обмен информацией по проводной сети. Так вот, оказалось, что эта библиотека попросту профнепригодна. На ней в принципе невозможно построить нормальное сетевое взаимодействие. Можно только «баловаться» одиночными запросами и ответами. Ни о каком построении серверов на базе этой библиотеки речь не может идти. Почему?
Потому, что эта библиотека имеет встроенный «баг», который подвешивает неодиночные запросы на время от трёх до десяти секунд и более. Баг именно встроенный и автор библиотеки об этом знал, о чём свидетельствует его пометки в исходниках (но об этом несколько позже).
Тут нужно понимать, что библиотека, поставляемая с официальной средой разработки является неким стандартом и если у вас не работает проект, то вы будете искать дефект где угодно, только не в стандартной библиотеке, ведь ей много лет и ею пользуются сотни тысяч, если не миллионы людей. Не могут же они все ошибаться?
Почему в природе не существует серверов на Arduino
Разработка проекта шла своим чередом и дело, наконец, дошло до оптимизации кода и увеличения скорости работы сервера и тут я столкнулся с такой ситуацией: приходящие запросы от браузера принимались и «подвешивались» на время от трёх до десяти секунд, в среднем и до двадцати и более секунд при более интенсивном обмене. Вот скриншот, на котором видно, что аномальная задержка ответов сервера «гуляет» по различным запросам.
Когда я стал разбираться, то выяснилось, что серверу ничто не мешает ответить, но, тем не менее, запрос «висит» девять с лишним секунд, а при другой итерации этот же запрос висит уже около трёх секунд.
Подобные наблюдения повергли меня в глубокую задумчивость и я перекопал весь код сервера (заодно размялся) но дефектов не обнаружил и вся логика вела к «святая святых» библиотеке Arduino Ethernet Library. Но крамольную мысль, что виновата стандартная библиотека я отбрасывал, как неадекватную. Ведь с библиотекой работают не только пользователи, но и огромное количество профессиональных разработчиков. Не могут же они все не видеть столь очевидных вещей?
Забегая вперёд, скажу, что когда выяснилось, что дело именно в стандартной библиотеке, стало понятно почему в природе не существует (нормальных) серверов на Arduino. Потому, что на базе стандартной библиотеки (с которой работает большинство разработчиков) построить сервер в принципе невозможно. Задержка ответа в десять секунд и более выводит сервер из категории собственно серверов и делает его просто (тормозной) игрушкой.
Промежуточный вывод. Это не Ардуино не подходит для построения серверов, а сетевая библиотека ставит крест на очень интересном классе устройств.
Анатомия проблемы
Теперь от лирики давайте перейдём к техническому описанию проблемы и её практическому решению. Для тех, кто не в курсе, стандартное место расположения библиотеки
arduinolibrariesEthernet
И первое, что мы рассмотрим, это функция из файла EthernetClient.cpp
EthernetClient EthernetServer::available() {
accept();
for (int sock = 0; sock < MAX_SOCK_NUM; sock++) {
EthernetClient client(sock);
if (EthernetClass::_server_port[sock] == _port &&
(client.status() == SnSR::ESTABLISHED ||
client.status() == SnSR::CLOSE_WAIT)) {
if (client.available()) {
// XXX: don't always pick the lowest numbered socket.
return client;
}
}
}
return EthernetClient(MAX_SOCK_NUM);
}
Когда я преодолел психологический барьер (под давлением логики) и начал искать дефект в Ethernet Library я начал именно с этой функции. Заметил я и странный комментарий автора, но не придал ему значения. Перелопатив всю библиотеку, я опять, но уже через пару дней и сильно продвинувшись в сетевых технологиях, вернулся к этой функции потому, что логика подсказывала, что проблема именно здесь и более внимательно посмотрел на комментарий.
// XXX: don't always pick the lowest numbered socket.
Друзья, там всё написано открытым текстом. В вольном переводе это звучит примерно так «это работает, но не всегда». Подождите минуточку, что значит «не всегда»? У нас же не воскресный клуб по игре в лото. А когда не работает, то что? А вот когда «не работает» и начинаются проблемы с задержкой в десять секунд.
И автор об этом определённо знал, о чём свидетельствует самооценка его творения — три икса. Без комментариев. Эта библиотека является основой для многих клонов и, внимание, эти три икса кочуют из одного проекта в другой. Если вы разработчик, то не заметить эту проблему можно только не разу не протестировав сетевой обмен. Тоже без комментариев.
Для тех, кто плохо разбирается в коде поясню суть проблемы простыми словами. Цикл перебирает сокеты и, как только находит подходящий, возвращает клиента, а остальных просто игнорирует. И они висят по десять секунд, пока «карты благоприятно не лягут».
Решение проблемы
Поняв причину проблемы, мы конечно не остановимся на этом и попробуем её решить. Для начала давайте перепишем функцию таким образом, чтобы получение или не получение сокета не зависело от воли случая и происходило всегда, если есть свободный. Это решит две проблемы:
- запросы не будут зависать
- «последовательные» запросы превратятся в «параллельные», что значительно ускорит работу
Итак, код новой функции:
EthernetClient EthernetServer::available_(int sock) {
accept_(sock);
EthernetClient client(sock);
if (EthernetClass::_server_port[sock] == _port &&
(client.status() == SnSR::ESTABLISHED ||
client.status() == SnSR::CLOSE_WAIT)) {
if (client.available()) {
return client;
}
}
return EthernetClient(MAX_SOCK_NUM);
}
Убираем цикл, явным образом указываем сокет и ничего не теряем — если он свободен, то мы гарантированно получаем клиента (если он нам подходит).
То же самое «по цепочке» проделываем с кодом функции accept, убираем цикл и явно указываем сокет.
void EthernetServer::accept_(int sock) {
int listening = 0;
EthernetClient client(sock);
if (EthernetClass::_server_port[sock] == _port) {
if (client.status() == SnSR::LISTEN) {
listening = 1;
}
else if (client.status() == SnSR::CLOSE_WAIT && !client.available()) {
client.stop();
}
}
if (!listening) {
//begin();
begin_(sock); // added
}
}
И не забываем поправить файл EthernetServer.h
class EthernetServer :
public Server {
private:
uint16_t _port;
//void accept();
void accept_(int sock);
public:
EthernetServer(uint16_t);
//EthernetClient available();
EthernetClient available_(int sock);
virtual void begin();
virtual void begin_(int sock);
virtual size_t write(uint8_t);
virtual size_t write(const uint8_t *buf, size_t size);
using Print::write;
};
Вот, собственно и всё. Мы внесли изменения в стандартную библиотеку и поведение сервера кардинально изменилось. Если раньше всё работало очень медленно, за гранью любых представление о юзабилити, то теперь скорость загрузки страниц значительно возрасла и стала вполне приемлемой для нормального использования.
Обратите внимание на уменьшение задержки в 3 — 5 раз для разных файлов и совсем другой характер загрузки, что очень заметно при практическом пользовании.
Mod for Arduino Mega Server project
fix bug delay answer server
*/
#include «w5100.h»
#include «socket.h»
extern «C» {
#include «string.h»
}
#include «Ethernet.h»
#include «EthernetClient.h»
#include «EthernetServer.h»
EthernetServer::EthernetServer(uint16_t port) {
_port = port;
}
void EthernetServer::begin() {
for (int sock = 0; sock < MAX_SOCK_NUM; sock++) {
EthernetClient client(sock);
if (client.status() == SnSR::CLOSED) {
socket(sock, SnMR::TCP, _port, 0);
listen(sock);
EthernetClass::_server_port[sock] = _port;
break;
}
}
}
void EthernetServer::begin_(int sock) {
EthernetClient client(sock);
if (client.status() == SnSR::CLOSED) {
socket(sock, SnMR::TCP, _port, 0);
listen(sock);
EthernetClass::_server_port[sock] = _port;
}
}
/*
void EthernetServer::accept() {
int listening = 0;
for (int sock = 0; sock < MAX_SOCK_NUM; sock++) {
EthernetClient client(sock);
if (EthernetClass::_server_port[sock] == _port) {
if (client.status() == SnSR::LISTEN) {
listening = 1;
}
else if (client.status() == SnSR::CLOSE_WAIT && !client.available()) {
client.stop();
}
}
}
if (!listening) {
begin();
}
}
*/
void EthernetServer::accept_(int sock) {
int listening = 0;
EthernetClient client(sock);
if (EthernetClass::_server_port[sock] == _port) {
if (client.status() == SnSR::LISTEN) {
listening = 1;
}
else if (client.status() == SnSR::CLOSE_WAIT && !client.available()) {
client.stop();
}
}
if (!listening) {
//begin();
begin_(sock); // added
}
}
/*
EthernetClient EthernetServer::available() {
accept();
for (int sock = 0; sock < MAX_SOCK_NUM; sock++) {
EthernetClient client(sock);
if (EthernetClass::_server_port[sock] == _port &&
(client.status() == SnSR::ESTABLISHED ||
client.status() == SnSR::CLOSE_WAIT)) {
if (client.available()) {
// XXX: don't always pick the lowest numbered socket.
return client;
}
}
}
return EthernetClient(MAX_SOCK_NUM);
}
*/
EthernetClient EthernetServer::available_(int sock) {
accept_(sock);
EthernetClient client(sock);
if (EthernetClass::_server_port[sock] == _port &&
(client.status() == SnSR::ESTABLISHED ||
client.status() == SnSR::CLOSE_WAIT)) {
if (client.available()) {
return client;
}
}
return EthernetClient(MAX_SOCK_NUM);
}
size_t EthernetServer::write(uint8_t b) {
return write(&b, 1);
}
size_t EthernetServer::write(const uint8_t *buffer, size_t size) {
size_t n = 0;
//accept();
for (int sock = 0; sock < MAX_SOCK_NUM; sock++) {
accept_(sock); // added
EthernetClient client(sock);
if (EthernetClass::_server_port[sock] == _port &&
client.status() == SnSR::ESTABLISHED) {
n += client.write(buffer, size);
}
}
return n;
}
Mod for Arduino Mega Server project
fix bug delay answer server
*/
#ifndef ethernetserver_h
#define ethernetserver_h
#include «Server.h»
class EthernetClient;
class EthernetServer:
public Server {
private:
uint16_t _port;
//void accept();
void accept_(int sock);
public:
EthernetServer(uint16_t);
//EthernetClient available();
EthernetClient available_(int sock);
virtual void begin();
virtual void begin_(int sock);
virtual size_t write(uint8_t);
virtual size_t write(const uint8_t *buf, size_t size);
using Print::write;
};
#endif
Оставшиеся проблемы
В таком виде сервер из демонстрации идеи переходит в категорию вещей, которыми можно пользоваться в повседневной жизни, но остались некоторые проблемы. Как вы видите на скриншоте ещё присутствует не принципиальная, но неприятная задержка в три секунды, которой не должно быть. Библиотека написана так, что мест, где код работает не так, как нужно очень много и если вы квалифицированный разработчик, то ваша помощь в установлении причины трёхсекундной задержки будет очень ценной. И для проекта Arduino Mega Server и для всех пользователей Arduino.
Последний момент
Поскольку мы изменили код стандартной библиотеки, то и вызывать её функции нужно несколько иным способом. Здесь я привожу код, который реально работает и который обеспечивал работу AMS на скриншоте выше.
for (int sock = 0; sock < MAX_SOCK_NUM; sock++) {
EthernetClient sclient = server.available_(sock);
serverWorks2(sclient);
}
Здесь задача перебора сокетов перенесена на уровень клиентского скетча и, что самое главное, и в чём смысл всего вышесказанного, не происходит «подвисания» запросов. И функция собственно сервера:
void serverWorks2(EthernetClient sclient) {
...
}
С полным кодом сервера вы можете ознакомиться на сайте MajorDoMo, где на форуме можно скачать последнюю актуальную версию Arduino Mega Server. Осталось решить последнюю проблему трёхсекундной задержки и у нас будет настоящий, быстро работающий сервер на Ардуино. Кстати, скоро выйдет новая версия AMS со всеми исправлениями и улучшениями в которой решена одна из самых актуальных проблем — автономная работа без поддержки сервера MajorDoMo.
И возможным это стало во многом благодаря тем исправлениям стандартной библиотеки Arduino Ethernet Library о которых я вам сейчас рассказал.
Автор: smart_alex