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

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

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

На сегодняшний день использование клиентов служб мгновенного обмена сообщениями (instant messanger) стало незаменимым средством для всех пользователей Интернета. Существует множество различных протоколов и клиентов (MSN, ICQ, Skype и т. д.), о которых каждый слышал и которые мы все ежедневно используем. Но не все знают, что положено в основу их роботы – собственно для этого статья и написана. Предположим, вы установили один из клиентов служб мгновенного обмена сообщениями на ваш компьютер. После его запуска и введения имени и пароля вашего пользователя, программа пытается подключиться к серверу. Что именно означает слово "подключиться"?

Каждый компьютер в сети имеет IP-адрес. Этот адрес похож на ваш домашний адрес – он однозначно идентифицирует ваш компьютер и позволяет другим общаться с вашим компьютером. Не будем вдаваться в подробности IP-адреса, так как эта статья не о них, хочу только отметить что IP-адрес это набор номеров разделенных точками (например, 64.104.137.158). Хотя существует и другой способ идентификации компьютеров в сети – доменное имя, которое более удобное и нагляднее идентифицирует компьютер, чем простой набор чисел (например, www.сайт). В Интернете существуют специальные компьютеры, которые осуществляют преобразование доменного имени в IP-адрес и наоборот.

На одном компьютере может параллельно исполняется несколько программ. Предположим, что вы запустили 10 программ на своем компьютере, и все они ожидают, чтоб другие компьютеры связались с ними. Можете представить себе это так: вас 10 человек в большом офисе с 1 телефоном и все ждут звонков от их собственных клиентов. Как вы это решите? Можно конечно назначить ответственного работника, и он будет приносить телефон соответственному человеку, которому звонят, но тогда другие клиенты не смогут дозвониться к другим людям. Кроме того, это очень трудно и нелепо иметь ответственного работника за маршрутизацию звонков к нужным людям. Вы должно быть, уже догадались, к чему я веду – если все эти программы, исполняющиеся на одном компьютере, с гордостью просят своих клиентов связаться с ними по определенному IP-адресу, то их клиенты не будут довольны. Идея состоит в следующем … иметь отдельный IP-адрес для каждой программы, верно? НЕ ВЕРНО! Суть вопроса не правильная – это также как спрашивать об отдельном офисе для каждого из вас. Ну, тогда … может отдельных телефонных номеров будет достаточно? ДА! На сетевом жаргоне "отдельные телефонные номера" имеют название порты. Порт – это просто число и каждая из программ, которая исполняется на определенном компьютере, может выбрать уникальное число порта, чтоб определить себя для внешнего мира. ЗАПОМНИТЕ – эти порты вы не сможете найти среди аппаратных средств компьютера (даже не старайтесь их искать). Эти числа – логические. Теперь все прояснилось: существует IP-адрес, с помощью которого другие компьютеры могут распознавать определенный компьютер в сети, и порт-число, которое определяет некую программу, работающую на компьютере. Также становиться понятным и то, что две программы на разных компьютерах могут использовать один и тот же порт (два дома на разных улицах тоже могут иметь один и тот самый номер, или нет?). Ну что же, мы практически у цели, только чтоб немного вас попугать, давайте выведем формулу:

IP-адрес = уникально определяет компьютер в сети.
Порт-число = уникально определяет программу, которая исполняется на соответствующем компьютере.

Если сложить вместе выше описанные уравнения, то получим:

IP-адрес + порт-число = _____

Другими словами:

Уникально определяет программу в сети. Если вы догадались до этого сами – значит мои усилия не пропали зря. Если нет, тогда прочитайте еще раз все сначала или воспользуйтесь Google для поиска лучшей статьи. _____ – это и есть … СОКЕТ!

Подведем итог, сокет – это комбинация IP-адреса и порта. Сокет адрес надает возможность другим компьютерам в сети находить определенную программу, которая исполняется на определенном компьютере. Вы можете отображать сокет адрес вот так 64.104.137.58:80, где 64.104.137.58 – IP-адрес и 80 – порт.

Как программировать с использованием сокетов?

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

1) Одна Джава программа будет пытаться связаться с другой Java программой (которая отчаянно ждет кого-то, чтоб с ней связался). Назовем первую программу Клиентом, а вторую Сервером.

2) После успешного связывания с сервером, клиент ждет ввода данных от вас и отсылает текст серверу.

3) Серверная программа отсылает клиенту назад тот;t текст (для того чтоб показать, что она умеет делать даже такое полезное действие).

4) Полученный от сервера текст, клиент показывает вам, чтоб показать вам мнение сервера о вас. Приготовились приступить к разработке? Начнем. Отмечу только, что я не буду учить вас программированию на Java с чистого листа, а только объясню код, который относится к сокетам. Создайте 2 новых Джава программы и назовите их Server.java и Client.java. Я привел код ниже, только не пугайтесь, я все объясню.


import java.net.*;
import java.io.*;
public class Server {
int port = 6666; // случайный порт (может быть любое число от 1025 до 65535)
try {
ServerSocket ss = new ServerSocket(port); // создаем сокет сервера и привязываем его к вышеуказанному порту
System.out.println("Waiting for a client...");

Socket socket = ss.accept(); // заставляем сервер ждать подключений и выводим сообщение когда кто-то связался с сервером
System.out.println("Got a client:) ... Finally, someone saw me through all the cover!");
System.out.println();

// Берем входной и выходной потоки сокета, теперь можем получать и отсылать данные клиенту.


String line = null;
while(true) {
line = in.readUTF(); // ожидаем пока клиент пришлет строку текста.
System.out.println("The dumb client just sent me this line: " + line);
System.out.println("I"m sending it back...");
out.writeUTF(line); // отсылаем клиенту обратно ту самую строку текста.
System.out.println("Waiting for the next line...");
System.out.println();
}
} catch(Exception x) { x.printStackTrace(); }
}
}

Import java.net.*;
import java.io.*;

public class Client {
public static void main(String ar) {
int serverPort = 6666; // здесь обязательно нужно указать порт к которому привязывается сервер.
String address = "127.0.0.1"; // это IP-адрес компьютера, где исполняется наша серверная программа.
// Здесь указан адрес того самого компьютера где будет исполняться и клиент.

Try {
InetAddress ipAddress = InetAddress.getByName(address); // создаем объект который отображает вышеописанный IP-адрес.
System.out.println("Any of you heard of a socket with IP address " + address + " and port " + serverPort + "?");
Socket socket = new Socket(ipAddress, serverPort); // создаем сокет используя IP-адрес и порт сервера.
System.out.println("Yes! I just got hold of the program.");

// Берем входной и выходной потоки сокета, теперь можем получать и отсылать данные клиентом.
InputStream sin = socket.getInputStream();
OutputStream sout = socket.getOutputStream();

// Конвертируем потоки в другой тип, чтоб легче обрабатывать текстовые сообщения.
DataInputStream in = new DataInputStream(sin);
DataOutputStream out = new DataOutputStream(sout);

// Создаем поток для чтения с клавиатуры.
BufferedReader keyboard = new BufferedReader(new InputStreamReader(System.in));
String line = null;
System.out.println("Type in something and press enter. Will send it to the server and tell ya what it thinks.");
System.out.println();

While (true) {
line = keyboard.readLine(); // ждем пока пользователь введет что-то и нажмет кнопку Enter.
System.out.println("Sending this line to the server...");
out.writeUTF(line); // отсылаем введенную строку текста серверу.
out.flush(); // заставляем поток закончить передачу данных.
line = in.readUTF(); // ждем пока сервер отошлет строку текста.
System.out.println("The server was very polite. It sent me this: " + line);
System.out.println("Looks like the server is pleased with us. Go ahead and enter more lines.");
System.out.println();
}
} catch (Exception x) {
x.printStackTrace();
}
}
}

Теперь скомпилируем код:

Javac Server.java Client.java

Откроем два командных окна (DOS). В одном окне введем:

Java Server

а в другом:

Java Client

Обязательно в таком порядке.

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

Объяснение кода работы с сокетами

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

Рассмотрим следующую часть кода сервера:

ServerSocket ss = new ServerSocket(port);
Socket socket = ss.accept();

Класс ServerSocket немного отличается от класса Socket. Класс Socket – это и есть сокет. Главное отличие ServerSocket заключается в том, что он умеет заставлять программу ждать подключений от клиентов. Когда вы его создаете, нужно указывать порт, с которым он будет работать, и вызвать его метод accept(). Этот метод заставляет программу ждать подключений по указанному порту. Исполнение программы зависает в этом месте, пока клиент не подключится. После успешного подключения клиентом, создается нормальный Socket объект, который вы можете использовать для выполнения все существующий операций с сокетом. Заметим также, что этот Socket объект отображает другой конец соединения. Если вы хотите отослать данные клиенту, то вы не можете использовать для этого ваш собственный сокет.

Следующим рассмотрим Socket класс. Вы можете создать Socket объект, указав IP-адрес и порт. Вы можете использовать InetAddress класс для отображения IP-адреса (этот способ более предпочтительный). Для создания InetAddress объекта используйте следующий метод:

InetAddress ipAddress = InetAddress.getByName(address);

Заметим, что в нашей программе мы использовали адрес 127.0.0.1. Это специальный адрес называется адрес замыкания – он просто отображает локальный компьютер. Если вы намерены запустить клиент и сервер на разных компьютерах, тогда нужно использовать соответственный IP-адрес сервера.

После того как мы создали InetAddress, то можно создать Socket:

Socket socket = new Socket(ipAddress, serverPort);

После создания Socket объекта, можно взять входной и выходной потоки сокета. Входной поток позволит вам читать с сокета, а выходной поток дает возможность писать в сокет.

InputStream sin = socket.getInputStream();
OutputStream sout = socket.getOutputStream();

Следующие строки просто конвертируют потоки в другие типы потоков. После этого нам легче будет работать с String объектами. Этот код ничего не делает с сетью.

DataInputStream in = new DataInputStream(sin);
DataOutputStream out = new DataOutputStream(sout);

Все остальное очень просто – простые манипуляции с объектами потоков (никаких сокетов). Вы можете использовать ваши любимые потоки, вызывать ваши любимые методы, и удостоверяться в передаче данных во второй конец. Если вы не очень осведомлены в использовании потоков, рекомендую вам найти статью о них и прочесть.

Последнее обновление: 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.

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

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

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

Однако как именованные каналы, так и почтовые ящики (в отношении которых для простоты мы будем использовать далее общий термин - "именованные каналы", если различия между ними не будут играть существенной роли) обладают тем недостатком, что они не являются промышленным стандартом. Это обстоятельство усложняет перенос программ наподобие тех, которые рассматривались в главе 11, в системы, не принадлежащие семейству Windows, хотя именованные каналы не зависят от протоколов и могут выполняться поверх многих стандартных промышленных протоколов, например TCP/IP.

Возможность взаимодействия с другими системами обеспечивается в Windows поддержкой сокетов (sockets) Windows Sockets - совместимого и почти точного аналога сокетов Berkeley Sockets, де-факто играющих роль промышленного стандарта. В этой главе использование API Windows Sockets (или "Winsock") показано на примере модифицированной клиент-серверной системы из главы 11. Результирующая система способна функционировать в глобальных сетях, использующих протокол TCP/IP, что, например, позволяет серверу принимать запросы от клиентов UNIX или каких-либо других, отличных от Windows систем.

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

Привлекая средства обеспечения взаимодействия между разнородными системами, ориентированные на стандарты, интерфейс Winsock открывает перед программистами возможность доступа к высокоуровневым протоколам и приложениям, таким как ftp, http, RPC и СОМ, которые в совокупности предоставляют богатый набор высокоуровневых моделей, обеспечивающих поддержку межпроцессного сетевого взаимодействия для систем с различной архитектурой.

В данной главе указанная клиент-серверная система используется в качестве механизма демонстрации интерфейса Winsock, и в процессе того, как сервер будет модифицироваться, в него будут добавляться новые интересные возможности. В частности, нами будут впервые использованы точки входа DLL (глава 5) и внутрипроцессные серверы DLL. (Эти новые средства можно было включить уже в первоначальную версию программы в главе 11, однако это отвлекло бы ваше внимание от разработки основной архитектуры системы.) Наконец, дополнительные примеры покажут вам, как создаются безопасные реентерабельные многопоточные библиотеки.

Поскольку интерфейс Winsock должен соответствовать промышленным стандартам, принятые в нем соглашения о правилах присвоения имен и стилях программирования несколько отличаются от тех, с которыми мы сталкивались в процессе работы с описанными ранее функциями Windows. Строго говоря, Winsock API не является частью Win32/64. Кроме того, Winsock предоставляет дополнительные функции, не подчиняющиеся стандартам; эти функции используются лишь в случае крайней необходимости. Среди других преимуществ, обеспечиваемых Winsock, следует отметить улучшенную переносимость результирующих программ на другие системы.

Сокеты Windows

Winsock API разрабатывался как расширение Berkley Sockets API для среды Windows и поэтому поддерживается всеми системами Windows. К преимуществам Winsock можно отнести следующее:

Перенос уже имеющегося кода, написанного для Berkeley Sockets API, осуществляется непосредственно.

Системы Windows легко встраиваются в сети, использующие как версию IPv4 протокола TCP/IP, так и постепенно распространяющуюся версию IPv6. Помимо всего остального, версия IPv6 допускает использование более длинных IP-адресов, преодолевая существующий 4-байтовый адресный барьер версии IPv4.

Сокеты могут использоваться совместно с перекрывающимся вводом/выводом Windows (глава 14), что, помимо всего прочего, обеспечивает возможность масштабирования серверов при увеличении количества активных клиентов.

Сокеты можно рассматривать как дескрипторы (типа HANDLE) файлов при использовании функций ReadFile и WriteFile и, с некоторыми ограничениями, при использовании других функций, точно так же, как в качестве дескрипторов файлов сокеты применяются в UNIX. Эта возможность оказывается удобной в тех случаях, когда требуется использование асинхронного ввода/вывода и портов завершения ввода/вывода.

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

Инициализация Winsock

Winsock API поддерживается библиотекой DLL (WS2_32.DLL), для получения доступа к которой следует подключить к программе библиотеку WS_232.LIB. Эту DLL следует инициализировать с помощью нестандартной, специфической для Winsock функции WSAStartup, которая должна быть первой из функций Winsock, вызываемых программой. Когда необходимость в использовании функциональных возможностей Winsock отпадает, следует вызывать функцию WSACleanup. Примечание. Префикс WSA означает "Windows Sockets asynchronous …" ("Асинхронный Windows Sockets …"). Средства асинхронного режима Winsock нами здесь не используются, поскольку при возникновении необходимости в выполнении асинхронных операций мы можем и будем использовать потоки.

Хотя функции WSAStartup и WSACleanup необходимо вызывать в обязательном порядке, вполне возможно, что они будут единственными нестандартными функциями, с которыми вам придется иметь дело. Распространенной практикой является применение директив препроцессора #ifdef для проверки значения символической константы _WIN32 (обычно определяется Visual C++ на стадии компиляции), в результате чего функции WSA будут вызываться только тогда, когда вы работаете в Windows). Разумеется, такой подход предполагает, что остальная часть кода не зависит от платформы.

int WSAStartup(WORD wVersionRequired, LPWSADATA ipWSAData);
Параметры

wVersionRequired - указывает старший номер версии библиотеки DLL, который вам требуется и который вы можете использовать. Как правило, версии 1.1 вполне достаточно для того, чтобы обеспечить любое взаимодействие с другими системами, в котором у вас может возникнуть необходимость. Тем не менее, во всех системах Windows, включая Windows 9x, доступна версия Winsock 2.0, которая и используется в приведенных ниже примерах. Версия 1.1 считается устаревшей и постепенно выходит из употребления.

Функция возвращает ненулевое значение, если запрошенная вами версия данной DLL не поддерживается.

Младший байт параметра wVersionRequired указывает основной номер версии, а старший байт - дополнительный. Обычно используют макрос MAKEWORD; таким образом, выражение MAKEWORD (2,0) представляет версию 2.0.

ipWSAData - указатель на структуру WSADATA, которая возвращает информацию о конфигурации DLL, включая старший доступный номер версии. О том, как интерпретировать ее содержимое, вы можете прочитать в материалах оперативной справки Visual Studio.

Чтобы получить более подробную информацию об ошибках, можно воспользоваться функцией WSAGetLastError, но для этой цели подходит также функция GetLastError, а также функция ReportError, разработанная в главе 2.

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

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

Инициализировав Winsock DLL, вы можете использовать стандартные (Berkeley Sockets) функции для создания сокетов и соединений, обеспечивающих взаимодействие серверов с клиентами или взаимодействие равноправных узлов сети между собой.

Используемый в Winsock тип данных SOCKET аналогичен типу данных HANDLE в Windows, и его даже можно применять совместно с функцией ReadFile и другими функциями Windows, требующими использования дескрипторов типа HANDLE. Для создания (или открытия) сокета служит функция socket.

SOCKET socket(int af, int type, int protocol);
Параметры

Тип данных SOCKET фактически определяется как тип данных int, потому код UNIX остается переносимым, не требуя привлечения типов данных Windows.

af - обозначает семейство адресов, или протокол; для указания протокола IP (компонент протокола TCP/IP, отвечающий за протокол Internet) следует использовать значение PF_INET (или AF_INET, которое имеет то же самое числовое значение, но обычно используется при вызове функции bind).

type - указывает тип взаимодействия: ориентированное на установку соединения (connection-oriented communication), или потоковое (SOCK_STREAM), и дейтаграммное (datagram communication) (SOCK_DGRAM), что в определенной степени сопоставимо соответственно с именованными каналами и почтовыми ящиками.

protocol - является излишним, если параметр af установлен равным AF_INET; используйте значение 0.

В случае неудачного завершения функция socket возвращает значение INVALID_SOCKET.

Winsock можно использовать совместно с протоколами, отличными от TCP/IP, указывая различные значения параметра protocol; мы же будем использовать только протокол TCP/IP.

Как и в случае всех остальных стандартных функций, имя функции socket не должно содержать прописных букв. Это является отходом от соглашений, принятых в Windows, и продиктовано необходимостью соблюдения промышленных стандартов.

Серверные функции сокета

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

Если не оговорено иное, типом сокетов в наших примерах всегда будет SOCK_STREAM. Сокеты типа SOCK_DGRAM рассматривается далее в этой главе.

Связывание сокета

Следующий шаг заключается в привязке сокета к его адресу и конечной точке (endpoint) (направление канала связи от приложения к службе). Вызов socket, за которым следует вызов bind, аналогичен созданию именованного канала. Однако не существует имен, используя которые можно было бы различать сокеты данного компьютера. Вместо этого в качестве конечной точки службы используется номер порта (port number). Любой заданный сервер может иметь несколько конечных точек. Прототип функции bind приводится ниже.

int bind(SOCKET s, const struct sockaddr *saddr, int namelen);
Параметры

s - несвязанный сокет, возвращенный функцией socket.

saddr - заполняется перед вызовом и задает протокол и специфическую для протокола информацию, как описано ниже. Кроме всего прочего, в этой структуре содержится номер порта.

namelen - присвойте значение sizeof (sockaddr).

В случае успешного выполнения функция возвращает значение 0, иначе SOCKET_ERROR. Структура sockaddr определяется следующим образом:

typedef struct sockaddr SOCKADDR, *PSOCKADDR;

Первый член этой структуры, sa_family, обозначает протокол. Второй член, sa_data, зависит от протокола. Internet-версией структуры sa_data является структура sockaddr_in:

short sin_family; /* AF_INET */
struct in_addr sin_addr; /* 4-байтовый IP-адрес */
typedef struct sockaddr_in SOCKADDR_IN, *PSOCKADDR IN;

Обратите внимание на использование типа данных short integer для номера порта. Кроме того, номер порта и иная информация должны храниться с соблюдением подходящего порядка следования байтов, при котором старший байт помещается в крайней позиции справа (big-endian), чтобы обеспечивалась двоичная совместимость с другими системами. В структуре sin_addr содержится подструктура s_addr, заполняемая уже знакомым нам 4-байтовым IP-адресом, например 127.0.0.1, указывающим систему, чей запрос на образование соединения должен быть принят. Обычно удовлетворяются запросы любых систем, в связи с чем следует использовать значение INADDR_ANY, хотя этот символический параметр должен быть преобразован к корректному формату, как показано в приведенном ниже фрагменте кода.

Для преобразования текстовой строки с IP-адресом к требуемому формату можно использовать функцию inet_addr, поэтому член sin_addr.s_addr переменной sockaddr_in инициализируется следующим образом:

sa.sin_addr.s_addr = inet_addr("192 .13.12.1");

О связанном сокете, для которого определены протокол, номер порта и IP-адрес, иногда говорят как об именованном сокете (named socket).

Перевод связанного сокета в состояние прослушивания

Функция listen делает сервер доступным для образования соединения с клиентом. Аналогичной функции для именованных каналов не существует.

int listen(SOCKET s, int nQueueSize);

Параметр nQueueSize указывает число запросов на соединение, которые вы намерены помещать в очередь сокета. В версии Winsock 2.0 значение этого параметра не имеет ограничения сверху, но в версии 1.1 оно ограничено предельным значением SOMAXCON (равным 5).

Прием клиентских запросов соединения

Наконец, сервер может ожидать соединения с клиентом, используя функцию accept, возвращающую новый подключенный сокет, который будет использоваться в операциях ввода/вывода. Заметьте, что исходный сокет, который теперь находится в состоянии прослушивания (listening state), используется исключительно в качестве параметра функции accept, а не для непосредственного участия в операциях ввода/вывода.

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

SOCKET accept(SOCKET s, LPSOCKADDR lpAddr, LPINT lpAddrLen);
Параметры

s - прослушивающий сокет. Чтобы перевести сокет в состояние прослушивания, необходимо предварительно вызвать функции socket, bind и listen.

lpAddr - указатель на структуру sockaddr_in, предоставляющую адрес клиентской системы.

lpAddrLen - указатель на переменную, которая будет содержать размер возвращенной структуры sockaddr_in. Перед вызовом функции accept эта переменная должна быть инициализирована значением sizeof(struct sockaddr_in).

Отключение и закрытие сокетов

Для отключения сокетов применяется функция shutdown(s, how). Аргумент how может принимать одно из двух значений: 1, указывающее на то, что соединение может быть разорвано только для посылки сообщений, и 2, соответствующее разрыву соединения как для посылки, так и для приема сообщений. Функция shutdown не освобождает ресурсы, связанные с сокетом, но гарантирует завершение посылки и приема всех данных до закрытия сокета. Тем не менее, после вызова функции shutdown приложение уже не должно использовать этот сокет.

Когда работа с сокетом закончена, его следует закрыть, вызвав функцию closesocket(SOCKET s). Сначала сервер закрывает сокет, созданный функцией accept, а не прослушивающий сокет, созданный с помощью функции socket. Сервер должен закрывать прослушивающий сокет только тогда, когда завершает работу или прекращает принимать клиентские запросы соединения. Даже если вы работаете с сокетом как с дескриптором типа HANDLE и используете функции ReadFile и WriteFile, уничтожить сокет одним только вызовом функции CloseHandle вам не удастся; для этого следует использовать функцию closesocket.

Пример: подготовка и получение клиентских запросов соединения

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

В этом примере используются две стандартные функции: htons ("host to network short" - "ближняя связь") и htonl ("host to network long" - "дальняя связь"), которые преобразуют целые числа к форме с обратным порядком байтов, требуемой протоколом IP.

Номером порта сервера может быть любое число из диапазона, допустимого для целых чисел типа short integer, но для определенных пользователем служб обычно используются числа в диапазоне 1025-5000. Порты с меньшими номерами зарезервированы для таких известных служб, как telnet или ftp, в то время как порты с большими номерами предполагаются для использования других стандартных служб.

struct sockaddr_in SrvSAddr; /* Адресная структура сервера. */
struct sockaddr_in ConnectAddr;
AddrLen = sizeof(ConnectAddr);
sockio = accept(SrvSock, (struct sockaddr *) &ConnectAddr, &AddrLen);
… Получение запросов и отправка ответов …

Клиентские функции сокета

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

Установление клиентского соединения с сервером

Если имеется сервер с сокетом в режиме прослушивания, клиент может соединиться с ним при помощи функции connect.

int connect(SOCKET s, LPSOCKADDR lpName, int nNameLen);
Параметры

s - сокет, созданный с использованием функции socket.

lpName - указатель на структуру sockaddr_in, инициализированную значениями номера порта и IP-адреса системы с сокетом, связанным с указанным портом, который находится в состоянии прослушивания.

Инициализируйте nNameLen значением sizeof (struct sockaddr_in).

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

Сокет s не обязательно должен быть связанным с портом до вызова функции connect, хотя это и может иметь место. При необходимости система распределяет порт и определяет протокол.

Пример: подключение клиента к серверу

Показанный ниже фрагмент кода обеспечивает соединение клиента с сервером. Для этого нужны только два вызова функций, но адресная структура должна быть инициализирована до вызова функции connect. Проверка возможных ошибок здесь отсутствует, но в реальные программы она должна включаться. В примере предполагается, что IP-адрес (текстовая строка наподобие "192.76.33.4") задается в аргументе argv командной строки.

ClientSAddr.sin_addr.s_addr = inet_addr(argv);
ConVal = connect(ClientSock, (struct sockaddr *)&ClientSAddr, sizeof(ClientSAddr));

Отправка и получение данных

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

int send(SOCKET s, const char * lpBuffer, int nBufferLen, int nFlags);

Возвращаемым значением является число фактически переданных байтов. Значение SOCKET_ERROR указывает на ошибку.

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

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

С сокетами могут использоваться также функции ReadFile и WriteFile, только в этом случае при вызове функции необходимо привести сокет к типу HANDLE.

Сравнение именованных каналов и сокетов

Именованные каналы, описанные в главе 11, очень похожи на сокеты, но в способах их использования имеются значительные различия.

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

Именованные каналы требуют использования функций ReadFile и WriteFile, в то время как сокеты могут обращаться также к функциям send и recv.

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

Сокеты основаны на промышленном стандарте, что обеспечивает их совместимость с системами, отличными от Windows.

Имеются также различия в моделях программирования сервера и клиента.

Сравнение серверов именованных каналов и сокетов

Установка соединения с несколькими клиентами при использовании сокетов требует выполнения повторных вызовов функции accept. Каждый из вызовов возвращает очередной подключенный сокет. По сравнению с именованными каналами имеются следующие отличия:

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

Допустимое количество клиентских сокетов ничем не ограничено (функция listen ограничивает лишь количество клиентов, помещаемых в очередь), в то время как количество экземпляров именованных каналов, в зависимости от того, что было указано при первом вызове функции CreateNamedPipe, может быть ограниченным.

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

Именованные каналы не имеют портов с явно заданными номерами и различаются по именам.

В случае сервера именованных каналов получение пригодного для работы дескриптора типа HANDLE требует вызова двух функций (CreateNamedPipe и ConnectNamedPipe), тогда как сервер сокета требует вызова четырех функций (socket, bind, listen и accept).

Сравнение клиентов именованных каналов и сокетов

В случае именованных каналов необходимо последовательно вызывать функции WaitNamedPipe и CreateFile. Если же используются сокеты, этот порядок вызовов обращается, поскольку можно считать, что функция socket создает сокет, а функция connect - блокирует.

Дополнительное отличие состоит в том, что функция connect является функцией клиента сокета, в то время как функция ConnectNamedPipe используется сервером именованного канала.

Пример: функция приема сообщений в случае сокета

Часто оказывается удобным отправлять и получать сообщения в виде единых блоков. Как было показано в главе 11, каналы позволяют это сделать. Однако в случае сокетов требуется создание заголовка, содержащего размер сообщения, за которым следует само сообщение. Для приема таких сообщений предназначена функция ReceiveMessage, которая будет использоваться в примерах. То же самое можно сказать и о функции SendMessage, предназначенной для передачи сообщений.

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

Примечание, относящееся к Win64

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

DWORD ReceiveMessage (MESSAGE *pMsg, SOCKET sd) {
/* Сообщение состоит из 4-байтового поля размера сообщения, за которым следует собственно содержимое. */
/* Считать сообщение. */
/* Сначала считывается заголовок, а затем содержимое. */
nRemainRecv = 4; /* Размер поля заголовка. */
pBuffer = (LPBYTE)pMsg; /* recv может не передать все запрошенные байты. */
while (nRemainRecv > 0 && !Disconnect) {
/* Считать содержимое сообщения. */
while (nRemainRecv > 0 && !Disconnect) {
nXfer = recv(sd, pBuffer, nRemainRecv, 0);

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

Программа 12.1 представляет собой переработанный вариант клиентской программы clientNP (программа 11.2), которая использовалась в случае именованных каналов. Преобразование программы осуществляется самым непосредственным образом и требует лишь некоторых пояснений.

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

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

Номер порта, SERVER_PORT, определен в заголовочном файле ClntSrvr.h.

Хотя код написан для выполнения под управлением Windows, единственная зависимость от Windows связана с использованием вызовов функций, имеющих префикс WSA.

Программа 12.1. clientSK: клиент на основе сокетов
/* Глава 12. clientSK.с */
/* Однопоточный клиент командной строки. */
/* ВЕРСИЯ НА ОСНОВЕ WINDOWS SOCKETS. */
/* Считывает последовательность команд для пересылки серверному процессу*/
/* через соединение с сокетом. Дожидается ответа и отображает его. */

#define _NOEXCLUSIONS /* Требуется для включения определений сокета. */
#include "ClntSrvr.h" /* Определяет структуры записей запроса и ответа. */

/* Функции сообщения для обслуживания запросов и ответов. */
/* Кроме того, ReceiveResponseMessage отображает полученные сообщения. */
static DWORD SendRequestMessage(REQUEST *, SOCKET);
static DWORD ReceiveResponseMessage(RESPONSE *, SOCKET);
struct sockaddr_in ClientSAddr; /* Адрес сокета клиента. */
int _tmain(DWORD argc, LPTSTR argv) {
SOCKET ClientSock = INVALID_SOCKET;
REQUEST Request; /* См. ClntSrvr.h. */
RESPONSE Response; /* См. ClntSrvr.h. */
TCHAR PromptMsg = _T("\nВведите команду> ");
TCHAR QuitMsg = _T("$Quit");
/* Запрос: завершить работу клиента. */
TCHAR ShutMsg = _T("$ShutDownServer"); /* Остановить все потоки. */
CHAR DefaultIPAddr = "127.0.0.1"; /* Локальная система. */
/* Подключиться к серверу. */
/* Следовать стандартной процедуре вызова последовательности функций socket/connect клиентом. */
ClientSock = socket(AF_INET, SOCK_STREAM, 0);
memset(&ClientSAddr, 0, sizeof(ClientSAddr));
ClientSAddr.sin_family = AF_INET;
if (argc >= 2) ClientSAddr.sin_addr.s_addr = inet_addr(argv );
else ClientSAddr.sin_addr.s_addr = inet_addr(DefaultIPAddr);
ClientSAddr.sin_port = htons(SERVER_PORT);
/* Номер порта определен равным 1070. */
connect(ClientSock, (struct sockaddr *)&ClientSAddr, sizeof(ClientSAddr));
/* Основной цикл для вывода приглашения на ввод команд, посылки запроса и получения ответа. */
_tprintf(_T("%s"), PromptMsg);
/* Ввод в формате обобщенных строк, но команда серверу должна указываться в формате ASCII. */
_fgetts(Req, MAX_RQRS_LEN-1, stdin);
for (j = 0; j <= _tcslen(Req) Request.Record[j] = Req[j];
/* Избавиться от символа новой строки в конце строки. */
Request.Record = "\0";
if (strcmp(Request.Record, QuitMsg) == 0 || strcmp(Request.Record, ShutMsg) == 0) Quit = TRUE;
SendRequestMessage(&Request, ClientSock);
ReceiveResponseMessage(&Response, ClientSock);
shutdown(ClientSock, 2); /* Запретить посылку и прием сообщений. */
_tprintf(_T("\n****Выход из клиентской программы\n"));

Пример: усовершенствованный сервер на основе сокетов

Программа serverSK (программа 12.2) аналогична программе serverNP (программа 11.3), являясь ее видоизмененным и усовершенствованным вариантом.

В усовершенствованном варианте программы серверные потоки создаются по требованию (on demand), а не в виде пула потоков фиксированного размера. Каждый раз, когда сервер принимает запрос клиента на соединение, создается серверный рабочий поток, и когда клиент прекращает работу, выполнение потока завершается.

Сервер создает отдельный поток приема (accept thread), что позволяет основному потоку опрашивать глобальный флаг завершения работы, пока вызов accept остается блокированным. Хотя сокеты и могут определяться как неблокирующиеся, потоки обеспечивают удобное универсальное решение. Следует отметить, что значительная часть расширенных функциональных возможностей Winsock призвана поддерживать асинхронные операции, тогда как потоки Windows дают возможность воспользоваться более простой и близкой к стандартам функциональностью синхронного режима работы сокетов.

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

Данный сервер поддерживает также внутрипроцессные серверы (in-process servers), что достигается путем загрузки библиотеки DLL во время инициализации. Имя библиотеки DLL задается в командной строке, и серверный поток сначала пытается определить точку входа этой DLL. В случае успеха серверный поток вызывает точку входа DLL; в противном случае сервер создает процесс аналогично тому, как это делалось в программе serverNP. Пример DLL приведен в программе 12.3. Поскольку генерация исключений библиотекой DLL будет приводить к уничтожению всего серверного процесса, вызов функции DLL защищен простым обработчиком исключений.

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

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

Программа 12.2. serverSK: сервер на основе сокета с внутрипроцессными серверами
/* Глава 12. Клиент-серверная система. ПРОГРАММА СЕРВЕРА. ВЕРСИЯ НА ОСНОВЕ СОКЕТА. */
/* Выполняет указанную в запросе команду и возвращает ответ. */
/* Если удается обнаружить точку входа разделяемой библиотеки, команды */
/* выполняются внутри процесса, в противном случае – вне процесса. */
/* ДОПОЛНИТЕЛЬНАЯ ВОЗМОЖНОСТЬ: argv может содержать имя библиотеки */
/* DLL, поддерживающей внутрипроцессные серверы. */

#include "ClntSrvr.h" /* Определяет структуру записей запроса и ответа. */

/* Адресная структура сокета сервера. */
struct sockaddr_in ConnectSAddr; /* Подключенный сокет. */
WSADATA WSStartData; /* Структура данных библиотеки сокета. */

typedef struct SERVER_ARG_TAG { /* Аргументы серверного потока. */
/* Пояснения содержатся в комментариях к основному потоку. */
HINSTANCE dlhandle; /* Дескриптор разделяемой библиотеки. */

volatile static ShutFlag = FALSE;
static SOCKET SrvSock, ConnectSock;
int _tmain(DWORD argc, LPCTSTR argv) {
/* Прослушивающий и подключенный сокеты сервера. */
SERVER_ARG srv_arg;
/* Инициализировать библиотеку WSA; задана версия 2.0, но будет работать и версия 1.1. */
WSAStartup(MAKEWORD(2, 0), &WSStartData);
/* Открыть динамическую библиотеку команд, если ее имя указано в командной строке. */
if (argc > 1) hDll = LoadLibrary(argv);
/* Инициализировать массив arg потока. */
for (ith = 0; ith < MAXCLIENTS; ith++) {
srv_arg.dlhandle = hDll;
/* Следовать стандартной процедуре вызова последовательности функций socket/bind/listen/accept клиентом. */
SrvSock = socket(AF_INET, SOCK_STREAM, 0);
SrvSAddr.sin_family = AF_INET;
SrvSAddr.sin_addr.s_addr = htonl(INADDR_ANY);
SrvSAddr.sin_port = htons(SERVER_PORT);
bind(SrvSock, (struct sockaddr *)&SrvSAddr, sizeof SrvSAddr);
listen(SrvSock, MAX_CLIENTS);

/* Основной поток становится потоком прослушивания/соединения/контроля.*/
/* Найти пустую ячейку в массиве arg потока сервера. */
/* параметр состояния: 0 – ячейка свободна; 1 – поток остановлен; 2 - поток выполняется; 3 – остановлена вся система. */
for (ith = 0; ith < MAX_CLIENTS && !ShutFlag;) {
if (srv_arg.status==1 || srv_arg.status==3) { /* Выполнение потока завершено либо обычным способом, либо по запросу останова. */
WaitForSingleObject(srv_arg.srv_thd INFINITE);
CloseHandle(srv_arg.srv_tnd);
if (srv_arg.status == 3) ShutFlag = TRUE;
else srv_arg.status = 0;
/* Освободить ячейку данного потока. */
if (srv_arg.status == 0 || ShutFlag) break;
ith = (ith + 1) % MAXCLIENTS;
/* Прервать цикл опроса. */
/* Альтернативный вариант: использовать событие для генерации сигнала, указывающего на освобождение ячейки. */
/* Ожидать попытки соединения через данный сокет. */
/* Отдельный поток для опроса флага завершения ShutFlag. */
hAcceptTh = (HANDLE)_beginthreadex(NULL, 0, AcceptTh, &srv_arg, 0, &ThId);
tstatus = WaitForSingleObject(hAcceptTh, CS_TIMEOUT);
if (tstatus == WAIT_OBJECT_0) break; /* Соединение установлено. */
hAcceptTh = NULL; /* Подготовиться к следующему соединению. */
_tprintf(_T("Остановка сервера. Ожидание завершения всех потоков сервера\n"));
/* Завершить принимающий поток, если он все еще выполняется. */
/* Более подробная информация об используемой логике завершения */
/* работы приведена на Web-сайте книги. */
if (hDll != NULL) FreeLibrary(hDll);
if (hAcceptTh != NULL) TerminateThread(hAcceptTh, 0);
/* Ожидать завершения всех активных потоков сервера. */
for (ith = 0; ith < MAXCLIENTS; ith++) if (srv_arg .status != 0) {
WaitForSingleObject(srv_arg.srv_thd, INFINITE);
CloseHandle(srv_arg.srv_thd);

static DWORD WINAPI AcceptTh(SERVER_ARG * pThArg) {
/* Принимающий поток, который предоставляет основному потоку возможность опроса флага завершения. Кроме того, этот поток создает серверный поток. */
AddrLen = sizeof(ConnectSAddr);
pThArg->sock = accept(SrvSock, /* Это блокирующий вызов. */
(struct sockaddr *)&ConnectSAddr, &AddrLen);
/* Новое соединение. Создать серверный поток. */
pThArg->srv_thd = (HANDLE)_beginthreadex (NULL, 0, Server, pThArg, 0, &ThId);
return 0; /* Серверный поток продолжает выполняться. */

static DWORD WINAPI Server(SERVER_ARG * pThArg)
/* Функция серверного потока. Поток создается по требованию. */
/* Каждый поток поддерживает в стеке собственные структуры данных запроса, ответа и регистрационных записей. */
/* … Стандартные объявления из serverNP опущены … */
int (*dl_addr)(char *, char *);
char *ws = " \0\t\n"; /* Пробелы. */
GetStartupInfo(&StartInfoCh);
/* Создать имя временного файла. */
sprintf(TempFile, "%s%d%s", "ServerTemp", pThArg->number, ".tmp");
while (!Done && !ShutFlag) { /* Основной командный цикл. */
Disconnect = ReceiveRequestMessage(&Request, ConnectSock);
Done = Disconnect || (strcmp(Request.Record, "$Quit") == 0) || (strcmp(Request.Record, "$ShutDownServer") == 0);
/* Остановить этот поток по получении команды "$Quit" или "$ShutDownServer". */
hTrapFile = CreateFile(TempFile, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, &TempSA, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
/* Проверка наличия этой команды в DLL. Для упрощения команды */
/* разделяемой библиотеки имеют более высокий приоритет по сравнению */
/* с командами процесса. Прежде всего, необходимо извлечь имя команды.*/
i = strcspn(Request.Record, ws); /* Размер лексемы. */
memcpy(sys_command, Request.Record, i) ;
dl_addr = NULL; /* Будет установлен в случае успешного выполнения функции GetProcAddress. */
if (pThArg->dlhandle != NULL) {/* Проверка поддержки "внутрипроцессного" сервера. */
dl_addr = (int (*)(char *, char *))GetProcAddress(pThArg->dlhandle, sys_command);
/* Защитить серверный процесс от исключений, возникающих в DLL*/
(*dl_addr)(Request.Record, TempFile);
} __except (EXCEPTION_EXECUTE_HANDLER) {
ReportError(_T("Исключение в DLL"), 0, FALSE);
if (dl_addr == NULL) { /* Поддержка внутрипроцессного сервера отсутствует. */
/* Создать процесс для выполнения команды. */
/* … То же, что в serverNP … */
} /* Конец основного командного цикла. Получить следующую команду. */
/* Конец командного цикла. Освободить ресурсы; выйти из потока. */
_tprintf(_T("Завершение работы сервера# %d\n"), pThArg->number);
if (strcmp(Request.Record, "$ShutDownServer") == 0) {

Замечания по поводу безопасности

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

Полное обсуждение методов построения безопасных систем выходит за рамки данной книги. Тем не менее, в главе 15 показано, как обезопасить объекты Windows, а в упражнении 12.14 предлагается воспользоваться протоколом SSL.

Внутрипроцессные серверы

Как ранее уже отмечалось, основное усовершенствование программы serverSK связано с включением в нее внутрипроцессных серверов. В программе 12.3 показано, как написать библиотеку DLL, обеспечивающую услуги подобного рода. В программе представлены две уже известные вам функции - функция, осуществляющая подсчет слов, и функция toupper.

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

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

Аналогично, функции не должны перенаправлять стандартный ввод и вывод.

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

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

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

В программе 12.3 представлена небольшая библиотека DLL, включающая две функции.

Программа 12.3. command: пример внутри процессных серверов
/* Глава 12. commands.с. */
/* Команды внутрипроцессного сервера для использования в serverSK и так далее. */
/* Имеется несколько команд, реализованных в виде библиотек DLLs. */
/* Функция каждой команды принимает два параметра и обеспечивает */
/* безопасное выполнение в многопоточном режиме. Первым параметром */
/* является строка: команда arg1 arg2 … argn */
/* (то есть обычная командная строка), а вторым – имя выходного файла. … */

static void extract_token(int, char *, char *);

int wcip(char * command, char * output_file)
/* Счетчик слов; внутрипроцессный. */
/* ПРИМЕЧАНИЕ: упрощенная версия; результаты могут отличаться от тех, которые обеспечивает утилита wc. */
while ((c = fgetc(fin)) != EOF) {
/* … Стандартный код - для данного примера не является существенным … */
/* Записать результаты. */
fprintf(fout, " %9d %9d %9d %s\n", nl, nw, nc, input_file);

int toupperip(char * command, char * output_file)
/* Преобразует входные данные к верхнему регистру; выполняется внутри процесса. */
/* Вторая лексема задает входной файл (первая лексема – "toupperip"). */
extract_token(1, command, input_file);
fin = fopen(input_file, "r");
fout = fopen(output_file, "w");
while ((c = fgetc (fin)) != EOF) {
if (isalpha(c)) с = toupper(c);

static void extract_token(int it, char * command, char * token) {
/* Извлекает из "команды" лексему номер "it" (номером первой лексемы */
/* является "0"). Результат переходит в "лексему" (token) */
/* В качестве разделителей лексем используются пробелы. … */

Ориентированные на строки сообщения, точкив хода DLL и TLS

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

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

В более общей формулировке, мы сталкиваемся здесь с проблемой сохранения долговременных состояний в многопоточной среде (multithreaded persistent state problem). Эта проблема возникает всякий раз, когда безопасная в отношении многопоточного выполнения функция должна поддерживать сохранение некоторой информации от одного вызова функции к другому. Такая же проблема возникает при работе с функцией strtook, входящей в стандартную библиотеку С, которая предназначена для просмотра строки для последовательного нахождения экземпляров определенной лексемы.

Решение проблемы долговременных состояний в многопоточной среде

В искомом решении сочетаются несколько компонентов:

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

Функция, представляющая точку входа в DLL.

Локальная область хранения потока (TLS, глава 7). Подключение процесса к библиотеке сопровождается созданием индекса DLL, а отключение - уничтожением. Значение индекса хранится в статическом хранилище, доступ к которому имеют все потоки.

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

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

Пример: безопасная многопоточная DLL для обмена сообщениями через сокет

Программа 12.4 представляет собой DLL, содержащую две функции для обработки символьных строк (в именах которых в данном случае присутствует "CS", от character string - строка символов), или потоковые функции сокета (socket streaming functions): SendCSMessage и ReceiveCSMessage, а также точку входа DllMain (см. главу 5). Указанные две функции играют ту же роль, что и функция ReceiveMessage, а также функции, использованные в программах 12.1 и 12.2, и фактически заменяют их.

Функция DllMain служит характерным примером решения проблемы долговременных состояний в многопоточной среде и объединяет TLS и библиотеки DLL.

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

Примечание

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

Использующие эту DLL коды клиента и сервера, незначительно измененные по сравнению с программами 12.1 и 12.2, доступны на Web-сайте книги.

Программа 12.4. SendReceiveSKST: безопасная многопоточная DLL
/* SendReceiveSKST.с - DLL многопоточного потокового сокета. */
/* В качестве разделителей сообщений используются символы конца */
/* строки ("\0"), так что размер сообщения заранее не известен. */
/* Поступающие данные буферизуются и сохраняются в промежутках между */
/* вызовами функций. */
/* Для этой цели используются локальные области хранения потоков */
/* (Thread Local Storage, TLS), обеспечивающие каждый из потоков */
/* собственным закрытым "статическим хранилищем". */

#include "ClntSrvr.h" /* Определяет записи запроса и ответа. */

/* "static_buf" содержит "static_buf_len" байтов остаточных данных. */
/* Символы конца строки (нулевые символы) могут присутствовать, а могут */
/* и не присутствовать. */
char static_buf ;

static DWORD TlsIx = 0; /* Индекс TLS – ДЛЯ КАЖДОГО ПРОЦЕССА СВОЙ ИНДЕКС.*/
/* Для однопоточной библиотеки использовались бы следующие определения:
static char static_buf ;
static LONG32 static_buf_len; */
/* Основная функция DLL. */

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) {
/* Для основного потока подключение отсутствует, поэтому во время подключения процесса необходимо выполнить также операции по подключению потока. */
/* Указать, что память не была распределена. */
return TRUE; /* В действительности это значение игнорируется. */
/* Отсоединить также основной поток. */

BOOL ReceiveCSMessage(REQUEST *pRequest, SOCKET sd) {
/* Возвращаемое значение TRUE указывает на ошибку или отсоединение. */
LONG32 nRemainRecv = 0, nXfer, k; /* Должны быть целыми со знаком. */
CHAR TempBuf;
p = (STATIC_BUF *)TlsGetValue(TlsIx);
if (p == NULL) { /* Инициализация при первом вызове. */
/* Распределять это хранилище будут только те потоки, которым оно */
/* необходимо. Другие типы потоков могут использовать TLS для иных целей. */
р = malloc(sizeof(STATIC_BUF));
if (p == NULL) return TRUE; /* Ошибка. */
p->static_buf_len = 0; /* Инициализировать состояние. */
/* Считать до символа новой строки, оставляя остаточные данные в статическом буфере. */
for (k = 0; k < p->static_buf_len && p->static_buf[k] != "\0"; k++) {
message[k] = p->static_buf[k];
} /* k – количество переданных символов. */
if (k < p->static_buf_len) { /* В статическом буфере обнаружен нулевой символ. */
p->static_buf_len –= (k + 1); /* Скорректировать состояние статического буфера. */
memcpy(p->static_buf, &(p->static_buf), p->static_buf_len);
return FALSE; /* Входные данные сокета не требуются. */

/* Передан весь статический буфер. Признак конца строки не обнаружен.*/
nRemainRecv = sizeof(TempBuf) – 1 – p->static_buf_len;
pBuffer = message + p->static_buf_len;
while (nRemainRecv > 0 && !Disconnect) {
nXfer = recv(sd, TempBuf, nRemainRecv, 0);
/* Передать в целевое сообщение все символы вплоть до нулевого, если таковой имеется. */
for (k =0; k < nXfer && TempBuf[k] != "\0"; k++) {
if (k >= nXfer) { /*Признак конца строки не обнаружен, читать дальше*/
} else { /* Обнаружен признак конца строки. */
memcpy(p->static_buf, &TempBuf, nXfer – k – 1);
p->static_buf_len = nXfer – k – 1;

BOOL SendCSMessage(RESPONSE *pResponse, SOCKET sd) {
/* Послать запрос серверу в сокет sd. */
nRemainSend = strlen(pBuffer) + 1;
while (nRemainSend > 0 && !Disconnect) {
/* Отправка еще не гарантирует, что будет отослано все сообщение. */
nXfer = send(sd, pBuffer, nRemainSend, 0);
fprintf(stderr, "\nОтключение сервера до посылки запроса завершения");

Сокеты

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

Первоначально сокеты были разработаны для 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() Запрещает операции отправки и получения данных на сокете.