К содержанию

 


Вот наконец та часть, в которой мы начнём говорить о коде!

Но сначала всё же давайте обсудим не-код. Итак. Для начала я хочу поговорить об IP-адресах и портах; для работы с ними нужно иметь представление, что же это такое. Затем мы поговорим о том, каким образом Socket API хранит и обрабатывает IP-адреса и другие данные.

IP-адреса, версии 4 и 6.

В старые добрые времена, когда Бена Кеноби всё ещё називали Оби Ван Кеноби, была чудесная система интернет-маршрутизации под названием Протокол Интернета Версии 4, также известная как IPv4. Адрес состоял из четырёх байтов (или четырёх «октетов»), и обычно писался в виде «Точек и цифр», например, так: 192.0.2.111

Вы, наверное, слышали о чём-то таком.
Фактически в данный момент каждый сайт в интернете использует IPv4.

Все, включая Оби Вана, были счастливы. Всё вокруг было чудесно, пока некто по имени Винт Серф не заявил, что у нас заканчиваются адреса IPv4!
(Кроме предупреждения всех о приближении Апокалипсиса IPv4, Винт Серф также известен как «Отец интернета». Поэтому у меня нет оснований сомневаться в его словах.)

Кончаются адреса? Как это может произойти? Я имею в виду, что есть миллиарды адресов IPv4. Неужели все они заняты компьютерами?

Да.

Кроме того, в начале, когда было всего несколько компьютеров, всем казалось, что миллиард — невероятно большое количество. Некотором организациям (например, IBM, Xerox, HP, Apple и др.) от всей души «отсыпали» миллионы IP-адресов для собственного использования.
Фактически, если бы не ряд временных мер, IP-адреса кончились бы уже давно.

Но сейчас мы живём в эпоху, когда IP-адрес приходится даже не на каждого человека, а на каждый калькулятор, телефон, стоянку (почему бы нет?) и (почему нет?) на каждого щенка.

Итак, родился IPv6. Поскольку Винт Серф, похоже, бессмертен (Даже если его физическое существование, не дай Бог, завершится, он, вероятно, уже существует где-нибуть в виде гипер-умного ИИ ELIZA в глубине Интернета-2), никто не хочет ещё раз услышать его фразу «я же вам говорил!», если вдруг нам не хватит и ipv6 через сколько-то лет.

Что из всего этого следует?

То, что нам нужно намного больше адресов. Не просто в два раза больше, даже не в миллиард и не в тысячу миллиардов раз, а В 79 МИЛЛИОНОВ МИЛЛИАРДОВ ТРИЛЛИОНОВ раз больше адресов! Это будет круто!

Вы скажете «Да ну, не может быть такого количества адресов». Хе-хе, разница между 32 и 128 битами не внушает трепета, это всего лишь дополнительные 96 бит, верно? НО: 32 бита дают нам около 4 миллиардов адресов, тогда как 128 бит дают миллиард миллиардов миллиардов адресов! Это всё равно, что миллион ipv4-адресов для каждой звезды во вселенной.

Забудьте о точках и цифрах в адресах IPv4. Теперь у нас только шестнадцатиричное представление, каждый кусок по два байта через двоеточие, например так:
2001:0db8:c9d2:aee5:73e3:934a:a5ae:9551.

Это ещё не всё! Часто могут встречаться IP-адреса с многими нулями, тогда вы можете пропускать 4 нуля между двоеточиями и писать просто «::». Также вы можете отбрасывать ведущие нули для каждой пары байт. Например, эти адреса эквивалентны:

2001:0db8:c9d2:0012:0000:0000:0000:0051
2001:db8:c9d2:12::512001:0db8:ab00:0000:0000:0000:0000:0000
2001:db8:ab00::0000:0000:0000:0000:0000:0000:0000:0001
::1

 

Адрес ::1 — лупбек (loopback) -адрес. Он всегда присутствует на каждой машине. В IPv4 такой адрес — 127.0.0.1.

Наконец, существует режим совместимости IPv4 и IPv6. Например, если вы хотите написать «192.0.2.33» в виде IPv6, получится «::ffff:192.0.2.33».

IPv6 настолько много, что создатели бесцеремонно обрезали нас 😉 зарезервировав триллионы и триллионы адресов. Но и без того у нас осталось их так много, что куда уж больше? Их осталось ещё очень и очень много на каждого мужчину, женщину, ребенка, щенка, стоянки на каждой планете в галлактике.

3.1.1. Подсети

По причинам удобства организации иногда заявляется, что «первая часть IP-адреса до такого-то бита содержит сетевую часть, а остальная часть адреса — хостовая часть«.
Например, в случае IPv4 у вас может быть адрес 192.0.2.12, и мы можем сказать, что первые три байта содержат адрес сети, а последний — адрес хоста. Или, другими словами, мы говорим о хосте 12 в сети 192.0.2.0.

А теперь немного устаревшая информация. В доисторические времена принято было делить сети на «классы». Разные классы сетей имели один, два и три байта для адреса сети и назывались «Класс А», «Класс B» и «Класс C» соответственно. Например, сеть «Класса А» содержит один байт сетевого адреса и может включать в себя 256*256*256 хостов. Сети «Класса B» — два байта адреса и 256*256 хостов, ну и сети «Класса C» — всего 256 хостов.

Сетевая часть IP-адреса описывается параметром, называемым «Маска подсети», биты которой указывают на число хостов в сети. Обычно она выглядит как-то так: 255.255.0.0. (То есть с адресом 192.0.2.12 в сети 192.0.2.0 у вас была бы маска подсети 255.255.255.0)

К сожалению, оказалось, что такого разделения недостаточно для потребностей Интернета. От разделения на классовые сети отказались почти сразу. Теперь маска может содержать произвольное число битов, а не только 8, 16 и 24. Так вы можете иметь маску, например, 255.255.255.252, что даёт нам 30-битную сеть, и два бита для хостов в сети (маска ВСЕГДА предусматривает два зарезервированных адреса: адрес сети и бродкаст-адрес).

Но использовать длинный набор цифр типа 255.192.0.0 в качестве маски — немного громоздко. Во-первых, люди не имеют интуитивного представления о том, сколько битов в этом выражении, а во-вторых это действительно просто громоздко. Пришедший следом за изменениями новый стиль написания маски намного приятнее. Необходимо просто поставить косую черту после IP-адреса, а за ней — число битов сети. Например так: 192.0.2.12/30

Или, для IPv6, как-то так: 2001:db8::/32 or 2001:db8:5413:4028::9db9/64.

3.1.2. Номера портов.

Если помните, ранее я описывал модель OSI, разделяющую интернет на слои протоколов: IP и TCP/UDP. Всмомните немного эту модель перед переходом к следующему пункту.

Оказывается, кроме Ip-адреса (используемого протоколом IP) есть ещё один адрес, используемый TCP (потоковыми сокетами) и UDP (дейтаграммными сокетами). Это номер порта. Это 16-разрядное число, что-то вроде локального адреса для связи.
Можно представить себе IP-адрес как адрес отеля, а номер порта — как номер комнаты в нём.
Скажем, вы хотите, чтобы один и тот же компьютер обрабатывал и входящую из интернета почту и веб-сервисы. Как вы различите их на одном и том же компьютере с одним IP-адресом?

Различные сетевые сервисы имеют различные номера портов. Протокол HTTP как правило использует порт 80, телнет — 23, SMTP — 25, и игра DOOM использует порт 666. И тк далее. Порты ниже 1024 считаются зарезервированными и как правило требуют для открытия особых прав в ОС.

Вот, собственно, и всё об этом.

3.2 Порядок следования байт

Нет простого способа объяснить, поэтому я просто ляпну: компьютер может хранить данные в обратном порядке.
Дело в том, что все в мире Интернета в целом согласились, что если вы, например, хотите передать двухбайтовое шестнадцатиричное значение, например b34f, то сначала передаём b3, а затем 4f. Думаю, следом за Уилфордом Бримли можно назвать этот принцип правильным. Порядок, при котором байты следуют «от старшего к младшему», называется Big-Endian.

К сожалению, парочка компьютеров, разбросанных там и сям по всему миру, а именно — что-нибуть с Intel или Intel-совместимым процессором, хранят байты в обратном порядке, так что b34f в памяти хранится так: сначала 4f, а затем b3. Такой способ хранения, когда байты следуют «от младшего к старшему», называется Little-Endian.

Погодите, ещё не всё с терминологией! Более очевидный порядок следования байт Big-Endian также называют «сетевой порядок следования байт» (network order), потому что именно так данные передаются по сети.

Итак, ваш компьютер хранит числа в машинной последовательности байт. Если процессор у вас Intel, эта последовательность — Big-Endian, а если, например, Motorola, то данные у вас хранятся в Little-Endian, или в сетевом порядке. Если у вас PowerPC, порядок следования байт у вас… Ну, там он может быть разным!

Множество раз, строя покеты или заполняя структуры данных, вам нужно убеждаться, что ваши двух- и четырёх-байтные числа построены в сетевом порядке следования. Но как же вам это делать, не зная, какая последовательность байт естесственна для вашей машины?

Хорошие новости! Нужно просто принять за аксиому, что поряток следования у вас неверен и просто прогонять необходимое значение через функцию, устанавливающую его в сетевой порядок следования байт. Эта функция магическим образом сконвертирует всё, если это необходимо, а если нет — оставит всё без изменений. Таким образом ваш код будет переносим на любые машины.

Всё верно. Есть два типа значений, которые вы можете конвертировать: short (два байта) и long (четыре байта). Есть функции для работы со всеми вариантами. Скажем, вы хотите сконвертировать short из машинного порядка следования байт в сетевой порядок. Начинается с «h» для «host», дальше «to», затем «n» для «network» и «s» для «socket»: h-t-o-n-s, или htons() (читается как «Host to network short»).

Всё почти что слишком просто…
Вы можете использовать любые вариации «n», «h», «s» и «l», исключая самые идиотские, конечно. Например, нет функции stolh() («Short to Long Host»). Но зато есть:

 

htons() host to network shorthtonl() host to network long

ntohs() network to host short

ntohl() network to host long

 

Собственно, вам нужно сконвертировать числа в сетевой порядок следования до того, как они выйдут наружу, и конвертировать обратно в машинный порядок после того, как они придут на вход.
Я не знаю подробностей о 64-битном варианте, извините. И если вам нужно передать число с плавающей точкой — см. раздел «Сериализация», который будет гораздо дальше.

Примем за факт, что числа в данном документе хранятся в машинном порядке следования байт, пока я не скажу иное.

3.3 Структуры данных

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

Сначала самое лёгкое: дескриптор сокета. Он имеет следующий тип:

int

 

Просто обычный int.

Моя первая структура™ — struct addrinfo. Эта структура — относительно недавнее нововведение, и используется она для подготовки адреса сокета для дальнейшего использования. Также она используется в разрешении имён хоста (днс) и имён сервисов. Вам всё это станет яснее далее, когда мы дойдём до фактического её использования, пока просто знайте, что это одна из первых вещей, необходимых при создании сетевого соединения.

struct addrinfo {
int              ai_flags;     // AI_PASSIVE, AI_CANONNAME, etc.
int              ai_family;    // AF_INET, AF_INET6, AF_UNSPEC
int              ai_socktype;  // SOCK_STREAM, SOCK_DGRAM
int              ai_protocol;  // use 0 for «any»
size_t           ai_addrlen;   // size of ai_addr in bytes
struct sockaddr *ai_addr;      // struct sockaddr_in or _in6
char            *ai_canonname; // full canonical hostnamestruct addrinfo *ai_next;      // linked list, next node
};

 

Вы слегка заполняете эту структуру, а затем вызываете ф-ю getaddrinfo(), которая вернёт вам указатель на новый связанный список этих структур, которые заполнены всем, что вам нужно.
Вы можете указать использовать IPv4 или IPv6 в поле ai_family, или оставить AF_UNSPEC, чтобы использовать что угодно. Это круто, потому что ваш код может быть IP-универсален.

Отметим, что это связанный список: ai_next указывает на следующий элемент — может быть несколько результатов на выбор. Я бы использовал первый работающий результат, но у вас могут возникнуть другие потребности, я не могу предвидеть всего, чувак!

Вы могли заметить, что поле ai_addr в struct addrinfo — указатель на структуру sockaddr. Это то место, где мы начинаем задавать все детали структуры IP-адреса.
Как правило, вам не нужно будет записывать что-то в эти структуры, зачастую вызов getaddrinfo() заполняет в них всё, что нужно. Однако вам нужно будет заглянуть внутрь этих структур, чтобы получить из них значения.
(Кроме того, весь код, написанный до изобретения структуры addrinfo, заполнял всё это вручную, так что вы увидите ещё немало дикого кода IPv4, делающего именно это. Например, в старых версиях этого учебника и т.д.)

Какие-то структуры могут быть IPv4, какие-то IPv6, а какие-то обоими сразу. Я буду указывать, какие что из себя представляют.

В общем, структура sockaddr содержит информацию об адресе сокета для множества типов сокетов.

struct sockaddr {
unsigned short    sa_family;    // address family, AF_xxx
char              sa_data[14];  // 14 bytes of protocol address
};

 

sa_family может принимать множество значений, но в этом документе мы затронем только AF_INET (IPv4) и AF_INET6 (IPv6).
sa_data содержит адрес получателя и номер порта для сокета. Это не столь важно, так как нет необходимости запихивать адреса в sa_data вручную.

Для работы со структурой sockaddr, программисты создали параллельную структуру: struct sockaddr_in («in» для «internet) для использования с IPv4.

Важно: указатель на структуру sockaddr_in может быть приведён к указателю на структуру sockaddr и наоборот. Поэтому даже если connect() требует в качестве аргумента sockaddr*, вы всё же можете передавать ему sockaddr_in.

// (IPv4 only — см. struct sockaddr_in6 для IPv6)

struct sockaddr_in {
short int          sin_family;  // Address family, AF_INET
unsigned short int sin_port;    // Номер порта
struct in_addr     sin_addr;    // Интернет-адрес
unsigned char      sin_zero[8]; // То же, что размер структуры sockaddr
};

 

Эта структура позволяет легко обращаться к элементам адреса сокета. Обратите внимание, что sin_zero должна быть инициализирована нулями с помощью memset(). Также заметьте, что sin_family соответствует sa_family в структуре sockaddr и должно быть установлено в AF_INET. Наконец, sin_port должен быть указан в сетевом порядке следования байт (с помощью htons()!).

Давайте копнём глубже! Поле sin_addr — это структура in_addr. Что это такое? Не хочется быть слишком драматичным, но это одна из самых страшных вещей за всё время:

// (IPv4 only — см. struct in6_addr для IPv6)

// Интернет-адрес (структура, сохранённая по историческим причинам)
struct in_addr {
uint32_t s_addr; // 32-битный int (4 байта)
};

 

Вау! Когда-то это использовалось как Объединение, но теперь эти дни ушли. Скатертью дорога. Так что если вы обьявили переменную ina как sockaddr_in, то ina.sin_addr.s_addr будет указателем на 4-байтовый IP-адрес (в сетевом порядке следования байтов). Отметим, что даже если ваша система по прежнему использует богопротивное объединение для структуры in_addr, вы всё ещё можете обращаться к 4-байтовому IP-адресу так, как я сделал выше (благодаря #дефайнам).

А что насчёт IPv6? Аналогичные структуры существуют и для него:

// (IPv6 only — см struct sockaddr_in и struct in_addr для IPv4)

struct sockaddr_in6 {
u_int16_t       sin6_family;   // address family, AF_INET6
u_int16_t       sin6_port;     // Номер порта, сетевой порядок следования байтов
u_int32_t       sin6_flowinfo; // IPv6 flow-информация
struct in6_addr sin6_addr;     // IPv6 адрес
u_int32_t       sin6_scope_id; // Scope ID
};

struct in6_addr {
unsigned char   s6_addr[16];   // IPv6 адрес
};

 

Обратите внимание: Ipv6 имеет адрес и номер порта точно так же, как и ipv4.
Также обратите внимание, что я не собираюсь ничего объяснять про поля flow-инфо и Scope ID… Это только начало гайда 🙂

Последнее по очередности, но не поважности: ещё одна простая структура, struct sockaddr_storage, которая призвана быть достаточно большой для хранения обоих структур — IPv4 и IPv6. (Например, вам нужно совершить несколько соединений, и вы не всегда знаете, с каким именно протоколом придётся работать. Тогда просто передаёте эту структуру, точно так же, как и остальные sockaddr_*:

struct sockaddr_storage {
sa_family_t  ss_family;     // address family// Всё это — специфика имплементации, игнорируйте это:
char      __ss_pad1[_SS_PAD1SIZE];
int64_t   __ss_align;
char      __ss_pad2[_SS_PAD2SIZE];
};

 

Важно то, что вы можете увидеть семейство адресов в переменной ss_family — она может быть AF_INET или AF_INET6. Затем вы можете привести структуру к sockaddr_in или sockaddr_in6.

3.4 IP-адреса, часть два

К счастью для вас, есть куча функций, позволяющих манипулировать IP-адресами. Нет необходимости парсить их вручную и запихивать в long оператором <<.

Во-первых, скажем, у вас есть struct sockaddr_in ina, и у вас есть IP-адрес «10.12.110.57» или «2001:db8:63b3:1::3490», который вы хотите в неё сохранить. Функция, которой вы должны воспользоваться — inet_pton(), она конвертирует IP-адрес из «цифр и точек» в struct in_addr или struct in6_addr, в зависимости от того, что вы укажете — AF_INET или AF_INET6. («pton» — это «printable to network», так будет проще запомнить). Преобразование может быть сделано следующим образом:

struct sockaddr_in sa; // IPv4
struct sockaddr_in6 sa6; // IPv6inet_pton(AF_INET, «192.0.2.1», &(sa.sin_addr)); // IPv4
inet_pton(AF_INET6, «2001:db8:63b3:1::3490», &(sa6.sin6_addr)); // IPv6

 

(Небольшое примечание: старый способ того же преобразования — функции inet_addr() или inet_aton(); сейчас они считаются устаревшими и не работают с ipv6).

Фрагмент кода выше не слишком надёжен, так как не содержит обработки ошибок.
inet_pton() возвращает отрицательное значение и меняет значение переменной errno на EAFNOSUPPORT, если af не содержит правильного типа адреса. Возвращается 0, если src не содержит строку символов, представляющую правильный сетевой адрес (для указанного типа адресов). Если сетевой адрес был успешно преобразован, то возвращается положительное значение. Поэтому всегда проверяйте, положительное ли значение вернула функция прежде, чем продолжать работу.

Ладно, теперь вы умеете преобразовывать строковое представление IP-адреса в двоичное. А наоборот? Что делать, если у вас есть структура in_addr и вам нужно напечатать IP-адрес из неё в виде «цифр и точек»? В этом случае нужно использовать функцию inet_ntop() («network to presentation» или «network to printable»). Как-то так:

// IPv4:

char ip4[INET_ADDRSTRLEN];  // для сохранения строкового значения IP-адреса
struct sockaddr_in sa;

inet_ntop(AF_INET, &(sa.sin_addr), ip4, INET_ADDRSTRLEN);

printf(«The IPv4 address is: %s
«
, ip4);

// IPv6:

char ip6[INET6_ADDRSTRLEN]; // для сохранения строкового значения IP-адреса
struct sockaddr_in6 sa6;

inet_ntop(AF_INET6, &(sa6.sin6_addr), ip6, INET6_ADDRSTRLEN);

printf(«The address is: %s
«
, ip6);

 

Когда вы её вызываете, то передаёте тип адреса (ipv4/6), адрес, указатель на строку, в которую сохранится результат, и максимальную длинну строки. (Есть два удобных макроса, хранящих длинны адресов ipv4 и ipv6: INET_ADDRSTRLEN и INET6_ADDRSTRLEN).

(Ещё одно небольшое примечание: старый способ делать то же самое — функция inet_ntoa(). Она также устарела и не будет работать с IPv6).

Наконец, укажем, что эти функции работают только с числовыми IP-адресами — они не умеют делать DNS-запросов по имени хоста. Для этого нужно использовать функцию getaddrinfo(). Позже вы увидите, как именно.

3.4.1. Частные (или отключенные, disconnected) сети.

Есть много мест, отключенных от остального мира файерволом. И зачастую брендмауер транслирует «внутренние» IP-адреса во «внешние» (которые известны всему остальному миру). Этот процесс называется Network Address Translation, или NAT.

Вы уже нервничаете? «Куда он клонит, говоря эти странные вещи?»

Расслабьтесь и купите себе безалкогольный (или алкогольный) напиток. Вам, как новичку, даже не придется беспокоиться о NAT, так как для вас он выглядит прозрачно. Но я хотел бы поговорить о сети за брандмауэром в случае, если вы начали путаться в IP-адресах, которые вы видите.

Например, у меня дома есть файервол (роутер). У меня есть два внешних IP-адреса, которые мне дал провайдер, и семь компьютеров в домашней сети. Как это возможно? Ведь два компьютера не могут иметь один и тот же IP-адрес, иначе данные не будут знать, куда течь 😉
А они и не используют одни и те же адреса. Они находятся в частной сети, для которой выделено 24 миллиона адресов. Все они — только для меня. Ну, для меня одного, пока ещё кто-то не подключился к моей сети. Вот как это работает:

Если я присоединяюсь к удалённому компьютеру, он говорит мне, что я зашел с адреса 192.0.2.3, который является публичным IP-адресом, который мне предоставил мой провайдер. Но если я спрошу свой компьютер, какой у него адрес, он мне скажет: 10.0.0.5. Кто же транслирует IP-адрес из одного в другой? Правильно, файервол! Это делает NAT!

10.xxx является одной из немногих зарезервированных сетей, которые могут быть использованы только в полностью изолированных сетях или сетях, скрытых за файерволом. Полный список зарезервированных сетей изложен в RFC 1918, но вот некоторые из них: 10.x.x.x, 192.168.x.x, 172.y.x.x (тут y может быть от 16 до 31).
Сети за брендмауером не обязаны быть в одном из этих диапазонов, но обычно именно их и используют.

(Забавный факт! Мой внешний IP-адрес на самом деле не 192.0.2.33. Сеть 192.0.2.x зарезервирована для псевдо-реальных адресов, которые должны использоваться в документации, точно как в этом учебнике! Круто!)

IPv6 тоже имеет приватные сети. Они начинаются с fdxx: (или, может быть в будущем, с fcxx:), согласно RFC4193. NAT и IPv6 как правило не смешиваются, потому что (если только вы не делаете шлюз ipv4->ipv6 — вещь, выходящая за рамки этого документа) — в теории у вас будет столько IP-адресов, что NAT вам будет больше просто не нужен.

К содержанию