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

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

#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 используется для удаления сокетного соединения.

Последнее обновление: 31.10.2015

В основе межсетевых взаимодействий по протоколам TCP и UDP лежат сокеты. В.NET сокеты представлены классом System.NET.Sockets.Socket , который предоставляет низкоуровневый интерфейс для приема и отправки сообщений по сети.

Рассмотрим основные свойства данного класса:

    AddressFamily: возвращает все адреса, используемые сокетом. Данное свойство представляет одно из значений, определенных в одноименном перечислении AddressFamily . Перечисление содержит 18 различных значений, наиболее используемые:

    • InterNetwork: адрес по протоколу IPv4

      InterNetworkV6: адрес по протоколу IPv6

      Ipx: адрес IPX или SPX

      NetBios: адрес NetBios

    Available: возвращает объем данных, которые доступны для чтения

    Connected: возвращает true, если сокет подключен к удаленному хосту

    LocalEndPoint: возвращает локальную точку, по которой запущен сокет и по которой он принимает данные

    ProtocolType: возвращает одно из значений перечисления ProtocolType , представляющее используемый сокетом протокол. Есть следующие возможные значения:

    • IPSecAuthenticationHeader (Заголовок IPv6 AH)

      IPSecEncapsulatingSecurityPayload (Заголовок IPv6 ESP)

      IPv6DestinationOptions (Заголовок IPv6 Destination Options)

      IPv6FragmentHeader (Заголовок IPv6 Fragment)

      IPv6HopByHopOptions (Заголовок IPv6 Hop by Hop Options)

      IPv6NoNextHeader (Заголовок IPv6 No next)

      IPv6RoutingHeader (Заголовок IPv6 Routing)

      Unknown (неизвестный протокол)

      Unspecified (неуказанный протокол)

    Каждое значение представляет соответствующий протокол, но наиболее используемыми являются Tcp и Udp.

    RemoteEndPoint: возвращает адрес удаленного хоста, к которому подключен сокет

    SocketType: возвращает тип сокета. Представляет одно из значений из перечисления SocketType :

    • Dgram: сокет будет получать и отправлять дейтаграммы по протоколу Udp. Данный тип сокета работает в связке с типом протокола - Udp и значением AddressFamily.InterNetwork

      Raw: сокет имеет доступ к нижележащему протоколу транспортного уровня и может использовать для передачи сообщений такие протоколы, как ICMP и IGMP

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

      Seqpacket: обеспечивает надежную двустороннюю передачу данных с установкой постоянного подключения

      Stream: обеспечивает надежную двустороннюю передачу данных с установкой постоянного подключения. Для связи используется протокол TCP, поэтому этот тип сокета используется в паре с типом протокола Tcp и значением AddressFamily.InterNetwork

      Unknown: адрес NetBios

Для создания объекта сокета можно использовать один из его конструкторов. Например, сокет, использующий протокол Tcp:

Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

Или сокет, использующий протокол Udp:

Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);

Таким образом, при создании сокета мы можем указывать разные комбинации протоколов, типов сокета, значений из перечисления AddressFamily. Однако в то же время не все комбинации являются корректными. Так, для работы через протокол Tcp, нам надо обязательно указать параметры: AddressFamily.InterNetwork, SocketType.Stream и ProtocolType.Tcp. Для Udp набор параметров будет другим: AddressFamily.InterNetwork, SocketType.Dgram и ProtocolType.Udp. Для других протоколов набор значений будет отличаться. Поэтому использование сокетов может потребовать некоторого знания принципов работы отдельных протоколов. Хотя в отношении Tcp и Udp все относительно просто.

Общий принцип работы сокетов

При работе с сокетами вне зависимости от выбранных протоколов мы будем опираться на методы класса Socket:

    Accept() : создает новый объект Socket для обработки входящего подключения

    Bind() : связывает объект Socket с локальной конечной точкой

    Close() : закрывает сокет

    Connect() : устанавливает соединение с удаленным хостом

    Listen() : начинает прослушивание входящих запросов

    Poll() : определяет состояние сокета

    Receive() : получает данные

    Send() : отправляет данные

    Shutdown() : блокирует на сокете прием и отправку данных

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

При применении протокола, который требует установление соединения, например, TCP, сервер должен вызвать метод Bind для установки точки для прослушивания входящих подключений и затем запустить прослушивание подключений с помощью метода Listen. Далее с помощью метода Accept можно получить входящие запросы на подключение в виде объекта Socket, который используется для взаимодействия с удаленным узла. У полученного объекта Socket вызываются методы Send и Receive соответственно для отправки и получения данных. Если необходимо подключиться к серверу, то вызывается метод Connect. Для обмена данными с сервером также применяются методы Send или Receive.

Если применяется протокол, для которого не требуется установление соединения, например, UDP, то после вызова метода Bind не надо вызывать метод Listen. И в этом случае для приема данных используется метод ReceiveFrom, а для отправки данных - метод SendTo.

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

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

Сокеты

Сокет - это один конец двустороннего канала связи между двумя программами, работающими в сети. Соединяя вместе два сокета, можно передавать данные между разными процессами (локальными или удаленными). Реализация сокетов обеспечивает инкапсуляцию протоколов сетевого и транспортного уровней.

Первоначально сокеты были разработаны для UNIX в Калифорнийском университете в Беркли. В UNIX обеспечивающий связь метод ввода-вывода следует алгоритму open/read/write/close. Прежде чем ресурс использовать, его нужно открыть, задав соответствующие разрешения и другие параметры. Как только ресурс открыт, из него можно считывать или в него записывать данные. После использования ресурса пользователь должен вызывать метод Close(), чтобы подать сигнал операционной системе о завершении его работы с этим ресурсом.

Когда в операционную систему UNIX были добавлены средства межпроцессного взаимодействия (Inter-Process Communication, IPC) и сетевого обмена, был заимствован привычный шаблон ввода-вывода. Все ресурсы, открытые для связи, в UNIX и Windows идентифицируются дескрипторами. Эти дескрипторы, или описатели (handles) , могут указывать на файл, память или какой-либо другой канал связи, а фактически указывают на внутреннюю структуру данных, используемую операционной системой. Сокет, будучи таким же ресурсом, тоже представляется дескриптором. Следовательно, для сокетов жизнь дескриптора можно разделить на три фазы: открыть (создать) сокет, получить из сокета или отправить сокету и в конце концов закрыть сокет.

Интерфейс IPC для взаимодействия между разными процессами построен поверх методов ввода-вывода. Они облегчают для сокетов отправку и получение данных. Каждый целевой объект задается адресом сокета, следовательно, этот адрес можно указать в клиенте, чтобы установить соединение с целью.

Типы сокетов

Существуют два основных типа сокетов - потоковые сокеты и дейтаграммные.

Потоковые сокеты (stream socket)

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

Потоковый сокет гарантирует исправление ошибок, обрабатывает доставку и сохраняет последовательность данных. На него можно положиться в доставке упорядоченных, сдублированных данных. Потоковый сокет также подходит для передачи больших объемов данных, поскольку накладные расходы, связанные с установлением отдельного соединения для каждого отправляемого сообщения, может оказаться неприемлемым для небольших объемов данных. Потоковые сокеты достигают этого уровня качества за счет использования протокола Transmission Control Protocol (TCP) . TCP обеспечивает поступление данных на другую сторону в нужной последовательности и без ошибок.

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

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

Потоки базируются на явных соединениях: сокет А запрашивает соединение с сокетом В, а сокет В либо соглашается с запросом на установление соединения, либо отвергает его.

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

Сервер электронной почты представляет пример приложения, которое должно доставлять содержание в правильном порядке, без дублирования и пропусков. Потоковый сокет рассчитывает, что TCP обеспечит доставку сообщений по их назначениям.

Дейтаграммные сокеты (datagram socket)

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

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

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

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

Сырые сокеты (raw socket)

Главная цель использования сырых сокетов состоит в обходе механизма, с помощью которого компьютер обрабатывает TCP/IP. Это достигается обеспечением специальной реализации стека TCP/IP, замещающей механизм, предоставленный стеком TCP/IP в ядре - пакет непосредственно передается приложению и, следовательно, обрабатывается гораздо эффективнее, чем при проходе через главный стек протоколов клиента.

По определению, сырой сокет - это сокет, который принимает пакеты, обходит уровни TCP и UDP в стеке TCP/IP и отправляет их непосредственно приложению.

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

Однако нечасто может потребоваться программа, работающая с сырыми сокетами. Если вы не пишете системное программное обеспечение или программу, аналогичную анализатору пакетов, вникать в такие детали не придется. Сырые сокеты главным образом используются при разработке специализированных низкоуровневых протокольных приложений. Например, такие разнообразные утилиты TCP/IP, как trace route, ping или arp, используют сырые сокеты.

Работа с сырыми сокетами требует солидного знания базовых протоколов TCP/UDP/IP.

Порты

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

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

За определенными службами номера портов зарезервированы - это широко известные номера портов, например порт 21, использующийся в FTP. Ваше приложение может пользоваться любым номером порта, который не был зарезервирован и пока не занят. Агентство Internet Assigned Numbers Authority (IANA) ведет перечень широко известных номеров портов.

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

Например, на стороне клиента, приложение должно знать адрес цели и номер порта. Отправляя запрос на соединение, клиент пытается установить соединение с сервером:

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

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

Работа с сокетами в.NET

Поддержку сокетов в.NET обеспечивают классы в пространстве имен System.Net.Sockets - начнем с их краткого описания.

Классы для работы с сокетами
Класс Описание
MulticastOption Класс MulticastOption устанавливает значение IP-адреса для присоединения к IP-группе или для выхода из нее.
NetworkStream Класс NetworkStream реализует базовый класс потока, из которого данные отправляются и в котором они получаются. Это абстракция высокого уровня, представляющая соединение с каналом связи TCP/IP.
TcpClient Класс TcpClient строится на классе Socket, чтобы обеспечить TCP-обслуживание на более высоком уровне. TcpClient предоставляет несколько методов для отправки и получения данных через сеть.
TcpListener Этот класс также построен на низкоуровневом классе Socket. Его основное назначение - серверные приложения. Он ожидает входящие запросы на соединения от клиентов и уведомляет приложение о любых соединениях.
UdpClient UDP - это протокол, не организующий соединение, следовательно, для реализации UDP-обслуживания в.NET требуется другая функциональность.
SocketException Это исключение порождается, когда в сокете возникает ошибка.
Socket Последний класс в пространстве имен System.Net.Sockets - это сам класс Socket. Он обеспечивает базовую функциональность приложения сокета.

Класс Socket

Класс Socket играет важную роль в сетевом программировании, обеспечивая функционирование как клиента, так и сервера. Главным образом, вызовы методов этого класса выполняют необходимые проверки, связанные с безопасностью, в том числе проверяют разрешения системы безопасности, после чего они переправляются к аналогам этих методов в Windows Sockets API.

Прежде чем обращаться к примеру использования класса Socket, рассмотрим некоторые важные свойства и методы этого класса:

Свойства и методы класса Socket
Свойство или метод Описание
AddressFamily Дает семейство адресов сокета - значение из перечисления Socket.AddressFamily.
Available Возвращает объем доступных для чтения данных.
Blocking Дает или устанавливает значение, показывающее, находится ли сокет в блокирующем режиме.
Connected Возвращает значение, информирующее, соединен ли сокет с удаленным хостом.
LocalEndPoint Дает локальную конечную точку.
ProtocolType Дает тип протокола сокета.
RemoteEndPoint Дает удаленную конечную точку сокета.
SocketType Дает тип сокета.
Accept() Создает новый сокет для обработки входящего запроса на соединение.
Bind() Связывает сокет с локальной конечной точкой для ожидания входящих запросов на соединение.
Close() Заставляет сокет закрыться.
Connect() Устанавливает соединение с удаленным хостом.
GetSocketOption() Возвращает значение SocketOption.
IOControl() Устанавливает для сокета низкоуровневые режимы работы. Этот метод обеспечивает низкоуровневый доступ к лежащему в основе классу Socket.
Listen() Помещает сокет в режим прослушивания (ожидания). Этот метод предназначен только для серверных приложений.
Receive() Получает данные от соединенного сокета.
Poll() Определяет статус сокета.
Select() Проверяет статус одного или нескольких сокетов.
Send() Отправляет данные соединенному сокету.
SetSocketOption() Устанавливает опцию сокета.
Shutdown() Запрещает операции отправки и получения данных на сокете.