Гнезда Internet

Одним из важных применений интерфейса «гнезд» является, пожалуй, построение приложений Internet, что мы и рассмотрим в данной лекции.

Создание гнезда Internet

Для создания гнезда Internet используется уже упоминавшаяся функция socket, первым аргументом которой передается константа (определение препроцессора) AF_INET6 или AF_INET.

Использование двух констант связано с тем, что в современном Internet параллельно используются две версии протокола сетевого уровня IP и, следовательно, два вида адресов:

Следует отметить, что в зависимости от системы и ее настроек может быть возможно использовать гнезда AF_INET6 для работы как с новой, так и со старой версиями протокола. В этом случае традиционные адреса IPv4 отображаются в адресное пространство IPv6 следующим образом: 192.0.2.34::ffff:192.0.2.34 (= 0:0:0:0:0:0:ffff:c000:0222.)

Вторым аргументом функции указывается тип предоставляемого сервиса. Двумя наиболее важными значениями являются следующие.

SOCK_STREAM

«Октетный поток» — этот сервис во многом подобен ранее рассмотренным «файловым» средствам межпроцессного взаимодействия — каналам и (в большей степени) псевдотерминалам: приложение получает двунаправленный надежный упорядоченный октет-ориентированный канал связи с удаленным процессом.

Обязательства по делению потока на подходящие для передачи по сети фрагменты (пакеты) берет на себя система; при потере данных — она же повторно их пересылает. Если данные приходят с нарушением порядка — порядок также восстанавливает система.

SOCK_DGRAM

«Дейтаграммы» — в некотором смысле, полная противоположность SOCK_STREAM: приложение вновь получает двунаправленный канал связи, но обязанности по делению данных на пакеты, по обнаружению потерь и повторной пересылке, по отслеживанию нарушения порядка (если необходимо) — остаются за приложением.

Основной недостаток SOCK_STREAM — неизбежные задержки: при потере данных система будет повторять попытку отправки до тех пор, пока их получение не будет подтверждено получателем; новые данные при этом передаваться не будут. Такое поведение недопустимо для некоторых приложений; например, при передачи голоса (VoIP) или видео в реальном времени лучше допустить искажение сигнала, нежели задержку. (Едва ли при телефонном разговоре будет удобно получить качественную передачу фразы собеседника — задержанную минут на десять.)

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

Так, например, для сообщений протокола HTTP/1.1 предусмотрено использование или поля Content-Length: — длина тела сообщения, или «кусочной» передачи (англ. chunked encoding), при которой тело, длина которого заведомо неизвестна, делится на фрагменты, перед каждым из которых тоже указывается его длина.

Наконец, третьим аргументом socket может быть 0 — в случае чего транспортный протокол, подходящий для запрошенного типа сервиса, будет выбран системой автоматически. На практике это означает, что для SOCK_STREAM будет выбран протокол TCP; для SOCK_DGRAMUDP.

Пример: создание гнезда Internet (IPv6) функцией socket.
   int so
     = socket (AF_INET6, SOCK_STREAM, 0);
   assert (so >= 0);

Подключение к серверу

При использовании протоколов ориентированных на соединение (в частности — TCP), одна из сторон соединения является инициатором соединения (клиентом), другая — ожидающей (сервером.) В рамках интерфейса гнезд инициировать соединение позволяет функция connect.

При использовании дейтаграммных протоколов функция connect устанавливает адрес для отправки данных по умолчанию. Приложение может отправлять данные на другие адреса, указывая их явно аргументом функции sendto.

Прежде чем использовать эту функцию, однако, как правило нужно найти транспортный адрес удаленного процесса. Обычно исходной информацией для подключения является имя хоста (например: example.com) и имя сетевого сервиса (приложения; например: http.) Найти необходимую для вызова connect информацию по этим данным позволяет функция getaddrinfo.

Отметим, что функция getaddrinfo возвращает, в числе прочего, и значения аргументов функции socket, которые можно использовать для создания подходящего для установления соединения с сервером гнезда.

Не углубляясь в подробности процесса разрешения имен хостов Internet рассмотрим следующий пример. (Отметим, однако, что более детальное изучение соответствующей инфраструктуры предполагается в разделе DNS лабораторной работы Internet.)

Поскольку после успешного завершения connect становится возможно использовать для передачи данных обычные функции read, write, здесь же рассмотрим отправку запроса и получение ответа сервера.

Пример: установление соединения с http://example.com/, отправка запроса и получение ответа.
#include <assert.h>
#include <netdb.h>              /* for getaddrinfo */
#include <stdio.h>              /* for fwrite */
#include <sys/socket.h>
#include <unistd.h>             /* for read, write */

#ifndef HTTP_HOST
#define HTTP_HOST   "example.com"
#endif

int
main ()
{
  /** Определяем способ подключения к серверу */
  struct addrinfo *ai;
  {
    int err
      = getaddrinfo (HTTP_HOST, "http", 0, &ai);
    assert (err == 0);
  }

  /** FIXME: строго говоря, ниже должен быть цикл, переходящий от ai к ai->next в случае, если попытка соединения — не удалась. */

  /** Создаем гнездо */
  int so
    = socket (ai->ai_family, ai->ai_socktype, ai->ai_protocol);
  assert (so >= 0);

  /** Устанавливаем соединение */
  {
    int r
      = connect (so, ai->ai_addr, ai->ai_addrlen);
    assert (r >= 0);
  }

  /** Отправляем запрос */
  {
    const char req[]
      =  ("HEAD / HTTP/1.1\r\n"
          "Host: " HTTP_HOST "\r\n"
          "Connection: close\r\n"
          "\r\n");
    const char *tail;
    size_t left;
    for (tail = req, left = sizeof (req); left > 0; ) {
      ssize_t wr
        = write (so, tail, left);
      assert (wr > 0);
      assert (wr <= left);
      tail
        += wr;
      left
        -= wr;
    }
  }

  /** Получаем ответ и выводим его на стандартный вывод */
  {
    char buf[4096];
    while (1) {
      /** Читаем данные */
      ssize_t rd
        = read (so, buf, sizeof (buf));
      assert (rd >= 0);
      if (rd <= 0) {
        break;
      }

      /** Выводим (без изменения) */
      ssize_t wr
        = fwrite (buf, 1, rd, stdout);
      assert (wr == rd);
    }
  }

  /* . */
  return 0;
}
Пример: вывод программы — ответ сервера.
HTTP/1.1 200 OK^M
Accept-Ranges: bytes^M
Age: 548335^M
Cache-Control: max-age=604800^M
Content-Type: text/html; charset=UTF-8^M
Date: Thu, 09 Apr 2020 07:05:50 GMT^M
Etag: "3147526947+ident"^M
Expires: Thu, 16 Apr 2020 07:05:50 GMT^M
Last-Modified: Thu, 17 Oct 2019 07:18:26 GMT^M
Server: ECS (phd/FD6D)^M
X-Cache: HIT^M
Content-Length: 1256^M
Connection: close^M
^M