А. Григорьев - О чём не пишут в книгах по Delphi
Многоадресная рассылка в IP является одноранговой и в плоскости управления, и в плоскости данных (в [3] вместо "одноранговая" употребляется слово "немаршрутизируемая" — видимо, переводчик просто перепутал слова non-rooted и non-routed). Это значит, что все сокеты, участвующие в ней, paвноправны. Каждый сокет без каких-либо ограничений может подключиться к многоадресной группе и получать все сообщения, отправленные на групповой адрес. При этом послать сообщение на групповой адрес может любой сокет, в том числе и не входящий в группу. Для групповых адресов протокол IP задействует диапазон от 224.0.0.0 до 239.255.255.255. Часть из этих адресов зарезервирована для стандартных служб, поэтому своим группам лучше назначать адреса, начиная с 225.0.0.0. Кроме того, весь диапазон от 224.0.0.0 до 224.0.0.255 зарезервирован для групповых сообщений, управляющих маршрутизаторами, поэтому сообщения, отправленные на эти адреса, никогда не передаются в соседние подсети.
Есть два варианта осуществления многоадресной рассылки с использованием IP средствами WinSock. Первый реализуется средствами WinSock 1 и жестко привязан к протоколу IP. Второй вариант подразумевает работу с WinSock 2 и осуществляется универсальными, не привязанными к конкретному протоколу средствами.
Если рассылка будет осуществляться средствами WinSock 1, то сокет, участвующий в ней, создается обычным образом — с помощью функции WSASocket со стандартным набором флагов или с помощью функции socket с обычными параметрами, задаваемыми при создании UDP-сокета. Если же используется WinSock 2, то сокет должен быть создан с указанием его роли в плоскостях управления и данных. Так как многоадресная рассылка в IP является одноранговой, все сокеты, участвующие в ней, могут быть только "листьями", поэтому сокет для рассылки должен создаваться функцией WSASocket с указанием флагов WSA_FLAG_MULTIPONT_C_LEAF (4) и WSA_FLAG_MULTIPOINT_D_LEAF (16). В [3] на странице 313 написано, что для рассылки средствами WinSock 2 можно создавать сокет функцией socket — это неверно. Впрочем, на странице 328 все-таки сказано, что указанные флаги задавать обязательно. Далее сокет, который планируется добавить в группу, привязывается к любому локальному порту обычным способом — с помощью функции bind. Этот шаг ничем не отличается от привязки к адресу обычного сокета, не использующего групповой адрес.
Затем выполняется собственно добавление сокета в группу. В WinSock 12 для этого потребуется функция setsockopt с параметром IP_ADD_MEMBERSHIP, в качестве уровня следует указать IPPROTO_IP. При этом через параметр optval передается указатель на запись ip_mreq, описанную так, как показано в листинге 2.79.
Листинг 2.79. Тип TIPMreq// ***** Описание на C++ *****
struct ip_mreq {
struct in_addr imr_multiaddr;
struct in_addr imr_interface;
}
// ***** Описание на Delphi *****
TIPMreq = packed record
IMR_MultiAddr: TSockAddr;
IMR_Interface: TSockAddr
end;
Поле IMR_MultiAddr задает групповой адрес, к которому присоединяется сокет. У этой структуры должны быть заполнены поля sin_family (значением AF_INET) и sin_addr. Номер порта здесь указывать не нужно, значение этого поля игнорируется. Поле IMR_Interface определяет адрес сетевого интерфейса, через который будет вестись прием многоадресной рассылки. Если программу устраивает интерфейс, выбираемый системой по умолчанию, значение поля IMR_Interface.sin_addr должно быть INADDR_ANY (на компьютерах с одним сетевым интерфейсом обычно используется именно это значение). Но если у компьютера несколько сетевых интерфейсов, которые связывают его с разными сетями, интерфейс для получения групповых пакетов, выбираемый системой по умолчанию, может быть связан не с той сетью, из которой они реально ожидаются. В этом случае программа может явно указать IP-адрес того интерфейса, через который данный сокет должен принимать групповые пакеты. Как и в поле IMR_MultiAddr, в поле IMR_Interface задействованы только поля sin_familу и sin_addr, а остальные поля игнорируются.
Для прекращения членства сокета в группе служит та же функция setsockopt, но с параметром IP_DROP_MEMBERSHIP. Через параметр optval при этом также передается структура ip_mreq, значимые поля которой должны быть заполнены так же, как и при добавлении данного сокета в данную группу. Несмотря на то, что структура ip_mreq относится к WinSock 1, в модуле WinSock ее описание отсутствует. Константы IP_ADD_MEMBERSHIP и IP_DROP_MEMBERSHIP в этом модуле объявлены, но работать с ними следует с осторожностью, потому что они должны иметь разные значения в WinSock 1 и WinSock 2. В WinSock 1 они должны иметь значения 5 и 6 соответственно, а в WinSock 2 — 12 и 13. Из-за этого нужно внимательно следить, чтобы значения соответствовали той библиотеке, из которой импортируется функция setsockopt: 5 и 6 — для WSock32.dll и 12 и 13 — для WS2_32.dll.
В WinSock 2 для присоединения сокета к группе объявлена функция WSAJoinLeaf, прототип которой приведен в листинге 2.80.
Листинг 2.80. Функция WSAJoinLeaf// ***** описание на C++ *****
SOCKET WSAJoinLeaf(SOCKET s, const struct sockaddr FAR *name, int namelen, LPWSABUF lpCallerData, LPWSABUF lpCalleeData, LPQOS lpSQOS, LPQOS lpGQOS, DWORD dwFlags);
// ***** описание на Delphi *****
function WSAJoinLeaf(S: TSocket; var Name: TSockAddr; NameLen: Integer; lpCallerData, lpCalleeData: PWSABuf; lpSQOS, lpGQOS: PQOS; dwFlags: DWORD): TSocket;
Параметры lpCallerData и lpCalleeData задают буферы, в которые помещаются данные, передаваемые и получаемые при присоединении к группе. Протокол IP не поддерживает передачу таких данных, поэтому при его использовании эти параметры должны быть равны nil. Параметры lpSQOS и lpGQOS относятся к качеству обслуживания, которое мы здесь не рассматриваем, поэтому их мы тоже полагаем равными nil.
Параметр S определяет сокет, который присоединяется к группе, Name — адрес группы, NameLen — размер буфера с адресом. Параметр dfFlags определяет, будет ли сокет служить для отправки данных (JL_SENDER_ONLY, 1), для получения данных (JL_RECEIVER_ONLY, 2) или и для отправки, и для получения (JL_BOTH, 4).
Функция возвращает сокет, который создан для взаимодействия с группой. В протоколах типа ATM подключение к группе похоже на установление связи в TCP, и функция WSAJoinLeaf, подобно функции accept, создаёт новый сокет, подключенный к группе. В случае UDP новый сокет не создается, и функция WSAJoinLeaf возвращает значение переданного ей параметра S.
Номер порта в параметре Name игнорируется. Для получения групповых сообщений используется тот интерфейс, который система назначает для этого по умолчанию.
Чтобы прекратить членство сокета в группе, в которую он был добавлен с помощью WSAJoinLeaf, нужно закрыть его посредством функции closesocket. Если сокет, для которого вызывается функция WSAJoinLeaf, находится в асинхронном режиме, то при успешном присоединении сокета к группе возникнет событие FD_CONNECT (в [3] написано, что в одноранговых плоскостях управления FD_CONNECT не возникает — это не соответствует действительности). Но в случае ненадежного протокола UDP возникновение этого события говорит лишь о том, что было отправлено IGMP-сообщение, извещающее о включении сокета в группу (это сообщение должны получить все маршрутизаторы сети, чтобы потом правильно передавать групповые сообщения в другие подсети). Однако FD_CONNECT не гарантирует, что это сообщение успешно принято всеми маршрутизаторами.
UDP-сокет, присоединившийся к многоадресной группе, не должен "подключаться" к какому-либо адресу с помощью функции connect или WSAConnect. Соответственно, для отправки данных такой сокет может использовать только sendto и WSASendTo. Сокет, присоединившийся к группе, может отправлять данные на любой адрес, но если используется поддержка качества обслуживания, она работает только при отправке данных на групповой адрес сокета. Отправка данных на групповой адрес не требует присоединения к группе, причем для сокета, отправляющего данные, нет никакой разницы между отправкой данных на обычный адрес и на групповой. И в том и в другом случае используется функция sendto или WSASendto (или sendWSASend с предварительным вызовом connect). Никаких дополнительных действий для отправки данных на групповой адрес выполнять не требуется. Порт при этом также указывается. Как мы уже видели, номер порта при добавлении сокета в группу не указывается, но сам сокет перед этим должен быть привязан к какому-либо порту. При отправке группового сообщения его получат только те сокеты, входящие в группу, чей порт привязки совпадает с портом, указанным в адресе назначения сообщения.
Если сокет, отправляющий сообщение на групповой адрес, сам является членом этой группы, он, в зависимости от настроек, может получать или не получать свое сообщение. Это определяется его параметром IP_MULTICAST_LOOP, имеющим тип BOOL. По умолчанию этот параметр равен True — это значит, что сокет будет получать свои собственные сообщения. С помощью функции setsockopt можно изменить значение этого параметра на False, и тогда сокет не будет принимать свои сообщения.