Ethernet Library или почему в природе не существует серверов на Arduino

от автора

Ethernet Library

В этой статье я опишу ситуацию с которой столкнулся во время разработки своего проекта Arduino Mega Server. Суть дела заключается в том, что существует такая библиотека Arduino Ethernet Library, написанная для поддержки сетевой платы Ethernet Shield на чипе W5100. Это стандартная плата и стандартная библиотека, которая многие годы поставляется в комплекте со средой разработки Arduino.

И эта библиотека является основой для всех проектов, использующих обмен информацией по проводной сети. Так вот, оказалось, что эта библиотека попросту профнепригодна. На ней в принципе невозможно построить нормальное сетевое взаимодействие. Можно только «баловаться» одиночными запросами и ответами. Ни о каком построении серверов на базе этой библиотеки речь не может идти. Почему?

Потому, что эта библиотека имеет встроенный «баг», который подвешивает неодиночные запросы на время от трёх до десяти секунд и более. Баг именно встроенный и автор библиотеки об этом знал, о чём свидетельствует его пометки в исходниках (но об этом несколько позже).

Тут нужно понимать, что библиотека, поставляемая с официальной средой разработки является неким стандартом и если у вас не работает проект, то вы будете искать дефект где угодно, только не в стандартной библиотеке, ведь ей много лет и ею пользуются сотни тысяч, если не миллионы людей. Не могут же они все ошибаться?

Почему в природе не существует серверов на Arduino

Разработка проекта шла своим чередом и дело, наконец, дошло до оптимизации кода и увеличения скорости работы сервера и тут я столкнулся с такой ситуацией: приходящие запросы от браузера принимались и «подвешивались» на время от трёх до десяти секунд, в среднем и до двадцати и более секунд при более интенсивном обмене. Вот скриншот, на котором видно, что аномальная задержка ответов сервера «гуляет» по различным запросам.

аномальная задержка

Когда я стал разбираться, то выяснилось, что серверу ничто не мешает ответить, но, тем не менее, запрос «висит» девять с лишним секунд, а при другой итерации этот же запрос висит уже около трёх секунд.

Подобные наблюдения повергли меня в глубокую задумчивость и я перекопал весь код сервера (заодно размялся) но дефектов не обнаружил и вся логика вела к «святая святых» библиотеке Arduino Ethernet Library. Но крамольную мысль, что виновата стандартная библиотека я отбрасывал, как неадекватную. Ведь с библиотекой работают не только пользователи, но и огромное количество профессиональных разработчиков. Не могут же они все не видеть столь очевидных вещей?

Забегая вперёд, скажу, что когда выяснилось, что дело именно в стандартной библиотеке, стало понятно почему в природе не существует (нормальных) серверов на Arduino. Потому, что на базе стандартной библиотеки (с которой работает большинство разработчиков) построить сервер в принципе невозможно. Задержка ответа в десять секунд и более выводит сервер из категории собственно серверов и делает его просто (тормозной) игрушкой.

Промежуточный вывод. Это не Ардуино не подходит для построения серверов, а сетевая библиотека ставит крест на очень интересном классе устройств.

Анатомия проблемы

Теперь от лирики давайте перейдём к техническому описанию проблемы и её практическому решению. Для тех, кто не в курсе, стандартное место расположения библиотеки

arduino\libraries\Ethernet

И первое, что мы рассмотрим, это функция из файла 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 раз для разных файлов и совсем другой характер загрузки, что очень заметно при практическом пользовании.

Оставшиеся проблемы

В таком виде сервер из демонстрации идеи переходит в категорию вещей, которыми можно пользоваться в повседневной жизни, но остались некоторые проблемы. Как вы видите на скриншоте ещё присутствует не принципиальная, но неприятная задержка в три секунды, которой не должно быть. Библиотека написана так, что мест, где код работает не так, как нужно очень много и если вы квалифицированный разработчик, то ваша помощь в установлении причины трёхсекундной задержки будет очень ценной. И для проекта 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 Mega Server

И возможным это стало во многом благодаря тем исправлениям стандартной библиотеки Arduino Ethernet Library о которых я вам сейчас рассказал.

ссылка на оригинал статьи http://geektimes.ru/post/259898/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *