Сетевое программирование - TCP | HTTP сервер

Продолжаем путешествие по сетевым протоколам.

TCP и UDP — оба протоколы транспортного уровня. UDP — это протокол без установления соединения и с негарантированной доставкой пакетов. TCP (Transmission Control Protocol) — это протокол с установлением соединения и с гарантированной доставкой пакетов. Сначала происходит рукопожатие (Привет. | Привет. | Поболтаем? | Давай.), после чего соединение считается установленным. Далее по этому соединению туда и обратно посылаются пакеты (идет беседа), причем с проверкой, дошел ли пакет до получателя. Если пакет потерялся, или дошел, но с битой контрольной суммой, то он посылается повторно («повтори, не расслышал»). Таким образом TCP более надёжен, но он сложнее с точки зрения реализации и соответственно требует больше тактов / памяти, что имеет не самое последнее значение для микроконтроллеров. В качестве примеров прикладных протоколов, использующих TCP, можно назвать FTP, HTTP, SMTP и многие другие.

TL;DR

HTTP (Hypertext Transfer Protocol) — прикладной протокол, с помощью которого сервер отдаёт странички нашему браузеру. HTTP в настоящее время повсеместно используется во Всемирной паутине для получения информации с веб-сайтов. На картинке светильник на микроконтроллере с ОС Contiki на борту, в котором цвета задаются через браузер.

IPv6 | IPv4 | исходники

screenshot

HTTP протокол текстовый и достаточно простой. Собственно вот так выглядит метод GET, посылаемый утилитой netcat на локальный IPv6 адрес сервера с лампочками:

~$ nc fe80::200:e2ff:fe58:b66b%mazko 80 <<EOF
GET /b HTTP/1.0

EOF

Метод HTTP (англ. HTTP Method) обычно представляет собой короткое английское слово, записанное заглавными буквами, чувствительно к регистру. Каждый сервер обязан поддерживать как минимум методы GET и HEAD. Кроме методов GET и HEAD, часто применяется методы POST, PUT и DELETE. Метод GET используется для запроса содержимого указанного ресурса, в нашем случае тут GET /b HTTP/1.0 где путь /b отвечает за цвет (синий). Ответ сервера:

HTTP/1.0 200 OK
Server: Contiki/2.4 http://www.sics.se/contiki/
Connection: close
Cache-Control: no-cache, no-store, must-revalidate
Pragma: no-cache
Expires: 0
Content-type: text/html

<html><head><title>Contiki RGB</title></head><body>
<p style='color:red;'>Red is <a href='/r'>OFF</a></p>
<p style='color:green;'>Green is <a href='/g'>OFF</a></p>
<p style='color:blue;'>Blue is <a href='/b'>ON</a></p>
</body></html>

Код состояния (у нас 200) является частью первой строки ответа сервера. Он представляет собой целое число из трёх цифр. Первая цифра указывает на класс состояния. За кодом ответа обычно следует отделённая пробелом поясняющая фраза на английском языке, которая разъясняет человеку причину именно такого ответа. В нашем случае сервер отработал без ошибок, всё пучком (ОК).

Как запрос, так и ответ содержат заголовки (каждая строка — отдельное поле заголовка, пара имя-значение разделена двоеточием). Заканчиваются заголовки пустой строкой, после чего могут идти данные.

Мой браузер отказывается открывать локальный IPv6-адрес, поэтому в прошивке микроконтроллера прописан дополнительный адрес и такой же префикс также нужно назначить виртуальному сетевому интерфейсу симулятора:

~$ sudo ip addr add abcd::1/64 dev mazko          # linux
~$ netsh interface ipv6 set address mazko abcd::1 # windows
~$ curl http://[abcd::200:e2ff:fe58:b66b]

Если curl отработал без ошибок, то ссылку можно спокойно открывать в браузере.

TCP

Здесь мы научим наш игрушечный TCP/IP стек понимать TCP протокол. Итак организация соединения по протоколу TCP начинается с т.н. трехстороннего квитирования (рукопожатия). Как всегда разобраться с сетью поможет Wireshark.

Когда требуется установить соединение с удалённым сервером, ему отправляется пакет с установленным флагом SYN, что означает инициализацию сессии. Тут есть поле Sequence Number (на картинке 1039510418), начальное значение этого поля выбирается случайным образом инициатором соединения.

screenshot

Сервер, в ответ на этот пакет, отвечает пакетом с битами SYN, ACK. Своё значение Sequence Number (на картинке 16770109) он тоже генерирует случайным образом. Также он должен заполнить поле Acknowledgment Number, которые будет равно сумме принятого Sequence Number плюс 1 т.е. 1039510419.

screenshot

Теперь инициатору подключения не остается ничего другого, как ответить ACK. Здесь Acknowledgment Number аналогично будет равно сумме принятого Sequence Number плюс 1 т.е. 16770110.

screenshot

С этого момента соединение считается установленным. Дальнейшие пакеты будут передавать уже полезную нагрузку – данные протоколов вышестоящих уровней. В отличие от UDP, тут на каждый пакет нужно отправлять подтверждение (флаг ACK), дабы удалённый узел знал, что отправленные им данные были успешно приняты. При этом так же происходит взаимное увеличение Sequence Number у сервера и у клиента, но только уже не на 1, а на размер отправляемых данных.

Для закрытия соединение удалённому узлу посылается пакет с установленным флагом FIN.

С точки зрения реализации TCP это классический конечный автомат. На картинке показаны как состояния сервера, так и клиента (разные цвета стрелок), на стрелках отображено событие/действие – например переход сервера (синяя стрелка) из состояния LISTEN в состояние SYN RECEIVED происходит по событию SYN, реакция на это событие отправить клиенту SYN+ACK в ответ:

screenshot

Теперь немного кода. Приведенный ниже пример очень сильно упрощён, тут нет состояний и реализована только минианимальная логика, достаточная для обмена данными утилитой netcat.

typedef struct tcp_packet {
  uint16_t from_port;
  uint16_t to_port;
  uint32_t seq_num;
  uint32_t ack_num;
  uint8_t data_offset;
  uint8_t flags;
  uint16_t window;
  uint16_t cksum;
  uint16_t urgent_ptr;
  uint8_t data[];
} tcp_packet_t;

static void tcp_reply(eth_frame_t *frame, uint16_t len)
{
  uint16_t temp;

  ip_packet_t *ip = (void*)(frame->data);
  tcp_packet_t *tcp = (void*)(ip->data);

  // swap src/dst, fill tcp hdr?
  temp = tcp->from_port;
  tcp->from_port = tcp->to_port;
  tcp->to_port = temp;
  tcp->window = htons(TCP_WINDOW_SIZE);
  tcp->urgent_ptr = 0;

  // SYN packet?
  if(tcp->flags & TCP_FLAG_SYN) {
    // add MSS option
    tcp->data_offset = (sizeof(tcp_packet_t) + 4) << 2;
    tcp->data[0] = 2; // MSS option
    tcp->data[1] = 4; // MSS option length = 4 bytes
    tcp->data[2] = TCP_SYN_MSS>>8;
    tcp->data[3] = TCP_SYN_MSS&0xff;
    len = 4;
  }
  else {
    // not syn packet - no options
    tcp->data_offset = sizeof(tcp_packet_t) << 2;
  }

  // calculate chksum
  len += sizeof(tcp_packet_t);
  tcp->cksum = 0;
  tcp->cksum = ip_cksum(len + IP_PROTOCOL_TCP,
      (void *)&ip->src, len + (2 * sizeof my_ip));

  ip->payload_len = htons(len);
  memcpy(&ip->dest, &ip->src, sizeof ip->src);
  memcpy(&ip->src, &my_ip, sizeof ip->src);
  eth_reply(frame, len + sizeof(ip_packet_t));
}

static void tcp_step(tcp_packet_t *tcp, uint16_t num)
{
  uint32_t ack_num;
  ack_num = ntohl(tcp->seq_num) + num;
  tcp->seq_num = tcp->ack_num;
  tcp->ack_num = htonl(ack_num);
}

static void tcp_filter(eth_frame_t *frame, uint16_t len)
{
  ip_packet_t *ip = (void*)(frame->data);
  tcp_packet_t *tcp = (void*)(ip->data);

  switch(tcp->flags) {
    case TCP_FLAG_SYN:
      tcp->flags = TCP_FLAG_SYN | TCP_FLAG_ACK;
      tcp_step(tcp, 1);
      tcp->seq_num = htonl(gettc()); // random number
      tcp_reply(frame, 0);
      break;

    case TCP_FLAG_PSH | TCP_FLAG_ACK:
      len -= sizeof (tcp_packet_t);
      tcp_step(tcp, len);
      tcp->flags = TCP_FLAG_ACK;
      // feed/read data to/from app
      tcp_reply(frame, tcp_data(frame, len));
      break;

    case TCP_FLAG_FIN | TCP_FLAG_ACK:
      tcp_step(tcp, 1);
      tcp_reply(frame, 0);
      break;
  }
}

Обработка полезных данных — выводим на экран, печатаем в UART:

uint16_t tcp_data(eth_frame_t *frame, uint16_t len) {
  ip_packet_t *ip = (void*)(frame->data);
  tcp_packet_t *tcp = (void*)(ip->data);
  char *data = (char*)tcp->data;

  draw_clr();
  data[len - 1] = 0;
  draw_str(1, 0, data);
  printf(">> %s\n", data);

  // piggyback
  const char* response = "!!! OK !!!\n";
  strcpy(data, response);
  return strlen(response);
}

IPv6 | IPv4 | исходники

screenshot

Чтобы вывести данные в дисплей, отправляем девайсу TCP-пакет на любой порт помощью netcat:

~$ cat <(echo 'hello TCP !') - | nc -6 fe80::0:ff:fe00:42%mazko 12345

Будучи однажды создан, канал TCP может существовать «вечно». А что будет, если некорректно разорвать соединение т.е. без флага FIN ? Такая ситуация возможна, если например выдернуть какой-нибудь кабель. Оба и клиент и сервер в этом случае будут безуспешно долго ждать данных друг от друга. Для отслеживания и отключения таких «мертвых» соединений часто используют механизм keep-alive – некое служебное сообщение, которое отсылается периодически. Если ответ на него не получен, то соединение разрывается.

Далее CoAP.

links

social