Что такое сокет?

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

Ок -- возможно, вы слышали от какого-нибуть Unix-хакера фразу типа "господи, всё, что есть в Unix - файлы!" Этот человек, возможно, имел в виду, что программы в Unix при абсолютно любом вводе-выводе читают или пишут в файловый дескриптор. Дескриптор файла - это простое целое число, связанное операционной системой с открытым файлов. Но (и в этом заключается ловушка) файлом может быть и сетевое подключение, и FIFO, и пайпы, и терминал, и реальный файл на диске, и просто что угодно другое. Всё в UNIX - это файл! Итак, просто поверьте, что собираясь общаться с другой программой через интернет, вам придется делать это через дескриптор файла.

"Эй, умник, а откуда мне взять этот дескриптор файла для работы в сети?" Отвечу.
Вы совершаете системный вызов socket(). Он возвращает дескриптор сокета, и вы общаетесь через него с помощью системных вызовов send() и recv() (man send, man recv).

"Но, эй!" могли бы вы воскликнуть. "Если это дескриптор файла, почему я не могу использовать простые функции read() и write(), чтобы общаться через него?". Ответ прост: "Вы можете!". Немного развернутый ответ: "Вы можете, но send() и recv() предлагают гораздо больший контроль над передачей ваших данных."

Что дальше? Как насчет этого: бывают разные виды сокетов. Есть DARPA инернет-адреса (Сокеты интернет), CCITT X.25 адреса (X.25 сокеты, которые вам не нужны), и, вероятно, многие другие в зависимости от особенностей вашей ОС. Этот документ описывает только первые, Интернет-Сокеты.

Два типа интернет-сокетов

Что? Есть два типа интернет сокетов? Да. Ну ладно, нет, я вру. Есть больше, но я не хочу вас пугать. Есть ещё raw-сокеты, очень мощная штука, вам стоит взглянуть на них.

Ну ладно. Какие два типа? Один из них - "потоковый сокет", второй - "сокет дейтаграмм", в дальнейшем они будут называться "SOCK_STREAM" и "SOCK_DGRAM" соответственно. Дейтаграммные сокеты иногда называют "сокетами без соединения" (хотя они могут и connect()`иться, если вам этого действительно захочется. См. connect() ниже.)

Потоковые сокеты обеспечивают надёжность своей двусторонней системой коммуникации. Если вы отправите в сокет два элемента в порядке "1, 2", они и "собеседнику" придут в том же порядке - "1, 2". Кроме того, обеспечивается защита от ошибок.

Что использует потоковые сокеты? Ну, вы наверно слышали о программе Telnet, да? Телнет использует потоковый сокет. Все символы, которые вы печатаете, должны прибыть на другой конец в том же порядке, верно? Кроме того, браузеры используют протокол HTTP, который в свою очередь использует потоковые сокеты для получения страниц. Если вы зайдёте телнетом на любой сайт, на порт 80 и наберёте что-то вроде "GET / HTTP/1.0" и нажмете ввод два раза, на вас свалится куча HTML ;)

Как потоковые сокеты достигают высокого уровня качества передачи данных? Они используют протокол под названием "The Transmission Control Protocol", иначе - "TCP". TCP гарантирует, что ваши данные передаются последовательно и без ошибок. Возможно, ранее вы слышали о TCP как о половине от "TCP/IP", где IP - это "Internet Protocol". IP имеет дело в первую очередь с маршрутизацей в Интернете и сам по себе не отвечает за целостность данных.

Круто. А что насчёт дейтаграммных сокетов? Почему они называются без-соединительными? В чем тут дело? Почему они ненадежны?
Ну, вот некоторые факты: если вы посылаете дейтаграмму, она может дойти. А может и не дойти. Но если уж приходит, то данные внутри пакета будут без ошибок.

Дейтаграммные сокеты также используют IP для роутинга, но не используют TCP; они используют "User Datagram Protocol", или "UDP".

Почему UDP не устанавливает соединения? Потому что вам не нужно держать открытое соединение с потоковыми сокетами. Вы просто строите пакет, формируете IP-заголовок с информацией о получателе, и посылаете пакет наружу. Устанавливать соединение нет необходимости. UDP как правило используется либо там, где стек TCP недоступен, либо там, где один-другой пропущеный пакет не приводит к концу света. Примеры приложений: TFTP (trivial file transfer protocol, младшый брат FTP), dhcpcd (DHCP клиент), сетевые игры, потоковое аудио, видео конференции и т.д.

"Подождите минутку! TFTP и DHCPcd используются для передачи бинарных данных с одного хоста на другой! Данные не могут быть потеряны, если вы хотите нормально с ними работать! Что это за темная магия?"

Нуу, мой человеческий друг, TFTP и подобные программы обычно строят свой собственный протокол поверх UDP. Например, TFTP протокол гласит, что для каждого принятого пакета получатель должен отправить обратно пакет, говорящий "я получил его!" ("ACK"-пакет). Если отправитель исходного пакета не получает ответ, скажем, в течение 5 секунд, он отправит пакет повторно, пока, наконец, не получит ACK. Подобные процедуры очень важны для реализации надёжных приложений, использующих SOCK_DGRAM.

Для приложений, не требующих такой надёжности - игры, аудио или видео, вы просто игнорируете потерянные пакеты или, возможно, пытаетесь как-то их компенсировать. (Игроки в quake обычно называют это явление "проклятый лаг", и "проклятый" - это ещё крайне мягкое высказывание).

Зачем вам может понадобиться использовать ненадежный базовый протокол? По двум причинам: скорость и скорость. Этот способ гораздо быстрее, выстрелил-и-забыл, чем постоянное слежение за тем, всё ли благополучно прибыло получателю. Если вы отправляете сообщение в чате, TCP великолепен, но если вы шлёте 40 позиционных обновлений персонажа в секунду, может быть, не так и важно, если один или два из них потеряются, и UDP тут будет неплохим выбором.

Теория сетей и низкие уровни

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

Эй, детишки, настало время поговорить об инкапсуляции данных! Это очень-очень важная вещь. Это настолько важно, что вам стоит выучить это наизусть.
В основном суть такова: пакет родился; пакет завёрнут ("инкапсулирован") в заголовок первым протоколом (скажем, протоколом TFTP), затем всё это (включая хидер TFTP) инкапсулируется вновь следующим протоколом (скажем, UDP), затем снова - следующим (например, IP), и наконец финальным, физическим протоколом (скажем, Ethernet).

Когда другой компьютер получает пакет, оборудование (сетевая карта) исключает Ethernet-заголовок (разворачивает пакет), ядро ОС исключает заголовки IP и UDP, программа TFTP исключает заголовок TFTP, и наконец мы получаем голые данные.

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

Собственно, вот все уровни полномасштабной модели:


  • Прикладной

  • Представительский

  • Сеансовый

  • Транспортный

  • Сетевой

  • Канальный

  • Аппаратный (физический)

Физический уровень - это оборудование; ком-порт, сетевая карта, модем и т.д. Прикладной слой - дальше всех отстоит от физического. Это то место, где пользователь взаимодействует с сетью.

Для нас эта модель слишком общая и обширная. Сетевая модель, которую можем использовать мы, может выглядеть так:


  • Уровень приложений (Telnet, FTP и т.д.)

  • Транспортный протокол хост-хост (TCP, UDP)

  • Интернет-уровень (IP и маршрутизация)

  • Уровень доступа к сети (Ethernet, Wi-Fi или что угодно)

Теперь вы можете четко видеть, как эти слои соответствуют инкапсуляции исходных данных.

Видите, как много работы заключается в создании одного простого пакета? Офигеть! И все эти заголовки пакетов вы должны самостоятельно набирать в блокноте! Шучу. Всё, что вам нужно сделать в случае потоковых сокетов - это послать (send()) данные наружу. Ядро ОС построит TCP и IP хидеры, а оборудование возьмет на себя уровень доступа к сети. Ах, я люблю современные технологии.

На этом наш краткий экскурс в теорию сетей завершен. Ах да, я забыл вам сказать: всё, что я хотел вам сказать о маршрутизации: ничего! Да-да, я ничего не буду говорить об этом. О таблице маршрутизации за вас позаботятся ОС и IP-протокол. Если вам действительно интересно, почитайте документацию в интернете, её море.

интерфейс сетевых системных вызовов ( socket() , bind() , recvfrom() , sendto() и т. д.) в операционной системе UNIX может применяться и для других стеков протоколов (и для протоколов, лежащих ниже транспортного уровня ).

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

Второй параметр служит для задания вида интерфейса работы с сокетом – будет это потоковый сокет , сокет для работы с датаграммами или какой-либо иной. Третий параметр указывает протокол для заданного типа интерфейса. В стеке протоколов TCP/IP существует только один протокол для потоковых сокетов – TCP и только один протокол для датаграммных сокетов – UDP , поэтому для транспортных протоколов TCP/IP третий параметр игнорируется.

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

Для транспортных протоколов TCP/IP мы всегда в качестве первого параметра будем указывать предопределенную константу AF_INET (Address family – Internet ) или ее синоним PF_INET ( Protocol family – Internet ).

Второй параметр будет принимать предопределенные значения SOCK_STREAM для потоковых сокетов и SOCK_DGRAM – для датаграммных.

Поскольку третий параметр в нашем случае не учитывается, в него мы будем подставлять значение 0 .

Ссылка на информацию о созданном сокете помещается в таблицу открытых файлов процесса подобно тому, как это делалось для pip ’ов и FIFO (см. семинар 5). Системный вызов возвращает пользователю файловый дескриптор , соответствующий заполненному элементу таблицы, который далее мы будем называть дескриптором сокета . Такой способ хранения информации о сокете позволяет, во-первых, процессам-детям наследовать ее от процессов-родителей, а, во-вторых, использовать для сокетов часть системных вызовов, которые уже знакомы нам по работе с pip ’ами и FIFO : close() , read() , write() .

Системный вызов для создания сокета

Прототип системного вызова

#include #include int socket(int domain, int type, int protocol);

Описание системного вызова

Системный вызов socket служит для создания виртуального коммуникационного узла в операционной системе. Данное описание не является полным описанием системного вызова, а предназначено только для использования в нашем курсе. За полной информацией обращайтесь к UNIX Manual.

Параметр domain определяет семейство протоколов, в рамках которого будет осуществляться передача информации. Мы рассмотрим только два таких семейства из нескольких существующих. Для них имеются предопределенные значения параметра:

  • PF_INET – для семейства протоколов TCP/IP ;
  • PF_UNIX – для семейства внутренних протоколов UNIX, иначе называемого еще UNIX domain.

Параметр type определяет семантику обмена информацией: будет ли осуществляться связь через сообщения (datagrams), с помощью установления виртуального соединения или еще каким-либо способом. Мы будем пользоваться только двумя способами обмена информацией с предопределенными значениями для параметра type :

  • SOCK_STREAM – для связи с помощью установления виртуального соединения ;
  • SOCK_DGRAM – для обмена информацией через сообщения.

Параметр protocol специфицирует конкретный протокол для выбранного семейства протоколов и способа обмена информацией. Он имеет значение только в том случае, когда таких протоколов существует несколько. В нашем случае семейство протоколов и тип обмена информацией определяют протокол однозначно. Поэтому данный параметр мы будем полагать равным 0 .

Возвращаемое значение

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

Адреса сокетов. Настройка адреса сокета. Системный вызов bind()

Когда сокет создан, необходимо настроить его адрес . Для этого используется системный вызов bind() . Первый параметр вызова должен содержать дескриптор сокета , для которого производится настройка адреса. Второй и третий параметры задают этот адрес .

Во втором параметре должен быть указатель на структуру struct sockaddr , содержащую удаленную и локальные части полного адреса.

Указатели типа struct sockaddr * встречаются во многих сетевых системных вызовах; они используются для передачи информации о том, к какому адресу привязан или должен быть привязан сокет . Рассмотрим этот тип данных подробнее. Структура struct sockaddr описана в файле следующим образом:

struct sockaddr { short sa_family; char sa_data; };

Такой состав структуры обусловлен тем, что сетевые системные вызовы могут применяться для различных семейств протоколов, которые по -разному определяют адресные пространства для удаленных и локальных адресов сокета . По сути дела, этот тип данных представляет собой лишь общий шаблон для передачи системным вызовам структур данных, специфических для каждого семейства протоколов. Общим элементом этих структур остается только поле short sa_family (которое в разных структурах, естественно, может иметь разные имена, важно лишь, чтобы все они были одного типа и были первыми элементами своих структур) для описания семейства протоколов. Содержимое этого поля системный вызов анализирует для точного определения состава поступившей информации.

Для работы с семейством протоколов TCP/IP мы будем использовать адрес сокета следующего вида, описанного в файле :

struct sockaddr _in{ short sin_family; /* Избранное семейство протоколов – всегда AF_INET */ unsigned short sin_port; /* 16-битовый номер порта в сетевом порядке байт */ struct in_addr sin_addr; /* Адрес сетевого интерфейса */ char sin_zero; /* Это поле не используется, но должно всегда быть заполнено нулями */ };

Первый элемент структуры – sin_family задает семейство протоколов . В него мы будем заносить уже известную нам предопределенную константу AF_INET (см. предыдущий раздел).

Удаленная часть полного адреса – IP-адрес – содержится в структуре типа struct in_addr , с которой мы встречались в разделе "Функции преобразования IP-адресов inet_ntoa() , inet_aton() " .

Для указания номера порта предназначен элемент структуры sin_port , в котором номер порта должен храниться в сетевом порядке байт . Существует два варианта задания номера порта : фиксированный порт по желанию пользователя и порт , который произвольно назначает операционная система . Первый вариант требует указания в качестве номера порта положительного заранее известного числа и для протокола UDP обычно используется при настройке адресов сокетов и при передаче информации с помощью системного вызова sendto() (см. следующий раздел). Второй вариант требует указания в качестве номера порта значения 0. В этом случае операционная система сама привязывает сокет к свободному номеру порта . Этот способ обычно используется при настройке сокетов программ клиентов, когда заранее точно знать номер порта программисту необязательно.

Какой номер порта может задействовать пользователь при фиксированной настройке? Номера портов с 1 по 1023 могут назначать сокетам только процессы, работающие с привилегиями системного администратора. Как правило, эти номера закреплены за системными сетевыми службами независимо от вида используемой операционной системы, для того чтобы пользовательские клиентские программы могли запрашивать обслуживание всегда по одним и тем же локальным адресам. Существует также ряд широко применяемых сетевых программ, которые запускают процессы с полномочиями обычных пользователей (например, X-Windows). Для таких программ корпорацией Internet по присвоению имен и номеров (

Создание сокета

Системный вызов socket создает сокет и возвращает дескриптор, который может применяться для доступа к сокету:

#include
#include
int socket(int domain, int type, int protocol);

Созданный сокет - это одна конечная точка линии передачи. Параметр domain задает семейство адресов, параметр type определяет тип используемого с этим сокетом обмена данными, a protocol - применяемый протокол.

В табл. 15.1 приведены имена доменов.

Таблица 15.1

К наиболее популярным доменам сокетов относятся AF_UNIX , применяемый для локальных сокетов, реализуемых средствами файловых систем UNIX и Linux, и AF_INET , используемый для сетевых сокетов UNIX. Сокеты домена AF_INET могут применяться программами, взаимодействующими в сетях на базе протоколов TCP/IP, включая Интернет. Интерфейс ОС Windows Winsock также предоставляет доступ к этому домену сокетов.

Параметр сокета type задает характеристики обмена данными, применяемые для нового сокета. Возможными значениями могут быть SOCK_STREAM и SOCK_DGRAM .

SOCK_STREAM - это упорядоченный, надежный, основанный на соединении, двунаправленный поток байтов. В случае домена сокетов AF_INET этот тип обмена данными по умолчанию обеспечивается TCP-соединением, которое устанавливается между двумя конечными точками потоковых сокетов при подключении. Данные могут передаваться в двух направлениях по линии связи сокетов. Протоколы TCP включают в себя средства фрагментации и последующей повторной сборки сообщений больших объемов и повторной передачи любых их частей, которые могли быть потеряны в сети.

SOCK_DGRAM - дейтаграммный сервис. Вы можете использовать такой сокет для отправки сообщений с фиксированным (обычно небольшим) максимальным объемом, но при этом нет гарантии, что сообщение будет доставлено или что сообщения не будут переупорядочены в сети. В случае сокетов домена AF_INET этот тип передачи данных обеспечивается дейтаграммами UDP (User Datagram Protocol, пользовательский протокол дейтаграмм).

Протокол, применяемый для обмена данными, обычно определяется типом сокета и доменом. Как правило, выбора нет. Параметр protocol применяется в тех случаях, когда выбор все же предоставляется. Задание 0 позволяет выбрать стандартный протокол, используемый во всех примерах данной главы.

Системный вызов socket возвращает дескриптор, во многом похожий на низкоуровневый файловый дескриптор. Когда сокет подключен к концевой точке другого сокета, для отправки и получения данных с помощью сокетов можно применять системные вызовы read и write с дескриптором сокета. Системный вызов close используется для удаления сокетного соединения.

Мне очень нравится весь цикл статей, плюс всегда хотелось попробовать себя в качестве переводчика. Возможно, опытным разработчикам статья покажется слишком очевидной, но, как мне кажется, польза от нее в любом случае будет.
Первая статья - http://habrahabr.ru/post/209144/

Прием и передача пакетов данных

Введение
Привет, меня зовут Гленн Фидлер и я приветствую вас в своей второй статье из цикла “Сетевое программирование для разработчиков игр”.

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

А сейчас я собираюсь рассказать вам, как на практике использовать UDP для отправки и приема пакетов.

BSD сокеты
В большинстве современных ОС имеется какая-нибудь реализация сокетов, основанная на BSD сокетах (сокетах Беркли).

Сокеты BSD оперируют простыми функциями, такими, как “socket”, “bind”, “sendto” и “recvfrom”. Конечно, вы можете обращаться к этим функциями напрямую, но в таком случае ваш код будет зависим от платформы, так как их реализации в разных ОС могут немного отличаться.

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

Особенности разных ОС
Для начала напишем код, который будет определять текущую ОС, чтобы мы могли учесть различия в работе сокетов:

// platform detection #define PLATFORM_WINDOWS 1 #define PLATFORM_MAC 2 #define PLATFORM_UNIX 3 #if defined(_WIN32) #define PLATFORM PLATFORM_WINDOWS #elif defined(__APPLE__) #define PLATFORM PLATFORM_MAC #else #define PLATFORM PLATFORM_UNIX #endif
Теперь подключим заголовочные файлы, нужные для работы с сокетами. Так как набор необходимых заголовочных файлов зависит от текущей ОС, здесь мы используем код #define, написанный выше, чтобы определить, какие файлы нужно подключать.

#if PLATFORM == PLATFORM_WINDOWS #include #elif PLATFORM == PLATFORM_MAC || PLATFORM == PLATFORM_UNIX #include #include #include #endif
В UNIX системах функции работы с сокетами входят в стандартные системные библиотеки, поэтому никакие сторонние библиотеки нам в этом случае не нужны. Однако в Windows для этих целей нам нужно подключить библиотеку winsock.

Вот небольшая хитрость, как можно это сделать без изменения проекта или makefile’а:

#if PLATFORM == PLATFORM_WINDOWS #pragma comment(lib, "wsock32.lib") #endif
Мне нравится этот прием потому, что я ленивый. Вы, конечно, можете подключить библиотеку в проект или в makefile.

Инициализация сокетов
В большинстве unix-like операционных систем (включая macosx) не требуется никаких особых действий для инициализации функционала работы с сокетами, но в Windows нужно сначала сделать пару па - нужно вызвать функцию “WSAStartup” перед использованием любых функций работы с сокетами, а после окончания работы - вызвать “WSACleanup”.

Давайте добавим две новые функции:

Inline bool InitializeSockets() { #if PLATFORM == PLATFORM_WINDOWS WSADATA WsaData; return WSAStartup(MAKEWORD(2,2), &WsaData) == NO_ERROR; #else return true; #endif } inline void ShutdownSockets() { #if PLATFORM == PLATFORM_WINDOWS WSACleanup(); #endif }
Теперь мы имеем независимый от платформы код инициализации и завершения работы с сокетами. На платформах, которые не требуют инициализации, данный код просто не делает ничего.

Создаем сокет
Теперь мы можем создать UDP сокет. Это делается так:

Int handle = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (handle <= 0) { printf("failed to create socket\n"); return false; }
Далее мы должны привязать сокет к определенному номеру порта (к примеру, 30000). У каждого сокета должен быть свой уникальный порт, так как, когда приходит новый пакет, номер порта определяет, какому сокету его передать. Не используйте номера портов меньшие, чем 1024 - они зарезервированы системой.

Если вам все равно, какой номер порта использовать для сокета, вы можете просто передать в функцию “0”, и тогда система сама выделит вам какой-нибудь незанятый порт.

Sockaddr_in address; address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; address.sin_port = htons((unsigned short) port); if (bind(handle, (const sockaddr*) &address, sizeof(sockaddr_in)) < 0) { printf("failed to bind socket\n"); return false; }
Теперь наш сокет готов для передачи и приема пакетов данных.

Но что это за таинственная функция “htons” вызывается в коде? Это просто небольшая вспомогательная функция, которая переводит порядок следования байтов в 16-битном целом числе - из текущего (little- или big-endian) в big-endian, который используется при сетевом взаимодействии. Ее нужно вызывать каждый раз, когда вы используете целые числа при работе с сокетами напрямую.

Вы встретите функцию “htons” и ее 32-битного двойника - “htonl” в этой статье еще несколько раз, так что будьте внимательны.

Перевод сокета в неблокирующий режим
По умолчанию сокеты находится в так называемом “блокирующем режиме”. Это означает, что если вы попытаетесь прочитать из него данные с помощью “recvfrom”, функция не вернет значение, пока не сокет не получит пакет с данными, которые можно прочитать. Такое поведение нам совсем не подходит. Игры - это приложения, работающие в реальном времени, со скоростью от 30 до 60 кадров в секунду, и игра не может просто остановиться и ждать, пока не придет пакет с данными!

Решить эту проблему можно переведя сокет в “неблокирующий режим” после его создания. В этом режиме функция “recvfrom”, если отсутствуют данные для чтения из сокета, сразу возвращает определенное значение, показывающее, что нужно будет вызвать ее еще раз, когда в сокете появятся данные.

Перевести сокет в неблокирующий режим можно следующим образом:

#if PLATFORM == PLATFORM_MAC || PLATFORM == PLATFORM_UNIX int nonBlocking = 1; if (fcntl(handle, F_SETFL, O_NONBLOCK, nonBlocking) == -1) { printf("failed to set non-blocking socket\n"); return false; } #elif PLATFORM == PLATFORM_WINDOWS DWORD nonBlocking = 1; if (ioctlsocket(handle, FIONBIO, &nonBlocking) != 0) { printf("failed to set non-blocking socket\n"); return false; } #endif
Как вы можете видеть, в Windows нет функции “fcntl”, поэтому вместе нее мы используем “ioctlsocket”.

Отправка пакетов
UDP - это протокол без поддержки соединений, поэтому при каждой отправке пакета нам нужно указывать адрес получателя. Можно использовать один и тот же UDP сокет для отправки пакетов на разные IP адреса - на другом конце сокета не обязательно должен быть один компьютер.

Переслать пакет на определенный адрес можно следующим образом:

Int sent_bytes = sendto(handle, (const char*)packet_data, packet_size, 0, (sockaddr*)&address, sizeof(sockaddr_in)); if (sent_bytes != packet_size) { printf("failed to send packet: return value = %d\n", sent_bytes); return false; }
Обратите внимание - возвращаемое функцией “sendto” значение показывает только, был ли пакет успешно отправлен с локального компьютера. Но оно не показывает, был ли пакет принят адресатом! В UDP нет средств для определения, дошел ли пакет по назначению или нет.

В коде, приведенном выше, мы передаем структуру “sockaddr_in” в качестве адреса назначения. Как нам получить эту структуру?

Допустим, мы хотим отправить пакет по адресу 207.45.186.98:30000.

Запишем адрес в следующей форме:

Unsigned int a = 207; unsigned int b = 45; unsigned int c = 186; unsigned int d = 98; unsigned short port = 30000;
И нужно сделать еще пару преобразований, чтобы привести его к форме, которую понимает “sendto”:

Unsigned int destination_address = (a << 24) | (b << 16) | (c << 8) | d; unsigned short destination_port = port; sockaddr_in address; address.sin_family = AF_INET; address.sin_addr.s_addr = htonl(destination_address); address.sin_port = htons(destination_port);
Как видно, сначала мы объединяем числа a, b, c, d (которые лежат в диапазоне ) в одно целое число, в котором каждый байт - это одно из исходных чисел. Затем мы инициализируем структуру “sockaddr_in” нашими адресом назначения и портом, при этом не забыв конвертировать порядок байтов с помощью функций “htonl” и “htons”.

Отдельно стоит выделить случай, когда нужно передать пакет самому себе: при этом не нужно выяснять IP адрес локальной машины, а можно просто использовать 127.0.0.1 в качестве адреса (адрес локальной петли), и пакет будет отправлен на локальный компьютер.

Прием пакетов
После того, как мы привязали UDP сокет к порту, все UDP пакеты, приходящие на IP адрес и порт нашего сокета, будут ставиться в очередь. Поэтому для приема пакетов мы просто в цикле вызываем “recvfrom”, пока он не выдаст ошибку, означающую, что пакетов для чтения в очерели не осталось.

Так как протокол UDP не поддерживает соединения, пакеты могут приходить с множества различных компьютеров сети. Каждый раз, когда мы принимаем пакет, функция “recvfrom” выдает нам IP адрес и порт отправителя, и поэтому мы знаем, кто отправил этот пакет.

Код приема пакетов в цикле:

While (true) { unsigned char packet_data; unsigned int maximum_packet_size = sizeof(packet_data); #if PLATFORM == PLATFORM_WINDOWS typedef int socklen_t; #endif sockaddr_in from; socklen_t fromLength = sizeof(from); int received_bytes = recvfrom(socket, (char*)packet_data, maximum_packet_size, 0, (sockaddr*)&from, &fromLength); if (received_bytes <= 0) break; unsigned int from_address = ntohl(from.sin_addr.s_addr); unsigned int from_port = ntohs(from.sin_port); // process received packet }
Пакеты, размер которых больше, чем размер буфера приема, будут просто втихую удалены из очереди. Так что, если вы используете буфер размером 256 байтов, как в примере выше, и кто-то присылает вам пакет в 300 байт, он будет отброшен. Вы не получите просто первые 256 байтов из пакета.

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

Закрытие сокета
На большинстве unix-like систем, сокеты представляют собой файловые дескрипторы, поэтому для того, чтобы закрыть сокеты после использования, можно использовать стандартную функцию “close”. Однако, Windows, как всегда, выделяется, и в ней нам нужно использовать “closesocket”.

#if PLATFORM == PLATFORM_MAC || PLATFORM == PLATFORM_UNIX close(socket); #elif PLATFORM == PLATFORM_WINDOWS closesocket(socket); #endif
Так держать, Windows!

Класс сокета
Итак, мы разобрались со всеми основными операциями: создание сокета, привязка его к порту, перевод в неблокирующий режим, отправка и прием пакетов, и, в конце, закрытие сокета.

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

Поэтому мы сделаем класс-обертку “Socket” для всех этих операций. Также мы создадим класс “Address”, чтобы было проще работать с IP адресами. Он позволит не проводить все манипуляции с “sockaddr_in” каждый раз, когда мы захотим отправить или принять пакет.

Итак, наш класс Socket:

Class Socket { public: Socket(); ~Socket(); bool Open(unsigned short port); void Close(); bool IsOpen() const; bool Send(const Address & destination, const void * data, int size); int Receive(Address & sender, void * data, int size); private: int handle; };
И класс Address:

Class Address { public: Address(); Address(unsigned char a, unsigned char b, unsigned char c, unsigned char d, unsigned short port); Address(unsigned int address, unsigned short port); unsigned int GetAddress() const; unsigned char GetA() const; unsigned char GetB() const; unsigned char GetC() const; unsigned char GetD() const; unsigned short GetPort() const; bool operator == (const Address & other) const; bool operator != (const Address & other) const; private: unsigned int address; unsigned short port; };
Использовать их для приема и передачи нужно следующим образом:

// create socket const int port = 30000; Socket socket; if (!socket.Open(port)) { printf("failed to create socket!\n"); return false; } // send a packet const char data = "hello world!"; socket.Send(Address(127,0,0,1,port), data, sizeof(data)); // receive packets while (true) { Address sender; unsigned char buffer; int bytes_read = socket.Receive(sender, buffer, sizeof(buffer)); if (!bytes_read) break; // process packet }
Как видите, это намного проще, чем работать с BSD сокетами напрямую. И также этот код будет одинаков для всех ОС, потому весь платформозависимый функционал находится внутри классов Socket и Address.

Заключение
Теперь у нас есть независимый от платформы инструмент для отправки и према UDP пакетов.

UDP не поддерживает соединения, и мне хотелось сделать пример, который бы четко это показал. Поэтому я написал небольшую программу , которая считывает список IP адресов из текстового файла и рассылает им пакеты, по одному в секунду. Каждый раз, когда программа принимает пакет, она выводит в консоль адрес и порт компьютера-отправителя и размер принятого пакета.

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

> Node 30000
> Node 30001
> Node 30002
И т.д…

Каждый из узлов будет пересылать пакеты всем остальным узлам, образуя нечто вроде мини peer-to-peer системы.

Я разрабатывал эту программу на MacOSX, но она должна компилироваться на любой unix-like ОС и на Windows, однако если вам для этого потребуется делать какие-либо доработки, сообщите мне.

Теги: Добавить метки

ВСЕВОЛОД СТАХОВ

Программирование сокетов

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

Таким образом, сетевые сокеты представляют собой парные структуры, жёстко между собой синхронизированные. Для создания сокетов в любой операционной системе, поддерживающей их, используется функция socket (к счастью, сокеты достаточно стандартизированы, поэтому их можно использовать для передачи данных между приложениями, работающими на разных платформах). Формат функции таков:

int socket(int domain, int type, int protocol);

Параметр domain задаёт тип транспортного протокола, т.е. протокола доставки пакетов в сети. В настоящее время поддерживаются следующие протоколы (но учтите, что для разных типов протокола тип адресной структуры будет разный):

  • PF_UNIX или PF_LOCAL – локальная коммуникация для ОС UNIX (и подобных).
  • PF_INET – IPv4, IP -протокол Интернета, наиболее распространён сейчас (32-х битный адрес).
  • PF_INET6 – IPv6, следующее поколение протокола IP (IPng) – 128 битный адрес.
  • PF_IPX – IPX – протоколы Novell.

Поддерживаются и другие протоколы, но эти 4 являются самыми популярными.

Параметр type означает тип сокета, т.е. то, как будут передаваться данные: обычно применяется константа SOCK_STREAM, её использование означает безопасную передачу данных двунаправленным потоком с контролем ошибок. При таком способе передачи данных программисту не приходится заботиться об обработке ошибок сети, хотя это не уберегает от логических ошибок, что актуально для сетевого сервера.

Параметр protocol определяет конкретный тип протокола для данного domain, например IPPROTO_TCP или IPPROTO_UDP (параметр type должен в данном случае быть SOCK_DGRAM).

Функция socket просто создаёт конечную точку и возвращает дескриптор сокета; до того, как сокет не соединён с удалённым адресом функцией connect, данные через него пересылать нельзя! Если пакеты теряются в сети, т.е. произошло нарушение связи, то приложению, создавшему сокет, посылается сигнал Broken Pipe – SIGPIPE, поэтому целесообразно присвоить обработчик данному сигналу функцией signal. После того, как сокет соединён с другим функцией connect, по нему можно пересылать данные либо стандартными функциями read – write, либо специализированными recv – send. После окончания работы сокет надо закрыть функцией close. Для создания клиентского приложения достаточно связать локальный сокет с удалённым (серверным) функцией connect. Формат этой функции такой:

int connect(int sock_fd, const struct *sockaddr serv_addr, socketlen_t addr_len);

При ошибке функция возвращает -1, статус ошибки можно получить средствами операционной системы. При успешной работе возвращается 0. Сокет, однажды связанный, чаще всего не может быть связан снова, так, например, происходит в протоколе ip. Параметр sock_fd задаёт дескриптор сокета, структура serv_addr назначает удалённый адрес конечной точки, addr_len содержит длину serv_addr (тип socketlen_t имеет историческое происхождение, обычно он совпадает с типом int). Самый важный параметр в этой функции – адрес удалённого сокета. Он, естественно, неодинаков для разных протоколов, поэтому я опишу здесь структуру адреса только для IP (v4)-протокола. Для этого используется специализированная структура sockaddr_in (её необходимо прямо приводить к типу sockaddr при вызове connect). Поля данной структуры выглядят следующим образом:

struct sockaddr_in{

Sa_family_t sin_family; – определяет семейство адресов, всегда должно быть AF_INET

U_int16_t sin_port; – порт сокета в сетевом порядке байт

Struct in_addr sin_addr; – структура, содержащая IP-адрес

Структура, описывающая IP-адрес:

struct in_addr{

U_int32_t s_addr; – IP-адрес сокета в сетевом порядке байт

Обратите внимание на особый порядок байт во всех целых полях. Для перевода номера порта в сетевой порядок байт можно воспользоваться макросом htons (unsigned short port). Очень важно использовать именно этот тип целого – беззнаковое короткое целое.

Адреса IPv4 делятся на одиночные, широковещательные (broadcast) и групповые (multicast). Каждый одиночный адрес указывает на один интерфейс хоста, широковещательные адреса указывают на все хосты в сети, а групповые адреса соответствуют всем хостам в группе (multicast group). В структуре in_addr можно назначать любой из этих адресов. Но для сокетных клиентов в подавляющем большинстве случаев присваивают одиночный адрес. Исключением является тот случай, когда необходимо просканировать всю локальную сеть в поисках сервера, тогда можно использовать в качестве адреса широковещательный. Затем, скорее всего, сервер должен сообщить свой реальный IP-адрес и сокет для дальнейшей передачи данных должен присоединяться именно к нему. Передача данных через широковещательные адреса не есть хорошая идея, так как неизвестно, какой именно сервер обрабатывает запрос. Поэтому в настоящее время сокеты, ориентированные на соединение, могут использовать лишь одиночные адреса. Для сокетных серверов, ориентированных на прослушивание адреса, наблюдается другая ситуация: здесь разрешено использовать широковещательные адреса, чтобы сразу же ответить клиенту на запрос о местоположении сервера. Но обо всём по порядку. Как вы заметили, в структуре sockaddr_in поле IP-адреса представлено как беззнаковое длинное целое, а мы привыкли к адресам либо в формате x.x.x.x (172.16.163.89) либо в символьном формате (myhost.com). Для преобразования первого служит функция inet_addr (const char *ip_addr), а для второго – функция gethostbyname (const char *host). Рассмотрим обе из них:

u_int32_t inet_addr(const char *ip_addr)

– возвращает сразу же целое, пригодное для использования в структуре sockaddr_in по IP-адресу, переданному ей в формате x.x.x.x. При возникновении ошибки возвращается значение INADDR_NONE.

struct HOSTENT* gethostbyname(const char *host_name)

– возвращает структуру информации о хосте, исходя из его имени. В случае неудачи возвращает NULL. Поиск имени происходит вначале в файле hosts, а затем в DNS. Структура HOSTENT предоставляет информацию о требуемом хосте. Из всех её полей наиболее значительным является поле char **h_addr_list, представляющее список IP-адресов данного хоста. Обычно используется h_addr_list, представляющая первый ip адрес хоста, для этого можно также использовать выражение h_addr. После выполнения функции gethostbyname в списке h_addr_list структуры HOSTENT оказываются простые символические ip адреса, поэтому необходимо воспользоваться дополнительно функцией inet_addr для преобразования в формат sockaddr_in.

Итак, мы связали клиентский сокет с серверным функцией connect. Далее можно использовать функции передачи данных. Для этого можно использовать либо стандартные функции низкоуровневого ввода/вывода для файлов, так как сокет – это, по сути дела файловый дескриптор. К сожалению, для разных операционных систем функции низкоуровневой работы с файлами могут различаться, поэтому надо посмотреть руководство к своей операционной системе. Учтите, что передача данных по сети может закончиться сигналом SIGPIPE, и функции чтения/записи вернут ошибку. Всегда нужно помнить о проверке ошибок, кроме того, нельзя забывать о том, что передача данных по сети может быть очень медленной, а функции ввода/вывода являются синхронными, и это может вызвать существенные задержки в работе программы.

Для передачи данных между сокетами существуют специальные функции, единые для всех ОС – это функции семейства recv и send. Формат их очень похож:

int send(int sockfd, void *data, size_t len, int flags);

– отправляет буфер data.

int recv(int sockfd, void *data, size_t len, int flags);

– принимает буфер data.

Первый аргумент – дескриптор сокета, второй – указатель на данные для передачи, третий – длина буфера и четвёртый – флаги. В случае успеха возвращается число переданных байт, в случае неудачи – отрицательный код ошибки. Флаги позволяют изменить параметры передачи (например, включить асинхронный режим работы), но для большинства задач достаточно оставить поле флагов нулевым для обычного режима передачи. При отсылке или приёме данных функции блокируют выполнение программы до того, как будет отослан весь буфер. А при использовании протокола tcp/ip от удалённого сокета должен прийти ответ об успешной отправке или приёме данных, иначе пакет пересылается ещё раз. При пересылке данных учитывайте MTU сети (максимальный размер передаваемого за один раз кадра). Для разных сетей он может быть разным, например, для сети Ethernet он равен 1500.

Итак, для полноты изложения приведу самый простенький пример программы на Си, реализующей сокетного клиента:

#include /* Стандартные библиотеки сокетов для Linux */

#include /* Для ОС Windows используйте #include */

#include

int main(){

Int sockfd = -1;

/* Дескриптор сокета */

Char buf;

Char s = "Client ready ";

HOSTENT *h = NULL;

Sockaddr_in addr;

Unsigned short port = 80;

Addr.sin_family = AF_INET;

/* Создаём сокет */

If(sockfd == -1)

/* Создан ли сокет */

Return -1;

H = gethostbyname("www.myhost.com");

/* Получаем адрес хоста */

If(h == NULL)

/* А есть ли такой адрес? */

Return -1;

Addr.sin_addr.s_addr = inet_addr(h->h_addr_list);

/* Переводим ip адрес в число */

If(connect(sockfd, (sockaddr*) &addr, sizeof(addr)))

/* Пытаемся соединится с удалённым сокетом */

Return -1;

/* Соединение прошло успешно - продолжаем */

If(send(sockfd, s, sizeof(s), 0) < 0)

Return -1;

If(recv(sockfd, buf, sizeof(buf), 0) < 0)

Return -1;

Close(sockfd);

/* Закрываем сокет */

/* Для Windows применяется функция closesocket(s) */

Return 0;

Вот видите, использовать сокеты не так трудно. В серверных приложениях используются совершенно другие принципы работы с сокетами. Вначале создается сокет, затем ему присваивается локальный адрес функцией bind, при этом можно присвоить сокету широковещательный адрес. Затем начинается прослушивание адреса функцией listen, запросы на соединение помещаются в очередь. То есть функция listen выполняет инициализацию сокета для приёма сообщений. После этого нужно применить функцию accept, которая возвращает новый, уже связанный с клиентом сокет. Обычно для серверов характерно принимать много соединений через небольшие промежутки времени. Поэтому нужно постоянно проверять очередь входящих соединений функцией accept. Для организации такого поведения чаще всего прибегают к возможностям операционной системы. Для ОС Windows чаще используется многопоточный вариант работы сервера (multi-threaded), после принятия соединения происходит создание нового потока в программе, который и обрабатывает сокет. В *nix-системах чаще используется порождение дочернего процесса функцией fork. При этом накладные расходы уменьшены за счёт того, что фактически происходит копия процесса в файловой системе proc. При этом все переменные дочернего процесса совпадают с родителем. И дочерний процесс может сразу же обрабатывать входящее соединение. Родительский же процесс продолжает прослушивание. Учтите, что порты с номерами от 1 до 1024 являются привилегированными и их прослушивание не всегда возможно. Ещё один момент: нельзя, чтобы два разных сокета прослушивали один и тот же порт по одному и тому же адресу! Для начала рассмотрим форматы вышеописанных функций для создания серверного сокета:

int bind(int sockfd, const struct *sockaddr, socklen_t addr_len);

– присваивает сокету локальный адрес для обеспечения возможности принимать входящие соединения. Для адреса можно использовать константу INADDR_ANY, которая позволяет принимать входящие соединения со всех адресов в данной подсети. Формат функции аналогичен connect. В случае ошибки возвращает отрицательное значение.

int listen(int sockfd, int backlog);

– функция создаёт очередь входящих сокетов (количество подключений определяется параметром backlog, оно не должно превышать числа SOMAXCONN, которое зависит от ОС). После создания очереди можно ожидать соединения функцией accept. Сокеты обычно являются блокирующими, поэтому выполнение программы приостанавливается, пока соединение не будет принято. В случае ошибки возвращается -1.

int accept(int sockfd, struct *sockaddr, socklen_t addr_len)

– функция дожидается входящего соединения (или извлекает его из очереди соединений) и возвращает новый сокет, уже связанный с удалённым клиентом. При этом исходный сокет sockfd остается в неизменном состоянии. Структура sockaddr заполняется значениями из удалённого сокета. В случае ошибки возвращается -1.

Итак, приведу пример простого сокетного сервера, использующего функцию fork для создания дочернего процесса, обрабатывающего соединение:

int main(){

Pid_t pid;

/* Идентификатор дочернего процесса */

Int sockfd = -1;

/* Дескриптор сокета для прослушивания */

Int s = -1;

/* Дескриптор сокета для приёма */

Char buf;

/* Указатель на буфер для приёма */

Char str = "Server ready ";

/* Строка для передачи серверу */

HOSTENT *h = NULL;

/* Структура для получения ip адреса */

Sockaddr_in addr;

/* Cтруктура tcp/ip протокола */

Sockaddr_in raddr;

Unsigned short port = 80;

/* Заполняем поля структуры: */

Addr.sin_family = AF_INET;

Addr.sin_port = htons(port);

sockfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);

/* Создаём сокет */

If(sockfd == -1)

/* Создан ли сокет */

Return -1;

Addr.sin_addr.s_addr = INADDR_ANY;

/* Слушаем на всех адресах */

If(bind(sockfd, (sockaddr*) &addr, sizeof(addr)))

/* Присваиваем сокету локальный адрес */

Return -1;

If(listen(sockfd, 1))

/* Начинаем прослушивание */

Return -1;

S = accept(sockfd, (sockaddr *) &raddr, sizeof(raddr));

/* Принимаем соединение */

Pid = fork();

/* порождаем дочерний процесс */

If(pid == 0){

/* Это дочерний процесс */

If(recv(s, buf, sizeof(buf), 0) < 0)

/* Посылаем удалённому сокету строку s */

Return -1;

If(send(s, str, sizeof(str), 0) < 0)

/* Получаем ответ от удалённого сервера */

Return -1;

Printf("Recieved string was: %s", buf);

/* Вывод буфера на стандартный вывод */

Close(s);

/* Закрываем сокет */

Return 0;

/* Выходим из дочернего процесса */

Close(sockfd);

/* Закрываем сокет для прослушивания */

Return 0;

При создании потока (thread) для обработки сокета смотрите руководство к ОС, так как для разных систем вызов функции создания потока может существенно различаться. Но принципы обработки для потока остаются теми же. Функции обработки необходимо только передать в качестве аргумента указатель на сокет (обычно в функцию потока можно передавать данные любого типа в формате void *, что требует использования приведения типов).

Важное замечание для систем Windows. Мною было замечено, что система сокетов не работает без применения функции WSAStartup для инициализации библиотеки сокетов. Программа с сокетами в ОС Windows должна начинаться так:

WSADATA wsaData;

WSAStartup(0x0101, &wsaData);

И при выходе из программы пропишите следующее:

WSACleanup();

Так как в основном операции с сокетами являются блокирующими, приходится часто прерывать исполнение задачи ожиданием синхронизации. Поэтому часто в *nix-подобных системах избегают блокировки консоли созданием особого типа программы – демона. Демон не принадлежит виртуальным консолям и возникает, когда дочерний процесс вызывает fork, а родительский процесс завершается раньше, чем 2-й дочерний (а это всегда бывает именно таким образом). После этого 2-й дочерний процесс становится основным и не блокирует консоль. Приведу пример такого поведения программы:

pid = fork();

/* Создание первого дочернего процесса */

if (pid <0){

/* Ошибка вызова fork */

Printf("Forking Error:) ");

Exit(-1);

}else if (pid !=0){

/* Это первый родитель! */

Printf(" This is a Father 1 ");

}else{

Pid = fork();

/* Работа 1-го родителя завершается */

/* И мы вызываем ещё один дочерний процесс */

If (pid <0){

Printf("Forking error:) ");

Exit(-1);

}else if (pid !=0){

/* Это второй родитель */

Printf(" This is a father 2 ");

}else{

/* А вот это тот самый 2-й дочерний процесс*/

/* Переход в "стандартный" режим демона */

Setsid();

/* Выполняем демон в режиме superuser */

Umask(0); /* Стандартная маска файлов */

Chdir("/"); /* Переход в корневой каталог */

Daemoncode(); /* Собственно сам код демона */

/* При вызове fork демона появляется потомок-демон */

Вот и всё. Я думаю, для создания простенького сокетного сервера этого достаточно.