Во второй главе нашей книги мы рассказали вам о том, как установить соединение с использованием протокола TCP/IP. Вы также научились работать в Internet в среде операционных систем Microsoft Windows 95, Microsoft Windows NT, а также OS/2 Warp Connect.
Нашей следующей задачей будет освоение основ программирования для сетей TCP/IP. Теперь мы расскажем вам о том, как составлять сетевые приложения, выполняющие обмен данными по глобальным и локальным сетям с использованием данного протокола. При этом из-за недостатка места в книге мы ограничимся описанием программного интерфейса Windows Sockets, который имеется в операционных системах Microsoft Windows 95 и Microsoft Windows NT.
Интерфейс Windows Sockets предоставляет в ваше распоряжение удобные средства организации передачи данных с использованием датаграмм и каналов связи между узлами сети.
В восьмом томе "Библиотеки системного программиста", который называется "Локальные сети персональных компьютеров. Использование протоколов IPX, SPX, NETBIOS " мы приводили соновные сведения об использовании протоколов IPX, SPX и NETBIOS в программах, передающих данные по сети Novell NetWare. Интерфейс Windows Sockets позволяет передавать данные не только с использованием протокола TCP/IP, но и других протоколов, например, IPX/SPX. Вы можете узнать об этом подробнее из документации, которая поставляется в составе SDK для программного интерфейса WIN32.
В локальных и глобальных сетях существует два принципиально разных способа передачи данных.
Первый из них предполагает посылку пакетов данных от одного узла другому (или сразу нескольким узлам) без получения подтверждения о доставке и даже без гарантии того, что передаваемые пакеты будут получены в правильной последовательности. Примером такого протокола может служить протокол UDP (User Datagram Protocol ), который используется в сетях TCP/IP, или протокол IPX, который является базовым в сетях Novell NetWare.
Основные преимущества датаграмных протоколов заключаются в высоком быстродействии и возможности широковещательной передачи данных, когда один узел отправляет сообщения, а другие их получают, причем все одновременно.
Второй способ передачи данных предполагает создание канала передачи данных между двумя различными узлами сети. При этом канал создается средствами датаграммных протоколов, однако доставка пакетов в канале является гарантированной. Пакеты всегда доходят в целостности и сохранности, причем в правильном порядке, хотя быстродействие получается в среднем ниже за счет посылки подтверждений. Примерами протоколов, использующих каналы связи, могут служить протоколы TCP и SPX (протокол NETBIOS допускает передачу данных с использованием как датаграмм, так и каналов связи).
Для передачи данных с использованием любого из перечисленных выше способов каждое приложение должно создать объект, который называется сокетом.
По своему назначению сокет больше всего похож на идентификатор файла (file handle), который нужен для выполнения над файлом операций чтения или записи. Прежде чем приложение, запущенное на узле сети сможет выполнять передачу или прием данных, оно должно создать сокет и проинициализировать его, указав некоторые параметры.
Для сокета необходимо указать три параметра. Это IP адрес, связанный с сокетом, номер порта, для которого будут выполняться операции передачи данных, а также тип сокета.
Что касается последнего параметра (тип сокета), то существуют сокеты двух типов. Первый тип предназначен для передачи данных в виде датаграмм, второй - с использованием каналов связи.
В процессе инициализации приложение должно зарегистрировать себя в библиотеке WSOCK32.DLL, которая предоставляет приложениям интерфейс Windows Sockets в среде операционных систем Microsoft Windows 95 и Microsoft Windows NT.
Для инициализации необходимо вызвать функцию WSAStartup, определенную следующим образом:
int WSAStartup (WORD wVersionRequested, LPWSADATA lpWSAData);
В параметре wVersionRequested вы должны указать версию интерфейса Windows Sockets, необходимую для работы вашего приложения. Старший байт параметра указывает младший номер версии (minor version ), младший байт - старший номер версии (major version ).
Перед вызовом функции WSAStartup параметр lpWSAData должен содержать указатель на структуру типа WSADATA, в которую будут записаны сведения о конкретной реализации интерфейса Windows Sockets.
В случае успеха функция WSAStartup возвращает
нулевое значение. Если происходит ошибка,
возвращается одно из следующих значений:
Значение |
Описание |
WSASYSNOTREADY |
Сетевое программное обеспечение не
готово для работы |
WSAVERNOTSUPPORTED |
Функция не поддерживается данной
реализацией интерфейса Windows Sockets |
WSAEINVAL |
Библиотека DLL,
обеспечивающая интерфейсe Windows Sockets, не
соответствуетверсии, указанной приложением
указанной в параметре wVersionRequested |
Ниже мы представили фрагмент исходного текста приложения SERVER, которое будет описано ниже, выполняющий инициализацию интерфейса Windows Sockets:
rc = WSAStartup (MAKEWORD(1, 1), &WSAData); if(rc != 0) { MessageBox(NULL, "WSAStartup Error", "Error", MB_OK); return FALSE; } // Отображаем описание и версию системы Windows Sockets // в окне органа управления Statusbar wsprintf(szTemp, "Server use %s %s", WSAData.szDescription,WSAData.szSystemStatus); hwndSb = CreateStatusWindow(WS_CHILD | WS_VISIBLE | WS_BORDER | SBARS_SIZEGRIP, szTemp, hWnd, IDS_STATUSBAR);
В операционных системах Microsoft Windows 95 и Microsoft Windows NT версии 3.51 встроена система Windows Sockets версии 1.1, поэтому именно это значение мы указали при вызове функции WSAStartup.
В следующих нескольких строках только что приведенного фрагмента кода содержимое двух полей структуры типа WSADATA отображается в окне органа управления Statusbar.
Определение структуры WSADATA и указателя на нее выглядят следующим образом:
typedef struct WSAData { WORD wVersion; WORD wHighVersion; char szDescription[WSADESCRIPTION_LEN+1]; char szSystemStatus[WSASYS_STATUS_LEN+1]; unsigned short iMaxSockets; unsigned short iMaxUdpDg; char FAR * lpVendorInfo; } WSADATA ; typedef WSADATA FAR *LPWSADATA;
Использованные выше поля szDescription и szSystemStatus после вызова функции WSAStartup содержат, соответственно, описание конкретной реализации интерфейса Windows Socket и текущее состояние этого интерфейса в виде текстовых строк.
В полях wVersion и wHighVersion записаны, соответственно, версия спецификации Windows Socket, которую будет использовать приложение, и версия спецификации, которой соответствует конкретная реализация интерфейса Windows Socket.
Приложение может одновременно создавать несколько сокетов, например, для использования в разных подзадачах одного процесса. В поле iMaxSockets хранится максимальное количество сокетов, которое можно получить для одного процесса.
В поле iMaxUdpDg записан максимальный размер пакета данных, который можно переслать с использованием датаграммного протокола UDP.
И, наконец, поле lpVendorInfo содержит указатель на дополнительную информацию, формат которой зависит от фирмы-изготовителя конкретной реализации системы Windows Sockets.
Перед тем, как завершить свою работу, приложение должно освободить ресурсы, полученные у операционной системы для работы с Windows Sockets. Для выполнения этой задачи приложение должно вызвать функцию WSACleanup, определенную так, как это показано ниже:
int WSACleanup (void);
Эта функция может возвратить нулевое значение при успехе или значение SOCKET_ERROR в случае ошибки.
Для получения кода ошибки вы должны воспользоваться функцией с именем WSAGetLastError :
int WSAGetLastError (void);
Функция WSAGetLastError позволяет определить код ошибки при неудачном завершении практически всех функций интерфейса Windows Sockets. Вы должны вызывать ее сразу вслед за функцией, завершившейся неудачно.
Если ошибка возникла при выполнении функции WSACleanup, функция WSAGetLastError может вернуть
одно из следующих значений:
Значение |
Описание |
WSANOTINITIALISED |
Интерфейс Windows Sockets не был
проинициализирован функцией WSAStartup |
WSAENETDOWN |
Сбой сетевого программного обеспечения |
WSAEINPROGRESS |
Во время вызыва функции WSACleanup выполнялась
одна из блокирующих функций интерфейса
Windows Sockets |
Сделаем небольшие пояснения относительно последней ошибки, приведенной в этом списке, и имеющей код WSAEINPROGRESS.
Некоторые функции интерфейса Windows Sockets способны блокировать работу приложения, так как они не возвращают управление до своего завершения. В операционных системах, использующих вытесняющую мультизадачность, к которым относятся Microsoft Windows 95 и Microsoft Windows NT, это не приводит к блокировке всей системы. Как вы увидите дальше, можно избежать использования блокирующих функций, так как для них в интерфейсе Windows Sockets существует замена.
После инициализации интерфейса Windows Sockets ваше приложение должно создать один или несколько сокетов, которые будут использованы для передачи данных.
Сокет создается с помощью функции socket, имеющей следующий прототип:
SOCKET socket (int af, int type, int protocol);
Параметр af определяет формат адреса. Для этого параметра вы должны указывать значение AF_INET, что соответствует формату адреса, принятому в Internet.
Параметры type и protocol определяют, сооветственно, тип сокета и протокол, который будет использован для данного сокета.
Можно указывать сокеты следующих двух типов:
Тип сокета |
Описание |
SOCK_STREAM |
Сокет будет использован для передачи
данных через канал связи с использованием
протокола TCP |
SOCK_DGRAM |
Передача данных будет выполняться без
создания каналов связи через датаграммный
протокол UDP |
Что же касается параметра protocol, то вы можете указать для него нулевое значение.
В случае успеха функция socket возвращает
дескриптор, который нужно использовать для
выполнения всех операций над данным сокетом.
Если же произошла ошибка, эта функция возвращает
значение INVALID_SOCKET. Для анализа
причины ошибки вы должны вызвать функцию
WSAGetLastError, которая в данном случае может вернуть
один из следующих кодов ошибки:
Код ошибки |
Описание |
WSANOTINITIALISED |
Интерфейс Windows Sockets не был
проинициализирован функцией WSAStartup |
WSAENETDOWN |
Сбой сетевого программного обеспечения |
WSAEAFNOSUPPORT |
Указан неправильный тип адреса |
WSAEINPROGRESS |
Выполняется блокирующая функция
интерфейса Windows Sockets |
WSAEMFILE |
Израсходован весь запас свободных
дескрипторов |
WSAENOBUFS |
Нет памяти для создания буфера |
WSAEPROTONOSUPPORT |
Указан неправильный протокол |
WSAEPROTOTYPE |
Указанный протокол несовместим с данным
типом сокета |
WSAESOCKTNOSUPPORT |
Указанный тип сокета несовместим с
данным типом адреса |
Ниже мы привели фрагмент кода, в котором создается сокет для передачи данных с использованием протокола TCP:
srv_socket = socket(AF_INET, SOCK_STREAM, 0); if(srv_socket == INVALID_SOCKET) { MessageBox(NULL, "socket Error", "Error", MB_OK); return; }
Для освобождения ресурсов приложение должно закрывать сокеты, которые ему больше не нужны, вызывая функцию closesocket :
int closesocket (SOCKET sock);
Ниже мы перечислили коды ошибок для этой
функции :
Код ошибки |
Описание |
WSANOTINITIALISED |
Перед использованием функции closesocket
необходимо вызвать функцию WSAStartup |
WSAENETDOWN |
Сбой в сети |
WSAENOTSOCK |
Указанный в параметре дескриптор не
является сокетом |
WSAEINPROGRESS |
Выполняется блокирующая функция
интерфейса Windows Sockets |
WSAEINTR |
Работа функции была отменена при помощи
функции WSACancelBlockingCall |
Перед использованием вы должны задать параметры сокета.
Для этого вы должны подготовить структуру типа sockaddr, определение которой показано ниже:
struct sockaddr { u_short sa_family; char sa_data[14]; }; typedef struct sockaddr SOCKADDR ; typedef struct sockaddr *PSOCKADDR ; typedef struct sockaddr FAR *LPSOCKADDR ;
Для работы с адресами в формате Internet используется другой вариант этой структуры, в котором детализируется формат поля sa_data:
struct sockaddr_in { short sin_family; u_short sin_port; struct in_addr sin_addr; char sin_zero[8]; }; typedef struct sockaddr_in SOCKADDR _IN; typedef struct sockaddr_in *PSOCKADDR _IN; typedef struct sockaddr_in FAR *LPSOCKADDR _IN;
Поле sin_family определяет тип адреса. Вы должны записать в это поле значение AF_INET, которое соответствует типу адреса, принятому в Internet:
srv_address.sin_family = AF_INET ;
Поле sin_port определяет номер порта, который будет использоваться для передачи данных.
Порт - это просто идентификатор программы, выполняющей обмен на сети. На одном узле может одновременно работать несколько программ, использующих разные порты.
Особенностью поля sin_port является использование так называемого сетевого формата данных. Этот формат отличается от того, что принят в процессорах с архитектурой Intel, а именно, младшие байты данных хранятся по старшим адресам памяти. Напомним, что архитектура процессоров Intel подразумевает хранение старщих байтов данных по младшим адресам.
Универсальный сетевой формат данных удобен при организации глобальных сетей, так как в узлах такой сети могут использоваться компьютеры с различной архитектурой.
Для выполнения преобразований из обычного формат в сетевой и обратно в интерфейсе Windows Sockets предусмотрен специальный набор функций. В частности, для заполнения поля sin_port нужно использовать функцию htons, выполняющую преобразование 16-разрядных данных из формата Intel в сетевой формат.
Ниже мы показали, как инициализируется поле sin_port в приложении SERVER, описанном далее:
#define SERV_PORT 5000 srv_address.sin_port = htons(SERV_PORT);
Вернемся снова к структуре sockaddr_in.
Поле sin_addr этой структуры представляет собой структуру in_addr:
struct in_addr { union { struct { u_char s_b1,s_b2,s_b3,s_b4; } S_un_b; struct { u_short s_w1,s_w2; } S_un_w; u_long S_addr; } S_un; }; #define s_addr S_un.S_addr #define s_host S_un.S_un_b.s_b2 #define s_net S_un.S_un_b.s_b1 #define s_imp S_un.S_un_w.s_w2 #define s_impno S_un.S_un_b.s_b4 #define s_lh S_un.S_un_b.s_b3
При инициализации сокета в этой структуре вы должны указать адрес IP, с которым будет работать данный сокет.
Если сокет будет работать с любым адресом (например, вы создаете сервер, который будет доступен из узлов с любым адресом), адрес для сокета можно указать следующим образом:
srv_address.sin_addr.s_addr = INADDR_ANY ;
В том случае, если сокет будет работать с определенным адресом IP ( например, вы создаете приложение-клиент, которое будет обращаться к серверу с конкретным адресом IP), в указанную структуру необходимо записать реальный адрес.
Датаграммный протокол UDP позволяет посылать пакеты данных одновременно всем рабочим станциям в широковещательном режиме. Для этого вы должны указать адрес как INADDR_BROADCAST.
Если вам известен адрес в виде четырех десятичных чисел, разделенных точкой (именно так его вводит пользователь), то вы можете заполнить поле адреса при помощи функции inet_addr :
dest_sin.sin_addr.s_addr = inet_addr ("200.200.200.201");
В случае ошибки функция возвращает значение INADDR_NONE, что можно использовать для проверки.
Обратное преобразование адреса IP в текстовую строку можно при необходимости легко выполнить с помощью функции inet_ntoa, имеющей следующий прототип:
char FAR * inet_ntoa (struct in_addr in);
При ошибке эта функция возвращает значение NULL.
Однако чаще всего пользователь работает с доменными именами, используя сервер DNS или файл HOSTS. В этом случае вначале вы должны воспользоваться функцией gethostbyname, возвращающей адрес IP, а затем записать полученный адрес в структуру sin_addr :
PHOSTENT phe; phe = gethostbyname ("ftp.microsoft.com"); if(phe == NULL) { closesocket (srv_socket); MessageBox(NULL, "gethostbyname Error", "Error", MB_OK); return; } memcpy((char FAR *)&(dest_sin.sin_addr ), phe->h_addr, phe->h_length);
В случае ошибки функция gethostbyname возвращает NULL. При этом причину ошибки можно выяснить, проверив код возврата функции WSAGetLastError.
Если же указанный узел найден в базе DNS или в файле HOSTS, функция gethostbyname возвращает указатель на структуру hostent, описанную ниже:
struct hostent { char FAR * h_name; // имя узла char FAR * FAR * h_aliases; // список альтернативных имен short h_addr type; // тип адреса узла short h_length; // длина адреса char FAR * FAR * h_addr _list; // список адресов #define h_addr h_addr_list[0] // адрес }; typedef struct hostent *PHOSTENT ; typedef struct hostent FAR *LPHOSTENT ;
Искомый адрес находится в первом элемента списка h_addr _list[0], на который можно также ссылаться при помощи h_addr. Длина поля адреса находится в поле h_length.
После того как вы подготовили структуру SOCKADDR, записав в нее параметры сокета (в частности, адрес), следует выполнить привязку адреса к сокету при помощи функции bind :
int bind ( SOCKET sock, const struct sockaddr FAR * addr, int namelen);
Параметр sock должен содержать дескриптор сокета, созданного функцией socket.
В поле addr следует записать указатель на подготовленную структуру SOCKADDR, а в поле namelen - размер этой структуры.
В случае ошибки функция bind возвращает значение
SOCKET_ERROR. Дальнейший анализ причин ошибки следует
выполнять при помощи функции WSAGetLastError. Возможные
коды ошибок перечислены ниже:
Код ошибки |
Описание |
WSANOTINITIALISED |
Перед использованием функции
необходимо вызвать функцию WSAStartup |
WSAENETDOWN |
Сбой в сети |
WSAEADDRINUSE |
Указанный адрес уже используется |
WSAEFAULT |
Значение параметра namelen меньше размера
структуры sockaddr |
WSAEINPROGRESS |
Выполняется блокирующая функция
интерфейса Windows Sockets |
WSAEAFNOSUPPORT |
Этот протокол не может работать с
указанным семейством адресов |
WSAEINVAL |
Сокет уже привязан к адресу |
WSAENOBUFS |
Установлено слишком много соединений |
WSAENOTSOCK |
Указанный в параметре дескриптор не
является сокетом |
Пример вызова функции bind показан ниже:
if(bind (srv_socket, (LPSOCKADDR )&srv_address, sizeof(srv_address)) == SOCKET_ERROR ) { closesocket (srv_socket); MessageBox(NULL, "bind Error", "Error", MB_OK); return; }
Если вы собираетесь передавать датаграммные сообщения при помощи протокола негарантированной доставки UDP, канал связи не нужен. Сразу после создания сокетов и их инициализации можно приступать к передаче данных. Но для передачи данных с использованием протокола TCP необходимо создать канал связи.
Рассмотрим процедуру создания канала связи со стороны сервера.
Прежде всего вы должны переключить сокет в режим приема для выполнения ожидания соединения с клиентом при помощи функции listen:
int listen(SOCKET sock, int backlog);
Через параметр sock функции необходимо передать дескриптор сокета, который будет использован для создания канала. Параметр backlog задает максимальный размер очереди для ожидания соединения (можно указывать значения от 1 до 5). Очередь содержит запросы на установку соединений для каждой пары значений (адрес IP, порт).
Ниже мы привели список возможных кодов ошибок
для функции listen.
Код ошибки |
Описание |
WSANOTINITIALISED |
Перед использованием функции
необходимо вызвать функцию WSAStartup |
WSAENETDOWN |
Сбой в сети |
WSAEADDRINUSE |
Указанный адрес уже используется |
WSAEINPROGRESS |
Выполняется блокирующая функция
интерфейса Windows Sockets |
WSAEINVAL |
Сокет еще не был привязан к адресу или
уже находится в подключенном состоянии |
WSAEISCONN |
Сокет уже находится в подключенном
состоянии |
WSAEMFILE |
Недостаточно дескрипторов файлов |
WSAENOBUFS |
Нет места для размещения буфера |
WSAENOTSOCK |
Указанный в параметре дескриптор не
является сокетом |
WSAEOPNOTSUPP |
Функция listen не работает с
сокетом указанного типа |
Ниже мы привели пример вызов функции listen:
if(listen(srv_socket, 1) == SOCKET_ERROR ) { closesocket (srv_socket); MessageBox(NULL, "listen Error", "Error", MB_OK); return; }
Далее необходимо выполнить ожидание соединения. Это можно выполнить двумя различными способами.
Первый способ заключается в циклическом вызове функции accept до тех пор, пока не будет установлено соединение. Затем можно будет приступать к обмену данными.
Функция accept имеет следующий прототип:
SOCKET accept (SOCKET sock, struct sockaddr FAR * addr, int FAR * addrlen);
Через параметр sock необходимо указать дескриптор сокета, который находится в режиме приема для выполнения ожидания.
Параметр addr должен содержать адрес буфера, в который будет записан адрес узла, подключившегося к серверу. Размер этого буфера необходимо указать в переменной типа int, адрес которой передается через параметр addrlen.
Если ожидание соединения в цикле не вызывает у вас особого энтузиазма, можно предложить более удобный способ, основанный на использовании расширения программного интерфейса Windows Socket, предназначенного для выполнения асинхронных операций.
Приведем список возможных кодов ошибок для
функции accept.
Код ошибки |
Описание |
WSANOTINITIALISED |
Перед использованием функции
необходимо вызвать функцию WSAStartup |
WSAENETDOWN |
Сбой в сети |
WSAEFAULT |
Значение параметра addrlen
меньше размера структуры адреса |
WSAEINTR |
Работа функции была отменена при помощи
функции WSACancelBlockingCall |
WSAEINPROGRESS |
Выполняется блокирующая функция
интерфейса Windows Sockets |
WSAEINVAL |
Перед вызовом функции accept не была
вызывана функция listen |
WSAEMFILE |
Нет доступных дескрипторов |
WSAENOBUFS |
Установлено слишком много соединений |
WSAENOTSOCK |
Указанный в параметре дескриптор не
является сокетом |
WSAEOPNOTSUPP |
Данный тип сокета нельзя использовать
при вызове функций, ориентированных на работу с
каналом связи |
WSAEWOULDBLOCK |
Сокет отмечен как неблокирующий и в
настоящее время нет каналов связи, которые нужно
устанавливать |
Вместо того чтобы ожидать соединение, вызывая в цикле функцию accept, ваше приложение может вызвать один раз функцию WSAAsyncSelect, указав ей, что при получении запроса на установку соединения функция окна вашего приложения должна получить сообщение:
#define WSA_ACCEPT (WM_USER + 1) // При попытке установки соединения главное окно приложения // получит сообщение WSA_ACCEPT rc = WSAAsyncSelect (srv_socket, hWnd, WSA_ACCEPT, FD_ACCEPT ); if(rc > 0) { closesocket (srv_socket); MessageBox(NULL, "WSAAsyncSelect Error", "Error", MB_OK); return; }
В данном случае ожидание соединения выполняется для сокета srv_socket. Последний параметр функции имеет значение FD_ACCEPT. Это означает, что при попытке создания канала связи функция окна с идентификатором hWnd получит сообщение WSA_ACCEPT, определенное в вашем приложении.
Обработчик этого сообщения может выглядеть, например, следующим образом:
void WndProc_OnWSAAccept(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { int rc; // При ошибке отменяем поступление извещений // в главное окно приложения if(WSAGETSELECTERROR(lParam) != 0) { MessageBox(NULL, "accept Error", "Error", MB_OK); WSAAsyncSelect (srv_socket, hWnd, 0, 0); return; } // Определяем размер адреса сокета acc_sin_len = sizeof(acc_sin); // Разрешаем установку соединения srv_socket = accept (srv_socket, (LPSOCKADDR )&acc_sin, (int FAR *)&acc_sin_len); if(srv_socket == INVALID_SOCKET) { MessageBox(NULL, "accept Error, invalid socket ", "Error", MB_OK); return; } // Если на данном сокете начнется передача данных от // клиента, в главное окно приложения поступит // сообщение WSA_NETEVENT. // Это же сообщение поступит при разрыве соединения rc = WSAAsyncSelect (srv_socket, hWnd, WSA_NETEVENT, FD_READ | FD_CLOSE ); if(rc > 0) { closesocket (srv_socket); MessageBox(NULL, "WSAAsyncSelect Error", "Error", MB_OK); return; } }
В данном случае обработчик сообщения вначале вызывает функцию accept, выполняющую создание канала передачи данных. После этого функция WSAAsyncSelect вызывается еще один раз для того чтобы установить асинхронную обработку приема данных от удаленного клиента, а также обработку ситуации разрыва канала связи.
Рассмотрим процедуру установки канала связи со стороны клиента, использованную нами в приложении CLIENT, исходные тексты которого будут приведены ниже.
Для установки соединения в приложении используется функция SetConnection:
SOCKADDR _IN dest_sin; void SetConnection(HWND hWnd) { PHOSTENT phe; // Создаем сокет srv_socket = socket(AF_INET, SOCK_STREAM, 0); if(srv_socket == INVALID_SOCKET) { MessageBox(NULL, "socket Error", "Error", MB_OK); return; } // Устанавливаем адрес IP и номер порта dest_sin.sin_family = AF_INET ; // Определяем адрес узла phe = gethostbyname ("localhost "); if(phe == NULL) { closesocket (srv_socket); MessageBox(NULL, "gethostbyname Error", "Error", MB_OK); return; } // Копируем адрес узла memcpy((char FAR *)&(dest_sin.sin_addr ), phe->h_addr, phe->h_length); // Копируем номер порта dest_sin.sin_port = htons(SERV_PORT); // Устанавливаем соединение if(connect(srv_socket, (PSOCKADDR )&dest_sin, sizeof(dest_sin)) < 0) { closesocket (srv_socket); MessageBox(NULL, "connect Error", "Error", MB_OK); return; } }
Вначале с помощью функции socket эта функция создает сокет. Затем выполняется заполнение адресной информацией структуры dest_sin.
Обратите внимание, что для получения адреса IP мы воспользовались функцией gethostbyname, указав ей имя узла localhost.
Это имя отображается в файле HOSTS на адрес 127.0.0.1 :
Адрес 127.0.0.1 является локальным. Вы можете использовать его для тестирования приложений, выполняющих обмен данными при помощи протокола TCP/IP, запуская сервер и клиент на одном и том же компьютере.
После заполнения структуры с адресной информацией функция connect создает канал связи с сервером.
После того как канал создан, можно начинать передачу данных. Для передачи данных при помощи протокола гарантированной доставки TCP вы можете воспользоваться функциями send и recv, которые входят в программный интерфейс Windows Sockets.
Функция передачи данных send имеет три параметра - дескриптор сокета sock, на котором выполняется передача, адрес буфера buf, содержащего передаваемое сообщение, размер этого буфера bufsize и флаги flags:
int send (SOCKET sock, const char FAR* buf, int bufsize, int flags);
В нашем приложении CLIENT мы передаем данные серверу следующим образом:
char szBuf[80]; lstrcpy(szBuf, "Test string"); send (srv_socket, szBuf, lstrlen(szBuf), 0);
Параметры функции recv, предназначенной для приема данных, аналогичны параметрам функции send :
int recv (SOCKET sock, char FAR * buf, int bufsize, int flags);
Заметим, что функции recv и send возвращают количество, соответственно, принятых и переданных байт данных. Приложение, которое принимает данные, должно вызывать функцию recv в цикле до тех пор, пока не будут приняты все переданные данные. При этом на один вызов функции send может приходиться несколько вызовов функции recv.
В случае ошибки обе эти функции возвращают значение SOCKET_ERROR. Для анализа причин возникновения ошибки следует воспользоваться функцией WSAGetLastError.
Приведем список кодов ошибок, которые могут
возникать при вызове команды send:
Код ошибки |
Описание |
WSANOTINITIALISED |
Перед использованием функции
необходимо вызвать функцию WSAStartup |
WSAENETDOWN |
Сбой в сети |
WSAEACCES |
Указанный адрес является
широковещательным (broadcast ),
однако перед вызовом функции не был установлен
соответствующий флаг |
WSAEINTR |
Работа функции была отменена при помощи
функции WSACancelBlockingCall |
WSAEINPROGRESS |
Выполняется блокирующая функция
интерфейса Windows Sockets |
WSAEFAULT |
Параметр buf указан неправильно (он не
указывает на адресное пространство,
принадлежащее приложению) |
WSAENETRESET |
Необходимо сбросить соединение |
WSAENOBUFS |
Возникла блокировка буфера |
WSAENOTCONN |
Сокет не подсоединен |
WSAENOTSOCK |
Указанный в параметре дескриптор не
является сокетом |
WSAESHUTDOWN |
Сокет был закрыт функцией shutdown |
WSAEWOULDBLOCK |
Сокет отмечен как неблокирующий, но
запрошенная операция приведет к блокировке |
WSAEMSGSIZE |
Был использован сокет типа SOCK_DGRAM
(предназначенный для передачи датаграмм). При
этом размер пакета данных превышает максимально
допустимый для данной реализации интерфейса Windows Sockets |
WSAEINVAL |
Сокет не был подключен функцией bind |
WSAECONNABORTED |
Сбой из-за слишком большой задержки или
по другой причине |
WSAECONNRESET |
Сброс соединения удаленным узлом |
При выполнении функции recv могут возникать
следующие ошибки:
Код ошибки |
Описание |
WSANOTINITIALISED |
Перед использованием функции
необходимо вызвать функцию WSAStartup |
WSAENETDOWN |
Сбой в сети |
WSAENOTCONN |
Сокет не подсоединен |
WSAEINTR |
Работа функции была отменена при помощи
функции WSACancelBlockingCall |
WSAEINPROGRESS |
Выполняется блокирующая функция
интерфейса Windows Sockets |
WSAENOTSOCK |
Указанный в параметре дескриптор не
является сокетом |
WSAESHUTDOWN |
Сокет был закрыт функцией shutdown |
WSAEWOULDBLOCK |
Сокет отмечен как неблокирующий, но
запрошенная операция приведет к блокировке |
WSAEMSGSIZE |
Размер пакета данных превышает размер
буфера, в результате чего принятый пакет был
обрезан |
WSAEINVAL |
Сокет не был подключен функцией bind |
WSAECONNABORTED |
Сбой из-за слишком большой задержки или
по другой причине |
WSAECONNRESET |
Сброс соединения удаленным узлом |
Передача и прием данных в цикле может привести к блокировке работы приложения. Если это неприемлимо, следует воспользоваться асинхронным расширением интерфейса Windows Sockets.
Наше приложение SERVER демонстрирует асинхронный прием данных.
После установки канала связи оно вызывает функцию WSAAsyncSelect, указывая ей в качестве последнего параметра комбинацию констант FD_READ и FD_CLOSE. При этом функция главного окна приложения будет получать сообщение WSA_NETEVENT в тот момент времени, когда чтение данных не вызовет блокировки приложения:
#define WSA_NETEVENT (WM_USER + 2) rc = WSAAsyncSelect (srv_socket, hWnd, WSA_NETEVENT, FD_READ | FD_CLOSE );
При необходимости выполнения асинхронной посылки данных вы можете указать функции WSAAsyncSelect еще и параметр FD_WRITE.
Если функция WSAAsyncSelect выполнилась успешно, она возвращает нулевое значение, при ошибке - значение SOCKET_ERROR.
В зависимости от значения последнего параметра
могут возникать разные коды ошибки, которые
можно получить при помощи функции WSAGetLastError.
Следующие ошибки могут возникнуть при любом
значении параметра:
Код ошибки |
Описание |
WSANOTINITIALISED |
Перед использованием функции
необходимо вызвать функцию WSAStartup |
WSAENETDOWN |
Сбой в сети |
WSAEINVAL |
Сокет не был подключен функцией bind |
WSAEINPROGRESS |
Выполняется блокирующая функция
интерфейса Windows Sockets |
Дополнительный код ошибки можно получить из параметра lParam при помощи макрокоманды WSAGETSELECTERROR.
При использовании параметра FD_CONNECT возможно
появление следующих ошибок:
Код ошибки |
Описание |
WSAEADDRINUSE |
Указанный адрес уже используется |
WSAEADDRNOTAVAIL |
Указанный адрес не доступен |
WSAEAFNOSUPPORT |
Для данного сокета нельзя использовать
указанное семейство адресов |
WSAECONNREFUSED |
Попытка установления канала связи была
отвергнута |
WSAEDESTADDRREQ |
Необходимо указать адрес получателя
пакета |
WSAEFAULT |
Неправильно указан параметр namelen |
WSAEINVAL |
Сокет уже подключен к адресу |
WSAEISCONN |
Сокет уже подсоединен |
WSAEMFILE |
Больше нет доступных дескрипторов |
WSAENETUNREACH |
Из данного узла и в данное время
невозможно получить доступ к сети |
WSAENOBUFS |
Нет места для размещения буфера |
WSAENOTCONN |
Сокет на подключен |
WSAENOTSOCK |
Указан дескриптор файла, а не сокета |
WSAETIMEDOUT |
При попытке установления канала связи
возникла задержка во времени |
Если используется параметр FD_CLOSE, может
возникнуть одна из следующих ошибок:
Код ошибки |
Описание |
WSAENETDOWN |
Сбой в сети |
WSAECONNRESET |
Сброс соединения удаленным узлом |
WSAECONNABORTED |
Сбой из-за слишком большой задержки или
по другой причине |
В том случае, когда указаны параметры FD_READ, FD_WRITE, FD_OOB, или FD_ACCEPT, может возникнуть ошибка с кодом WSAENETDOWN.
Обработчик сообщения WSA_NETEVENT должен выполнить анализ причины, по которой он был вызван, так как за один вызов функции WSAAsyncSelect можно задать несколько событий, вызывающих генерацию сообщения. Этот анализ проводится, например, следующим образом:
void WndProc_OnWSANetEvent(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { char szTemp[256]; int rc; // Если на сокете выполняется передача данных, // принимаем и отображаем эти данные в виде // текстовой строки if(WSAGETSELECTEVENT(lParam) == FD_READ ) { rc = recv ((SOCKET)wParam, szTemp, 256, 0); if(rc) { szTemp[rc] = '\0'; MessageBox(NULL, szTemp, "Reсeived data", MB_OK); } return; } // Если соединение завершено, выводми об этом сообщение else if(WSAGETSELECTEVENT(lParam) == FD_CLOSE ) { MessageBox(NULL, "Connection closed", "Server", MB_OK); } }
Отметим, что параметр wParam содержит дескриптор сокета, на котором выполняется передача данных, а параметр lParam - код события, которое произошло в сети.
В этом разделе мы представим вам исходные тексты приложения SERVER, которое выполняет прием сообщений от приложения CLIENT с использованием протокола гарантированной доставки TCP и канала связи. При необходимости вы сможете самостоятельно организовать передачу данных в обратном направлении.
Вы можете запускать приложения SERVER и CLIENT как на одном, так и на разных компьютерах, соединенных локальной или глобальной сетью TCP/IP. В случае запуска этих приложений на одном и том же компьютере в качестве адреса IP используется локальный тестовый адрес 127.0.0.1.
Создавая проект для этого, а также всех остальных приложений, приведенных в нашей книге, вы должны указать, что для разрешения внешних ссылок необходимо использовать библиотеки объектных модулей wsock32.lib и comctl32.lib. Первая из них нужна для работы с программным интерфейсом Windows Sockets, вторая - для работы с органом управления Statusbar.
Для подключения указанных библиотек из меню Build системы разработки Microsoft Visual C++ версии 4.0 нужно выбрать строку Settings. На экране появится блокнот project Settings, который следует открыть на странице Link. Затем вы должны дописать названия библиотек в поле Object/library modules и нажать кнопку OK.
Исходный текст приложения SERVER представлен в листинге 5.1.
Листинг 5.1. Файл server/server.c
#include <windows.h> #include <windowsx.h> #include <winsock.h> #include <commctrl.h> #include "resource.h" // ----------------------------------------------------- // Описание функций // ----------------------------------------------------- // Функция главного окна LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); // Функция для обработки сообщения WM_CREATE BOOL WndProc_OnCreate(HWND hWnd, LPCREATESTRUCT lpCreateStruct); // Функция для обработки сообщения WM_DESTROY void WndProc_OnDestroy(HWND hWnd); // Функция для обработки сообщения WM_COMMAND void WndProc_OnCommand(HWND hWnd, int id, HWND hwndCtl, UINT codeNotify); // Функция для обработки сообщения WM_SIZE void WndProc_OnSize(HWND hWnd, UINT state, int cx, int cy); // Запуск сервера void ServerStart(HWND hWnd); // Останов сервера void ServerStop(HWND hWnd); // Обработка сообщения WSA_ACCEPT void WndProc_OnWSAAccept(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); // Обработка сообщения WSA_NETEVENT void WndProc_OnWSANetEvent(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); // Порт сервера #define SERV_PORT 5000 #define IDS_STATUSBAR 802 // Определение кодов сообщений #define WSA_ACCEPT (WM_USER + 1) #define WSA_NETEVENT (WM_USER + 2) // ----------------------------------------------------- // Глобальные переменные // ----------------------------------------------------- // Идентификатор приложения HINSTANCE hInst; // Название приложения char szAppName[] = "WServer"; // Заголовок главного окна приложения char szAppTitle[] = "Windows Socket Server Demo"; // Идентификатор органа Statusbar HWND hwndSb; // Сокет сервера SOCKET srv_socket ; // Длина использованного сокета int acc_sin_len; // Адрес использованного сокета SOCKADDR _IN acc_sin; // Локальный сокет SOCKADDR _IN local_sin; // ----------------------------------------------------- // Функция WinMain // ----------------------------------------------------- int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { WNDCLASSEX wc; HWND hWnd; MSG msg; hInst = hInstance; // Преверяем, не было ли это приложение запущено ранее hWnd = FindWindow(szAppName, NULL); if(hWnd) { // Если окно приложения было свернуто в пиктограмму, // восстанавливаем его if(IsIconic(hWnd)) ShowWindow(hWnd, SW_RESTORE); // Выдвигаем окно приложения на передний план SetForegroundWindow(hWnd); return FALSE; } // Регистрируем класс окна memset(&wc, 0, sizeof(wc)); // Поля wc.cbSize и wc.hIconSm определены в структуре // WNDCLASSEX, которой можно пользоваться для // регистрации класса окна в Windows 95 wc.cbSize = sizeof(WNDCLASSEX); // Поле wc.hIconSm задает идентификатор маленькой // пиктограммы, которая будет отображаться в левой // части заголовка окна (в области системного меню). // Загружаем пиктограмму из ресурсов приложения при // помощи функции LoadImage, так как функция // LoadIcon может загрузить только обычную пиктограмму wc.hIconSm = LoadImage(hInst, MAKEINTRESOURCE(IDI_APPICON_SM), IMAGE_ICON, 16, 16, 0); // Завершаем заполнение структуры WNDCLASSEX wc.style = CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc = (WNDPROC)WndProc; wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = hInst; // Для загрузки обычной пиктограммы вы можете // использовать функцию LoadImage wc.hIcon = LoadImage(hInst, MAKEINTRESOURCE(IDI_APPICON), IMAGE_ICON, 32, 32, 0); wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH) (COLOR_WINDOW + 1); wc.lpszMenuName = MAKEINTRESOURCE(IDR_MENU1); wc.lpszClassName = szAppName; // Вызываем функцию RegisterClassEx, которая выполняет // регистрацию окна if(!RegisterClassEx(&wc)) // В случае ошибки пытаемся зарегистрировать окно // функцией RegisterClass if(!RegisterClass((LPWNDCLASS)&wc.style)) return FALSE; // Инициализация библиотеки органов управления // общего назначения. Необходима для работы с // органом управления Statusbar InitCommonControls(); // Создаем главное окно приложения hWnd = CreateWindow(szAppName, szAppTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInst, NULL); if(!hWnd) return(FALSE); // Отображаем окно ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); // Запускаем цикл обработки сообщений while(GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; } // ----------------------------------------------------- // Функция WndProc // ----------------------------------------------------- LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch(msg) { // Вызываем обработчик сообщения WSA_ACCEPT case WSA_ACCEPT: WndProc_OnWSAAccept(hWnd, msg, wParam, lParam); break; // Вызываем обработчик сообщения WSA_NETEVENT case WSA_NETEVENT: WndProc_OnWSANetEvent(hWnd, msg, wParam, lParam); break; // Для сообщения WM_CREATE назначаем обработчик, // расположенный в функции WndProc_OnCreate HANDLE_MSG(hWnd, WM_CREATE, WndProc_OnCreate); // Для сообщения WM_COMMAND назначаем обработчик, // расположенный в функции WndProc_OnCommand HANDLE_MSG(hWnd, WM_COMMAND, WndProc_OnCommand); // Для сообщения WM_SIZE назначаем обработчик, // расположенный в функции WndProc_OnSize HANDLE_MSG(hWnd, WM_SIZE, WndProc_OnSize); // Для сообщения WM_DESTROY назначаем обработчик, // расположенный в функции WndProc_OnDestroy HANDLE_MSG(hWnd, WM_DESTROY, WndProc_OnDestroy); default: return(DefWindowProc(hWnd, msg, wParam, lParam)); } } // ----------------------------------------------------- // Функция WndProc_OnCreate // ----------------------------------------------------- BOOL WndProc_OnCreate(HWND hWnd, LPCREATESTRUCT lpCreateStruct) { int rc; WSADATA WSAData; char szTemp[128]; // Инициализация и проверка версии Windows Sockets rc = WSAStartup (MAKEWORD(1, 1), &WSAData); if(rc != 0) { MessageBox(NULL, "WSAStartup Error", "Error", MB_OK); return FALSE; } // Отображаем описание и версию системы Windows Sockets // в окне органа управления Statusbar wsprintf(szTemp, "Server use %s %s", WSAData.szDescription,WSAData.szSystemStatus); hwndSb = CreateStatusWindow(WS_CHILD | WS_VISIBLE | WS_BORDER | SBARS_SIZEGRIP, szTemp, hWnd, IDS_STATUSBAR); return TRUE; } // ----------------------------------------------------- // Функция WndProc_OnDestroy // ----------------------------------------------------- // Отключаем предупреждающее сообщение о том, что // функция типа void возвращает управление при помощи // оператора return. Этот оператор нужен для // использования макрокоманды FORWARD_WM_LBUTTONDOWN #pragma warning(disable: 4098) void WndProc_OnDestroy(HWND hWnd) { // Освобождение ресурсов, полученных для // работы с Windows Sockets WSACleanup (); // Завершение цикла обработки сообщений PostQuitMessage(0); return FORWARD_WM_DESTROY (hWnd, DefWindowProc); } // ----------------------------------------------------- // Функция WndProc_OnSize // ----------------------------------------------------- #pragma warning(disable: 4098) void WndProc_OnSize(HWND hWnd, UINT state, int cx, int cy) { SendMessage(hwndSb, WM_SIZE, cx, cy); return FORWARD_WM_SIZE (hWnd, state, cx, cy, DefWindowProc); } // ----------------------------------------------------- // Функция WndProc_OnCommand // ----------------------------------------------------- #pragma warning(disable: 4098) void WndProc_OnCommand(HWND hWnd, int id, HWND hwndCtl, UINT codeNotify) { switch (id) { case IDM_EXIT: // Уничтожение главного окна приложения DestroyWindow(hWnd); break; case IDM_START: // Запуск сервера ServerStart(hWnd); break; case IDM_STOP: // Останов сервера ServerStop(hWnd); break; default: MessageBox(NULL, "Unknown command", "Error", MB_OK); } return FORWARD_WM_COMMAND (hWnd, id, hwndCtl, codeNotify, DefWindowProc); } // ----------------------------------------------------- // Функция ServerStart // ----------------------------------------------------- void ServerStart(HWND hWnd) { struct sockaddr_in srv_address; int rc; // Создаем сокет сервера для работы с потоком данных srv_socket = socket(AF_INET, SOCK_STREAM, 0); if(srv_socket == INVALID_SOCKET) { MessageBox(NULL, "socket Error", "Error", MB_OK); return; } // Устанавливаем адрес IP и номер порта srv_address.sin_family = AF_INET ; srv_address.sin_addr.s_addr = INADDR_ANY ; srv_address.sin_port = htons(SERV_PORT); // Связываем адрес IP с сокетом if(bind (srv_socket, (LPSOCKADDR )&srv_address, sizeof(srv_address)) == SOCKET_ERROR ) { // При ошибке закрываем сокет closesocket (srv_socket); MessageBox(NULL, "bind Error", "Error", MB_OK); return; } // Устанавливаем сокет в режим приема для // выполнения ожидания соединения с клиентом if(listen(srv_socket, 1) == SOCKET_ERROR ) { closesocket (srv_socket); MessageBox(NULL, "listen Error", "Error", MB_OK); return; } // При попытке установки соединения главное окно приложения // получит сообщение WSA_ACCEPT rc = WSAAsyncSelect (srv_socket, hWnd, WSA_ACCEPT, FD_ACCEPT ); if(rc > 0) { closesocket (srv_socket); MessageBox(NULL, "WSAAsyncSelect Error", "Error", MB_OK); return; } // Выводим в окна Statusbar сообщение о запуске сервера SendMessage(hwndSb, SB_SETTEXT, 0, (LPARAM)"Server started"); } // ----------------------------------------------------- // Функция ServerStop // ----------------------------------------------------- void ServerStop(HWND hWnd) { // Отменяем приход любых извещений в главную функцию // окна при возникновении любых событий, связанных // с системой Windows Sockets WSAAsyncSelect (srv_socket, hWnd, 0, 0); // Если сокет был создан, закрываем его if(srv_socket != INVALID_SOCKET) { closesocket (srv_socket); } // Выводим в окна Statusbar сообщение об останове сервера SendMessage(hwndSb, SB_SETTEXT, 0, (LPARAM)"Server stopped"); } // ----------------------------------------------------- // Функция WndProc_OnWSAAccept // ----------------------------------------------------- void WndProc_OnWSAAccept(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { int rc; // При ошибке отменяем поступление извещений // в главное окно приложения if(WSAGETSELECTERROR(lParam) != 0) { MessageBox(NULL, "accept Error", "Error", MB_OK); WSAAsyncSelect (srv_socket, hWnd, 0, 0); return; } // Определяем размер адреса сокета acc_sin_len = sizeof(acc_sin); // Разрешаем установку соединения srv_socket = accept (srv_socket, (LPSOCKADDR )&acc_sin, (int FAR *)&acc_sin_len); if(srv_socket == INVALID_SOCKET) { MessageBox(NULL, "accept Error, invalid socket ", "Error", MB_OK); return; } // Если на данном сокете начнется передача данных от // клиента, в главное окно приложения поступит // сообщение WSA_NETEVENT. // Это же сообщение поступит при разрыве соединения rc = WSAAsyncSelect (srv_socket, hWnd, WSA_NETEVENT, FD_READ | FD_CLOSE ); if(rc > 0) { closesocket (srv_socket); MessageBox(NULL, "WSAAsyncSelect Error", "Error", MB_OK); return; } } // ----------------------------------------------------- // Функция WndProc_OnWSANetEvent // ----------------------------------------------------- void WndProc_OnWSANetEvent(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { char szTemp[256]; int rc; // Если на сокете выполняется передача данных, // принимаем и отображаем эти данные в виде // текстовой строки if(WSAGETSELECTEVENT(lParam) == FD_READ ) { rc = recv ((SOCKET)wParam, szTemp, 256, 0); if(rc) { szTemp[rc] = '\0'; MessageBox(NULL, szTemp, "Reсeived data", MB_OK); } return; } // Если соединение завершено, выводми об этом сообщение else if(WSAGETSELECTEVENT(lParam) == FD_CLOSE ) { MessageBox(NULL, "Connection closed", "Server", MB_OK); } }
Функция WinMain сохраняет идентификатор приложения и затем проверяет, не было ли это приложение уже запущено. При этом используется техника, описанная нами в 22 томе "Библиотеки системного программиста", который называется "Операционная система Windows 95 для программиста".
Далее выполняется обычная регистрация класса главного окна приложения, инициализируется библиотека орагнов управления общего назначения и создается главное окно приложения. После этого окно отображается на экране и запускается цикл обработки сообщений.
Функция окна WndProc обрабатывает как стандартные сообщения WM_CREATE, WM_COMMAND, WM_SIZE, WM_DESTROY, так и сообщения WSA_ACCEPT и WSA_NETEVENT. Первое из них возникает при установке канала связи с клиентом, второе - при поступлении данных от клиента и при разрыве канала связи.
Обработчик сообщения WM_CREATE инициализирует библиотеку Windows Sockets и создает орган управления Statusbar. В окне этого органа управления отображается текущая версия и описание состояния системы Windows Sockets. Если вы не знакомы с указанным органам управления, отсылаем вас к упомянутому 22 тому "Библиотеки системного программиста".
Обработчик сообщения WM_DESTROY вызывает функцию WSACleanup, освобождающую ресурсы, полученные для приложения у системы Windows Sockets и затем завершает цикл обработки сообщений.
Единственное назначение обработчика сообщений WM_SIZE заключается в изменении размеров окна органа управления Statusbar при изменении размеров главного окна приложения.
Обработчик сообщения WM_COMMAND получает управление, когда пользователь выбирает одну из строк в меню File главного меню приложения. Если пользователь выберет строку Start server, будет вызвана функция ServerStart, назначение которой очевидно из ее названия. Аналогично, при выборе строки Stop server будет вызвана функция ServerStop. Если же из меню File выбрать строку Exit, будет уничтожено главное окно приложения.
Функция ServerStart создает сокет для работы с потоком данных и инициализирует его. При этом мы используем произвольно выбранный порт с номером 5000.
Далее функция выполняет привязку сокета к адресу и переключает сокет в режим приема для выполнения ожидания соединения с клиентом, т. е. выполняет описанные нами ранее действия, необходимые для создания канала связи.
Затем вызывается функция WSAAsyncSelect, которой в качестве последнего параметра передается значение FD_ACCEPT, а в качестве предпоследнего - значение WSA_ACCEPT. В результате при поступлении от клиента запроса на создание канала связи функция главного окна приложения получит сообщение WSA_ACCEPT.
Перед возвратом управления функция ServerStart выводит сообщение о запуске сервера в окне органа управления Statusbar.
Функция ServerStop отменяет все извещения, поступающие в главное окно приложения при возникновении событий в сети, вызывая функцию WSAAsyncSelect с нулевым значением двух последних параметров:
WSAAsyncSelect (srv_socket, hWnd, 0, 0);
Затем она закрывает сокет, вызывая функцию closesocket, и выводит в окне органа управления Statusbar сообщение о завершении работы сервера.
Функция WndProc_OnWSAAccept обрабатывает сообщение WSA_ACCEPT, выполняя описанную нами ранее процедуру создания канала связи.
И, наконец, функция WndProc_OnWSANetEvent выполняет прием строки сообщения, полученной от клиента с отображением этой строки на экране в диалоговой панели.
Файл resource.h, показанный в листинге 5.2 создается автоматически и содержит описание идентификаторов ресурсов приложения.
Листинг 5.2. Файл server/resource.h
//{{NO_DEPENDENCIES}} // Microsoft Developer Studio generated include file. // Used by server.rc // #define IDI_APPICON 101 #define IDI_APPICON_SM 102 #define IDR_MENU1 105 #define IDM_START 40001 #define IDM_EXIT 40002 #define IDM_STOP 40003 // Next default values for new objects // #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NO_MFC 1 #define _APS_NEXT_RESOURCE_VALUE 106 #define _APS_NEXT_COMMAND_VALUE 40004 #define _APS_NEXT_CONTROL_VALUE 1000 #define _APS_NEXT_SYMED_VALUE 101 #endif #endif
Ресурсы приложения определены в файле server.rc, который представлен в листинге 5.3.
Листинг 5.3. Файл server/server.rc
//Microsoft Developer Studio generated resource script. // #include "resource.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "afxres.h" ///////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////// // Russian resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_RUS) #ifdef _WIN32 LANGUAGE LANG_RUSSIAN, SUBLANG_DEFAULT #pragma code_page(1251) #endif //_WIN32 #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE DISCARDABLE BEGIN "resource.h\0" END 2 TEXTINCLUDE DISCARDABLE BEGIN "#include ""afxres.h""\r\n" "\0" END 3 TEXTINCLUDE DISCARDABLE BEGIN "\r\n" "\0" END #endif // APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////// // // Icon // // Icon with lowest ID value placed first to ensure application icon // remains consistent on all systems. IDI_APPICON ICON DISCARDABLE "server.ico" IDI_APPICON_SM ICON DISCARDABLE "serversm.ico" ///////////////////////////////////////////////////////////////////// // // Menu // IDR_MENU1 MENU DISCARDABLE BEGIN POPUP "&File" BEGIN MENUITEM "&Start server", IDM_START MENUITEM "S&top server", IDM_STOP MENUITEM SEPARATOR MENUITEM "&Exit", IDM_EXIT END END #endif // Russian resources ///////////////////////////////////////////////////////////////////// #ifndef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 3 resource. // ///////////////////////////////////////////////////////////////////// #endif // not APSTUDIO_INVOKED
Исходные тексты приложения CLIENT, предназначенного для совместного использования с только что описанным приложением SERVER, приведены в листинге 5.4.
После запуска этого приложения вы должны создать канал связи с приложением SERVER, выбрав из меню File строку Connect, после чего можно посылать сообщение Test message, выбирая из этого же меню строку Send Message.
Сервер, получив сообщение, отобразит его на экране в отдельной диалоговой панели.
Листинг 5.4. Файл client/client.c
#include <windows.h> #include <windowsx.h> #include <winsock.h> #include <commctrl.h> #include "resource.h" // ----------------------------------------------------- // Описание функций // ----------------------------------------------------- // Функция главного окна LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); // Функция для обработки сообщения WM_CREATE BOOL WndProc_OnCreate(HWND hWnd, LPCREATESTRUCT lpCreateStruct); // Функция для обработки сообщения WM_DESTROY void WndProc_OnDestroy(HWND hWnd); // Функция для обработки сообщения WM_COMMAND void WndProc_OnCommand(HWND hWnd, int id, HWND hwndCtl, UINT codeNotify); // Функция для обработки сообщения WM_SIZE void WndProc_OnSize(HWND hWnd, UINT state, int cx, int cy); // Установка соединения void SetConnection(HWND hWnd); // Передача сообщения void SendMsg(HWND hWnd); // Порт сервера #define SERV_PORT 5000 #define IDS_STATUSBAR 802 // ----------------------------------------------------- // Глобальные переменные // ----------------------------------------------------- // Идентификатор приложения HINSTANCE hInst; // Название приложения char szAppName[] = "WClient"; // Заголовок главного окна приложения char szAppTitle[] = "Windows Socket Client Demo"; // Идентификатор органа управления Statusbar HWND hwndSb; // Сокет клиента SOCKET srv_socket ; // Локальный сокет SOCKADDR _IN local_sin; // Адрес сервера SOCKADDR _IN dest_sin; // ----------------------------------------------------- // Функция WinMain // ----------------------------------------------------- int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { WNDCLASSEX wc; HWND hWnd; MSG msg; hInst = hInstance; // Преверяем, не было ли это приложение запущено ранее hWnd = FindWindow(szAppName, NULL); if(hWnd) { // Если окно приложения было свернуто в пиктограмму, // восстанавливаем его if(IsIconic(hWnd)) ShowWindow(hWnd, SW_RESTORE); // Выдвигаем окно приложения на передний план SetForegroundWindow(hWnd); return FALSE; } // Регистрируем класс окна memset(&wc, 0, sizeof(wc)); // Поля wc.cbSize и wc.hIconSm определены в структуре // WNDCLASSEX, которой можно пользоваться для // регистрации класса окна в Windows 95 wc.cbSize = sizeof(WNDCLASSEX); // Поле wc.hIconSm задает идентификатор маленькой // пиктограммы, которая будет отображаться в левой // части заголовка окна (в области системного меню). // Загружаем пиктограмму из ресурсов приложения при // помощи функции LoadImage, так как функция // LoadIcon может загрузить только обычную пиктограмму wc.hIconSm = LoadImage(hInst, MAKEINTRESOURCE(IDI_APPICON_SM), IMAGE_ICON, 16, 16, 0); // Завершаем заполнение структуры WNDCLASSEX wc.style = CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc = (WNDPROC)WndProc; wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = hInst; // Для загрузки обычной пиктограммы вы можете // использовать функцию LoadImage wc.hIcon = LoadImage(hInst, MAKEINTRESOURCE(IDI_APPICON), IMAGE_ICON, 32, 32, 0); wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH) (COLOR_WINDOW + 1); wc.lpszMenuName = MAKEINTRESOURCE(IDR_MENU1); wc.lpszClassName = szAppName; // Вызываем функцию RegisterClassEx, которая выполняет // регистрацию окна if(!RegisterClassEx(&wc)) // В случае ошибки пытаемся зарегистрировать окно // функцией RegisterClass if(!RegisterClass((LPWNDCLASS)&wc.style)) return FALSE; // Инициализация библиотеки органов управления // общего назначения. Необходима для работы с // органом управления Statusbar InitCommonControls(); // Создаем главное окно приложения hWnd = CreateWindow(szAppName, szAppTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInst, NULL); if(!hWnd) return(FALSE); // Отображаем окно ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); // Запускаем цикл обработки сообщений while(GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; } // ----------------------------------------------------- // Функция WndProc // ----------------------------------------------------- LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch(msg) { // Для сообщения WM_CREATE назначаем обработчик, // расположенный в функции WndProc_OnCreate HANDLE_MSG(hWnd, WM_CREATE, WndProc_OnCreate); // Для сообщения WM_COMMAND назначаем обработчик, // расположенный в функции WndProc_OnCommand HANDLE_MSG(hWnd, WM_COMMAND, WndProc_OnCommand); // Для сообщения WM_SIZE назначаем обработчик, // расположенный в функции WndProc_OnSize HANDLE_MSG(hWnd, WM_SIZE, WndProc_OnSize); // Для сообщения WM_DESTROY назначаем обработчик, // расположенный в функции WndProc_OnDestroy HANDLE_MSG(hWnd, WM_DESTROY, WndProc_OnDestroy); default: return(DefWindowProc(hWnd, msg, wParam, lParam)); } } // ----------------------------------------------------- // Функция WndProc_OnCreate // ----------------------------------------------------- BOOL WndProc_OnCreate(HWND hWnd, LPCREATESTRUCT lpCreateStruct) { int rc; WSADATA WSAData; char szTemp[128]; // Инициализация и проверка версии Windows Sockets rc = WSAStartup (MAKEWORD(1, 1), &WSAData); if(rc != 0) { MessageBox(NULL, "WSAStartup Error", "Error", MB_OK); return FALSE; } // Отображаем описание и версию системы Windows Sockets // в окне органа управления Statusbar wsprintf(szTemp, "Server use %s %s", WSAData.szDescription,WSAData.szSystemStatus); hwndSb = CreateStatusWindow(WS_CHILD | WS_VISIBLE | WS_BORDER | SBARS_SIZEGRIP, szTemp, hWnd, IDS_STATUSBAR); return TRUE; } // ----------------------------------------------------- // Функция WndProc_OnDestroy // ----------------------------------------------------- #pragma warning(disable: 4098) void WndProc_OnDestroy(HWND hWnd) { // Освобождение ресурсов, полученных для // работы с Windows Sockets WSACleanup (); // Завершение цикла обработки сообщений PostQuitMessage(0); return FORWARD_WM_DESTROY (hWnd, DefWindowProc); } // ----------------------------------------------------- // Функция WndProc_OnSize // ----------------------------------------------------- #pragma warning(disable: 4098) void WndProc_OnSize(HWND hWnd, UINT state, int cx, int cy) { SendMessage(hwndSb, WM_SIZE, cx, cy); return FORWARD_WM_SIZE (hWnd, state, cx, cy, DefWindowProc); } // ----------------------------------------------------- // Функция WndProc_OnCommand // ----------------------------------------------------- #pragma warning(disable: 4098) void WndProc_OnCommand(HWND hWnd, int id, HWND hwndCtl, UINT codeNotify) { switch (id) { case IDM_EXIT: // Уничтожение главного окна прилоджения DestroyWindow(hWnd); break; case IDM_CONNECT: // Установка соединения с сервером SetConnection(hWnd); break; case IDM_SEND: // Посылка сообщения серверу SendMsg(hWnd); break; default: MessageBox(NULL, "Unknown command", "Error", MB_OK); } return FORWARD_WM_COMMAND (hWnd, id, hwndCtl, codeNotify, DefWindowProc); } // ----------------------------------------------------- // Функция SetConnection // ----------------------------------------------------- void SetConnection(HWND hWnd) { PHOSTENT phe; // Создаем сокет srv_socket = socket(AF_INET, SOCK_STREAM, 0); if(srv_socket == INVALID_SOCKET) { MessageBox(NULL, "socket Error", "Error", MB_OK); return; } // Устанавливаем адрес IP и номер порта dest_sin.sin_family = AF_INET ; // Определяем адрес узла // Адрес локального узла для отладки phe = gethostbyname ("localhost "); // Адрес удаленного узла //phe = gethostbyname ("frolov"); if(phe == NULL) { closesocket (srv_socket); MessageBox(NULL, "gethostbyname Error", "Error", MB_OK); return; } // Копируем адрес узла memcpy((char FAR *)&(dest_sin.sin_addr ), phe->h_addr, phe->h_length); // Другой способ указания адреса узла // dest_sin.sin_addr.s_addr = inet_addr ("200.200.200.201"); // Копируем номер порта dest_sin.sin_port = htons(SERV_PORT); // Устанавливаем соединение if(connect(srv_socket, (PSOCKADDR )&dest_sin, sizeof(dest_sin)) < 0) { closesocket (srv_socket); MessageBox(NULL, "connect Error", "Error", MB_OK); return; } // В случае успеха выводим сообщение об установке // соединения с узлом SendMessage(hwndSb, SB_SETTEXT, 0, (LPARAM)"Connected"); } // ----------------------------------------------------- // Функция SendMsg // ----------------------------------------------------- void SendMsg(HWND hWnd) { char szBuf[80]; lstrcpy(szBuf, "Test string"); // Посылаем сообщение send (srv_socket, szBuf, lstrlen(szBuf), 0); }
Вы сможете разобраться с исходными текстами этого прилжоения самостоятельно, так как все использованные в нем функции были нами уже описаны. Обратим ваше внимание только на функцию SetConnection, предназначенную для установки канала связи с сервером.
Если вы будете проверять работу приложений SERVER и CLIENT на одном и том же компьютере, адрес сервера должен быть указан следующим образом:
phe = gethostbyname ("localhost ");
Для того чтобы установить канал связи с компьютером по его имени, закройте символом комментария приведенную выше строку и уберите символ комментария со строки, расположенной ниже:
//phe = gethostbyname ("frolov");
Разумеется, вы должны также изменить имя компьютера.
Можно также указать адрес узла в виде десятичных цифр, для чего следует убрать символ комментария со следующей строки:
// dest_sin.sin_addr.s_addr = inet_addr ("200.200.200.201");
Идентификаторы ресурсов приложения CLIENT определены в файле resource.h, представленном в листинге 5.5.
Листинг 5.5. Файл client/resource.h
//{{NO_DEPENDENCIES}} // Microsoft Developer Studio generated include file. // Used by CLIENT.rc // #define IDI_APPICON 101 #define IDI_APPICON_SM 102 #define IDR_MENU1 105 #define IDM_START 40001 #define IDM_EXIT 40002 #define IDM_STOP 40003 #define IDM_CONNECT 40004 #define IDM_SEND 40005 // Next default values for new objects // #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NO_MFC 1 #define _APS_NEXT_RESOURCE_VALUE 106 #define _APS_NEXT_COMMAND_VALUE 40006 #define _APS_NEXT_CONTROL_VALUE 1000 #define _APS_NEXT_SYMED_VALUE 101 #endif #endif
Файл client.rc (листинг 5.6) содержит определения ресурсов приложения.
Листинг 5.6. Файл client/client.rc
//Microsoft Developer Studio generated resource script. // #include "resource.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "afxres.h" ///////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////// // Russian resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_RUS) #ifdef _WIN32 LANGUAGE LANG_RUSSIAN, SUBLANG_DEFAULT #pragma code_page(1251) #endif //_WIN32 #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE DISCARDABLE BEGIN "resource.h\0" END 2 TEXTINCLUDE DISCARDABLE BEGIN "#include ""afxres.h""\r\n" "\0" END 3 TEXTINCLUDE DISCARDABLE BEGIN "\r\n" "\0" END #endif // APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////// // // Icon // // Icon with lowest ID value placed first to ensure application icon // remains consistent on all systems. IDI_APPICON ICON DISCARDABLE "client.ico" IDI_APPICON_SM ICON DISCARDABLE "clientsm.ico" ///////////////////////////////////////////////////////////////////// // // Menu // IDR_MENU1 MENU DISCARDABLE BEGIN POPUP "&File" BEGIN MENUITEM "&Connect", IDM_CONNECT MENUITEM "S&end message", IDM_SEND MENUITEM SEPARATOR MENUITEM "&Exit", IDM_EXIT END END #endif // Russian resources ///////////////////////////////////////////////////////////////////// #ifndef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 3 resource. // ///////////////////////////////////////////////////////////////////// #endif // not APSTUDIO_INVOKED
В некоторых случаях целесообразно использовать протокол негарантированной доставки UDP, так как он, например, допускает одновременную рассылку пакетов всем узлам сети (в режиме broadcast).
Если узлы обмениваются данными с использованием датаграммного протокола UDP, им не требуется создавать канал данных, поэтому процедура инициализации получается проще.
Сервер UDP должен создать сокет с помощью функции socket и привязать к нему адрес IP, вызвав функцию bind. Клиент UDP выполняет создание и инициализацию сокетов аналогичным образом с помощью все тех же функций socket и bind.
Такие известные вам из предыдущих приложений функции, как connect, listen и accept в приложениях UDP использовать не нужно.
Для обмена данными приложения UDP вызывают функции send to и recv from, аналогичные функциям send и recv, но имеющие одно отличие - при вызове этих функций им необходимо задавать дополнительные параметры, имеющие отношение к адресам узлов. Функции sendto нужно указать адрес, по которому будет отправлен пакет данных, а функции recvfrom - указатель на структуру, в которую будет записан адрес отправителя пакета.
В нашей книге мы привели исходные тексты приложений SERVERD и CLIENTD, которые выполняют те же задачи, что и только что рассмотренные приложения SERVER и CLIENT, но при этом они передают данные при помощи датаграммного протокола UDP.
Исходный текст приложения SERVERD приведен в листинге 5.7.
Листинг 5.7. Файл serverd/serverd.c
#include <windows.h> #include <windowsx.h> #include <winsock.h> #include <commctrl.h> #include "resource.h" // ----------------------------------------------------- // Описание функций // ----------------------------------------------------- // Функция главного окна LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); // Функция для обработки сообщения WM_CREATE BOOL WndProc_OnCreate(HWND hWnd, LPCREATESTRUCT lpCreateStruct); // Функция для обработки сообщения WM_DESTROY void WndProc_OnDestroy(HWND hWnd); // Функция для обработки сообщения WM_COMMAND void WndProc_OnCommand(HWND hWnd, int id, HWND hwndCtl, UINT codeNotify); // Функция для обработки сообщения WM_SIZE void WndProc_OnSize(HWND hWnd, UINT state, int cx, int cy); // Запуск сервера void ServerStart(HWND hWnd); // Останов сервера void ServerStop(HWND hWnd); // Обработка сообщения WSA_NETEVENT void WndProc_OnWSANetEvent(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); // Порт сервера #define SERV_PORT 5000 #define IDS_STATUSBAR 802 // Определение кодов сообщений #define WSA_NETEVENT (WM_USER + 1) // ----------------------------------------------------- // Глобальные переменные // ----------------------------------------------------- // Идентификатор приложения HINSTANCE hInst; // Название приложения char szAppName[] = "WServerUDP "; // Заголовок главного окна приложения char szAppTitle[] = "Windows Socket UDP Server Demo"; // Идентификатор органа Statusbar HWND hwndSb; // Сокет сервера SOCKET srv_socket ; // ----------------------------------------------------- // Функция WinMain // ----------------------------------------------------- int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { WNDCLASSEX wc; HWND hWnd; MSG msg; hInst = hInstance; // Преверяем, не было ли это приложение запущено ранее hWnd = FindWindow(szAppName, NULL); if(hWnd) { // Если окно приложения было свернуто в пиктограмму, // восстанавливаем его if(IsIconic(hWnd)) ShowWindow(hWnd, SW_RESTORE); // Выдвигаем окно приложения на передний план SetForegroundWindow(hWnd); return FALSE; } // Регистрируем класс окна memset(&wc, 0, sizeof(wc)); wc.cbSize = sizeof(WNDCLASSEX); wc.hIconSm = LoadImage(hInst, MAKEINTRESOURCE(IDI_APPICON_SM), IMAGE_ICON, 16, 16, 0); wc.style = CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc = (WNDPROC)WndProc; wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = hInst; wc.hIcon = LoadImage(hInst, MAKEINTRESOURCE(IDI_APPICON), IMAGE_ICON, 32, 32, 0); wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH) (COLOR_WINDOW + 1); wc.lpszMenuName = MAKEINTRESOURCE(IDR_MENU1); wc.lpszClassName = szAppName; // Вызываем функцию RegisterClassEx, которая выполняет // регистрацию окна if(!RegisterClassEx(&wc)) if(!RegisterClass((LPWNDCLASS)&wc.style)) return FALSE; InitCommonControls(); // Создаем главное окно приложения hWnd = CreateWindow(szAppName, szAppTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInst, NULL); if(!hWnd) return(FALSE); // Отображаем окно ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); // Запускаем цикл обработки сообщений while(GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; } // ----------------------------------------------------- // Функция WndProc // ----------------------------------------------------- LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch(msg) { // Вызываем обработчик сообщения WSA_NETEVENT case WSA_NETEVENT: WndProc_OnWSANetEvent(hWnd, msg, wParam, lParam); break; HANDLE_MSG(hWnd, WM_CREATE, WndProc_OnCreate); HANDLE_MSG(hWnd, WM_COMMAND, WndProc_OnCommand); HANDLE_MSG(hWnd, WM_SIZE, WndProc_OnSize); HANDLE_MSG(hWnd, WM_DESTROY, WndProc_OnDestroy); default: return(DefWindowProc(hWnd, msg, wParam, lParam)); } } // ----------------------------------------------------- // Функция WndProc_OnCreate // ----------------------------------------------------- BOOL WndProc_OnCreate(HWND hWnd, LPCREATESTRUCT lpCreateStruct) { int rc; WSADATA WSAData; char szTemp[128]; // Инициализация и проверка версии Windows Sockets rc = WSAStartup (MAKEWORD(1, 1), &WSAData); if(rc != 0) { MessageBox(NULL, "WSAStartup Error", "Error", MB_OK); return FALSE; } // Отображаем описание и версию системы Windows Sockets // в окне органа управления Statusbar wsprintf(szTemp, "Server use %s %s", WSAData.szDescription,WSAData.szSystemStatus); hwndSb = CreateStatusWindow(WS_CHILD | WS_VISIBLE | WS_BORDER | SBARS_SIZEGRIP, szTemp, hWnd, IDS_STATUSBAR); return TRUE; } // ----------------------------------------------------- // Функция WndProc_OnDestroy // ----------------------------------------------------- #pragma warning(disable: 4098) void WndProc_OnDestroy(HWND hWnd) { // Освобождение ресурсов, полученных для // работы с Windows Sockets WSACleanup (); // Завершение цикла обработки сообщений PostQuitMessage(0); return FORWARD_WM_DESTROY (hWnd, DefWindowProc); } // ----------------------------------------------------- // Функция WndProc_OnSize // ----------------------------------------------------- #pragma warning(disable: 4098) void WndProc_OnSize(HWND hWnd, UINT state, int cx, int cy) { SendMessage(hwndSb, WM_SIZE, cx, cy); return FORWARD_WM_SIZE (hWnd, state, cx, cy, DefWindowProc); } // ----------------------------------------------------- // Функция WndProc_OnCommand // ----------------------------------------------------- #pragma warning(disable: 4098) void WndProc_OnCommand(HWND hWnd, int id, HWND hwndCtl, UINT codeNotify) { switch (id) { case IDM_EXIT: // Уничтожение главного окна прилоджения DestroyWindow(hWnd); break; case IDM_START: // Запуск сервера ServerStart(hWnd); break; case IDM_STOP: // Останов сервера ServerStop(hWnd); break; default: MessageBox(NULL, "Unknown command", "Error", MB_OK); } return FORWARD_WM_COMMAND (hWnd, id, hwndCtl, codeNotify, DefWindowProc); } // ----------------------------------------------------- // Функция ServerStart // ----------------------------------------------------- void ServerStart(HWND hWnd) { struct sockaddr_in srv_address; int rc; // Создаем сокет сервера для работы с потоком данных srv_socket = socket(AF_INET, SOCK_DGRAM, 0); if(srv_socket == INVALID_SOCKET) { MessageBox(NULL, "socket Error", "Error", MB_OK); return; } // Устанавливаем адрес IP и номер порта srv_address.sin_family = AF_INET ; srv_address.sin_addr.s_addr = INADDR_ANY ; srv_address.sin_port = htons(SERV_PORT); // Связываем адрес IP с сокетом if(bind (srv_socket, (LPSOCKADDR )&srv_address, sizeof(srv_address)) == SOCKET_ERROR ) { // При ошибке закрываем сокет closesocket (srv_socket); MessageBox(NULL, "bind Error", "Error", MB_OK); return; } // Если на данном сокете начнется передача данных от // клиента, в главное окно приложения поступит // сообщение WSA_NETEVENT. rc = WSAAsyncSelect (srv_socket, hWnd, WSA_NETEVENT, FD_READ ); if(rc > 0) { closesocket (srv_socket); MessageBox(NULL, "WSAAsyncSelect Error", "Error", MB_OK); return; } // Выводим в окна Statusbar сообщение о запуске сервера SendMessage(hwndSb, SB_SETTEXT, 0, (LPARAM)"Server started"); } // ----------------------------------------------------- // Функция ServerStop // ----------------------------------------------------- void ServerStop(HWND hWnd) { // Отменяем приход любых извещений в главную функцию // окна при возникновении любых событий, связанных // с системой Windows Sockets WSAAsyncSelect (srv_socket, hWnd, 0, 0); // Если сокет был создан, закрываем его if(srv_socket != INVALID_SOCKET) { closesocket (srv_socket); } // Выводим в окна Statusbar сообщение об останове сервера SendMessage(hwndSb, SB_SETTEXT, 0, (LPARAM)"Server stopped"); } // ----------------------------------------------------- // Функция WndProc_OnWSANetEvent // ----------------------------------------------------- void WndProc_OnWSANetEvent(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { char szTemp[256]; int rc; SOCKADDR _IN addr; int nAddrSize; char szBuf[80]; LPSTR lpAddr; if(WSAGETSELECTEVENT(lParam) == FD_READ ) { // Принимаем данные rc = recv from((SOCKET)wParam, szTemp, 256, 0, (PSOCKADDR )&addr, &nAddrSize); if(rc) { szTemp[rc] = '\0'; strcpy(szBuf, "Received from "); // Преобразовываем адрес IP удаленного клиента // в текстовую строку lpAddr = inet_ntoa (addr.sin_addr ); strcat(szBuf, lpAddr); // Отображаем адрес удаленного клиента // и полученную от него строку MessageBox(NULL, szTemp, szBuf, MB_OK); } return; } }
Приложение SERVERD во многом напоминает приложение SERVER, поэтому мы рассмотрим только отличия.
Первое отличие заключается в том, что при запуске сервера тип создаваемого сокета указывается как SOCK_DGRAM:
srv_socket = socket(AF_INET, SOCK_DGRAM, 0);
Далее выполняется инициализация сокета и его привязка к адресу, для чего вызывается функция bind. Эта операция, как и в случае протокола TCP, не обязательна.
После выполнения привязки можно приступать к получению пакетов данных от клиента. Для того чтобы не выполнять ожидание пакетов в цикле, наше приложение использует функцию WSAAsyncSelect, указывая с ее помощью, что при получении пакетов данных главное окно приложения должно получать сообщения с кодом WSA_NETEVENT:
rc = WSAAsyncSelect (srv_socket, hWnd, WSA_NETEVENT, FD_READ );
На этом инициализация сервера завершается.
Обработчик сообщения WSA_NETEVENT читает полученный пакет с помощью функции recv from:
SOCKADDR _IN addr; int nAddrSize; rc = recv from((SOCKET)wParam, szTemp, 256, 0, (PSOCKADDR )&addr, &nAddrSize);
В качестве предпоследнего параметра этой функции передается адрес структуры типа SOCKADDR _IN, куда функция записывает адрес узла, приславшего пакет. Последний параметр функции recv from должен содержать размер указанной структуры.
Ниже мы привели возможные коды ошибок для
функции recv from.
Код ошибки |
Описание |
WSANOTINITIALISED |
Перед использованием функции
необходимо вызвать функцию WSAStartup |
WSAENETDOWN |
Сбой в сети |
WSAEFAULT |
Слишком малое значение параметра,
определяющего размер буфера для приема данных |
WSAEINTR |
Работа функции была отменена при помощи
функции WSACancelBlockingCall |
WSAEINPROGRESS |
Выполняется блокирующая функция
интерфейса Windows Sockets |
WSAEINVAL |
Сокет не был подключен функцией bind |
WSAENOTSOCK |
Указанный дескриптор не является
дескриптором сокета |
WSAESHUTDOWN |
Сокет был закрыт функцией shutdown |
WSAEWOULDBLOCK |
Сокет отмечен как неблокирующий, но
запрошенная операция приведет к блокировке |
WSAEMSGSIZE |
Размер датаграммы слишком большой,
поэтому соответствующий блок данных не
помещается в буфер. Принятый блок данных был
обрезан |
WSAECONNABORTED |
Сбой из-за слишком большой задержки или
по другой причине |
WSAECONNRESET |
Сброс соединения удаленным узлом |
Заметим, что при обмене данных с использованием протокола UDP на каждый вызов функции send to должен приходиться один вызов функции recv from. Если же вы передается данные через канал с использованием протокола TCP, на один вызов функции send может приходиться несколько вызовов функции recv.
Для отображения адреса узла, пославшего пакет UDP, наше приложение преобразует этот адрес в символьную строку с помощью функции inet_ntoa :
lpAddr = inet_ntoa (addr.sin_addr );
Эта функция записывает полученную строку в статическую область памяти, принадлежащую системе Windows Sockets, поэтому для дальнейшего использования необходимо скопировать строку до следующего вызова любой функции программного интерфейса Windows Sockets.
Исходные тексты приложения CLIENTD, предназначенного для совместной работы с приложением SERVERD, представлены в листинге 5.8. Так как это приложение очень похоже на приложение CLIENT, мы опишем только отличия.
Листинг 5.8. Файл clientd/clientd.c
#include <windows.h> #include <windowsx.h> #include <winsock.h> #include <commctrl.h> #include "resource.h" // ----------------------------------------------------- // Описание функций // ----------------------------------------------------- // Функция главного окна LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); // Функция для обработки сообщения WM_CREATE BOOL WndProc_OnCreate(HWND hWnd, LPCREATESTRUCT lpCreateStruct); // Функция для обработки сообщения WM_DESTROY void WndProc_OnDestroy(HWND hWnd); // Функция для обработки сообщения WM_COMMAND void WndProc_OnCommand(HWND hWnd, int id, HWND hwndCtl, UINT codeNotify); // Функция для обработки сообщения WM_SIZE void WndProc_OnSize(HWND hWnd, UINT state, int cx, int cy); // Установка соединения void SetConnection(HWND hWnd); // Передача сообщения void SendMsg(HWND hWnd); // Порт сервера #define SERV_PORT 5000 #define IDS_STATUSBAR 802 // ----------------------------------------------------- // Глобальные переменные // ----------------------------------------------------- // Идентификатор приложения HINSTANCE hInst; // Название приложения char szAppName[] = "WClientUDP "; // Заголовок главного окна приложения char szAppTitle[] = "Windows Socket UDP Client Demo"; // Идентификатор органа управления Statusbar HWND hwndSb; // Сокет клиента SOCKET srv_socket ; // Адрес сервера SOCKADDR _IN dest_sin; // ----------------------------------------------------- // Функция WinMain // ----------------------------------------------------- int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { WNDCLASSEX wc; HWND hWnd; MSG msg; hInst = hInstance; // Преверяем, не было ли это приложение запущено ранее hWnd = FindWindow(szAppName, NULL); if(hWnd) { // Если окно приложения было свернуто в пиктограмму, // восстанавливаем его if(IsIconic(hWnd)) ShowWindow(hWnd, SW_RESTORE); // Выдвигаем окно приложения на передний план SetForegroundWindow(hWnd); return FALSE; } // Регистрируем класс окна memset(&wc, 0, sizeof(wc)); wc.cbSize = sizeof(WNDCLASSEX); wc.hIconSm = LoadImage(hInst, MAKEINTRESOURCE(IDI_APPICON_SM), IMAGE_ICON, 16, 16, 0); wc.style = CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc = (WNDPROC)WndProc; wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = hInst; wc.hIcon = LoadImage(hInst, MAKEINTRESOURCE(IDI_APPICON), IMAGE_ICON, 32, 32, 0); wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH) (COLOR_WINDOW + 1); wc.lpszMenuName = MAKEINTRESOURCE(IDR_MENU1); wc.lpszClassName = szAppName; // Вызываем функцию RegisterClassEx, которая выполняет // регистрацию окна if(!RegisterClassEx(&wc)) if(!RegisterClass((LPWNDCLASS)&wc.style)) return FALSE; InitCommonControls(); // Создаем главное окно приложения hWnd = CreateWindow(szAppName, szAppTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInst, NULL); if(!hWnd) return(FALSE); // Отображаем окно ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); // Запускаем цикл обработки сообщений while(GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; } // ----------------------------------------------------- // Функция WndProc // ----------------------------------------------------- LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch(msg) { HANDLE_MSG(hWnd, WM_CREATE, WndProc_OnCreate); HANDLE_MSG(hWnd, WM_COMMAND, WndProc_OnCommand); HANDLE_MSG(hWnd, WM_SIZE, WndProc_OnSize); HANDLE_MSG(hWnd, WM_DESTROY, WndProc_OnDestroy); default: return(DefWindowProc(hWnd, msg, wParam, lParam)); } } // ----------------------------------------------------- // Функция WndProc_OnCreate // ----------------------------------------------------- BOOL WndProc_OnCreate(HWND hWnd, LPCREATESTRUCT lpCreateStruct) { int rc; WSADATA WSAData; char szTemp[128]; // Инициализация и проверка версии Windows Sockets rc = WSAStartup (MAKEWORD(1, 1), &WSAData); if(rc != 0) { MessageBox(NULL, "WSAStartup Error", "Error", MB_OK); return FALSE; } // Отображаем описание и версию системы Windows Sockets // в окне органа управления Statusbar wsprintf(szTemp, "Server use %s %s", WSAData.szDescription,WSAData.szSystemStatus); hwndSb = CreateStatusWindow(WS_CHILD | WS_VISIBLE | WS_BORDER | SBARS_SIZEGRIP, szTemp, hWnd, IDS_STATUSBAR); return TRUE; } // ----------------------------------------------------- // Функция WndProc_OnDestroy // ----------------------------------------------------- #pragma warning(disable: 4098) void WndProc_OnDestroy(HWND hWnd) { // Освобождение ресурсов, полученных для // работы с Windows Sockets WSACleanup (); // Завершение цикла обработки сообщений PostQuitMessage(0); return FORWARD_WM_DESTROY (hWnd, DefWindowProc); } // ----------------------------------------------------- // Функция WndProc_OnSize // ----------------------------------------------------- #pragma warning(disable: 4098) void WndProc_OnSize(HWND hWnd, UINT state, int cx, int cy) { SendMessage(hwndSb, WM_SIZE, cx, cy); return FORWARD_WM_SIZE (hWnd, state, cx, cy, DefWindowProc); } // ----------------------------------------------------- // Функция WndProc_OnCommand // ----------------------------------------------------- #pragma warning(disable: 4098) void WndProc_OnCommand(HWND hWnd, int id, HWND hwndCtl, UINT codeNotify) { switch (id) { case IDM_EXIT: // Уничтожение главного окна прилоджения DestroyWindow(hWnd); break; case IDM_CONNECT: // Установка соединения с сервером SetConnection(hWnd); break; case IDM_SEND: // Посылка сообщения серверу SendMsg(hWnd); break; default: MessageBox(NULL, "Unknown command", "Error", MB_OK); } return FORWARD_WM_COMMAND (hWnd, id, hwndCtl, codeNotify, DefWindowProc); } // ----------------------------------------------------- // Функция SetConnection // ----------------------------------------------------- void SetConnection(HWND hWnd) { PHOSTENT phe; // Создаем сокет srv_socket = socket(AF_INET, SOCK_DGRAM, 0); if(srv_socket == INVALID_SOCKET) { MessageBox(NULL, "socket Error", "Error", MB_OK); return; } // Связываем адрес IP с сокетом dest_sin.sin_family = AF_INET ; dest_sin.sin_addr.s_addr = INADDR_ANY ; dest_sin.sin_port = 0; if(bind (srv_socket, (LPSOCKADDR )&dest_sin, sizeof(dest_sin)) == SOCKET_ERROR ) { // При ошибке закрываем сокет closesocket (srv_socket); MessageBox(NULL, "bind Error", "Error", MB_OK); return; } // Устанавливаем адрес IP и номер порта dest_sin.sin_family = AF_INET ; // Определяем адрес узла // Адрес локального узла для отладки phe = gethostbyname ("localhost "); // Адрес удаленного узла // phe = gethostbyname ("maxsinev"); if(phe == NULL) { closesocket (srv_socket); MessageBox(NULL, "gethostbyname Error", "Error", MB_OK); return; } // Копируем адрес узла memcpy((char FAR *)&(dest_sin.sin_addr ), phe->h_addr, phe->h_length); // Другой способ указания адреса узла // dest_sin.sin_addr.s_addr = inet_addr ("200.200.200.201"); // Копируем номер порта dest_sin.sin_port = htons(SERV_PORT); // В случае успеха выводим сообщение об установке // соединения с узлом SendMessage(hwndSb, SB_SETTEXT, 0, (LPARAM)"Connected"); } // ----------------------------------------------------- // Функция SendMsg // ----------------------------------------------------- void SendMsg(HWND hWnd) { char szBuf[80]; lstrcpy(szBuf, "Test string"); // Посылаем сообщение send to(srv_socket, szBuf, lstrlen(szBuf), 0, (PSOCKADDR )&dest_sin, sizeof(dest_sin)); }
Функция SetConnection создает сокет типа SOCK_DGRAM, так как передача данных будет выполняться с использованием протокола UDP :
srv_socket = socket(AF_INET, SOCK_DGRAM, 0);
Далее выполняется привязка сокета к адресу с помощью функции bind. При этом указывается нулевое значение порта и адрес INADDR_ANY, так как на данном этапе эти параметры не имеют значения.
Затем функция SetConnection записывает адрес сервера в структуру dest_sin. Этот адрес потребуется для передачи сообщений серверу.
При использовании протокола UDP и если не создан канал между приложениями, для передачи данных следует использовать функцию send to:
send to(srv_socket, szBuf, lstrlen(szBuf), 0, (PSOCKADDR )&dest_sin, sizeof(dest_sin));
В качестве предпоследнего параметра этой фукнции нужно передать адрес заполненной структуры, содержащей адрес узла, куда будет посылаться пакет данных. Через последний параметр функции send to необходимо передать размер указанной структуры.
Привдедем список возможных кодов ошибок для
функции send to:
Код ошибки |
Описание |
WSANOTINITIALISED |
Перед использованием функции
необходимо вызвать функцию WSAStartup |
WSAENETDOWN |
Сбой в сети |
WSAEACCES |
Не был установлен флаг
широковещательного адреса |
WSAEINTR |
Работа функции была отменена при помощи
функции WSACancelBlockingCall |
WSAEINPROGRESS |
Выполняется блокирующая функция
интерфейса Windows Sockets |
WSAEFAULT |
Неправильно указан адрес буфера,
содержащего передаваемые данные |
WSAENETRESET |
Необходимо сбросить соединение |
WSAENOBUFS |
Произошло зацикливание буферов |
WSAENOTSOCK |
Указанный дескриптор не является
дескриптором сокета |
WSAESHUTDOWN |
Сокет был закрыт функцией shutdown |
WSAEWOULDBLOCK |
Сокет отмечен как неблокирующий, но
запрошенная операция приведет к блокировке |
WSAEMSGSIZE |
Размер датаграммы больше, чем это
допускается данной реализацией интерфейса Windows Sockets |
WSAECONNABORTED |
Сбой из-за слишком большой задержки или
по другой причине |
WSAECONNRESET |
Сброс соединения удаленным узлом |
WSAEADDRNOTAVAIL |
Указанный адрес недоступен |
WSAEAFNOSUPPORT |
Данный тип сокета не может работать с
указанным семейством адресов |
WSAEDESTADDRREQ |
Необходимо указать адрес получателя
датаграммы |
WSAENETUNREACH |
В данное время и из данного узла
невозможно получить доступ к сети |
Заметим, что клиент может создать канал связи с сервером, вызвав функцию connect, и передавать по этому каналу пакеты UDP, пользуясь функциями send и recv. Этот способ удобен тем, что при передаче пакета не нужно каждый раз указывать адрес получателя.