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

image

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

Полный код изменённого EthernetServer.cpp
/*
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;
}


Полный код изменённого EthernetServer.h
/*
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 Mega Server

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

© Geektimes