|
||||
|
ГЛАВА 12Сетевое программирование с помощью сокетов Windows Именованные каналы пригодны для организации межпроцессного взаимодействия как в случае процессов, выполняющихся на одной и той же системе, так и в случае процессов, выполняющихся на компьютерах, связанных друг с другом локальной или глобальной сетью. Эти возможности были продемонстрированы на примере клиент-серверной системы, разработанной в главе 11, начиная с программы 11.2. Однако как именованные каналы, так и почтовые ящики (в отношении которых для простоты мы будем использовать далее общий термин — "именованные каналы", если различия между ними не будут играть существенной роли) обладают тем недостатком, что они не являются промышленным стандартом. Это обстоятельство усложняет перенос программ наподобие тех, которые рассматривались в главе 11, в системы, не принадлежащие семейству Windows, хотя именованные каналы не зависят от протоколов и могут выполняться поверх многих стандартных промышленных протоколов, например TCP/IP. Возможность взаимодействия с другими системами обеспечивается в Windows поддержкой сокетов (sockets) Windows Sockets — совместимого и почти точного аналога сокетов Berkeley Sockets, де-факто играющих роль промышленного стандарта. В этой главе использование API Windows Sockets (или "Winsock") показано на примере модифицированной клиент-серверной системы из главы 11. Результирующая система способна функционировать в глобальных сетях, использующих протокол TCP/IP, что, например, позволяет серверу принимать запросы от клиентов UNIX или каких-либо других, отличных от Windows систем. Читатели, знакомые с интерфейсом Berkeley Sockets, при желании могут сразу же перейти непосредственно к рассмотрению примеров, в которых не только используются сокеты, но также вводятся новые возможности сервера и демонстрируются дополнительные методы работы с библиотеками, обеспечивающими безопасную многопоточную поддержку. Привлекая средства обеспечения взаимодействия между разнородными системами, ориентированные на стандарты, интерфейс Winsock открывает перед программистами возможность доступа к высокоуровневым протоколам и приложениям, таким как ftp, http, RPC и СОМ, которые в совокупности предоставляют богатый набор высокоуровневых моделей, обеспечивающих поддержку межпроцессного сетевого взаимодействия для систем с различной архитектурой. В данной главе указанная клиент-серверная система используется в качестве механизма демонстрации интерфейса Winsock, и в процессе того, как сервер будет модифицироваться, в него будут добавляться новые интересные возможности. В частности, нами будут впервые использованы точки входа DLL (глава 5) и внутрипроцессные серверы DLL. (Эти новые средства можно было включить уже в первоначальную версию программы в главе 11, однако это отвлекло бы ваше внимание от разработки основной архитектуры системы.) Наконец, дополнительные примеры покажут вам, как создаются безопасные реентерабельные многопоточные библиотеки. Поскольку интерфейс Winsock должен соответствовать промышленным стандартам, принятые в нем соглашения о правилах присвоения имен и стилях программирования несколько отличаются от тех, с которыми мы сталкивались в процессе работы с описанными ранее функциями Windows. Строго говоря, Winsock API не является частью Win32/64. Кроме того, Winsock предоставляет дополнительные функции, не подчиняющиеся стандартам; эти функции используются лишь в случае крайней необходимости. Среди других преимуществ, обеспечиваемых Winsock, следует отметить улучшенную переносимость результирующих программ на другие системы. Сокеты WindowsWinsock API разрабатывался как расширение Berkley Sockets API для среды Windows и поэтому поддерживается всеми системами Windows. К преимуществам Winsock можно отнести следующее: • Перенос уже имеющегося кода, написанного для Berkeley Sockets API, осуществляется непосредственно. • Системы Windows легко встраиваются в сети, использующие как версию IPv4 протокола TCP/IP, так и постепенно распространяющуюся версию IPv6. Помимо всего остального, версия IPv6 допускает использование более длинных IP-адресов, преодолевая существующий 4-байтовый адресный барьер версии IPv4. • Сокеты могут использоваться совместно с перекрывающимся вводом/выводом Windows (глава 14), что, помимо всего прочего, обеспечивает возможность масштабирования серверов при увеличении количества активных клиентов. • Сокеты можно рассматривать как дескрипторы (типа HANDLE) файлов при использовании функций ReadFile и WriteFile и, с некоторыми ограничениями, при использовании других функций, точно так же, как в качестве дескрипторов файлов сокеты применяются в UNIX. Эта возможность оказывается удобной в тех случаях, когда требуется использование асинхронного ввода/вывода и портов завершения ввода/вывода. • Существуют также дополнительные, непереносимые расширения. Инициализация WinsockWinsock API поддерживается библиотекой DLL (WS2_32.DLL), для получения доступа к которой следует подключить к программе библиотеку WS_232.LIB. Эту DLL следует инициализировать с помощью нестандартной, специфической для Winsock функции WSAStartup, которая должна быть первой из функций Winsock, вызываемых программой. Когда необходимость в использовании функциональных возможностей Winsock отпадает, следует вызывать функцию WSACleanup. Примечание. Префикс WSA означает "Windows Sockets asynchronous …" ("Асинхронный Windows Sockets …"). Средства асинхронного режима Winsock нами здесь не используются, поскольку при возникновении необходимости в выполнении асинхронных операций мы можем и будем использовать потоки. Хотя функции WSAStartup и WSACleanup необходимо вызывать в обязательном порядке, вполне возможно, что они будут единственными нестандартными функциями, с которыми вам придется иметь дело. Распространенной практикой является применение директив препроцессора #ifdef для проверки значения символической константы _WIN32 (обычно определяется Visual C++ на стадии компиляции), в результате чего функции WSA будут вызываться только тогда, когда вы работаете в Windows). Разумеется, такой подход предполагает, что остальная часть кода не зависит от платформы. Параметрыint WSAStartup(WORD wVersionRequired, LPWSADATA ipWSAData); wVersionRequired — указывает старший номер версии библиотеки DLL, который вам требуется и который вы можете использовать. Как правило, версии 1.1 вполне достаточно для того, чтобы обеспечить любое взаимодействие с другими системами, в котором у вас может возникнуть необходимость. Тем не менее, во всех системах Windows, включая Windows 9x, доступна версия Winsock 2.0, которая и используется в приведенных ниже примерах. Версия 1.1 считается устаревшей и постепенно выходит из употребления. Функция возвращает ненулевое значение, если запрошенная вами версия данной DLL не поддерживается. Младший байт параметра wVersionRequired указывает основной номер версии, а старший байт — дополнительный. Обычно используют макрос MAKEWORD; таким образом, выражение MAKEWORD (2,0) представляет версию 2.0. ipWSAData — указатель на структуру WSADATA, которая возвращает информацию о конфигурации DLL, включая старший доступный номер версии. О том, как интерпретировать ее содержимое, вы можете прочитать в материалах оперативной справки Visual Studio. Чтобы получить более подробную информацию об ошибках, можно воспользоваться функцией WSAGetLastError, но для этой цели подходит также функция GetLastError, а также функция ReportError, разработанная в главе 2. По окончании работы программы, а также в тех случаях, когда необходимости в использовании сокетов больше нет, следует вызывать функцию WSACleanup, чтобы библиотека WS_32.DLL, обслуживающая сокеты, могла освободить ресурсы, распределенные для этого процесса. Создание сокетаИнициализировав Winsock DLL, вы можете использовать стандартные (Berkeley Sockets) функции для создания сокетов и соединений, обеспечивающих взаимодействие серверов с клиентами или взаимодействие равноправных узлов сети между собой. Используемый в Winsock тип данных SOCKET аналогичен типу данных HANDLE в Windows, и его даже можно применять совместно с функцией ReadFile и другими функциями Windows, требующими использования дескрипторов типа HANDLE. Для создания (или открытия) сокета служит функция socket. ПараметрыSOCKET socket(int af, int type, int protocol); Тип данных SOCKET фактически определяется как тип данных int, потому код UNIX остается переносимым, не требуя привлечения типов данных Windows. af — обозначает семейство адресов, или протокол; для указания протокола IP (компонент протокола TCP/IP, отвечающий за протокол Internet) следует использовать значение PF_INET (или AF_INET, которое имеет то же самое числовое значение, но обычно используется при вызове функции bind). type — указывает тип взаимодействия: ориентированное на установку соединения (connection-oriented communication), или потоковое (SOCK_STREAM), и дейтаграммное (datagram communication) (SOCK_DGRAM), что в определенной степени сопоставимо соответственно с именованными каналами и почтовыми ящиками. protocol — является излишним, если параметр af установлен равным AF_INET; используйте значение 0. В случае неудачного завершения функция socket возвращает значение INVALID_SOCKET. Winsock можно использовать совместно с протоколами, отличными от TCP/IP, указывая различные значения параметра protocol; мы же будем использовать только протокол TCP/IP. Как и в случае всех остальных стандартных функций, имя функции socket не должно содержать прописных букв. Это является отходом от соглашений, принятых в Windows, и продиктовано необходимостью соблюдения промышленных стандартов. Серверные функции сокетаВ нижеследующем обсуждении под сервером будет пониматься процесс, который принимает запросы на образование соединения через заданный порт. Несмотря на то что сокеты, подобно именованным каналам, могут использоваться для создания соединений между равноправными узлами сети, введение указанного различия между узлами является весьма удобным и отражает различия в способах, используемых обеими системами для соединения друг с другом. Если не оговорено иное, типом сокетов в наших примерах всегда будет SOCK_STREAM. Сокеты типа SOCK_DGRAM рассматривается далее в этой главе. Связывание сокетаСледующий шаг заключается в привязке сокета к его адресу и конечной точке (endpoint) (направление канала связи от приложения к службе). Вызов socket, за которым следует вызов bind, аналогичен созданию именованного канала. Однако не существует имен, используя которые можно было бы различать сокеты данного компьютера. Вместо этого в качестве конечной точки службы используется номер порта (port number). Любой заданный сервер может иметь несколько конечных точек. Прототип функции bind приводится ниже. Параметрыint bind(SOCKET s, const struct sockaddr *saddr, int namelen); s — несвязанный сокет, возвращенный функцией socket. saddr — заполняется перед вызовом и задает протокол и специфическую для протокола информацию, как описано ниже. Кроме всего прочего, в этой структуре содержится номер порта. namelen — присвойте значение sizeof (sockaddr). В случае успешного выполнения функция возвращает значение 0, иначе SOCKET_ERROR. Структура sockaddr определяется следующим образом: struct sockaddr {u_short sa_family;char sa_data[14] ;};typedef struct sockaddr SOCKADDR, *PSOCKADDR; Первый член этой структуры, sa_family, обозначает протокол. Второй член, sa_data, зависит от протокола. Internet-версией структуры sa_data является структура sockaddr_in: struct sockaddr_in {short sin_family; /* AF_INET */u_short sin_port;struct in_addr sin_addr; /* 4-байтовый IP-адрес */char sin_zero[8];};typedef struct sockaddr_in SOCKADDR_IN, *PSOCKADDR IN; Обратите внимание на использование типа данных short integer для номера порта. Кроме того, номер порта и иная информация должны храниться с соблюдением подходящего порядка следования байтов, при котором старший байт помещается в крайней позиции справа (big-endian), чтобы обеспечивалась двоичная совместимость с другими системами. В структуре sin_addr содержится подструктура s_addr, заполняемая уже знакомым нам 4-байтовым IP-адресом, например 127.0.0.1, указывающим систему, чей запрос на образование соединения должен быть принят. Обычно удовлетворяются запросы любых систем, в связи с чем следует использовать значение INADDR_ANY, хотя этот символический параметр должен быть преобразован к корректному формату, как показано в приведенном ниже фрагменте кода. Для преобразования текстовой строки с IP-адресом к требуемому формату можно использовать функцию inet_addr, поэтому член sin_addr.s_addr переменной sockaddr_in инициализируется следующим образом: sa.sin_addr.s_addr = inet_addr("192 .13.12.1"); О связанном сокете, для которого определены протокол, номер порта и IP-адрес, иногда говорят как об именованном сокете (named socket). Перевод связанного сокета в состояние прослушиванияФункция listen делает сервер доступным для образования соединения с клиентом. Аналогичной функции для именованных каналов не существует. int listen(SOCKET s, int nQueueSize); Параметр nQueueSize указывает число запросов на соединение, которые вы намерены помещать в очередь сокета. В версии Winsock 2.0 значение этого параметра не имеет ограничения сверху, но в версии 1.1 оно ограничено предельным значением SOMAXCON (равным 5). Прием клиентских запросов соединенияНаконец, сервер может ожидать соединения с клиентом, используя функцию accept, возвращающую новый подключенный сокет, который будет использоваться в операциях ввода/вывода. Заметьте, что исходный сокет, который теперь находится в состоянии прослушивания (listening state), используется исключительно в качестве параметра функции accept, а не для непосредственного участия в операциях ввода/вывода. Функция accept блокируется до тех пор, пока от клиента не поступит запрос соединения, после чего она возвращает новый сокет ввода/вывода. Хотя рассмотрение этого и выходит за рамки данной книги, возможно создание неблокирующихся сокетов, а в сервере (программа 12.2) для приема запроса используется отдельный поток, что позволяет создавать также неблокирующиеся серверы. ПараметрыSOCKET accept(SOCKET s, LPSOCKADDR lpAddr, LPINT lpAddrLen); s — прослушивающий сокет. Чтобы перевести сокет в состояние прослушивания, необходимо предварительно вызвать функции socket, bind и listen. lpAddr — указатель на структуру sockaddr_in, предоставляющую адрес клиентской системы. lpAddrLen — указатель на переменную, которая будет содержать размер возвращенной структуры sockaddr_in. Перед вызовом функции accept эта переменная должна быть инициализирована значением sizeof(struct sockaddr_in). Отключение и закрытие сокетовДля отключения сокетов применяется функция shutdown(s, how). Аргумент how может принимать одно из двух значений: 1, указывающее на то, что соединение может быть разорвано только для посылки сообщений, и 2, соответствующее разрыву соединения как для посылки, так и для приема сообщений. Функция shutdown не освобождает ресурсы, связанные с сокетом, но гарантирует завершение посылки и приема всех данных до закрытия сокета. Тем не менее, после вызова функции shutdown приложение уже не должно использовать этот сокет. Когда работа с сокетом закончена, его следует закрыть, вызвав функцию closesocket(SOCKET s). Сначала сервер закрывает сокет, созданный функцией accept, а не прослушивающий сокет, созданный с помощью функции socket. Сервер должен закрывать прослушивающий сокет только тогда, когда завершает работу или прекращает принимать клиентские запросы соединения. Даже если вы работаете с сокетом как с дескриптором типа HANDLE и используете функции ReadFile и WriteFile, уничтожить сокет одним только вызовом функции CloseHandle вам не удастся; для этого следует использовать функцию closesocket. Пример: подготовка и получение клиентских запросов соединенияНиже приводится фрагмент кода, показывающий, как создать сокет и организовать прием клиентских запросов соединения. В этом примере используются две стандартные функции: htons ("host to network short" — "ближняя связь") и htonl ("host to network long" — "дальняя связь"), которые преобразуют целые числа к форме с обратным порядком байтов, требуемой протоколом IP. Номером порта сервера может быть любое число из диапазона, допустимого для целых чисел типа short integer, но для определенных пользователем служб обычно используются числа в диапазоне 1025—5000. Порты с меньшими номерами зарезервированы для таких известных служб, как telnet или ftp, в то время как порты с большими номерами предполагаются для использования других стандартных служб. struct sockaddr_in SrvSAddr; /* Адресная структура сервера. */ struct sockaddr_in ConnectAddr; SOCKET SrvSock, sockio; … SrvSock = socket(AF_INET, SOCK_STREAM, 0); SrvSAddr.sin_family = AF_INET; SrvSAddr.sin_addr.s_addr = htonl(INADDR_ANY); SrvSAddr.sin_port = htons(SERVER_PORT); bind(SrvSock, (struct sockaddr *)&SrvSAddr, sizeof SrvSAddr); listen(SrvSock, 5); AddrLen = sizeof(ConnectAddr); sockio = accept(SrvSock, (struct sockaddr *) &ConnectAddr, &AddrLen); … Получение запросов и отправка ответов … shutdown(sockio); closesocket(sockio); Клиентские функции сокетаКлиентская станция, которая желает установить соединение с сервером, также должна создать сокет, вызвав функцию socket. Следующий шаг заключается в установке соединения сервером, а, кроме того, необходимо указать номер порта, адрес хоста и другую информацию. Имеется только одна дополнительная функция – connect. Установление клиентского соединения с серверомЕсли имеется сервер с сокетом в режиме прослушивания, клиент может соединиться с ним при помощи функции connect. Параметрыint connect(SOCKET s, LPSOCKADDR lpName, int nNameLen); s — сокет, созданный с использованием функции socket. lpName — указатель на структуру sockaddr_in, инициализированную значениями номера порта и IP-адреса системы с сокетом, связанным с указанным портом, который находится в состоянии прослушивания. Инициализируйте nNameLen значением sizeof (struct sockaddr_in). Возвращаемое значение 0 указывает на успешное завершение функции, тогда как значение SOCKET_ERROR указывает на ошибку, которая, в частности, может быть обусловлена отсутствием прослушивающего сокета по указанному адресу. Сокет s не обязательно должен быть связанным с портом до вызова функции connect, хотя это и может иметь место. При необходимости система распределяет порт и определяет протокол. Пример: подключение клиента к серверуПоказанный ниже фрагмент кода обеспечивает соединение клиента с сервером. Для этого нужны только два вызова функций, но адресная структура должна быть инициализирована до вызова функции connect. Проверка возможных ошибок здесь отсутствует, но в реальные программы она должна включаться. В примере предполагается, что IP-адрес (текстовая строка наподобие "192.76.33.4") задается в аргументе argv[1] командной строки. SOCKET ClientSock; … ClientSock = socket(AF_INET, SOCK_STREAM, 0); memset(&ClientSAddr, 0, sizeof(ClientSAddr)); ClientSAddr.sin_family = AF_INET; ClientSAddr.sin_addr.s_addr = inet_addr(argv[1]); ClientSAddr.sin_port = htons(SERVER_PORT); ConVal = connect(ClientSock, (struct sockaddr *)&ClientSAddr, sizeof(ClientSAddr)); Отправка и получение данныхПрограммы, использующие сокеты, обмениваются данными с помощью функций send и recv, прототипы которых почти совпадают (перед указателем буфера функции send помещается модификатор const). Ниже представлен только прототип функции send. int send(SOCKET s, const char * lpBuffer, int nBufferLen, int nFlags); Возвращаемым значением является число фактически переданных байтов. Значение SOCKET_ERROR указывает на ошибку. nFlags — может использоваться для обозначения степени срочности сообщений (например, экстренных сообщений), а значение MSG_PEEK позволяет просматривать получаемые данные без их считывания. Самое главное, что вы должны запомнить — это то, что функции send и recv не являются атомарными (atomic), и поэтому нет никакой гарантии, что затребованные данные будут действительно отправлены или получены. Передача "коротких" сообщений ("short sends") встречается крайне редко, хотя и возможна, что справедливо и по отношению к приему "коротких" сообщений ("short receives"). Понятие сообщения в том смысле, который оно имело в случае именованных каналов, здесь отсутствует, и поэтому вы должны проверять возвращаемое значение и повторно отправлять или принимать данные до тех пор, пока все они не будут переданы. С сокетами могут использоваться также функции ReadFile и WriteFile, только в этом случае при вызове функции необходимо привести сокет к типу HANDLE. Сравнение именованных каналов и сокетовИменованные каналы, описанные в главе 11, очень похожи на сокеты, но в способах их использования имеются значительные различия. • Именованные каналы могут быть ориентированными на работу с сообщениями, что значительно упрощает программы. • Именованные каналы требуют использования функций ReadFile и WriteFile, в то время как сокеты могут обращаться также к функциям send и recv. • В отличие от именованных каналов сокеты настолько гибки, что предоставляют пользователям возможность выбрать протокол для использования с сокетом, например, TCP или UDP. Кроме того, пользователь имеет возможность выбирать протокол на основании характера предоставляемой услуги или иных факторов. • Сокеты основаны на промышленном стандарте, что обеспечивает их совместимость с системами, отличными от Windows. Имеются также различия в моделях программирования сервера и клиента. Сравнение серверов именованных каналов и сокетовУстановка соединения с несколькими клиентами при использовании сокетов требует выполнения повторных вызовов функции accept. Каждый из вызовов возвращает очередной подключенный сокет. По сравнению с именованными каналами имеются следующие отличия: • В случае именованных каналов требуется, чтобы каждый экземпляр именованного канала и дескриптор типа HANDLE создавались с помощью функции CreateNamedPipe, тогда как для создания экземпляров сокетов применяется функция accept. • Допустимое количество клиентских сокетов ничем не ограничено (функция listen ограничивает лишь количество клиентов, помещаемых в очередь), в то время как количество экземпляров именованных каналов, в зависимости от того, что было указано при первом вызове функции CreateNamedPipe, может быть ограниченным. • Не существует вспомогательных функций для работы с сокетами, аналогичных функции TransactNamedPipe. • Именованные каналы не имеют портов с явно заданными номерами и различаются по именам. В случае сервера именованных каналов получение пригодного для работы дескриптора типа HANDLE требует вызова двух функций (CreateNamedPipe и ConnectNamedPipe), тогда как сервер сокета требует вызова четырех функций (socket, bind, listen и accept). Сравнение клиентов именованных каналов и сокетовВ случае именованных каналов необходимо последовательно вызывать функции WaitNamedPipe и CreateFile. Если же используются сокеты, этот порядок вызовов обращается, поскольку можно считать, что функция socket создает сокет, а функция connect — блокирует. Дополнительное отличие состоит в том, что функция connect является функцией клиента сокета, в то время как функция ConnectNamedPipe используется сервером именованного канала. Пример: функция приема сообщений в случае сокетаЧасто оказывается удобным отправлять и получать сообщения в виде единых блоков. Как было показано в главе 11, каналы позволяют это сделать. Однако в случае сокетов требуется создание заголовка, содержащего размер сообщения, за которым следует само сообщение. Для приема таких сообщений предназначена функция ReceiveMessage, которая будет использоваться в примерах. То же самое можно сказать и о функции SendMessage, предназначенной для передачи сообщений. Обратите внимание, что сообщение принимается в виде двух частей: заголовка и содержимого. Ниже мы предполагаем, что пользовательскому типу MESSAGE соответствует 4-байтовый заголовок. Но даже для 4-байтового заголовка требуются повторные вызовы функции recv, чтобы гарантировать его полное считывание, поскольку функция recv не является атомарной.
DWORD ReceiveMessage (MESSAGE *pMsg, SOCKET sd) { /* Сообщение состоит из 4-байтового поля размера сообщения, за которым следует собственно содержимое. */ DWORD Disconnect = 0; LONG32 nRemainRecv, nXfer; LPBYTE pBuffer; /* Считать сообщение. */ /* Сначала считывается заголовок, а затем содержимое. */ nRemainRecv = 4; /* Размер поля заголовка. */ pBuffer = (LPBYTE)pMsg; /* recv может не передать все запрошенные байты. */ while (nRemainRecv > 0 && !Disconnect) { nXfer = recv(sd, pBuffer, nRemainRecv, 0); Disconnect = (nXfer == 0); nRemainRecv –=nXfer; pBuffer += nXfer; } /* Считать содержимое сообщения. */ nRemainRecv = pMsg->RqLen; while (nRemainRecv > 0 && !Disconnect) { nXfer = recv(sd, pBuffer, nRemainRecv, 0); Disconnect = (nXfer == 0); nRemainRecv –=nXfer; pBuffer += nXfer; } return Disconnect; } Пример: клиент на основе сокетаПрограмма 12.1 представляет собой переработанный вариант клиентской программы clientNP (программа 11.2), которая использовалась в случае именованных каналов. Преобразование программы осуществляется самым непосредственным образом и требует лишь некоторых пояснений. • Вместо обнаружения сервера с помощью почтовых ящиков пользователь вводит IP-адрес сервера в командной строке. Если IP-адрес не указан, используется заданный по умолчанию адрес 127.0.0.1, соответствующий локальной системе. • Для отправки и приема сообщений применяются функции, например, ReceiveMessage, которые здесь не представлены. • Номер порта, SERVER_PORT, определен в заголовочном файле ClntSrvr.h. Хотя код написан для выполнения под управлением Windows, единственная зависимость от Windows связана с использованием вызовов функций, имеющих префикс WSA. Программа 12.1. clientSK: клиент на основе сокетов/* Глава 12. clientSK.с */ /* Однопоточный клиент командной строки. */ /* ВЕРСИЯ НА ОСНОВЕ WINDOWS SOCKETS. */ /* Считывает последовательность команд для пересылки серверному процессу*/ /* через соединение с сокетом. Дожидается ответа и отображает его. */ #define _NOEXCLUSIONS /* Требуется для включения определений сокета. */ #include "EvryThng.h" #include "ClntSrvr.h" /* Определяет структуры записей запроса и ответа. */ /* Функции сообщения для обслуживания запросов и ответов. */ /* Кроме того, ReceiveResponseMessage отображает полученные сообщения. */ static DWORD SendRequestMessage(REQUEST *, SOCKET); static DWORD ReceiveResponseMessage(RESPONSE *, SOCKET); struct sockaddr_in ClientSAddr; /* Адрес сокета клиента. */ int _tmain(DWORD argc, LPTSTR argv[]) { SOCKET ClientSock = INVALID_SOCKET; REQUEST Request; /* См. ClntSrvr.h. */ RESPONSE Response; /* См. ClntSrvr.h. */ WSADATA WSStartData; /* Структура данных библиотеки сокета. */ BOOL Quit = FALSE; DWORD ConVal, j; TCHAR PromptMsg[] = _T("\nВведите команду> "); TCHAR Req[MAX_RQRS_LEN]; TCHAR QuitMsg[] = _T("$Quit"); /* Запрос: завершить работу клиента. */ TCHAR ShutMsg[] = _T("$ShutDownServer"); /* Остановить все потоки. */ CHAR DefaultIPAddr[] = "127.0.0.1"; /* Локальная система. */ /* Инициализировать библиотеку WSA; задана версия 2.0, но будет работать и версия 1.1. */ WSAStartup(MAKEWORD(2, 0), &WSStartData); /* Подключиться к серверу. */ /* Следовать стандартной процедуре вызова последовательности функций socket/connect клиентом. */ ClientSock = socket(AF_INET, SOCK_STREAM, 0); memset(&ClientSAddr, 0, sizeof(ClientSAddr)); ClientSAddr.sin_family = AF_INET; if (argc >= 2) ClientSAddr.sin_addr.s_addr = inet_addr(argv [1]); else ClientSAddr.sin_addr.s_addr = inet_addr(DefaultIPAddr); ClientSAddr.sin_port = htons(SERVER_PORT); /* Номер порта определен равным 1070. */ connect(ClientSock, (struct sockaddr *)&ClientSAddr, sizeof(ClientSAddr)); /* Основной цикл для вывода приглашения на ввод команд, посылки запроса и получения ответа. */ while (!Quit) { _tprintf(_T("%s"), PromptMsg); /* Ввод в формате обобщенных строк, но команда серверу должна указываться в формате ASCII. */ _fgetts(Req, MAX_RQRS_LEN-1, stdin); for (j = 0; j <= _tcslen(Req) Request.Record[j] = Req[j]; /* Избавиться от символа новой строки в конце строки. */ Request.Record[strlen(Request.Record) – 1] = '\0'; if (strcmp(Request.Record, QuitMsg) == 0 || strcmp(Request.Record, ShutMsg) == 0) Quit = TRUE; SendRequestMessage(&Request, ClientSock); ReceiveResponseMessage(&Response, ClientSock); } shutdown(ClientSock, 2); /* Запретить посылку и прием сообщений. */ closesocket(ClientSock); WSACleanup(); _tprintf(_T("\n****Выход из клиентской программы\n")); return 0; } Пример: усовершенствованный сервер на основе сокетовПрограмма serverSK (программа 12.2) аналогична программе serverNP (программа 11.3), являясь ее видоизмененным и усовершенствованным вариантом. • В усовершенствованном варианте программы серверные потоки создаются по требованию (on demand), а не в виде пула потоков фиксированного размера. Каждый раз, когда сервер принимает запрос клиента на соединение, создается серверный рабочий поток, и когда клиент прекращает работу, выполнение потока завершается. • Сервер создает отдельный поток приема (accept thread), что позволяет основному потоку опрашивать глобальный флаг завершения работы, пока вызов accept остается блокированным. Хотя сокеты и могут определяться как неблокирующиеся, потоки обеспечивают удобное универсальное решение. Следует отметить, что значительная часть расширенных функциональных возможностей Winsock призвана поддерживать асинхронные операции, тогда как потоки Windows дают возможность воспользоваться более простой и близкой к стандартам функциональностью синхронного режима работы сокетов. • За счет некоторого усложнения программы усовершенствовано управление потоками, что позволило обеспечить поддержку состояний каждого потока. • Данный сервер поддерживает также внутрипроцессные серверы (in-process servers), что достигается путем загрузки библиотеки DLL во время инициализации. Имя библиотеки DLL задается в командной строке, и серверный поток сначала пытается определить точку входа этой DLL. В случае успеха серверный поток вызывает точку входа DLL; в противном случае сервер создает процесс аналогично тому, как это делалось в программе serverNP. Пример DLL приведен в программе 12.3. Поскольку генерация исключений библиотекой DLL будет приводить к уничтожению всего серверного процесса, вызов функции DLL защищен простым обработчиком исключений. При желании можно включить внутрипроцессные серверы и в программу serverNP. Самым большим преимуществом внутрипроцессных серверов является то, что они не требуют никакого контекстного переключения на другие процессы, в результате чего производительность может заметно улучшиться. Поскольку в коде сервера использованы специфические для Windows возможности, в частности, возможности управления потоками и некоторые другие, он, в отличие от кода клиента, оказывается привязанным к Windows. Программа 12.2. serverSK: сервер на основе сокета с внутрипроцессными серверами/* Глава 12. Клиент-серверная система. ПРОГРАММА СЕРВЕРА. ВЕРСИЯ НА ОСНОВЕ СОКЕТА. */ /* Выполняет указанную в запросе команду и возвращает ответ. */ /* Если удается обнаружить точку входа разделяемой библиотеки, команды */ /* выполняются внутри процесса, в противном случае – вне процесса. */ /* ДОПОЛНИТЕЛЬНАЯ ВОЗМОЖНОСТЬ: argv [1] может содержать имя библиотеки */ /* DLL, поддерживающей внутрипроцессные серверы. */ #define _NOEXCLUSIONS #include "EvryThng.h" #include "ClntSrvr.h" /* Определяет структуру записей запроса и ответа. */ struct sockaddr_in SrvSAddr; /* Адресная структура сокета сервера. */ struct sockaddr_in ConnectSAddr; /* Подключенный сокет. */ WSADATA WSStartData; /* Структура данных библиотеки сокета. */ typedef struct SERVER_ARG_TAG { /* Аргументы серверного потока. */ volatile DWORD number; volatile SOCKET sock; volatile DWORD status; /* Пояснения содержатся в комментариях к основному потоку. */ volatile HANDLE srv_thd; HINSTANCE dlhandle; /* Дескриптор разделяемой библиотеки. */ } SERVER_ARG; volatile static ShutFlag = FALSE; static SOCKET SrvSock, ConnectSock; int _tmain(DWORD argc, LPCTSTR argv[]) { /* Прослушивающий и подключенный сокеты сервера. */ BOOL Done = FALSE; DWORD ith, tstatus, ThId; SERVER_ARG srv_arg[MAX_CLIENTS]; HANDLE hAcceptTh = NULL; HINSTANCE hDll = NULL; /* Инициализировать библиотеку WSA; задана версия 2.0, но будет работать и версия 1.1. */ WSAStartup(MAKEWORD(2, 0), &WSStartData); /* Открыть динамическую библиотеку команд, если ее имя указано в командной строке. */ if (argc > 1) hDll = LoadLibrary(argv[1]); /* Инициализировать массив arg потока. */ for (ith = 0; ith < MAXCLIENTS; ith++) { srv_arg[ith].number = ith; srv_arg[ith].status = 0; srv_arg[ith].sock = 0; srv_arg[ith].dlhandle = hDll; srv_arg[ith].srv_thd = NULL; } /* Следовать стандартной процедуре вызова последовательности функций socket/bind/listen/accept клиентом. */ SrvSock = socket(AF_INET, SOCK_STREAM, 0); SrvSAddr.sin_family = AF_INET; SrvSAddr.sin_addr.s_addr = htonl(INADDR_ANY); SrvSAddr.sin_port = htons(SERVER_PORT); bind(SrvSock, (struct sockaddr *)&SrvSAddr, sizeof SrvSAddr); listen(SrvSock, MAX_CLIENTS); /* Основной поток становится потоком прослушивания/соединения/контроля.*/ /* Найти пустую ячейку в массиве arg потока сервера. */ /* параметр состояния: 0 – ячейка свободна; 1 – поток остановлен; 2 — поток выполняется; 3 – остановлена вся система. */ while (!ShutFlag) { for (ith = 0; ith < MAX_CLIENTS && !ShutFlag; ) { if (srv_arg[ith].status==1 || srv_arg[ith].status==3) { /* Выполнение потока завершено либо обычным способом, либо по запросу останова. */ WaitForSingleObject(srv_arg[ith].srv_thd INFINITE); CloseHandle(srv_arg[ith].srv_tnd); if (srv_arg[ith].status == 3) ShutFlag = TRUE; else srv_arg[ith].status = 0; /* Освободить ячейку данного потока. */ } if (srv_arg[ith].status == 0 || ShutFlag) break; ith = (ith + 1) % MAXCLIENTS; if (ith == 0) Sleep(1000); /* Прервать цикл опроса. */ /* Альтернативный вариант: использовать событие для генерации сигнала, указывающего на освобождение ячейки. */ } /* Ожидать попытки соединения через данный сокет. */ /* Отдельный поток для опроса флага завершения ShutFlag. */ hAcceptTh = (HANDLE)_beginthreadex(NULL, 0, AcceptTh, &srv_arg[ith], 0, &ThId); while (!ShutFlag) { tstatus = WaitForSingleObject(hAcceptTh, CS_TIMEOUT); if (tstatus == WAIT_OBJECT_0) break; /* Соединение установлено. */ } CloseHandle(hAcceptTh); hAcceptTh = NULL; /* Подготовиться к следующему соединению. */ } _tprintf(_T("Остановка сервера. Ожидание завершения всех потоков сервера\n")); /* Завершить принимающий поток, если он все еще выполняется. */ /* Более подробная информация об используемой логике завершения */ /* работы приведена на Web-сайте книги. */ if (hDll != NULL) FreeLibrary(hDll); if (hAcceptTh != NULL) TerminateThread(hAcceptTh, 0); /* Ожидать завершения всех активных потоков сервера. */ for (ith = 0; ith < MAXCLIENTS; ith++) if (srv_arg [ith].status != 0) { WaitForSingleObject(srv_arg[ith].srv_thd, INFINITE); CloseHandle(srv_arg[ith].srv_thd); } shutdown(SrvSock, 2); closesocket(SrvSock); WSACleanup(); return 0; } static DWORD WINAPI AcceptTh(SERVER_ARG * pThArg) { /* Принимающий поток, который предоставляет основному потоку возможность опроса флага завершения. Кроме того, этот поток создает серверный поток. */ LONG AddrLen, ThId; AddrLen = sizeof(ConnectSAddr); pThArg->sock = accept(SrvSock, /* Это блокирующий вызов. */ (struct sockaddr *)&ConnectSAddr, &AddrLen); /* Новое соединение. Создать серверный поток. */ pThArg->status = 2; pThArg->srv_thd = (HANDLE)_beginthreadex (NULL, 0, Server, pThArg, 0, &ThId); return 0; /* Серверный поток продолжает выполняться. */ } static DWORD WINAPI Server(SERVER_ARG * pThArg) /* Функция серверного потока. Поток создается по требованию. */ { /* Каждый поток поддерживает в стеке собственные структуры данных запроса, ответа и регистрационных записей. */ /* … Стандартные объявления из serverNP опущены … */ SOCKET ConnectSock; int Disconnect = 0, i; int (*dl_addr)(char *, char *); char *ws = " \0\t\n"; /* Пробелы. */ GetStartupInfo(&StartInfoCh); ConnectSock = pThArg->sock; /* Создать имя временного файла. */ sprintf(TempFile, "%s%d%s", "ServerTemp", pThArg->number, ".tmp"); while (!Done && !ShutFlag) { /* Основной командный цикл. */ Disconnect = ReceiveRequestMessage(&Request, ConnectSock); Done = Disconnect || (strcmp(Request.Record, "$Quit") == 0) || (strcmp(Request.Record, "$ShutDownServer") == 0); if (Done) continue; /* Остановить этот поток по получении команды "$Quit" или "$ShutDownServer". */ hTrapFile = CreateFile(TempFile, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, &TempSA, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); /* Проверка наличия этой команды в DLL. Для упрощения команды */ /* разделяемой библиотеки имеют более высокий приоритет по сравнению */ /* с командами процесса. Прежде всего, необходимо извлечь имя команды.*/ i = strcspn(Request.Record, ws); /* Размер лексемы. */ memcpy(sys_command, Request.Record, i) ; sys_command[i] = '\0'; dl_addr = NULL; /* Будет установлен в случае успешного выполнения функции GetProcAddress. */ if (pThArg->dlhandle != NULL) {/* Проверка поддержки "внутрипроцессного" сервера. */ dl_addr = (int (*)(char *, char *))GetProcAddress(pThArg->dlhandle, sys_command); if (dl_addr != NULL) __try { /* Защитить серверный процесс от исключений, возникающих в DLL*/ (*dl_addr)(Request.Record, TempFile); } __except (EXCEPTION_EXECUTE_HANDLER) { ReportError(_T("Исключение в DLL"), 0, FALSE); } } if (dl_addr == NULL) { /* Поддержка внутрипроцессного сервера отсутствует. */ /* Создать процесс для выполнения команды. */ /* … То же, что в serverNP … */ } /* … То же, что в serverNP … */ } /* Конец основного командного цикла. Получить следующую команду. */ /* Конец командного цикла. Освободить ресурсы; выйти из потока. */ _tprintf(_T("Завершение работы сервера# %d\n"), pThArg->number); shutdown(ConnectSock, 2); closesocket(ConnectSock); pThArg->status = 1; if (strcmp(Request.Record, "$ShutDownServer") == 0) { pThArg->status = 3; ShutFlag = TRUE; } return pThArg->status; } Замечания по поводу безопасностиВ том виде, как она здесь представлена, данная клиент-серверная система не является безопасной. Если на вашей системе выполняется сервер и кому-то известен номер порта, через который вы работаете, и имя компьютера, то он может атаковать вашу систему. Другой пользователь, запустив клиентскую программу на своем компьютере, сможет выполнить на вашей системе команды, позволяющие, например, удалить или изменить файлы. Полное обсуждение методов построения безопасных систем выходит за рамки данной книги. Тем не менее, в главе 15 показано, как обезопасить объекты Windows, а в упражнении 12.14 предлагается воспользоваться протоколом SSL. Внутрипроцессные серверыКак ранее уже отмечалось, основное усовершенствование программы serverSK связано с включением в нее внутрипроцессных серверов. В программе 12.3 показано, как написать библиотеку DLL, обеспечивающую услуги подобного рода. В программе представлены две уже известные вам функции — функция, осуществляющая подсчет слов, и функция toupper. В соответствии с принятым соглашением первым параметром является командная строка, а вторым — имя выходного файла. Кроме того, следует всегда помнить о том, что функция будет выполняться в том же потоке, что и сервер, и это диктует необходимость соблюдения жестких требований относительно безопасности потоков, включая, но не ограничиваясь только этим, следующее: • Функции никоим образом не должны изменять окружение процесса. Например, если одна из функций изменит рабочий каталог, то это окажет воздействие на весь процесс. • Аналогично, функции не должны перенаправлять стандартный ввод и вывод. • Такие ошибки программирования, как выход индекса или указателя за пределы отведенного диапазона или переполнение стека, могут приводить к порче памяти, относящейся к другому потоку или самому процессу. • Утечка ресурсов, возникшая, например, в результате того, что системе не была своевременно возвращена освободившаяся память или не были закрыты дескрипторы, в конечном счете, окажет отрицательное воздействие на работу всей серверной системы. Столь жесткие требования не предъявляются к процессам по той причине, что один процесс, как правило, не может нанести ущерб другим процессу, а после того, как процесс завершает свое выполнение, занимаемые им ресурсы автоматически освобождаются. В связи с этим служба, как правило, разрабатывается и отлаживается как поток, и лишь после того, как появится уверенность в надежности ее работы, она преобразуется в DLL. В программе 12.3 представлена небольшая библиотека DLL, включающая две функции. Программа 12.3. command: пример внутри процессных серверов/* Глава 12. commands.с. */ /* Команды внутрипроцессного сервера для использования в serverSK и так далее. */ /* Имеется несколько команд, реализованных в виде библиотек DLLs. */ /* Функция каждой команды принимает два параметра и обеспечивает */ /* безопасное выполнение в многопоточном режиме. Первым параметром */ /* является строка: команда arg1 arg2 … argn */ /* (то есть обычная командная строка), а вторым – имя выходного файла. … */ static void extract_token(int, char *, char *); _declspec(dllexport) int wcip(char * command, char * output_file) /* Счетчик слов; внутрипроцессный. */ /* ПРИМЕЧАНИЕ: упрощенная версия; результаты могут отличаться от тех, которые обеспечивает утилита wc. */ { extract_token(1, command, input_file); fin = fopen(input_file, "r"); /* … */ ch = nw = nc = nl = 0; while ((c = fgetc(fin)) != EOF) { /* … Стандартный код — для данного примера не является существенным … */ } fclose(fin); /* Записать результаты. */ fout = fopen(output_file, "w"); if (fout == NULL) return 2; fprintf(fout, " %9d %9d %9d %s\n", nl, nw, nc, input_file); fclose(fout); return 0; } _declspec(dllexport) int toupperip(char * command, char * output_file) /* Преобразует входные данные к верхнему регистру; выполняется внутри процесса. */ /* Вторая лексема задает входной файл (первая лексема – "toupperip"). */ { /* … */ extract_token(1, command, input_file); fin = fopen(input_file, "r"); if (fin == NULL) return 1; fout = fopen(output_file, "w"); if (fout == NULL) return 2; while ((c = fgetc (fin)) != EOF) { if (c == '\0') break; if (isalpha(c)) с = toupper(c); fputc(c, fout); } fclose(fin); fclose(fout); return 0; } static void extract_token(int it, char * command, char * token) { /* Извлекает из "команды" лексему номер "it" (номером первой лексемы */ /* является "0"). Результат переходит в "лексему" (token) */ /* В качестве разделителей лексем используются пробелы. … */ return; } Ориентированные на строки сообщения, точкив хода DLL и TLSПрограммы serverSK и clientSK взаимодействуют между собой, обмениваясь сообщениями, каждое из которых состоит из 4-байтового заголовка, содержащего размер сообщения, и собственно содержимого. Обычной альтернативой такому подходу служат сообщения, отделяемые друг от друга символами конца строки (или перевода строки). Трудность работы с такими сообщениями заключается в том, что длина сообщения заранее не известна, в связи с чем приходится проверять каждый поступающий символ. Однако получение по одному символу за один раз крайне неэффективно, и поэтому символы сохраняются в буфере, содержимое которого может включать один или несколько символов конца строки и составные части одного или нескольких сообщений. При этом в промежутках между вызовами функции получения сообщений необходимо поддерживать неизменным содержимое и состояние буфера. В однопоточной среде для этой цели могут быть использованы ячейки статической памяти, но совместное использование несколькими потоками одной и той же статической переменной невозможно. В более общей формулировке, мы сталкиваемся здесь с проблемой сохранения долговременных состояний в многопоточной среде (multithreaded persistent state problem). Эта проблема возникает всякий раз, когда безопасная в отношении многопоточного выполнения функция должна поддерживать сохранение некоторой информации от одного вызова функции к другому. Такая же проблема возникает при работе с функцией strtook, входящей в стандартную библиотеку С, которая предназначена для просмотра строки для последовательного нахождения экземпляров определенной лексемы. Решение проблемы долговременных состояний в многопоточной средеВ искомом решении сочетаются несколько компонентов: • Библиотека DLL, в которой содержатся функции, обеспечивающие отправку и прием сообщений. • Функция, представляющая точку входа в DLL. • Локальная область хранения потока (TLS, глава 7). Подключение процесса к библиотеке сопровождается созданием индекса DLL, а отключение — уничтожением. Значение индекса хранится в статическом хранилище, доступ к которому имеют все потоки. • Структура, в которой хранится буфер и его текущее состояние. Структура распределяется всякий раз, когда к библиотеке подключается новый поток, и его адрес сохраняется в записи TLS для данного потока. При отсоединении потока от библиотеки память, занимаемая его структурой, освобождается. Таким образом, TLS играет роль статического хранилища, и у каждого потока имеется собственная уникальная копия этого хранилища. Пример: безопасная многопоточная DLL для обмена сообщениями через сокетПрограмма 12.4 представляет собой DLL, содержащую две функции для обработки символьных строк (в именах которых в данном случае присутствует "CS", от character string — строка символов), или потоковые функции сокета (socket streaming functions): SendCSMessage и ReceiveCSMessage, а также точку входа DllMain (см. главу 5). Указанные две функции играют ту же роль, что и функция ReceiveMessage, а также функции, использованные в программах 12.1 и 12.2, и фактически заменяют их. Функция DllMain служит характерным примером решения проблемы долговременных состояний в многопоточной среде и объединяет TLS и библиотеки DLL. Освобождать ресурсы при отсоединении потоков (случай DLL_THREAD_DETACH) особенно важно в случае серверной среды; если этого не делать, то ресурсы сервера, в конечном счете, исчерпаются, что может привести к сбоям в его работе или снижению производительности или к тому и другому одновременно. Программа 12.4. SendReceiveSKST: безопасная многопоточная DLL /* SendReceiveSKST.с — DLL многопоточного потокового сокета. */ /* В качестве разделителей сообщений используются символы конца */ /* строки ('\0'), так что размер сообщения заранее не известен. */ /* Поступающие данные буферизуются и сохраняются в промежутках между */ /* вызовами функций. */ /* Для этой цели используются локальные области хранения потоков */ /* (Thread Local Storage, TLS), обеспечивающие каждый из потоков */ /* собственным закрытым "статическим хранилищем". */ #define _NOEXCLUSIONS #include "EvryThng.h" #include "ClntSrvr.h" /* Определяет записи запроса и ответа. */ typedef struct STATIC_BUF_T { /* "static_buf" содержит "static_buf_len" байтов остаточных данных. */ /* Символы конца строки (нулевые символы) могут присутствовать, а могут */ /* и не присутствовать. */ char static_buf[MAX_RQRS_LEN] ; LONG32 static_buf_len; } STATIC_BUF; static DWORD TlsIx = 0; /* Индекс TLS – ДЛЯ КАЖДОГО ПРОЦЕССА СВОЙ ИНДЕКС.*/ /* Для однопоточной библиотеки использовались бы следующие определения: static char static_buf [MAX_RQRS_LEN]; static LONG32 static_buf_len; */ /* Основная функция DLL. */ BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) { STATIC_BUF * pBuf; switch (fdwReason) { case DLL_PROCESS_ATTACH: TlsIx = TlsAlloc(); /* Для основного потока подключение отсутствует, поэтому во время подключения процесса необходимо выполнить также операции по подключению потока. */ case DLL_THREAD_ATTACH: /* Указать, что память не была распределена. */ TlsSetValue(TlsIx, NULL); return TRUE; /* В действительности это значение игнорируется. */ case DLL_PROCESS_DETACH: /* Отсоединить также основной поток. */ pBuf = TlsGetValue(TlsIx); if (pBuf != NULL) { free(pBuf); pBuf = NULL; } return TRUE; case DLL_THREAD_DETACH: pBuf = TlsGetValue(TlsIx); if (pBuf != NULL) { free(pBuf); pBuf = NULL; } return TRUE; } } _declspec(dllexport) BOOL ReceiveCSMessage(REQUEST *pRequest, SOCKET sd) { /* Возвращаемое значение TRUE указывает на ошибку или отсоединение. */ BOOL Disconnect = FALSE; LONG32 nRemainRecv = 0, nXfer, k; /* Должны быть целыми со знаком. */ LPSTR pBuffer, message; CHAR TempBuf[MAX_RQRS_LEN + 1]; STATIC_BUF *p; p = (STATIC_BUF *)TlsGetValue(TlsIx); if (p == NULL) { /* Инициализация при первом вызове. */ /* Распределять это хранилище будут только те потоки, которым оно */ /* необходимо. Другие типы потоков могут использовать TLS для иных целей. */ р = malloc(sizeof(STATIC_BUF)); TlsSetValue(TlsIx, p); if (p == NULL) return TRUE; /* Ошибка. */ p->static_buf_len = 0; /* Инициализировать состояние. */ } message = pRequest->Record; /* Считать до символа новой строки, оставляя остаточные данные в статическом буфере. */ for (k = 0; k < p->static_buf_len && p->static_buf[k] != '\0'; k++) { message[k] = p->static_buf[k]; } /* k – количество переданных символов. */ if (k < p->static_buf_len) { /* В статическом буфере обнаружен нулевой символ. */ message[k] = '\0'; p->static_buf_len –= (k + 1); /* Скорректировать состояние статического буфера. */ memcpy(p->static_buf, &(p->static_buf[k + 1]), p->static_buf_len); return FALSE; /* Входные данные сокета не требуются. */ } /* Передан весь статический буфер. Признак конца строки не обнаружен.*/ nRemainRecv = sizeof(TempBuf) – 1 – p->static_buf_len; pBuffer = message + p->static_buf_len; p->static_buf_len = 0; while (nRemainRecv > 0 && !Disconnect) { nXfer = recv(sd, TempBuf, nRemainRecv, 0); if (nXfer <= 0) { Disconnect = TRUE; continue; } nRemainRecv –= nXfer; /* Передать в целевое сообщение все символы вплоть до нулевого, если таковой имеется. */ for (k =0; k < nXfer && TempBuf[k] != '\0'; k++) { *pBuffer = TempBuf[k]; pBuffer++; } if (k >= nXfer) { /*Признак конца строки не обнаружен, читать дальше*/ nRemainRecv –= nXfer; } else { /* Обнаружен признак конца строки. */ *pBuffer = '\0'; nRemainRecv = 0; memcpy(p->static_buf, &TempBuf[k + 1], nXfer – k – 1); p->static_buf_len = nXfer – k – 1; } } return Disconnect; } _declspec(dllexport) BOOL SendCSMessage(RESPONSE *pResponse, SOCKET sd) { /* Послать запрос серверу в сокет sd. */ BOOL Disconnect = FALSE; LONG32 nRemainSend, nXfer; LPSTR pBuffer; pBuffer = pResponse->Record; nRemainSend = strlen(pBuffer) + 1; while (nRemainSend > 0 && !Disconnect) { /* Отправка еще не гарантирует, что будет отослано все сообщение. */ nXfer = send(sd, pBuffer, nRemainSend, 0); if (nXfer <= 0) { fprintf(stderr, "\nОтключение сервера до посылки запроса завершения"); Disconnect = TRUE; } nRemainSend –=nXfer; pBuffer += nXfer; } return Disconnect; } Комментарии по поводу DLL и безопасной многопоточной среды• Всякий раз, когда создается новый поток, вызывается функция DllMain с опцией DLL_THREAD_ATTACH, но для основного потока отдельного вызова с опцией DLL_THREAD_ATTACH не существует. В случае основного потока должна использоваться опция DLL_PROCESS_ATTACH. • Вообще говоря, в том числе и в данном случае (возьмите, например, поток, принимающий сообщения (accept thread)), некоторым потокам распределение памяти может и не требоваться, но DllMain не в состоянии различать отдельные типы потоков. Поэтому на участке кода, соответствующем варианту выбора DLL_THREAD_ATTACH, фактического распределения памяти не происходит; здесь только инициализируется параметр TLS. Распределение памяти осуществляется точкой входа ReceiveCSMessage при первом ее вызове. Благодаря этому собственная память выделяется только тем потокам, которые в этом действительно нуждаются, и различные типы потоков получают ровно столько ресурсов, сколько им требуется. • Хотя рассматриваемая библиотека DLL и обеспечивает безопасную многопоточную поддержку, любой поток в каждый момент времени может работать только с одним сокетом, поскольку долговременные состояния ассоциируются не с сокетами, а с потоками. Этот момент учитывается в следующем примере. • Исходным кодом DLL, размещенным на Web-сайте, предусмотрен вывод общего количества вызовов DllMain в соответствии с их типами. • Даже при таком решении существует риск утечки ресурсов. Некоторые потоки, например поток приема сообщений, могут вообще не завершаться, и поэтому не будут отсоединены от библиотеки DLL. Для остающихся активных потоков функция ExitProcess вызовет DllMain с опцией DLL_PROCESS_DETACH, а не DLL_THREAD_DETACH. В данном случае никаких проблем не возникает, поскольку поток приема сообщений никаких ресурсов не распределяет, а освобождение памяти происходит по завершении процесса. Однако, проблемы возможны в тех случаях, когда потоки распределяют такие ресурсы, как временные файлы. Поэтому окончательное решение должно предусматривать создание глобально доступного списка ресурсов. Тогда участок кода, соответствующий опции DLL_PROCESS_DETACH, мог бы взять на себя просмотр этого списка и освобождение ненужных ресурсов. Пример: альтернативная стратегия создания безопасных библиотек DLL с много поточной поддержкойХотя программа 12.4 и демонстрирует пример типичного объединения TLS и DllMain для создания библиотек, обеспечивающих безопасное многопоточное выполнение, в ней имеется одно слабое место, о котором говорится в комментариях к предыдущему разделу. В частности, "состояние" ассоциируется не с сокетом, а с потоком, поэтому в каждый момент времени любой поток может работать только с одним сокетом. Эффективной альтернативой безопасной библиотеке функций является создание структуры, выступающей в качестве своего рода дескриптора, передаваемого при каждом вызове функции. Тогда состояние можно было бы хранить в этой структуре. Во многих системах на основе UNIX эта методика используется для создания безопасных библиотек С, обеспечивающих многопоточную поддержку. Основной недостаток такого подхода заключается в том, что для указания структуры состояния требуется вводить дополнительный параметр при вызове функции. Программа 12.5 является видоизмененным вариантом программы 12.4. Заметьте, что DllMain теперь не требуется, но появились две новые функции, предназначенные для инициализации и освобождения ресурсов структуры состояния. Для функций send и receive потребовались лишь самые минимальные изменения. Соответствующая программа сервера, serverSKHA, доступна на Web-сайте книги и содержит лишь незначительные изменения, обеспечивающие создание и закрытие дескриптора сокета (НА означает "handle" — дескриптор). Программа 12.5. SendReceiveSKHA: безопасная многопоточная DLL со структурой состояния/* SendReceiveSKHA.с – многопоточный потоковый сокет. */ /* Данная программа представляет собой модифицированную версию программы*/ /* SendReceiveSKST.c, которая иллюстрирует другую методику, основанную */ /* на безопасной библиотеке с многопоточной поддержкой. */ /* Состояние сохраняется не в TLS, а в структуре состояния, напоминающей*/ /* дескриптор HANDLE. Благодаря этому поток может использовать сразу */ /* несколько сокетов. Сообщения разделяются символами конца строки ('\0')*/ #define _NOEXCLUSIONS #include "EvryThng.h" #include "ClntSrvr.h " /* Определяет записи запроса и ответа. */ typedef struct SOCKET_HANDLE_T { /* Текущее состояние сокета в структуре "handle". */ /* Структура содержит "static_buf_len" символов остаточных данных. */ /* Символы конца строки (нулевые символы) могут присутствовать, */ /* а могут и не присутствовать. */ SOCKET sk; /* Сокет, связанный с указанной структурой "handle". */ char static_buf[MAX_RQRS_LEN]; LONG32 static_buf_len; } SOCKET_HANDLE, * PSOCKET_HANDLE; /* Функции для создания и закрытия "дескрипторов потоковых сокетов". */ _declspec(dllexport) PVOID CreateCSSocketHandle(SOCKET s) { PVOID p; PSOCKET_HANDLE ps; p = malloc(sizeof(SOCKET_HANDLE)); if (p == NULL) return NULL; ps = (PSOCKET_HANDLE)p; ps->sk = s; ps->static_buf_len = 0; /* Инициализировать состояние буфера. */ return p; } _declspec(dllexport) BOOL CloseCSSocketHandle(PVOID p) { if (p == NULL) return FALSE; free(p); return TRUE; } _declspec(dllexport) BOOL ReceiveCSMessage(REQUEST *pRequest, PVOID sh) /* Тип PVOID используется для того, чтобы избежать включения */ /* в вызывающую программу определения структуры SOCKET_HANDLE. */ { /* Возвращаемое значение TRUE указывает на ошибку или отсоединение. … */ PSOCKET_HANDLE p; SOCKET sd; р = (PSOCKET_HANDLE)sh; if (p == NULL) return FALSE; sd = p->sk; /* Этим исчерпываются все отличия от SendReceiveSKST! … */ } _declspec(dllexport) BOOL SendCSMessage(RESPONSE *pResponse, PVOID sh) { /* Послать запрос серверу в сокет sd. … */ SOCKET sd; PSOCKET_HANDLE p; p = (PSOCKET_HANDLE)sh; if (p == NULL) return FALSE; sd = p->sk; /* Этим исчерпываются все отличия от SendReceiveSKST! … */ } ДейтаграммыДейтаграммы аналогичны почтовым ящикам и используются при сходных обстоятельствах. Соединение между отправителем и получателем отсутствует, а получателей может быть несколько. Ни почтовые ящики, ни дейтаграммы не гарантируют доставку данных получателю, а последовательные сообщения не обязательно будут получены в той же очередности, в которой они были отправлены. Первым шагом при использовании дейтаграмм является создание сокета посредством вызова функции socket с указанием значения SOCK_DGRAM в поле type. Далее необходимо использовать функции sendto и recvfrom, которые принимают те же аргументы, что и функции send и recv, но имеют по два дополнительных аргумента, относящихся к станции-партнеру. Так, функция sendto имеет следующий прототип: int sendto(SOCKET s, LPSTR lpBuffer, int nBufferLen, int nFlags, LPSOCKADDR lpAddr, int nAddrLen); lpAddr — указывает на адресную структуру, в которой вы можете задать имя конкретной системы и номер порта или же указать на необходимость рассылки дейтаграммы заданной совокупности систем. Используя функцию recvfrom, вы указываете систему или системы (возможно, все), от которых вы хотите принимать дейтаграммы. Использование дейтаграмм для удаленного вызова процедурОбычно дейтаграммы применяются для реализации RPC. По сути дела, в самых распространенных ситуациях клиент посылает запрос серверу, используя дейтаграммы. Поскольку доставка запроса не гарантируется, клиент должен повторно передать запрос, если по истечении заданного периода ожидания ответ от сервера (для посылки которого также используются дейтаграммы) не получен. Сервер должен быть готов к тому, что один и тот же запрос может направляться ему несколько раз. Важно отметить, что ни клиенту, ни серверу RPC служебные сигналы, которые, например, необходимы при образовании соединения через потоковый сокет, не требуются; вместо этого они связываются друг с другом посредством запросов и ответов. В качестве дополнительной возможности RPC может гарантировать надежность взаимодействия путем повторной передачи запросов по истечении периода ожидания, что упрощает разработку приложений. Выражаясь иначе, часто говорят о том, что клиент и сервер RPC не имеют состояния (они не хранят никакой информации относительно состояния текущего запроса или запросов, на которые еще не получен ответ). Отсюда следует, что результат обработки на сервере множества идентичных клиентских запросов будет тем же, что и результат обработки одиночного запроса. Это также значительно упрощает проектирование приложений и реализацию их логики. Сравнение Berkeley Sockets и Windows SocketsПрограммы, использующие стандартные вызовы Berkeley Sockets, будут работать и с Windows Sockets, если вы учтете следующие важные моменты: • Для инициализации Winsock DLL вы должны вызвать функцию WSAStartup. • Для закрытия сокета вы должны использовать не функцию close (которая является переносимой), а функцию closesocket (которая таковой не является). • Для закрытия библиотеки DLL вы должны вызвать функцию WSACleanup. При желании вы можете использовать типы данных Windows, например, SOCKET и LONG вместо int, как было сделано в этой главе. Программы 12.1 и 12.2 были перенесены из UNIX, и для этого потребовались самые минимальные усилия. Вместе с тем, потребовалось модифицировать DLL и разделы, осуществляющие управление процессами. В упражнении 12.13 вам предлагается перенести эти две программы обратно в UNIX. Перекрывающийся ввод/вывод с использованием Windows SocketsВ главе 14 описывается асинхронный ввод/вывод, позволяющий потоку продолжать свое выполнение в процессе выполнения операции ввода/вывода. В той же главе обсуждается и совместное использование сокетов с асинхронным вводом/выводом Windows. Большинство задач, связанных с программированием асинхронных операций, можно легко решить, применяя однотипный подход с использованием потоков. Так, в программе serverSK вместо неблокирующегося сокета используется принимающий поток (accept thread). Тем не менее, порты завершения ввода/вывода, связанные с асинхронным вводом/выводом, играют важную роль в обеспечении масштабируемости в случае большого количества клиентов. Эта тема также рассматривается в главе 14. Windows Sockets 2Версия Windows Sockets 2 вводит новые сферы функциональности и доступна на всех системах Windows, хотя системы Windows 9x требуют установки пакета обновления. В примерах использована версия 2.0, но можно применять и версию 1.1, если требуется совместимость с необновленными системами Windows 9х. Кроме того, возможностей версии 1.1 в большинстве случаев вам будет вполне достаточно. Версия Windows Sockets 2.0 обеспечивает, в частности, следующие возможности: • Стандартизованная поддержка перекрывающегося ввода/вывода (см. главу 14). Эту возможность можно считать самым важным усовершенствованием. • Фрагментированный ввод/вывод (scatter/gather I/O) (при посылке и получении данных используются буферы, расположенные в памяти вразброс). • Возможность запрашивать качество обслуживания (скорость и надежность передачи информации). • Возможность групповой организации сокетов. Допускается конфигурирование качества обслуживания группы сокетов, поэтому можно не делать этого для каждого сокета по отдельности. Кроме того, входящим в группу сокетам можно назначать приоритеты. • Имеется возможность совмещения передачи прямых и обратных пакетов с запросами соединения (piggybacking). • Создание многоточечных соединений (multipoint connections) (сопоставимо с подключениями по типу конференц-связи). РезюмеИнтерфейс Windows Sockets предоставляет возможность использования API, отвечающего требованиям промышленного стандарта, что гарантирует работу ваших программ на различном оборудовании и почти полную переносимость на уровне исходного кода. Winsock способен поддержать практически любой сетевой протокол, однако в большинстве случаев применяется протокол TCP/IP. Winsock сопоставим с именованными каналами (и почтовыми ящиками) как в отношении функциональных возможностей, так и в отношении производительности, в наибольшей степени проявляя свои преимущества в тех случаях, когда на первый план выступают вопросы совместимости и переносимости программного обеспечения. Имейте в виду, что сокеты ввода/вывода не являются атомарными, поэтому необходимо специально заботиться о том, чтобы сообщения передавались полностью. В этой главе были изложены наиболее существенные сведения о Winsock, которых достаточно для построения работоспособной системы. Вместе с тем, за рамками нашего рассмотрения осталось очень многое, в том числе и применение Winsock в асинхронных операциях; для получения более подробной информации по этому вопросу обратитесь к источникам, указанным в разделе "Дополнительная литература". Кроме того, в этой главе были приведены примеры использования библиотек DLL для реализации внутрипроцессных серверов и создания безопасных в отношении многопоточного выполнения библиотек. В следующих главахВ главах 11 и 12 было показано, как разрабатывать серверы, отвечающие на запросы клиентов. Серверы, в их различных воплощениях, являются распространенным типом приложений Windows. В главе 13 описываются службы Windows (Windows Services), которые обеспечивают стандартный способ создания серверов и управления ими в виде служб, что дает возможность организовать их запуск, остановку и мониторинг в автоматическом режиме. В главе 13 показано, как превратить сервер в управляемую службу. Дополнительная литератураWindows SocketsСокетам Windows посвящена книга [28], а также сайт поддержки http://www.sockets.com. Однако указанная книга во многих аспектах устарела, и в ней не используются потоки. Более полезными для многих читателей будут книги, которые упоминаются ниже. Berkeley Sockets и TCP/IPВ книге [41] рассмотрены не только сокеты, но и многое другое, тогда как в первых двух томах этой серии описаны протоколы и их реализация. Исчерпывающее рассмотрение интересующего нас вопроса содержится в книге [42], которая представляет ценность даже для тех, кто имеет дело с другими семействами ОС. Среди источников, заслуживающих внимания, можно назвать [8] и [12]. Упражнения12.1. Используя функцию WSAStartup, определите старший и младший номера версий Winsock, поддерживаемые на доступных вам системах. 12.2. Используйте программу JobShell из главы 6 для запуска сервера и нескольких клиентов, причем каждый клиент должен создаваться с опцией "отсоединения" (-d). Для окончания работы остановите сервер, послав сигнал управляющего события консоли посредством команды kill. Можете ли вы предложить какие-либо улучшения в организации остановки сервера в программе serverSK. 12.3. Модифицируйте программы клиента и сервера (программы 12.1 и 12.2) таким образом, чтобы для обнаружения сервера использовались дейтаграммы. В качестве отправной точки может быть использовано решение на основе почтового ящика из главы 11. 12.4. Модифицируйте сервер именованного канала из главы 11 (программа 11.3) таким образом, чтобы в нем использовались не потоки из пула потоков сервера, а потоки, создаваемые по требованию. Вместо предварительного указания максимально допустимого количества экземпляров именованного канала предоставьте системе возможность самостоятельно определять максимальное значение этого параметра. 12.5. Проведите эксперименты, чтобы определить, действительно ли внутрипроцессные серверы работают быстрее внепроцессных. Для этого, например, может быть использована программа подсчета слов (программа 12.3); имеется исполняемый файл этой программы (wc), а также функция библиотеки DLL, представленная в программе 12.3. 12.6. Количество клиентов, поддержку которых может обеспечить программа serverSK, ограничивается размером массива аргументов потоков сервера. Модифицируйте программу, сняв это ограничение. Для этого вам потребуется создать структуру данных, позволяющую добавлять и удалять аргументы потоков, а также обеспечить возможность просмотра структуры с целью отслеживания потоков сервера, завершивших выполнение. 12.7. Разработайте внутрипроцессные серверы другого рода. Например, с этой целью преобразуйте соответствующим образом программу grep (см. главу 6). 12.8. Усовершенствуйте сервер (программа 12.2) таким образом, чтобы можно было указывать несколько библиотек DLL в командной строке. Если разместить все DLL в памяти невозможно, разработайте стратегию их загрузки и выгрузки. 12.9. Исследуйте функцию setsockopt и использование опции SO_LINGER. Примените указанную опцию в одном из примеров сервера. 12.10. Используйте возможности фрагментированного ввода/вывода Windows Sockets 2.0 для упрощения функций отправки и приема сообщений в программах 12.1 и 12.2. 12.11. Обеспечьте невозможность утечки ресурсов в программе serverSK (за дополнительными разъяснениями обратитесь к упражнению 11.8). Проделайте то же самое с программой serverSKST, которая была модифицирована для использования DLL в программе 12.4. 12.12. Расширьте возможности обработчика исключений в программе 12.3 таким образом, чтобы он заносил информацию об исключении и типе исключения в конец временного файла, используемого для сохранения результатов работы сервера. 12.13. Расширенное упражнение (требуется дополнительное оборудование). Если у вас имеется доступ к UNIX-системе, связанной через сеть с Windows-системой, перенесите на UNIX-систему программу clientSK и попытайтесь, получив с ее помощью доступ к программе serverSK, запускать различные Windows-программы. Разумеется, при этом вам придется преобразовать такие типы данных, как DWORD или SOCKET, в другие типы (в данном случае, соответственно, в unsigned int и int). Кроме того, вы должны убедиться в том, что данные, образующие сообщения, передаются в формате с обратным порядком байтов. Для выполнения соответствующих преобразований данных используйте такие функции, как htonl. Наконец, перенесите в UNIX программу serverSK, чтобы Windows-системы могли выполнять команды в UNIX. Преобразуйте вызов DLL в вызовы функций разделяемой библиотеки. 12.14. Ознакомьтесь с протоколом защищенных сокетов (Secure Sockets Layer, SSL), обратившись к материалам MSDN или источникам, указанным в разделе "Дополнительная литература". Усовершенствуйте программы, применив SSL для обеспечения безопасности связи клиента с сервером.s |
|
||
Главная | В избранное | Наш E-MAIL | Добавить материал | Нашёл ошибку | Наверх |
||||
|