Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Малышев_Сетевое программирование 19.12.18

.pdf
Скачиваний:
1
Добавлен:
21.11.2025
Размер:
896.22 Кб
Скачать

11

{ Теперь соединение установлено. Допустимы следующие дейст-

вия:

Recv(S,…) - получить данные от сервера Send(S,…) - отправить данные серверу} Close(…) {закрываем сокет}

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

2. ОРГАНИЗАЦИЯ СЕТЕВЫХ СОЕДИНЕНИЙ НА ОСНОВЕ TCP И UDP

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

Рассмотрим стек протоколов TCP/IP (нередко можно услышать словосочетание "протокол TCP/IP" – это не совсем корректно: TCP/IP не протокол, а стек протоколов). Название этот стек получил по названию двух самых известных своих протоколов – TCP и IP.

Физический и канальный уровень полностью реализуются сетевым адаптером (модемом или другим устройством, выполняющим ту же функцию) и его драйвером. Здесь достигнута настолько полная абстракция, что программист обычно не задумывается о том, какая используется сеть [3].

На канальном уровне существует адресация узлов, основанная на т. н. MAC-адресе сетевого адаптера (MAC Media Access Control). Этот ад-

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

12

торый решал такую задачу, был протокол IP, и с его помощью стала возможной передача пакетов между сетями. Поэтому он и получил название межсетевого протокола. IP расшифровывается как Internet Protocol. Это название иногда ошибочно переводят как "протокол Интернета" или "протокол для Интернета". На самом деле, когда разрабатывался этот протокол, никакого Интернета ещё и в помине не было, поэтому правильный перевод

межсетевой протокол.

Впротоколе IP, в частности, вводится важный параметр для каждого пакета: максимальное число маршрутизаторов, которое он может пройти, прежде чем попадёт к адресату (TTL Time to live). Это позволяет защититься от бесконечного блуждания пакетов по сети.

Программисту достаточно знать, что каждый узел имеет уникальный IP-адрес, для которого принята запись в виде четырёх цифровых полей, разделённых точками (IP v.4), например, 192.168.200.217. Также следует знать, что адреса из диапазона 127.0.0.1 – 127.255.255.255 задают т. н. локальный узел: через эти адреса могут связываться программы, работающие на одном компьютере (localhost). Таким образом, обеспечивается прозрачность местонахождения адресата. Кроме того, один компьютер может иметь несколько IP-адресов, используемых как для одного, так и для разных сетевых интерфейсов.

Кроме IP, в стеке TCP/IP существует ещё несколько протоколов, решающих задачи сетевого уровня. Эти протоколы не являются полноценными протоколами и не могут заменить IP. Они используются только для решения некоторых частных задач. Это протоколы ICMP, IGMP и ARP [3].

Протокол ICMP (Internet Control Message Protocol – протокол межсе-

тевых управляющих сообщений) обеспечивает диагностику связи на сетевом уровне. Многим знакома утилита ping, позволяющая проверить связь с удалённым узлом. В основе её работы лежат специальные запросы и ответы, определяемые в рамках протокола ICMP (эхо-пакеты). Кроме того, этот же протокол определяет сообщения об ошибках, которые, например, получает узел, отправивший IP-пакет, если этот пакет по каким-то причинам не доставлен.

Протокол IGMP (Internet Group Management Protocol – протокол управления межсетевыми группами) предназначен для управления группами узлов, которые имеют один групповой IP-адрес. Отправка пакета по такому адресу можно рассматривать как нечто среднее между (одно) адресной и широковещательной рассылкой, т. к. такой пакет будет получен сразу всеми узлами, входящими в группу.

13

Протокол ARP (Address Resolution Protocol – протокол разрешения адресов) используется для установления соответствия между IP- и MAC- адресами. Каждый узел имеет таблицу соответствия. Исходящий пакет содержит два адреса узла: MAC-адрес для канального уровня и IP-адрес для сетевого. Отправляя пакет, узел находит в своей таблице MAC-адрес, соответствующий IP-адресу получателя, и добавляет его в заголовок пакета. Если в таблице такой адрес не найден, отправляется широковещательное сообщение, формат которого определяется протоколом ARP. Получив такое сообщение, узел, чей IP-адрес соответствует искомому, отправляет ответ, в котором указывает свой MAC-адрес. Этот ответ также является широковещательным, поэтому его получают все узлы, а не только отправивший запрос, и все узлы обновляют свои таблицы соответствия.

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

Протоколы самых верхних уровней (прикладного и представительного) HTTP, FTP, SMTP и др. при использовании сокетов в общем случае не нужны: программист сам определяет формат пакетов, отправляемых с помощью TCP или UDP.

Протоколами транспортного и сеансового уровня в стеке TCP/IP являются протоколы TCP и UDP. Строго говоря, они решают не только задачи транспортного уровня, но и небольшую часть задач уровня сессии (сеансового). Тем не менее, они традиционно называются транспортными.

Новички нередко думают, что фраза "программа поддерживает соединение через TCP/IP" полностью описывает то, как можно связаться с программой и получить данные. На самом деле необходимо знать формат пакетов, которые эта программа может принимать и отправлять, т. е. должны быть согласованы протоколы уровня сессии и представлений. Гибкость сокетов даёт программисту возможность самостоятельно определить этот формат, т. е., по сути, придумать и реализовать собственный протокол поверх TCP или UDP. Без описания этого протокола организовать обмен данными с программой невозможно.

2.1. ПРОТОКОЛ UDP

Протокол UDP (User Datagram Protocol – протокол пользовательских дейтаграмм) используется реже, чем TCP, но он проще для понимания.

14

Коротко UDP можно описать как ненадёжный протокол без соединения, основанный на дейтаграммах [3].

UDP не имеет никаких дополнительных средств управления пакетами по сравнению с IP. Это значит, что пакеты, отправленные с помощью UDP, могут теряться, дублироваться и менять порядок следования. В локальной сети без маршрутизаторов ничего этого с пакетами почти никогда не происходит (путь передачи всегда один единственный, нет петель в сети), и UDP может условно считаться надёжным протоколом. Сети с маршрутизаторами строятся, таким образом, чтобы подобные случаи происходили как можно реже, но полностью исключить их, тем не менее, нельзя. Происходит это из-за того, что передача данных может идти несколькими путями через разные маршрутизаторы. Например, пакет может пропасть, если короткий путь к удалённому узлу временно недоступен, а в длинном приходится пройти больше маршрутизаторов, чем это разрешено. Дублироваться пакеты могут, если они ошибочно передаются двумя путями, а порядок следования может изменяться, если пакет, посланный первым, идёт по более длинному пути, чем пакет, посланный вторым.

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

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

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

15

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

Максимальный размер одной дейтаграммы IP равен 65535 байтам. Из них не менее 20 байт занимает заголовок IP. Заголовок UDP имеет размер 8 байт. Таким образом, максимальный размер поля данных IP или размер одной дейтаграммы UDP (которая туда вкладывается) составляет

65507 байт.

Типичная область применения UDP – это программы, для которых потеря пакетов некритична. Например, некоторые сетевые 3D-стрелялки в локальной сети используют UDP, т. к. очень часто посылают пакеты, информирующие о действиях игрока, и потеря одного пакета не приведёт к существенным проблемам: следующий пакет доставит необходимые данные.

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

Ещё одним достоинством UDP является возможность отправки широковещательных дейтаграмм. Для этого нужно указать широковещательный IP-адрес (обычно это 255.255.255.255, но в некоторых случаях могут использоваться адреса типа 192.168.100.255 для вещания в пределах сети 192.168.100.ХХ и т. п.), и такую дейтаграмму получат все сокеты в локальной сети, привязанные к заданному порту. Это нередко используется программами, которые заранее не знают, с какими компьютерами должны связаться. Они посылают широковещательное сообщение и связываются со всеми узлами, которые распознали это сообщение и прислали на него соответствующий ответ. По умолчанию для широковещательных пакетов число маршрутизаторов, через которые они могут пройти (TTL), устанав-

16

ливается равным нулю, поэтому такие пакеты не выходят за пределы подсети.

Процесс получения данных на сокете, не требующем соединения, прост. Сначала создают сокет функцией socket или WSASocket. Затем выполняют привязку сокета к интерфейсу, на котором будут принимать данные, функцией bind (как и в случае протоколов, ориентированных на сеансы). Разница в том, что вместо вызова listen или accept нужно просто ожидать приема входящих данных. Поскольку в этом случае соединения нет, принимающий сокет может получать дейтаграммы от любой машины в се-

ти [3].

2.2. ПРОТОКОЛ TCP

Протокол TCP (Transmission Control Protocol – протокол управления передачей) является надёжным потоковым протоколом с установлением соединения, т. е. полной противоположностью UDP. Единственное, что у этих протоколов общее – это способ адресации: в TCP каждому сокету также назначается уникальный номер порта. Уникальность номера порта требуется только в пределах протокола: два сокета могут использовать одинаковые номера портов, если один из них работает через TCP, а другой

через UDP.

ВTCP предусмотрены т. н. хорошо известные (well-known) порты, которые зарезервированы для нужд системы и не должны использоваться программами. Стандарт TCP определяет диапазон хорошо известных портов от 0 до 255, в Windows и в некоторых других системах этот диапазон расширен до 0 – 1023. Часть портов UDP тоже используется для системных нужд, но зарезервированного диапазона в UDP нет. Следует также отметить, что некоторые системные утилиты используют порты за пределами диапазона 0 – 1023.

Установление TCP-соединения позволяет осуществлять проверку доставки пакета, соблюдение очерёдности и отсутствия дублирования [3]. Механизмы обеспечения надёжности достаточно сложны. Для начала программисту достаточно знать, что данные, переданные с помощью TCP, не теряются, не дублируются и доставляются в том порядке, в каком были отправлены. В противном случае отправитель автоматически получает сообщение об ошибке. Соединённые сокеты время от времени автоматически обмениваются между собой специальными пакетами для проверки соединения. Если в сети произошёл разрыв связи (из-за неполадок), то при попытке отправить или прочитать данные клиент получит отказ, а соеди-

17

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

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

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

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

TCP – потоковый протокол, потому что он собирает входящие пакеты в один поток. Например, если в буфере сокета лежат 30 байт, принятые по сети, то нет возможности определить, были ли эти 30 байт отправлены одним пакетом, 30-ю пакетами по 1 байту или ещё как-то. Гарантируется только порядок байт в буфере, совпадающий с отправленным. Принимающая сторона также не ограничена в том, как она будет читать информацию из буфера: всю сразу или по частям. Это существенно отличает TCP от UDP, в котором дейтаграммы не объединяются и не разбиваются на части (за один раз читается только одна дейтаграмма и только целиком).

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

Другим недостатком TCP по сравнению с UDP является то, что один сокет может использоваться только для отправки пакетов по одному адре-

18

су, в то время как UDP позволяет с одного сокета отправлять разные пакеты по разным адресам.

Кроме того, TCP не позволяет рассылать широковещательные сообщения.

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

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

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

Несмотря на все эти недостатки, TCP используется существенно чаще, чем UDP, потому что автоматическая проверка целостности данных,

19

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

3.БЛОКИРУЮЩИЙ И НЕБЛОКИРУЮЩИЙ РЕЖИМЫ

3.1.РЕЖИМЫ ПРОГРАММИРОВАНИЯ

ВWindows существует два режима (или две модели) программирования сокетов – блокирующий и неблокирующий [7]. Иногда они также называются как – синхронный (blocking) и асинхронный (non-blocking).

На платформе Unix поддерживается только блокирующий режим.

Блокирующий (Синхронный). При этом приложение ждет завершения операции (например, обмена информацией) перед выполнением следующей строки кода.

Неблокирующий (Асинхронный). В этом случае операция (обмен информацией) не приостанавливает выполнение другого кода в сетевом приложении.

3.2.ДРУГИЕ РЕЖИМЫ

Вдействительности существует еще несколько реализованных моделей – это порты завершения (completion ports) и перекрытый ввод/вывод (overlapped I/O). Однако использование этих моделей требует гораздо больше кода и обычно используется только в очень сложных серверных приложениях.

Кроме того, данные модели не кроссплатформенные и их реализация сильно отличается под разные операционные системы.

Компоненты Indy 10, содержащие поддержку и этих моделей, будут рассмотрены позже.

3.3. БЛОКИРУЮЩИЙ РЕЖИМ

Блокирующие вызовы сокетов очень похожи на чтение/запись файлов: когда читается или пишется файл, возврат из функции не происходит до ее окончания. Различие состоит в том, что обычно требуется значительно больше времени до окончания, т. к. операции чтения и записи зависят от скорости сети [7].

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

20

тановления соединения. Если же соединение не произойдет, то будет возбуждено исключение.

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

Блокирующие сокеты намного лучше для поточности, безопасности и по другим аспектам.

Достоинства блокирующего режима.

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

Кросс-платформенность – поскольку Unix использует блокирующие сокеты, то переносимый код легче писать. Indy позволяет использовать свой код на разных платформах. Другие кросс-платформенные сокеткомпоненты на самом деле эмулируют это с помощью внутреннего вызова блокирующих сокетов.

Удобнее работать с потоками – поскольку у блокирующих сокетов последовательность приобретена по наследственности, поэтому их очень просто использовать в потоках.

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

Недостаток блокирующего режима.

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

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

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