Книга: Сетевое программирование. От основ до приложений
Назад: Глава 13. Специальные файловые системы
Дальше: Глава 15. Адресация в ОС Windows

Глава 14. Введение в сетевое программирование для ОС Windows

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

Билл Гейтс, «Why I Hate Spam», 2003

Введение

Читателю может показаться, что сетевые приложения в основном рассматриваются в контексте Unix-подобных ОС и серверов. Но это не так.

Сетевые приложения — это про взаимодействие, а во взаимодействии помимо серверов участвуют еще и клиенты. На данный момент преобладающей ОС в клиентском сегменте является Windows, и не замечать ее нельзя.

Цель этой главы — сформировать у читателя представление о составе Windows Network API, выделить наиболее актуальные элементы, которые мы более подробно рассмотрим в следующих главах, и дать рекомендации по использованию функций для эффективной работы.

Для начала мы рассмотрим структуру сетевого API операционной системы ОС Windows. Объем и функциональность этого интерфейса значительно больше сокетного POSIX API.

Начнем с обзора основных компонентов и архитектуры API, затем рассмотрим ключевые отличия от POSIX.

Также кратко разберем историю API и узнаем про устаревший API NetBIOS.

Затем познакомимся со стандартом WinSock 2, коснемся особенностей написания кода в ОС Windows: типов, констант, состава функций и библиотек, кодировок символов и т.п.

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

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

Внимание! Объем книги ограничен, и поэтому мы совершенно не коснемся работы сокетов в режиме ядра, в том числе WinSock Kernel. Объять необъятное не получится даже в нескольких книгах. Желающим придется разбираться с этой темой самостоятельно, читая профильную литературу или MSDN: .

Можно порекомендовать также книгу Марка Руссиновича и др. «Внутреннее устройство Windows», 7-е изд. (изд-во «Питер»), в которой содержится отдельный раздел о сети, и книгу Павла Йосифовича «Работа с ядром Windows» (изд-во «Питер»).

Состав сетевого API Windows

ОС Windows имеет крайне обширный сетевой API, который постоянно растет и меняется. Некоторые разделы API устаревают, например разделы Dynamic Data Exchange — DDE — и NetBIOS, и на смену данным технологиям приходят другие.

Однако с целью обратной совместимости большая часть API еще долго сохраняется в более новых версиях ОС.

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

Раздел Networking and Internet

Этот раздел включает большую часть сетевого API, как низкоуровневого, такого как сокеты, так и более высокого уровня.

Посмотреть состав данного раздела API и его расширенное описание читатель может в , в секции Networking and Internet, откуда по ссылкам можно изучить конкретные разделы.

Адресация

Расширенная функциональность для работы с адресами в ОС Windows имеет свою специфику, и набор функций для управления разрешением адресов различается для разных POSIX-систем.

Эти функции описаны в следующих разделах MSDN:

, — клиентские функции для разрешения имен через DNS и функции управления сервером DNS. Заголовочные файлы: windns.h.

, — многоадресный протокол динамического распределения адресов клиентов, позволяющий клиентам запрашивать адреса многоадресной рассылки с серверов. Используя этот API, клиенты MADCAP могут получать, обновлять и освобождать многоадресные адреса. Заголовочные файлы: madcapcl.h.

Нас прежде всего интересует работа с DNS.

Сокеты

Сокетный API — это то, что мы в основном изучали до этого, и в ОС Windows он, конечно, тоже есть и описан в разделе MSDN . Описание дано как для POSIX-совместимого, так и для специфичного для Windows API сокета.

В API сокетов входит не только обмен данными, но и работа с сокетными провайдерами, то есть специальными системными приложениями, реализующими обмен. Заголовочные файлы, в которых описано большинство его функций и структур, — winsock2.h, winsock.h.

В зависимости от версии ОС могут быть и другие заголовочные файлы.

Альтернативные средства IPC

Помимо сокетов, ОС Windows предоставляет . Прежде всего это:

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

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

Основной заголовочный файл для этих примитивов: winbase.h. Их мы рассмотрим в соответствующей главе.

Служебный API

Для упрощения работы с некоторыми аспектами IP-стека, а также для настройки есть набор API и вспомогательных функций:

— API, позволяющий извлекать и изменять параметры конфигурации сети для локального узла. Заголовочные файлы: iphlpapi.h, icmpapi.h, iptypes.h, ntddk.h, netioapi.h и др. Часть заголовочных файлов относится к DDK — пакету для разработки драйверов.

— API базы MIB, содержащей управляющую информацию, которую используют службы Remote Access Routing Services, вспомогательный протокол IP Helper, SNMP. Заголовочные файлы: lpmib.h, ifmib.h, tcpmib.h, udpmib.h, netioapi.h и прочие.

, — широкий набор QoS API для управления качеством обслуживания и передачей трафика. Заголовочные файлы: qosname.h, traffic.h, qos.h, qos2.h, qosobjs.h, qospol.h, qossp.h.

Работа в сети интернет

Чтобы работать через интернет по таким высокоуровневым протоколам, как HTTP, существует комплекс функций, реализованных в нескольких библиотеках, поставляемых вместе с ОС:

, — API HTTP-клиента для работы по HTTP. Заголовочные файлы: winhttp.h.

— API HTTP-сервера. Заголовочные файлы: http.h.

— WebSocket API, который обеспечивает асинхронные двунаправленные каналы, используя протокол WebSocket поверх HTTP. Клиент использует HTTP для связи с сервером, а затем обе стороны переключаются на использование протокола более низкого уровня, например TCP или TLS. Заголовочные файлы: websocket.h.

, — высокоуровневый интерфейс для работы по FTP, HTTP и другим стандартным интернет-протоколам. Заголовочные файлы: proofofpossessioncookieinfo.h, wininet.h, winineti.h.

— API для работы с каналами RSS, или Really Simple Syndication, каналами новостей из приложений. Это преимущественно JavaScript API, доступный в браузерах от Microsoft, таких как IE.

Межсетевой экран и фильтрация трафика

Для работы с трафиком, его фильтрации и более сложной обработки Windows предоставляет упомянутый ранее аналог BPF — платформу WFP.

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

, — платформа фильтрации Windows предоставляет набор API и служб для создания приложений сетевой фильтрации. Заголовочные файлы: fwpmu.h.

— API межсетевого экрана Windows, позволяющий совместно использовать подключения к интернету, защищать подключения с помощью межсетевого экрана и обеспечивать NAT. Ранее назывался Internet Connection Sharing и Internet Connection Firewall. Заголовочные файлы: netfw.h, networkisolation.h и прочие.

, — API для NAT traversal дает приложениям возможность настраивать сопоставление портов на удаленных шлюзах, которое использует NAT. Заголовочные файлы: natupnp.h.

Удаленный вызов процедур, очереди, сериализация

Windows API предоставляет достаточно сложные и высокоуровневые системы обмена сообщениями и вызова процедур на удаленных серверах:

— функции и COM-компоненты для работы с очередями сообщений. Заголовочные файлы: mq.h.

— API удаленного вызова процедур. Как серверная, так и клиентская часть. Заголовочные файлы: rpcndr.h, rpc.h, rpcasync.h, midles.h, rpcdce.h и прочие.

, — реализация SOAP. Поддерживает набор протоколов, в том числе на .NET. Заголовочные файлы: webservices.h, icontentprefetchertasktrigger.h.

— API для выполнения XML-запросов по HTTP в несколько потоков. Использует обратные вызовы для получения уведомлений при обработке ответа. API предоставляет интерфейс IXMLHTTPRequest. Нечто похожее на браузерный интерфейс для AJAX. ­Заголовочные файлы: msxml6.h.

Мы рассмотрим некоторые концепции и протоколы в книге 3.

Управление сетью

Функции для управления сетью и SNMP:

— функции управления сетью: учетными записями пользователей, сетевыми ресурсами. Заголовочные файлы: atacct.h, lmalert.h, lmat.h, lmaudit.h, lmconfig.h и прочие.

, — различные диалоги: добавление, удаление, изменение, запуск подключения и т.п. Заголовочные файлы: winnetwk.h.

, — SNMP используется для настройки удаленных устройств, мониторинга производительности сети, аудита, обнаружения сбоев или несанкционированного доступа. Заголовочные файлы: mgmtapi.h, snmp.h, winsnmp.h.

Подключения

API для работы с настройками подключений, DHCP и т.д. описаны в разделах:

— функции для работы с подключениями. Например, CreateVPNConnection() для создания VPN-подключения. Его функции не объявлены в заголовочных файлах SDK, поэтому требуется получать их адреса через GetProcAddress().

, — получение IP-адресов, адресов DNS-серверов, адреса роутера и прочих настроек соединения через DHCP. Заголовочные файлы: dhcpsapi.h, dhcpcsdk.h для клиента, dhcpssdk.h для сервера и dhcpv6csdk.h для поддержки IPv6.

, — позволяет приложениям получать список доступных сетевых подключений. Заголовочные файлы: netlistmgr.h и др.

Беспроводные сети

Набор функций для беспроводных сетей разделен по технологиям и компонентам:

— сокеты и API, поддерживающие стек Bluetooth. Заголовочные файлы: bluetoothapis.h.

— сокеты и стек для работы с инфракрасным портом. Заголовочные файлы: af_irda.h.

— API мобильного широкополосного доступа. Используется для подключения к сотовым сетям. Заголовочные файлы: mbnapi.h и прочие.

— API компонента автоматической настройки беспроводных сетей. Хранит профили сетей как XML-документы. Заголовочные файлы: wlanapi.h.

— позволяет мобильным, IoT-устройствам, точкам доступа 802.11, компьютерам подключаться и безопасно обмениваться настройками друг с другом. Заголовочные файлы: wcndevice.h.

— диспетчер соединений Windows, позволяющий создавать и настраивать сетевые подключения. Заголовочные файлы: wcmapi.h.

Телефония

Ранее были актуальны функции для работы с телефонией, модемами и факсами:

— набор функций для управления аппаратурой и службой факсов. Заголовочные файлы: faxdev.h, faxcom.h, winfax.h и прочие.

, — API службы удаленного доступа, управляющий подключениями через модем и записями в телефонной книге. Раньше этот API использовался для программ-«звонилок», выполняющих дозвон через модем, чтобы затем установить интернет-подключение. Заголовочные файлы: ras.h, rasdlg.h, rasshost.h и прочие.

, — API классической и IP-телефонии. C API, COM API и несколько провайдеров телефонии. Заголовочные файлы: tapi.h, tapi3.h и прочие.

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

Общие сетевые ресурсы

Набор API для работы с общими сетевыми ресурсами, распределенными и сетевыми ФС:

— управление и мониторинг каналов Server Message Block. Заголовочные файлы: lmshare.h и прочие.

— управление SMB. Позволяют создавать общие ресурсы, удалять их, работать с файлами. Также содержит высокоуровневый интерфейс и оснастки WMI. Заголовочные файлы: smbmanagement.h, lm.h, lmcons.h и прочие.

, — расширение HTTP для удаленной работы с файлами и каталогами. Заголовочные файлы: websocket.h.

— API распределенной файловой системы. Заголовочные файлы: lmdfs.h.

— API одноранговой сети, появившийся в Windows Vista. Заголовочные файлы: p2p.h, peerdist.h, drt.h, pnrpdef.h, pnrpns.h.

— виртуализация сети. Позволяет создавать сети виртуальных машин и переносить IP-адреса, а также целые топологии в облачные центры обработки данных. Заголовочные файлы: wnvapi.h.

Функции других разделов

В других, «несетевых», разделах тоже есть несколько относящихся к сети функций.

Диагностика и мониторинг

Мониторинговые и отладочные функции:

— API сетевого монитора, кратко рассмотренного в главе 21. Заголовочные файлы: netmon.h и прочие.

, — фреймворк для сетевой диагностики. Позволяет обрабатывать распространенные сетевые проблемы. Заголовочные файлы: ndfapi.h, ndhelper.h, ndattrib.h.

Сетевой монитор мы рассмотрим в главе 22.

Прочее

В ОС Windows реализовано множество других API для работы с различными подсистемами и протоколами:

, — платформа, которая предоставляет набор компонентов для защищенного доступа к сетям. Ограничивает доступ клиента до тех пор, пока не будут выполнены требования политик безопасности. Заголовочные файлы: NapUtil.h, NapProtocol.h и прочие.

, — серверные расширения политик сети для Network Policy Server. По сути, реализация RADIUS-сервера и прокси-сервера. Заголовочные файлы: authif.h, sdoias.h.

, — кластеры, балансирующие сетевую нагрузку между несколькими портами и серверами в соответствии с настраиваемым набором правил. Набор WMI-классов.

, — API для обмена сообщениями, основанный на COM. Обычно используется для взаимодействия с Microsoft Exchange Server, что дает возможность приложениям работать с e-mail.

, в том числе , — API для взаимодействия с веб-сервером Microsoft Internet Information Services.

— API для служб терминалов и удаленных рабочих столов: wtsapi32.h и т.п.

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

На этом сетевые возможности не заканчиваются, так как существует еще большое количество оберток, например MFC — классы, в которых поддерживаются сокеты, — а также подобные API. Также в ОС Windows существует подсистема WSL, сетевые особенности которой мы рассмотрим в главе 20.

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

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

Модули Python

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

Некоторая специфичная для ОС Windows функциональность все же доступна в модуле socket. Например, для Windows модуль предоставляет следующие дополнительные опции:

SIO_RCVALL.

SIO_KEEPALIVE_VALS.

SIO_LOOPBACK_FAST_PATH.

RCVALL_IPLEVEL.

RCVALL_MAX.

RCVALL_OFF.

RCVALL_ON.

RCVALL_SOCKETLEVELONLY.

Модуль не требует инициализации либо деинициализации сети через вызовы WSAStartup() и WSACleanup() — он вызывает их при загрузке и выгрузке автоматически.

Также существуют пакеты, которые позволяют использовать функциональность, специфичную для конкретной ОС.

Для Windows это сторонний пакет , который предоставляет доступ к большому количеству функций WinAPI.

Он содержит модули, в которых реализован доступ к сетевому API:

— почтовые ящики и функция DeviceIoControl(). Этот же модуль предоставляет функции TransmitFile(), ConnectEx(), AcceptEx(), GetAcceptExSockaddrs(), о которых мы расскажем в книге 2, а также стандартные функции WinSock2:

WSAEventSelect().

WSAEnumNetworkEvents().

WSAAsyncSelect().

WSASend().

WSARecv().

— каналы, именованные либо анонимные.

— сетевой API ОС Windows: WNetOpenEnum(), WNetGetConnection(), NetBIOS API и др.

— RAS API.

— API для работы с сетевыми группами и общими ресурсами: NetGroupAdd(), NetShareEnum() и т.п.

— высокоуровневый Windows Internet API, то есть функция InternetOpen(), а также сопряженные.

— интернет-интерфейсы ActiveX.

— COM-интерфейс для MAPI.

— поддержка ISAPI.

— интерфейс к API служб терминалов.

— COM-интерфейс для API Microsoft Exchange.

Особенностью пакета pywin32 является то, что предоставляемые им функции почти в точности повторяют WinAPI.

Модуль win32file содержит несколько функций для управления коммуникационными ресурсами. Также модуль предоставляет еще несколько полезных для сети функций:

def CalculateSocketEndPointSize(socket)

def GetAcceptExSockaddrs(sAccepting, buffer) -> \

    tuple[iFamily, LocalSockAddr, RemoteSockAddr]

Функция CalculateSocketEndPointSize() вычисляет, сколько байтов необходимо для буфера сокета, а функция GetAcceptExSockaddrs() разбирает конечные точки соединения из буфера, переданного в функцию AcceptEx().

Некоторый API реализуется и в других пакетах. Например, своя реализация именованных каналов есть в асинхронной . Ее мы подробнее рассмотрим в книге 2.

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

Чтобы использовать произвольный API, который предоставляет ОС, в Стандартной библиотеке Python существует . С помощью этого модуля можно загрузить динамическую библиотеку, экспортировать из нее функции, а также создавать требуемые C-вызовами структуры и реализовывать функции обратного вызова на основе функций Python.

Хотя ctypes позволяет загружать любые библиотеки, он предоставляет уже готовый набор основных предварительно загруженных библиотек для ОС Windows, таких как kernel32.dll и даже iphlpapi.dll, на примере которого в главе 19 мы и рассмотрим, как можно использовать этот модуль.

В дополнение к стандартным типам C для работы с функциями WinAPI уже определен набор основных типов, используемых в его функциях: DWORD, USHORT, HANDLE и прочих.

Внимание! В Python используются значения из POSIX-совместимых систем, например из Linux. И в случае ОС Windows значения констант могут отличаться. Поэтому их необходимо применять с осторожностью и проверять значение перед использованием.

Поверх pywin32 и ctypes реализован сторонний пакет — набор инструментов на Python для решения административных задач. Он будет полезен разработчику под ОС Windows.

Можно использовать предоставляемые инструменты, подключать их как Python-модули и переиспользовать их код в своих проектах.

В частности, в winsys есть , в которой реализованы каналы и почтовые ящики.

NetBIOS — предшественник сокетов

NetBIOS, или Network Basic Input/Output System, являлся первой реализацией сети в ОС Windows. Он был разработан фирмой Sytek Inc. в 1983 году.

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

История NetBIOS

NetBIOS поддерживал возможность работы в локальных сетях из узлов IBM/PC поверх одной из первых технологий локальных сетей — IBM PC Network.

Впоследствии был создан NetBIOS Enhanced User Interface, или NetBEUI, — эмулятор, позволяющий NetBIOS работать в сетях Token Ring.

Сетевой и транспортный уровень был реализован в протоколе NBF, — NetBIOS Frames. Этот протокол не является маршрутизируемым, то есть создавать соединенные сети он не позволяет.

Одну из старых версий NBF в Microsoft также назвали NetBEUI, что вносит дополнительную путаницу.

В годы расцвета Novel Netware был реализован NBX — NetBIOS over IPX/SPX. В 1987-м NetBIOS стал работать поверх Ethernet, используя канальный протокол IEEE 802.2.

Тогда же появился новый протокол NBT — NetBIOS over TCP/IP. Протокол описан в RFC 1001 «Protocol standard for a NetBIOS service on a TCP/UDP transport: Concepts and methods» и RFC 1002 «Protocol standard for a NetBIOS service on a TCP/UDP transport: Detailed specifications».

NetBIOS давно уже не поддерживается в ОС Windows — его поддержка была удалена еще в Windows Vista и Windows Server 2008.

В состав NetBIOS входят:

Протокол сеансового уровня.

• Транспортный протокол NBT, работающий поверх TCP и UDP.

• Набор служб:

• NetBIOS-NS. Служба имен. При использовании NBT данную службу реализует сервер WINS — Windows Internet Name Service. Он позволяет регистрировать и освобождать имена. WINS необходим для определения корректных IP-адресов. Он использует порты 137 TCP и UDP.

• NetBIOS-DGM. Обмен дейтаграммами для связи без установления соединения. Использует UDP-порт 138.

• NetBIOS-SSN. Служба сеансов для связи с установлением соединения. TCP-порт 139.

Инструменты диагностики, такие как программа nbtstat, а также группа параметров NetBIOS команды ipconfig.

Если не считать того, что NetBIOS устарел, NBT остался единственным транспортным протоколом, поверх которого работают сервисы NetBIOS. Стек протоколов NetBIOS изображен на рис. 14.1: NBF и IPX — устаревшие компоненты.

Рис. 14.1. Состав NetBIOS

Когда NetBIOS работает через NBT, узлу назначается тип, определяемый тем, как узел работает с IP-адресами:

B — широковещательные узлы. Устанавливают связь с абонентом, используя широковещательную рассылку.

• P — узлы «точка-точка». Поиск узлов по именам работает через WINS.

• M — узлы смешанного типа. Сначала используют широковещательную рассылку, затем WINS.

H — гибридные узлы. Сначала используют WINS, затем широковещательную рассылку.

Используемый тип узла отображается при открытии командной строки и вводе ipconfig /all.

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

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

Типы служб в NetBIOS

Длина имени службы или группы служб в NetBIOS — 16 символов.

Последний символ — это суффикс, который обозначает тип службы, например:

00 — служба рабочей станции, возвращающая имя рабочей станции.

03 — служба обмена сообщениями Windows.

06 — служба удаленного доступа.

20 — файловая служба.

21 — клиент службы удаленного доступа.

Или тип группы:

00 — службы рабочей станции: рабочая группа или имя домена.

— контроллеры домена.

Для установления связи и передачи данных приложениям необходимо знать адреса ресурсов.

Самый простой вариант — использовать службу дейтаграмм, которая позволяет отправлять сообщения группе узлов. Однако для передачи больших объемов данных в NetBIOS используется служба сеансов, которая работает поверх TCP.

В случае NetBIOS поверх TCP IP-адрес узла с определенным именем может быть определен через WINS или путем широковещательной отправки пакета Name Query.

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

Пример API NetBIOS

API NetBIOS был относительно прост и представлен одной структурой и одной функцией:

#include <nb30.h>

 

typedef struct _NCB

{

    // Команда NetBIOS.

    UCHAR ncb_command;

    // Код возврата операции.

    UCHAR ncb_retcode;

    // Локальный номер сеанса.

    UCHAR ncb_lsn;

    // Номер имени локальной сети.

    UCHAR ncb_num;

    // Буфер с данными.

    PUCHAR ncb_buffer;

    // Размер буфера.

    WORD ncb_length;

    // Имя удаленного приложения.

    UCHAR ncb_callname[NCBNAMSZ];

    // Имя локального приложения.

    UCHAR ncb_name[NCBNAMSZ];

    // Тайм-аут приема данных.

    UCHAR ncb_rto;

    // Тайм-аут отправки данных.

    UCHAR ncb_sto;

    // Адрес подпрограммы для вызова после завершения асинхронной команды.

    void()(_NCB *) *ncb_post;

    // Номер сетевого адаптера для выполняемой команды.

    // LANA — Local Area Network Adapter.

    UCHAR ncb_lana_num;

    // Код возврата операции.

    UCHAR ncb_cmd_cplt;

    // Всегда 0.

#if _WIN64

    UCHAR ncb_reserve[18];

#else

    UCHAR ncb_reserve[10];

#endif

    // Дескриптор события, активируемого при завершении команды.

    HANDLE ncb_event;

} NCB, *PNCB;

 

UCHAR Netbios(PNCB pncb);

Структура NCB — блок управления сетью, или Network Control Block.

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

Сначала сохранить все номера локальных сетей:

// Структура, которая содержит локальные сети.

LANA_ENUM l_enum;

 

NCB ncb;

 

ncb.ncb_command = NCBENUM;

ncb.ncb_buffer = reinterpret_cast<PUCHAR>(&l_enum);

ncb.ncb_length = sizeof(LANA_ENUM);

 

// Вызов команды для заполнения структуры.

if (NRC_GOODRET != Netbios(&ncb))

{

    return ncb.ncb_retcode;

}

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

std::cout << "LANA count = " << +l_enum.length << std::endl;

 

std::array<UCHAR, 1024> data_buffer;

 

// Имя узла.

constexpr char local_name[] = "corp.node_name.com";

 

for (int i = 0; i < l_enum.length; ++i)

{

    // Структура переиспользуется, для чего требуется ее обнулить.

    std::fill_n(reinterpret_cast<char*>(&ncb), sizeof(NCB), 0);

    // Команда получения статистики.

    ncb.ncb_command = NCBASTAT;

    // Здесь используется номер сети.

    ncb.ncb_lana_num = l_enum.lana[i];

    ncb.ncb_buffer = data_buffer.data();

    ncb.ncb_length = data_buffer.size();

 

    std::copy(local_name.c_str(), local_name.c_str() + local_name.size(),

              ncb.ncb_callname);

 

    Netbios(&ncb);

 

    if (NRC_GOODRET != ncb.ncb_retcode) std::cerr << "Error" << std::endl;

}

Команд NetBIOS достаточно много: команды вызова RPC, отправки и получения данных, установки соединения, опроса аппаратуры и т.п.

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

Введение в WinSock

Windows Sockets — это API сокетов в ОС Windows, который поддерживает обмен данными между приложениями с использованием различных сетевых протоколов, таких как TCP/IP или устаревшего IPX. Высокоуровневая структура его реализации изображена на рис. 14.2.

Актуальная версия — Windows Sockets 2.2 API, а его краткое название — WinSock.

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

Рис. 14.2. Структура реализации API Windows Sockets

Часть WinSock API реализована так, чтобы обеспечить совместимость с BSD-сокетами, но часть API специфична для ОС Windows.

История сокетов в Windows

Ранние операционные системы Microsoft Windows имели ограничения по работе с сетью, которые были связаны с использованием подсистемы NetBIOS. В Microsoft в то время не считали необходимым поддерживать работу с TCP/IP.

Для работы с TCP/IP в MS-DOS свои решения предоставляли университетские группы, в том числе из MIT, и коммерческие фирмы, такие как FTP Software, Sun Microsystems, Ungermann-Bass и Excelan, причем иногда как часть конкретных программно-аппаратных комплексов.

После выпуска Microsoft Windows 2.0 к ним присоединились и другие, такие как Distinct и NetManage. Они помогли реализовать TCP/IP-стек для Windows. Каждая из реализаций использовала свой API. Без единого стандартного интерфейса независимым прикладным разработчикам приходилось трудно. Стало понятно, что требуется стандартизация.

Модель сокетного API была разработана рабочей группой Birds of a Feather и согласована на BBS-сети CompuServe в октябре 1991 года.

Первое издание спецификации написали Мартин Холл и Марк Tовфик из Microdyne, Джефф Арнольд из Sun Microsystems, Генри Сандерс и Дж. Алард из Microsoft при участии многих других разработчиков.

Сокетный API не заменил NetBIOS, однако постепенно, с появлением новых сервисов на основе WinSock и расширения возможностей API, NetBIOS стал отмирать, пока не устарел окончательно и не был убран из поставки ОС.

WinSock описан достаточно формальной спецификацией, и даже Microsoft не расширяет его бесконтрольно.

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

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

Каждое приложение могло выбирать, инициализировать ли ему сетевую подсистему и загружать ли необходимые WinSock библиотеки.

«Сетевые» операционные системы линейки Windows NT были ориентированы на многопользовательскую работу и позже стали иметь в составе полноценный сетевой стек по умолчанию. Но требование инициализации библиотеки в каждом приложении сохранилось.

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

Инициализация библиотеки WinSock выполняется вызовом функции WSAStartup():

int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);

Параметры функции WSAStartup():

wVersionRequested — требуемая версия библиотеки, которую нужно загрузить. Старший байт указывает младшую версию запрошенной библиотеки WinSock, а младший байт — основную версию.

lpWSAData — указатель на структуру WSADATA, которую WSAStartup() заполняет информацией, относящейся к версии загружаемой библиотеки.

Аббревиатура WSA означает Windows Sockets Application.

Не все сетевые функции начинаются с данной аббревиатуры, например: GetExtendedUdpTable(), HttpSendRequestW(), FwpmProviderAdd0() и подобные.

WSA говорит о том, что данная функция относится именно к сокетному API для приложений.

Чтобы сформировать версию, обычно используется макрос MAKEWORD(x, y), в котором x — старший байт, а y — младший байт. На момент написания книги версия 2.2 является актуальной, а версии 1.x — устаревшими.

Внимание! Порядок байтов версии обратный: сначала — младший, потом — старший.

В случае успеха функция вернет 0. Если же не удается загрузить библиотеку WinSock, функция возвращает ошибку:

WSASYSNOTREADY — сетевая подсистема не готова к сетевому взаимодействию.

• WSAVERNOTSUPPORTED — запрошенная версия WinSock не предоставляется реализацией.

• WSAEINPROGRESS — выполняется блокирующая операция сокетов Win­dows 1.1.

• WSAEPROCLIM — достигнуто ограничение на количество задач, поддерживаемых реализацией сокетов.

WSAEFAULT — параметр lpWSAData не является допустимым указателем.

Расширенный код ошибки можно получить, используя функцию WSAGetLastError().

Кроме инициализации, функция WSAStartup() выполняет согласование. Вызывающая сторона передает в wVersionRequested максимальную версию специ­фикации Windows Sockets, которую она поддерживает.

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

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

Внимание! Без инициализации сетевой подсистемы большинство сетевых функций будет возвращать ошибку SOCKET_ERROR. Код ошибки, получаемый через вызов WSAGetLastError(), будет установлен в WSANOTINITIALISED.

Большая часть современного WinSock API реализована в библиотеке ws2_32 dll.

Она поддерживает спецификации WinSock:

• 1.0;

• 1.1;

• 2.0;

• 2.1;

• 2.2.

Версию 2.2 поддерживает большинство ОС Windows, в том числе NT 4.0 и Windows 95 с последними патчами. А версию 1.1 можно найти разве что на Windows NT 3.5, оригинальной Windows 95 и Windows CE.

В классе библиотеки-обертки SocketWrapper вы также могли видеть метод initialize(), вызываемый из конструктора. В Unix-подобных системах этот метод пустой, так как сеть инициализируется во время старта операционной системы.

В ОС Windows этот метод вызывает функцию WSAStartup():

void initialize()

{

    WSADATA wsaData;

    // Инициализировать Winsock.

    if (auto result = WSAStartup(MAKEWORD(2, 2), &wsaData); result != 0)

    {

        // WSAStartup() вернет код ошибки.

        throw std::system_error(result, std::system_category(),

                                "WSAStartup()");

    }

 

    // Если инициализация прошла, иногда можно проверить версию.

    /*

    if (LOBYTE(wsaData.wVersion) != 2 ||

        HIBYTE(wsaData.wVersion) != 2)

    {

    ...

    }

    */

}

Методу передается указатель на структуру WSAData, в которую сокетный провайдер запишет некоторые свои параметры.

Кратко рассмотрим эту структуру:

typedef struct WSAData

{

    // Запрошенная версия.

    WORD wVersion;

    // Старший байт поддерживаемой версии.

    WORD wHighVersion;

 

    // Нуль-завершенная ASCII-строка, в которую Ws2_32.dll

    // скопирует описание реализации сокетов Windows до 256 символов длиной.

    char szDescription[WSADESCRIPTION_LEN + 1];

 

    // Нуль-завершенная ASCII-строка, в которую Ws2_32.dll

    // скопирует информацию о состоянии или конфигурации.

    char szSystemStatus[WSASYS_STATUS_LEN + 1];

 

    // Максимальное количество сокетов, которые могут быть открыты. Устарел.

    unsigned short iMaxSockets;

 

    // Максимальный размер сообщения дейтаграммы.

    // Атрибут игнорируется для сокетов Windows версии 2 и более поздних.

    unsigned short iMaxUdpDg;

 

    // Указатель на информацию о провайдере.

    // Атрибут нужно игнорировать для WinSock, начиная с версии 2.

    char FAR * lpVendorInfo;

} WSADATA, *LPWSADATA;

Внимание! Поля iMaxSockets, iMaxUdpDg и lpVendorInfo могут идти перед szDescription, то есть их порядок зависит от версии библиотеки сокетов и на этот порядок нельзя полагаться в приложениях.

Атрибуты iMaxSockets, iMaxUdpDg и lpVendorInfo сохранены для совместимости со спецификацией Windows Sockets 1.1. Их не следует использовать при разработке новых приложений.

LPWSADATA означает Long Pointer to WSADATA. Это «венгерская нотация», которая была принята в Microsoft при разработке Word, Excel и других приложений, а затем и в Microsoft Windows, в том числе для Windows API.

Когда приложение завершает работу с интерфейсом WinSock, следует вызвать функцию WSACleanup():

int WSACleanup();

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

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

WSANOTINITIALISED — сетевая подсистема не была инициализирована вызовом WSAStartup().

• WSAENETDOWN — сетевая подсистема вышла из строя.

WSAEINPROGRESS — выполняется блокирующий вызов Windows Sockets 1.1, или провайдер обрабатывает функцию обратного вызова.

В обертке эту функцию вызывает метод deinitialize():

void deinitialize()

{

    WSACleanup();

}

Внимание! Если в приложении было несколько вызовов WSAStartup(), функция WSACleanup() должна быть вызвана для каждого.

Требуется ли проверять значения, возвращаемые WSACleanup()? Как правило, этого не делают, однако такая практика порочна.

Необходимо проверять всё и всегда.

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

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

В версии WinSock 1.1 и ранее перед вызовом WSACleanup() необходимо было вызвать функцию WSACancelBlockingCall(), чтобы отменить блокирующие вызовы:

int WSAAPI WSACancelBlockingCall();

В версии WinSock 2.2.0 данная функция была удалена.

Основные заголовочные файлы и DLL для Windows

Как и в Unix-подобных системах, в ОС Windows существует набор заголовочных файлов, но он значительно отличается.

Заголовочные файлы Windows Sockets следующие:

af_irda.h — семейство адресов для инфракрасных трансиверов. Структура адреса SOCKADDR_IRDA.

in6addr.h — семейство адресов IPv6. Структура адреса IN6_ADDR.

mstcpip.h — структуры и перечисления для TCP.

mswsock.h — расширения Microsoft к спецификации WinSock. Дополнительные функции, например AcceptEx(), TransmitFile(), WSARecvEx(), GetAcceptExSockaddrs(), функции для работы с зарегистрированными буферами.

mswsockdef.h — структура RIOBUF. Описывает порцию данных зарегистрированного буфера.

• nsemail.h — структуры и перечисления для провайдера электронной почты.

• nspapi.h — функции для работы с протоколами и службами.

• socketapi.h — содержит только одну функцию SetSocketMediaStreamingMode() и, вероятно, нужен для работы с QoS.

• sporder.h — функции, управляющие порядком провайдеров пространств имен в Windows Sockets.

• transportsettingcommon.h — структуры для ioctl SIO_QUERY_TRANSPORT_SETTING.

• winsock.h и winsock2.h — все основные функции сокетов, включая инициализацию и DNS. Файл winsock.h относится к более старой версии и используется с winsock.dll. А в файле winsock2.h содержатся функции WinSock 2.x.

• ws2atm.h — структуры для работы с сокетами Asynchronous Transfer Mode.

• ws2spi.h — интерфейс провайдеров транспортов и пространств имен.

• ws2tcpip.h — некоторые функции, относящиеся к TCP/IP, функции для работы с адресами, порядком байтов и некоторыми параметрами UDP.

• wsipv6ok.h — содержит макросы, определяющие совместимость различных функций с IPv6.

• wsipx.h — некоторые функции, относящиеся к IPX.

• wsnetbs.h — функциональность для работы NetBIOS.

• wsnwlink.h — функциональность и структуры для работы с IPX/SPX.

• wspiapi.h — основной заголовочный файл, требуется объявлять перед ws2tcpip.h.

wsrm.h — функционал для работы с надежной многоадресной передачей.

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

Обычно при работе с сокетами в Windows включают заголовочный файл winsock2.h.

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

ANSI- и UNICODE-версии функций

Некоторые функции Windows API имеют несколько имен, оканчивающихся на A, W или вообще не имеющих суффикса.

На A заканчиваются функции, которые работают с ANSI-символами. Функции, заканчивающиеся на W, используются для работы с широкими символами типа wchar_t.

ASCII — 7-битная таблица символов, содержащая символы от 0 до 127.

ANSI — 8-битный наборов символов, в котором первая часть таблицы сохраняется неизменной.

Вторая часть таблицы в ANSI — это национальные символы и типографика. Реальные образы символов второй части таблицы определяются кодовой страницей, или кодировкой.

В американских и западноевропейских версиях ОС Windows это обычно CP-1252, которая содержит большинство нелатинских символов для различных европейских алфавитов.

В России — CP-1251, содержащая кириллические символы. Пример кодовых таблиц показан на рис. 14.3.

Рис. 14.3. Кодовые таблицы ASCII и ANSI

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

Рассмотрим отличие на примере функций преобразования адресов, которые подробно будут описаны далее. Эти функции объявлены в WS2tcpip.h таким образом:

#include <WS2tcpip.h>

 

int WSAAPI InetPtonW(

    int family,

    // "Широкая" строка.

    PCWSTR pszAddrString,

    PVOID pAddrBuf

);

 

PCWSTR WSAAPI InetNtopW(

    int family,

    const void *pAddr,

    // "Широкая" строка.

    PWSTR pStringBuf,

    size_t stringBufSize

);

 

// Псевдонимы для ASCII-строк.

#define InetPtonA inet_pton

#define InetNtopA inet_ntop

// Выбор значения макроса, именующего функции.

#ifdef UNICODE

#define InetPton InetPtonW

#define InetNtop InetNtopW

#else

#define InetPton InetPtonA

#define InetNtop InetNtopA

#endif

Иными словами, функции InetNtopA() и InetPtonA() — это просто другие названия функций inet_ntop() и inet_pton(), которые работают с ANSI-символами.

Но функции InetNtopW() и InetPtonW() используются для работы с широкими символами. Им необходимо передавать буфер типа wchar_t. Если разработчик использует макросы InetNtop() и InetPton(), то есть хочет поддержать оба варианта, он должен это учитывать.

Для большинства функций Windows API макрос UNICODE определяет порядок работы с текстом.

В Windows есть макрос TEXT(), который на этапе компиляции позволяет вставить необходимый строковый тип в зависимости от значения макроса UNICODE.

Этот макрос определен в заголовочном файле winnt.h.

Полностью аналогичен ему макрос _T(), который определён в заголовочном файле tchar.h. Использовать можно любой из них.

Внимание! Используйте макрос TEXT() всегда, когда передаете строковый буфер WinSock функциям, которые начинаются с префикса, например WSA, и не содержат явного указания на используемый тип символов в суффиксе.

Вместо типов char или wchar_t используйте тип TCHAR. В зависимости от макроса UNICODE он становится либо WCHAR, либо char. Строки, которые могут работать, как в ANSI-, так и в UNICODE-вариантах компиляции, имеют тип LPTSTR.

Вызывать функции нужно так:

uint32_t ip;

 

// ANSI-версия:

auto res = InetPtonA(AF_INET, "192.168.1.1", &ip);

 

// UNICODE-версия:

auto res = InetPtonW(AF_INET, L"192.168.1.1", &ip);

 

// Общий случай:

auto res = InetPton(AF_INET, TEXT("192.168.1.1"), &ip);

Внимание! POSIX-совместимые функции, такие как inet_ntop() и inet_pton(), всегда используют тип char, поэтому вызываются как обычно.

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

• POSIX-вариант работает всегда с ANSI-буферами.

• Вместо WinAPI-функций всегда используются макросы без суффикса.

В современных версиях ОС Windows компания Microsoft рекомендует использовать только Unicode-функции, чтобы избежать проблем с кодировками. Но для кросс-платформенных приложений это не всегда выполнимо, поэтому часто желательно пользоваться только ANSI-версиями. И даже только для Windows-приложений следование рекомендации Microsoft снижает их переносимость между разными версиями ОС Windows.

Подключение библиотек

Библиотека WinSock не добавляется к приложению автоматически. Обычно приложения связывают с библиотекой ws2_32.lib, которую требуется подключить явно.

Один из вариантов — использование директивы pragma:

#pragma comment(lib, "Ws2_32.lib")

Однако этот способ не очень хорош, так как засоряет код директивами, специ­фичными для компоновщика Microsoft.

Внимание! Не подключайте библиотеки через pragma.

Лучше оставить компоновку на усмотрение системы сборки, например CMake:

add_library("${PROJECT_NAME}" ${${PROJECT_NAME}_SRC})

 

...

 

if (WIN32)

    # Подключить библиотеки Winsock.

    target_link_libraries("${PROJECT_NAME}" PUBLIC wsock32 ws2_32)

endif()

Можно также добавить библиотеку wsock32, а большинство остальных библиотек компонуются неявно.

Более полный набор DLL, в которых реализован API сокетов:

ws2_32.dll — большинство функций для сокетов из Winsock API.

mswsock.dll — функции для работы с файлами и WSAIoctl().

msafd.dll — service provider, провайдер транспорта для работы сетевых протоколов, таких как TCP/IP, UDP/IP и т.д.

При использовании библиотеки обертки или враппера, которая применяется в примерах для книги, добавлять явно сокетные библиотеки не требуется: враппер уже с ними скомпонован, а CMake автоматически добавит их в библио­теки, компонуемые с приложением.

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

import win32api

 

def GetDnsHostName():

    Win32_ComputerNameDnsHostname = 1

    try:

        return win32api.GetComputerNameEx(Win32_ComputerNameDnsHostname)

    except win32api.error as details:

        # Обработать ошибку.

        return ''

либо через динамическую загрузку библиотек, как показано в главе 10, в разделе о перечислении интерфейсов, а также в главе 20.

Расширения WinSock

Нестандартные функции, не имеющие отношения к спецификации WinSock, реализуются в отдельных библиотеках, у которых свои заголовочные файлы. Например, в Linux есть функция accept4(), а у Microsoft — своя функция AcceptEx().

Существуют расширения WinSock 2 от разных компаний, таких как Intel, Broadcom и Realtek, которые предоставляют дополнительные возможности для работы с сетевым оборудованием. Некоторые из этих расширений включают в себя поддержку различных аппаратных платформ, дополнительных протоколов, улучшенные функции безопасности и прочее.

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

О провайдерах будет рассказано далее, в главе 16.

Основан этот механизм на команде SIO_GET_EXTENSION_FUNCTION_POINTER, вызываемой функцией WSAIoctl().

Вызов обрабатывается провайдером, который возвращает указатель на функцию по ее GUID. Затем приложение может вызвать функцию напрямую, минуя библиотеку ws2_32.dll.

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

Пример загрузки расширения:

// GUID функции TransmitPackets().

// Доступны также WSAID_ACCEPTEX, WSAID_CONNECTEX, WSAID_POLL и прочие.

GUID transmit_packets_guid = WSAID_TRANSMITPACKETS;

LPFN_TRANSMITPACKETS TransmitPackets = nullptr;

DWORD dwBytes;

 

if (SOCKET_ERROR == WSAIoctl(sock, SIO_GET_EXTENSION_FUNCTION_POINTER,

                             // Входной буфер содержит идентификатор

                             // функции расширения.

                             static_cast<void*>(&transmit_packets_guid),

                             // Размер входного буфера с GUID.

                             sizeof(transmit_packets_guid),

                             // Сюда будет сохранен указатель на функцию.

                             &TransmitPackets,

                             // Размер выходного буфера с указателем.

                             sizeof(TransmitPackets),

                             &dwBytes, nullptr, nullptr))

{

    // Ошибка.

    ...

 

}

 

// Здесь указатель TransmitPackets можно использовать как обычную функцию.

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

Стандарт менять долго, но Microsoft требуется реализовывать новую функциональность, поэтому они вынесли свои расширения в отдельную библиотеку, которую включили в состав ОС. Поэтому в ОС Windows, начиная с версии Vista, расширения от Microsoft, такие как WSAPoll(), WSASendMsg(), TransmitPackets() и подобные, экспортируются непосредственно из библиотеки WinSock и вызов функции WSAIoctl() для их загрузки не требуется. Но если требуется использовать расширения сторонних провайдеров, описанный механизм все еще актуален.

Нерекомендованные функции

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

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

Те же причины служат основаниями для объявления функции не рекомендованной к использованию и в Unix-подобных системах.

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

Предупреждение C4996 компилятор Microsoft Visual Studio выводит, если символ является устаревшим, то есть явно помечен модификатором __declspec(deprecated) или атрибутом, [[deprecated]] введенным в C++14.

Предупреждения можно подавить, используя директивы компилятора, например, через директиву pragma:

#pragma warning(disable: 4996)

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

#define _WINSOCK_DEPRECATED_NO_WARNINGS

 

// Включение заголовочных файлов WinSock.

#include <winsock2.h>

 

...

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

Откуда берутся примеры кода с нерекомендованными функциями

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

Но авторы примеров вполне могут позволить себе небрежное отношение, руководствуясь принципом «работает — и ладно». Они не обязаны обеспечивать надежность и работоспособность своего кода. Их проекты не проходят код-ревью, и им не нужно отвечать требованиям политик безопасной разработки. Поэтому на такие примеры ориентироваться не стоит, поскольку на практике в ИТ-компаниях вам, скорее всего, потребуется забыть то, что в них написано, открыть документацию и учиться пользоваться другим API.

Учитесь программировать так, как рекомендовано в «наборах лучших практик», — это менее затратно.

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

Макросы, предоставляемые WinAPI, и практики написания кода в ОС Windows

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

Вот пример макросов для работы с ошибками из заголовочного файла winerror.h:

//

// Значения кодов ошибок.

//

 

#define SEVERITY_SUCCESS  0

#define SEVERITY_ERROR    1

 

//

// Общая проверка на успех вызова. Неотрицательные числа — успех операции.

//

 

#define SUCCEEDED(hr) (((HRESULT)(hr)) >= 0)

 

//

// Инверсия проверки на успех.

//

 

#define FAILED(hr) (((HRESULT)(hr)) < 0)

 

//

// Проверка на ошибку.

//

 

#define IS_ERROR(Status) (((unsigned long)(Status)) >> 31 == SEVERITY_ERROR)

Для работы с COM API, возвращающим HRESULT, может применяться свой набор макросов:

//

// Вернуть код ошибки из HRESULT.

//

 

#define HRESULT_CODE(hr)  ((hr) & 0xFFFF)

#define SCODE_CODE(sc)    ((sc) & 0xFFFF)

 

//

// Вернуть из HRESULT объект, указывающий API или фреймворк, — причину ошибки.

//

 

#define HRESULT_FACILITY(hr)  (((hr) >> 16) & 0x1fff)

#define SCODE_FACILITY(sc)    (((sc) >> 16) & 0x1fff)

 

//

//  Вернуть флаг наличия ошибки.

//

 

#define HRESULT_SEVERITY(hr)  (((hr) >> 31) & 0x1)

 

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

// и анализировать.

#define SCODE_SEVERITY(sc)    (((sc) >> 31) & 0x1)

Для получения поля структуры по адресу можно использовать макрос CONTAINING_RECORD:

#include <ntdef.h>

 

PCHAR CONTAINING_RECORD(address, type, field);

Он позволяет получить адрес поля по его «имени». Это может быть полезно, когда порядок полей в структуре разный. Используется данный макрос, например, для работы с перекрывающимся вводом-выводом, который рассмотрен в книге 2:

socket_data *sock_data = nullptr;

 

OVERLAPPED *lp_overlapped = nullptr;

ret = GetQueuedCompletionStatus(c_port_handle, &transfered,

                                reinterpret_cast<PULONG_PTR>(&completion_key),

                                &lp_overlapped, INFINITE);

 

sock_data = CONTAINING_RECORD(lp_overlapped, socket_data, overlapped);

Это, а также особенности названий, в которых используется «венгерская нотация», отличает Windows API от уже знакомых нам POSIX API и порождает вопросы о том, как правильно и в каком стиле писать приложения под ОС Windows. А примеры кода от Microsoft вносят еще больше неопределенности и порождают споры.

Чтобы понять, «как правильно», необходимо помнить о простом правиле.

Разработчик пишет код, чтобы его могли читать люди.

Тогда становится понятно, что «венгерская нотация» — далеко не лучший стиль. Возможно, она была полезна, когда Стандартная библиотека C++ была очень бедной, а IDE только зарождались, так как эта нотация позволяла видеть по имени тип переменной.

Венгерской она называется потому, что ввел ее венгр Чарльз Симони, старший архитектор в Microsoft во времена разработки MS-DOS. Эта нотация служила для упрощения разработки и рефакторинга: нормальных IDE еще не было, а названный специальным образом тип или переменная упрощали разработку, так как программист сразу понимал, что это.

Сейчас от данной практики отходят, но она сохранилась в ОС Windows как наследие и еще долго будет использоваться в WinAPI.

В новых проектах от этого стиля именования отходят даже в Microsoft.

Внимание! Не используйте «венгерскую нотацию» и стиль Microsoft, а также не равняйтесь на код Microsoft.

Подробнее о рекомендуемом Microsoft стиле см. в , но этот стиль применим и к C++.

Код примеров, которые предоставляет Microsoft, очень старый и также имеет тенденцию следовать нотации, используемой в API.

При работе с Windows API желательно понимать «венгерскую нотацию», чтобы легко читать типы. Она довольно проста. Например, тип PCSTR говорит о том, что аргумент является указателем на константную строку:

P — pointer.

C — constant.

STR — тип строки, может быть просто S.

То есть этот параметр может быть только входным, поскольку он константный.

Если какой-то признак отсутствует, как правило, он не указывается. Та же строка, но уже не константная, будет иметь тип PSTR или LPSTR — Long Pointer.

Данная нотация описана во множестве источников, с которыми рекомендуется ознакомиться. В том числе существует краткое описание в разделе MSDN.

При написании приложения или системы следуйте единому стилю кода, например принятому в компании, независимо от платформы.

Обратите внимание, что следует избегать явных выделений и освобождений памяти через функции WinAPI, приведений типов в стиле C и т.п. Всегда используйте возможности, предоставляемые новыми Стандартами C++.

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

Некоторые константы явно рекомендуются к использованию при вызове WinAPI. Например, константы NO_ERROR и ERROR_SUCCESS, также определенные в winerror.h, широко используются API IP Helper:

#define ERROR_SUCCESS 0L

#define NO_ERROR 0L

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

Эта же логика применима и к другим вспомогательным функциям и макросам, не имеющим POSIX-аналогов, например, к рассмотренным далее функциям-оберткам над опциями, которые упрощают код, но делают его непортируемым.

Типы

Функции WinSock используют в качестве типа сокета SOCKET — целочисленное положительное значение. С точки зрения ядра это индекс в массиве указателей на параметры сокетов, то есть то же самое, что и дескриптор в POSIX-системах.

В ОС Windows дескриптор называется хэндл и может принадлежать, например, окну. Кроме окон, некоторые объекты ядра, адресуемые дескрипторами, могут принимать сообщения. Но в отличие от Unix-подобных систем, использоваться с файловым API может не любой дескриптор. Поэтому в ранних версиях ОС Windows файловый API был неприменим к сокетам. Однако, начиная с WinSock 2, с дескрипторами сокетов могут работать такие файловые API, как ReadFile() и WriteFile().

В ОС Windows файловый API работает с дескрипторами сокетов через виртуальную файловую систему IFS.

IFS — инсталлируемая файловая система. Это общий внутриядерный API, позволяющий операционной системе поддерживать файловые системы как драйверы, то есть не встраивать поддержку ФС непосредственно в код ядра.

IFS частично используется для сокетов, которые реализуют псевдофайловую систему, что позволяет работать с ними, например, таким функциям, как ReadFile() и WriteFile(), при том что дескриптор сокета в ОС Windows не является реальным дескриптором файла.

Если IFS поддерживается конкретным сокетным провайдером, в структуре WSAPROTOCOLINFO будет установлен флаг XP1_IFS_HANDLES, рассматриваемый в главе 18. С сокетами через него лучше не работать — это может привести к снижению производительности.

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

#define INVALID_SOCKET ((SOCKET)(~0))

#define INVALID_HANDLE_VALUE ((HANDLE)(LONG_PTR)-1)

Конечно, будет работать и знаковый тип, поэтому сравнение результата, возвращаемого POSIX-совместимым WinSock API, не приведет к ошибке, но вызовет предупреждение компилятора. Чтобы избежать таких предупреждений, необходимо использовать константу INVALID_SOCKET.

Предполагается, что используется система с дополнением до двух или кодирование отрицательных чисел дополнительным кодом. В этом случае (SOCKET)(~0), означающий установку всех битов переменной типа SOCKET в 1, будет кодировать –1. И это всегда будет работать в ОС Windows, по той причине, что данная ОС работает только на таких архитектурах.

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

Из сказанного также следует, что переменная типа SOCKET может принимать целые значения от 0 до INVALID_SOCKET 1. На практике же значение дескриптора обычно достаточно большое число.

Внимание! В дальнейшем понятия «дескриптор» и «хэндл» будут использоваться как взаимозаменяемые.

Тип ssize_t в ОС Windows отсутствует и обычно заменяется типом int. В обертке над сокетами для учета всех этих особенностей были введены следующие псевдонимы типов:

#if defined(_WIN32)

extern "C"

{

#    include <winsock2.h>

#    include <ws2tcpip.h>

}

 

#    include <cinttypes>

 

// Дескриптор сокета в ОС Windows.

using SocketDescriptorType = SOCKET;

// Отсутствующий ssize_t.

using ssize_t = int;

// Тип для функции ioctlsocket() и подобных, описываемых в главе 18.

using IoctlType = u_long;

 

#    if !defined(in_addr_t)

// Тип IPv4-адреса, если не определен.

using in_addr_t = uint32_t;

#    endif

 

#else  // не WIN32.

// Для не-Windows платформ, использующих POSIX-сокеты.

extern "C"

{

#    include <arpa/inet.h>

#    include <sys/socket.h>

// Нужно для getaddrinfo() и freeaddrinfo().

#    include <netdb.h>

// Здесь объявлена функция close().

#    include <unistd.h>

}

// Тип дескриптора сокета в Unix-подобных ОС.

using SocketDescriptorType = int;

using IoctlType = int;

 

#endif

 

// Константа, аналогичная INVALID_SOCKET в ОС Windows.

#if !defined(INVALID_SOCKET)

#    define INVALID_SOCKET (-1)

#endif

 

// Константа для проверки корректности дескриптора сокета.

#if !defined(SOCKET_ERROR)

#    define SOCKET_ERROR (-1)

#endif

Несколько констант, определенных для Windows, предлагается использовать для работы с POSIX-сокетами в кросс-платформенном коде.

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

typedef int BOOL;

typedef BYTE BOOLEAN;

typedef unsigned char BYTE;

typedef int INT;

typedef unsigned long DWORD;

...

Эти типы используются по назначению, то есть BOOL-размерности int всегда используются как bool и никогда как int. Иными словами, они могут быть заменены на стандартные типы. Многие из этих типов определены в заголовочных файлах WinNT.h и WinDef.h. А их назначение описано в разделе MSDN «».

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

typedef struct _WSABUF

{

    // Длина буфера в байтах.

    ULONG len;

    // Буфер.

    char *buf;

} WSABUF, *LPWSABUF;

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

Порядок байтов

Для преобразования из локального порядка байтов в сетевой доступны функции типа htonl() и htons():

#include <winsock2.h>

 

// Преобразование в сетевой порядок байтов.

u_long htonl(u_long hostlong);

u_short htons(u_short hostshort);

 

// Преобразование в порядок байтов узла.

u_long ntohl(u_long netlong);

u_short ntohs(u_short netshort);

Эти функции не требуют инициализации сети и работают в точности как описано в главе 1.

Кроме них имеются непереносимые функции для нестандартных типов аргументов:

#include <winsock2.h>

 

unsigned __int64 htonll(unsigned __int64 value);

unsigned __int64 ntohll(unsigned __int64 value);

 

// Берет 4 байта float, переставляет при необходимости и возвращает uint32.

unsigned __int32 htonf(float value);

// Обратная функция.

float ntohf(unsigned __int32 value);

 

unsigned __int64 htond(double value);

double ntohd(unsigned __int64 value);

Специфичными для WinSock являются функции WSAHtonl(), WSAHtons(), WSANtohs() и WSANtohl(). Их отличие в том, что переданный им сокет используется для определения сетевого порядка байтов.

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

Прототипы функций:

#include <winsock2.h>

 

// Преобразование в сетевой порядок байтов.

int WSAAPI WSAHtons(

    SOCKET  s,

    u_short hostshort,

    u_short *lpnetshort

);

 

int WSAAPI WSAHtonl(

    SOCKET s,

    u_long hostlong,

    u_long *lpnetlong

);

 

// Преобразование в порядок байтов узла.

int WSAAPI WSANtohs(

    SOCKET  s,

    u_short netshort,

    u_short *lphostshort

);

 

int WSAAPI WSANtohl(

    SOCKET s,

    u_long netlong,

    u_long *lphostlong

);

Параметры функций WSAHtonl(), WSAHtons(), WSANtohs() и WSANtohl():

s — дескриптор сокета.

• hostlong и hostshort — числа в хостовом порядке байтов для преобразования.

• lpnetlong и lpnetshort — указатель на число для сохранения значения, преобразованного в сетевой порядок.

• netlong и netshort — числа в сетевом порядке байтов для преобразования.

lphostlong и lphostshort — указатель на число для сохранения преобразованного в хостовой порядок байтов значения.

Внимание! Как и все функции WSA, эти функции требуют инициализированной сетевой подсистемы.

Макрос WSAAPI обычно задает конвенцию вызова. Скорее всего, макрос будет раскрыт в директиву __stdcall.

Помимо кодов возврата WSANOTINITIALISED, WSAENETDOWN, описанных выше для WSAStartup(), функции могут вернуть WSAENOTSOCK, если дескриптор s не сокет, и WSAEFAULT, если параметр назначения является некорректным, то есть содержит либо 0, либо адрес, не относящийся к пользовательскому пространству.

Данные функции не являются заменой ntohl(), htonl() и прочим. И в случае доменов AF_INET и AF_INET6 лучше их не использовать.

Требуются эти функции в случае наличия других семейств протоколов, в которых сетевой порядок байтов может отличаться от big-endian.

Резюме

История сетевого программирования в операционной системе Windows началась давно. Сетевой API ОС Windows крайне обширен и активно развивался, начиная с эпохи NetBIOS, предшественника сокетов.

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

С появлением API сокетов Windows Sockets, или WinSock, NetBIOS постепенно уступил свои позиции и в конечном итоге был вытеснен. На данный момент актуален стандарт WInSock 2.2.

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

Объекты ядра в ОС Windows идентифицируются дескрипторами, или хэндлами, имеющими тип HANDLE. Этот тип представляет собой беззнаковое целое, а тип SOCKET является подтипом HANDLE. При возникновении ошибок сокетные функции возвращают некорректный хэндл — константу INVALID_SOCKET.

В операционной системе Windows сокет не является файловым дескриптором, хотя некоторые файловые API, такие как ReadFile() и WriteFile(), могут работать с сокетами через IFS.

Интерфейс WinSock частично совместим с BSD-сокетами, но часть его API специфична и адаптирована для операционной системы Windows. Так, состав заголовочных файлов в ОС Windows и в POSIX-системах не совпадает полностью. А, например, функции для работы с порядком байтов бывают как POSIX-совместимые, так и специфичные для ОС Windows.

Многие функции имеют несколько вариантов: некоторые из них работают с символами UNICODE, тогда как другие — с ANSI. Все POSIX-функции работают с ANSI.

Кроме инициализации, приложение необходимо явно связать с сокетной библиотекой, обычно с ws2_32.dll.

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

Вопросы и задания

1. Почему, несмотря на устаревание некоторых разделов API, Microsoft сохраняет их в новых версиях ОС Windows?

2. В чем заключаются главные отличия сетевого Windows API от POSIX API?

3. Какие основные разделы входят в состав сетевого API ОС Windows?

4. Какие пакеты Python можно использовать для работы с сетевым Windows API?

5. Можно ли применять числовые значения констант в коде, если какой-то из констант не хватает? Почему?

6. Что такое NetBIOS и зачем он был необходим?

7. Какая версия стандарта WinSock поддерживается в ОС Windows? Можно ли использовать сокеты более старых версий?

8. Для чего WinSock-приложениям требуется инициализация сетевой библио­теки? Приносит ли инициализация пользу в современных версиях ОС Windows?

9. Как правильно установить версию при инициализации?

10. Как определить, что сетевая подсистема не была инициализирована, если результат вызова WSAStartup() неизвестен?

11. Сколько раз нужно вызывать WSACleanup()?

12. Как правильно интерпретировать ошибку WSAEFAULT, возникающую при работе с сетевым API?

13. Что такое «венгерская нотация» и как она связана с названиями в WinSock2?

14. Какие заголовочные файлы необходимо включать в большинство WinSock-приложений?

15. Чем отличаются функции с суффиксом A и с суффиксом W? Существуют ли другие типы функций?

16. Для чего используется макрос TEXT()?

17. Какой тип символов используют POSIX-совместимые функции?

18. В чем общее отличие POSIX-совместимых функций от их аналогов в Windows с префиксом WSA?

19. Какую разделяемую библиотеку должны подключать сокетные приложения? Как подключить библиотеку правильно?

20. Для чего нужны расширения WinSock и почему просто не добавить функции в основную библиотеку?

21. Как избавиться от предупреждений об использовании нерекомендованных функций? Допустимо ли это и в каких случаях?

22. Хорошая ли идея проверить ошибку, используя макрос FAILED() в переносимом коде? Почему?

23. Какой тип является дескриптором сокета в ОС Windows, что он собой представляет и чем отличается от типа дескриптора POSIX-сокетов?

24. Какие функции лучше использовать в коде и когда: ntohl() и htonl() или WSAHtons() и WSAHtonl()?

25. Напишите клиент и сервер, используя WinSock. Сервер должен ожидать соединения по TCP на свободном порту в блокирующем режиме и выдавать на экран приходящий текст. Клиент должен отправлять данные по TCP-серверу.


)

)

)

)

Назад: Глава 13. Специальные файловые системы
Дальше: Глава 15. Адресация в ОС Windows