Задачей протокола транспортного уровня UDP (User Datagram Protocol) является передача данных между прикладными процессами без гарантий доставки, поэтому его пакеты могут быть потеряны, продублированы или прийти не в том порядке, в котором они были отправлены.
Формат сообщений UDP
Единица данных протокола UDP называется UDP-пакетом или пользовательской дейтаграммой (user datagram). UDP-пакет состоит из заголовка и поля данных, в котором размещается пакет прикладного уровня. Заголовок имеет простой формат и состоит из четырех двухбайтовых полей:
· UDP source port - номер порта процесса-отправителя,
· UDP destination port - номер порта процесса-получателя,
· UDP message length - длина UDP-пакета в байтах,
· UDP checksum - контрольная сумма UDP-пакета
Не все поля UDP-пакета обязательно должны быть заполнены. Если посылаемая дейтаграмма не предполагает ответа, то на месте адреса отправителя могут помещаться нули. Можно отказаться и от подсчета контрольной суммы, однако следует учесть, что протокол IP подсчитывает контрольную сумму только для заголовка IP-пакета, игнорируя поле данных.
порт отправителя | порт получателя | ||
длина сообщения | контрольная сумма | ||
данные | |||
................... |
Рис.2. Формат UDP дейтаграммы
3.3. Протокол надежной доставки сообщений TCP
В стеке протоколов TCP/IP протокол TCP (Transmission Control Protocol) работает так же, как и протокол UDP, на транспортном уровне. Он обеспечивает надежную транспортировку данных между прикладными процессами путем установления логического соединения.
Формат сообщений TCP
Сообщения протокола TCP называются сегментами и состоят из заголовка и блока данных. Заголовок сегмента имеет следующие поля:
· Порт источника (SOURS PORT) занимает 2 байта, идентифицирует процесс-отправитель;
|
· Порт назначения (DESTINATION PORT) занимает 2 байта, идентифицирует процесс-получатель;
· Последовательный номер (SEQUENCE NUMBER) занимает 4 байта, указывает номер байта, который определяет смещение сегмента относительно потока отправляемых данных;
· Подтвержденный номер (ACKNOWLEDGEMENT NUMBER) занимает 4 байта, содержит максимальный номер байта в полученном сегменте, увеличенный на единицу; именно это значение используется в качестве квитанции;
· Длина заголовка (HLEN) занимает 4 бита, указывает длину заголовка сегмента TCP, измеренную в 32-битовых словах. Длина заголовка не фиксирована и может изменяться в зависимости от значений, устанавливаемых в поле Опции;
· Резерв (RESERVED) занимает 6 битов, поле зарезервировано для последующего использования;
· Кодовые биты (CODE BITS) занимают 6 битов, содержат служебную информацию о типе данного сегмента, задаваемую установкой в единицу соответствующих бит этого поля:
· URG - срочное сообщение;
· ACK - квитанция на принятый сегмент;
· PSH - запрос на отправку сообщения без ожидания заполнения буфера;
· RST - запрос на восстановление соединения;
· SYN - сообщение используемое для синхронизации счетчиков переданных данных при установлении соединения;
· FIN - признак достижения передающей стороной последнего байта в потоке передаваемых данных.
· Окно (WINDOW) занимает 2 байта, содержит объявляемое значение размера окна в байтах;
· Контрольная сумма (CHECKSUM) занимает 2 байта, рассчитывается по сегменту;
· Указатель срочности (URGENT POINTER) занимает 2 байта, используется совместно с кодовым битом URG, указывает на конец данных, которые необходимо срочно принять, несмотря на переполнение буфера;
|
· Опции (OPTIONS) - это поле имеет переменную длину и может вообще отсутствовать, максимальная величина поля 3 байта; используется для решения вспомогательных задач, например, при выборе максимального размера сегмента;
· Заполнитель (PADDING) может иметь переменную длину, представляет собой фиктивное поле, используемое для доведения размера заголовка до целого числа 32-битовых слов.
4. Программирование по схеме “клиент-сервер” с использованием интерфейса Windows Sockets
В локальных и глобальных сетях существуют два принципиально разных способа передачи данных. Первый из них предполагает посылку данных от одного узла к другому (или сразу нескольким узлам) без получения подтверждения о доставке и даже без гарантии того, что передаваемые пакеты будут получены в правильной последовательности. Примером такого протокола служит UDP, который используется в сетях TCP/IP, или протокол IPX, который является базовым в сетях Novell NetWare. Основные преимущества дейтаграммных протоколов заключаются в высоком быстродействии и возможности широковещательной передачи данных, когда один узел отправляет сообщения, а несколько других их получают, причём одновременно.
Второй способ передачи предполагает создание канала передачи данных между двумя различными узлами сети. При этом канал создаётся средствами дейтаграммных протоколов, однако доставка пакетов в канале является гарантированной. Пакеты всегда доходят в целостности и сохранности (по крайней мере до прикладного уровня протокола), причём в правильном порядке. Быстродействие получается в среднем ниже, чем у дейтаграммных протоколов, за счёт посылки подтверждений. Примерами протоколов, использующих канал связи, могут служить протоколы TCP и SPX (протокол NETBIOS допускает передачу данных с использованием как дейтаграмм, так и каналов связи). В данном приложении для передачи тестовых данных используется протокол пользовательских дейтаграмм UDP, так как он не гарантирует доставки пакетов, и следовательно годится для тестирования сетей.
|
Для передачи данных с помощью любого из вышеперечисленных двух способов каждое приложение должно создать объект, называемый сокетом. По своему назначению сокет больше всего похож на идентификатор файла (file handle), который нужен для выполнения над файлом операций чтения или записи. Прежде чем приложение, запущенное на узле сети, сможет выполнять передачу или приём данных, оно должно создать сокет и проинициализировать его, указав некоторые параметры. Для сокета необходимо указать три параметра. Это IP адрес, связанный с сокетом, номер порта, для которого будут выполняться операции передачи данных, а так же тип сокета. Что касается последнего параметра (тип сокета), то существуют сокеты двух типов. Первый тип предназначен для передачи данных в виде дейтаграмм, второй – с использованием каналов связи.
Инициализация приложения и завершение его работы.
В процессе инициализации приложение должно зарегистрировать себя в библиотеке winsock32.dll, которая предоставляет приложениям интерфейс Windows Sockets в среде операционных систем MS Windows9x и MS Windows NT.
Для инициализации необходимо вызвать функцию WSAStartup, определённую следующим образом:
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
В параметре wVersionRequested указывается версия интерфейса WS, необходимая для работы данного приложения. Старший байт указывает младший номер версии (minor version), младший байт – старший номер версии (major version).
Перед вызовом функции WSAStartup параметр lpWSAData должен содержать указатель на структуру типа WSADATA, в которую будут записаны сведения о конкретной реализации интерфейса Windows Sockets. В случае успеха функция WSAStartup возвращает нулевое значение.
Если происходит ошибка, возвращается одно из следующих значений:
Значение | Описание |
WSASYSNOTREADY | Сетевое программное обеспечение не готово для работы |
WSAVERNOTSUPPORTED | Функция не поддерживается данной реализацией интерфейса Windows Sockets |
WSAEINVAL | Библиотека dll, обеспечивающая интерфейс Windows Sockets, не соответствует версии, указанной приложением в параметре wVersionRequested |
Фрагмент текста программы, выполняющий инициализацию интерфейса Windows Sockets:
#include <winsock2.h> ………………………….. Rc=WSAStartup(MAKEWORD(2,0), &WSAData); if(rc!= 0) { mMessageBox(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); |
Определение структуры WSADATA и указателя на неё выглядит следующим образом:
typedef struct WSAData { WORD wVersion; WORD wHighVersion; charszDescription[WSADESCRIPTION_LEN+1]; char szSystemstatus[WSASYS_STATUS_LEN+1]; unsigned short iMaxSockets; unsigned short iMaxUdpDg; char FAR * lpVendorInfo; } WSADTA; typedef WSADATA FAR *LPWSADATA; |
Использованные выше поля szDescription и szSystemStatus после вызова функции WSAStartup содержат соответственно описание конкретной реализации интерфейса Windows Sockets и текущее состояние этого интерфейса в виде текстовых строк. В полях wVersion и wHighVersion записаны соответственно версия спецификации Windows Sockets, которую будет использовать приложение, и версия спецификации, которой соответствует конкретная реализация интерфейса Windows Sockets. Приложение может создавать одновременно несколько сокетов, например для использования в различных подзадачах одного процесса. В поле iMaxSockets хранится максимальное количество сокетов, которое можно получить для одного процесса. В поле iMaxUdpDg записан максимальный размер пакета данных, который можно делать с использованием дейтаграмного протокола UDP. Поле lpVendorInfo содержит указатель на дополнительную информацию, формат которой зависит от фирмы – изготовителя конкретной реализации системы Windows Sockets.
Перед тем, как завершить свою работу, приложение должно освободить ресурсы, полученные у операционной системы для работы с Windows Sockets. Для выполнения этой задачи приложение должно вызвать функцию WSACleanup, определённую так, как это показано ниже:
int WSACleanup(void);
Эта функция может возвратить нулевое значение при успехе или значение SOCKET_ERROR в случае ошибки. Для получения кода последней ошибки, возвращённой Windows Sockets, используется функция:
int WSAGetLastError(void);
Эта функция позволяет определить код ошибки при неудачном завершении практически всех функций интерфейса Windows Sockets. Её нужно вызывать сразу вслед за функцией, завершившейся неудачно. Если ошибка возникла при выполнении функции WSACleanup, функция WSAGetLastError может вернуть одно из следующих значений:
Значение | Описание |
WSANOTINITIALISED | Интерфейс Windows Sockets не был проинициализирован функцией WSAStartup |
WSAENETDOWN | Сбой сетевого программного обеспечения |
WSAEINPROGRESS | Во время вызова функции WSACleanup выполнялась одна из блокирующих функций интерфейса Windows Sockets |
Сделаем небольшие пояснения относительно последней ошибки, приведённой в этом списке, имеющей код WSAEINPROGRESS. Некоторые функции интерфейса Windows Sockets способны блокировать работу приложения, так как они не возвращают управление до своего завершения. В операционных системах, использующих вытесняющую мультизадачность, к которым относятся Microsoft Windows 9x/Me, Windows NT4/5, это не приводит к блокировке всей системы. Можно избежать использования блокирующих функций, так как для них в интерфейсе Windows Sockets существует замена.
Создание и инициализация сокета.
После инициализации интерфейса Windows Sockets приложение должно создать один или несколько сокетов, которые будут использованы для передачи данных.
Сокет создаётся с помощью функции:
SOCKET socket(int af, int type, int protocol);
API сокетов не зависит от протокола и может поддерживать разные адресные домены. Параметр af – это константа, указывающая какой домен нужен сокету (определяет формат адреса). Этому параметру соответствуют домены AF_INET, что соответствует формату адреса, принятому в Internet, AF_LOCAL (или AF_UNIX) и AF_UNSPEC. Домен AF_LOCAL применяется для межпроцессного взаимодействия (IPC) на одной и той же машине. В устаревших вариантах вызова socket константы доменов обозначали как PF_* вместо современных AF_*. Но поскольку оба набора констант определены одинаково – в действительности одни константы просто выражаются через другие, - на практике можно употреблять оба варианта.
Домен AF_UNSPEC применяется в тех случаях, когда спецификация сокета определяется двумя другими параметрами.
Параметры type и protocol определяют соответственно тип сокета и протокол, который будет использован для данного сокета.
Тип сокета | Описание |
SOCK_STREAM | Сокет будет использован для передачи данных через канал связи с использованием протокола TCP |
SOCK_DGRAM | Сокет будет использован для передачи данных без создания канала связи через дейтаграммный протокол UDP |
SOCK_RAW | Сокет будет использован для доступа к некоторым дейтаграммам на уровне протокола IP. Они используются в особых случаях, например для просмотра всех ICMP сообщений. |
Параметр протокол должен быть определен константами связанными с протоколом, такими как IPPROTO_TCP и IPPROTO_UDP для протоколов TCP и UDP соответственно. Однако при использовании домена AF_INET достаточно указать только тип сокета, а для параметра 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, IPPROTO_TCP); if (srv_socket == INVALID_SOCKET) { MessageBox(NULL, “socket error”, “error”, MB_OK); return 0; } |
Удаление сокета
Для освобождения ресурсов приложение должно закрывать сокеты, которые ему больше не нужны, вызывая функцию
int closesocket(SOCKET sock);
Значение | Описание |
WSANOTINITIALISED | Интерфейс Windows Sockets не был проинициализирован функцией WSAStartup |
WSAENETDOWN | Сбой сетевого программного обеспечения |
WSAENONSOCK | Указанный в параметре дескриптор не является сокетом |
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; |
Однако, теперь она считается устаревшей, и в Winsock 2.x на смену ей пришла структура sockaddr_in, определенная так:
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_port (номера порта), sin_addr (IP-адреса узла) и "хвоста" из восьми нулевых байтов, который остался от четырнадцатисимвольного массива sa_data. Для чего он нужен? Дело в том, что структура sockaddr не привязана именно к TCP/IP, и может работать и с другими сетевыми протоколами. Адреса же некоторых сетей требуют для своего представления гораздо больше четырех байт.
Поле sin_family определяет тип адреса. Нужно записать в это поле значение AF_INET, которое соответствует типу адреса, принятому в Internet: srv_adress.sin_family = AF_INET;
Поле sin_port определяет номер порта, который будет использоваться для передачи данных. Порт – это просто идентификатор программы, выполняющей обмен в сети. На одном узле может одновременно работать несколько программ, использующих разные порты. Особенностью поля sin_port является использование так называемого сетевого формата данных. Этот формат отличается от того, что принят в процессорах с архитектурой Intel, а именно младшие байты данных хранятся по старшим
адресам памяти. Напомним, что архитектура процессоров Intel подразумевает хранение старших байтов данных по младшим адресам. Универсальный сетевой формат удобен при организации глобальных сетей, так как в узлах такой сети могут использоваться компьютеры с различной архитектурой.
Для выполнения преобразования из обычного формата в сетевой и обратно в интерфейсе Windows Sockets предусмотрен специальный набор функций. В частности, для заполнения поля sin_port нужно использовать функцию htons, выполняющую преобразование 16-ти разрядных данных или htonl, выполняющую преобразование 32-х разрядных данных из формата Intel в сетевой формат.
Инициализация поля sin_port:
srv_address.sin_port = htons (5000);
где 5000 – номер порта.
Вернёмся снова к структуре 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_ipm S_un.S_un_w.s_w2 #define s_impno S_un.S_un_b.s_b3 |
При инициализации сокета в этой структуре нужно указать адрес IP, с которым будет работать данный сокет. Если сокет будет работать с любым адресом (например, вы создаёте сервер, который будет доступен из узлов с любым адресом), адрес для сокета можно указать следующим образом:
srv_adress.sin_addr.s_addr = INADDR_ANY;
В этом случае, если сокет будет работать с определённым адресом IP (например вы создаёте приложение-клиент, которое будет обращаться к серверу с конкретным адресом IP), в указанную структуру необходимо записать реальный адрес.
Дейтаграммный протокол UDP позволяет посылать пакеты данных одновременно всем рабочим станциям в широковещательном режиме. Для этого нужно указать адрес как INADDR_BROADCAST.
Если известен адрес в виде четырёх десятичных чисел, разделённых точкой (именно так его вводит пользователь), то можно заполнить поле адреса при помощи inet_addr:
dest_sin.sin_addr.s_addr = inet_addr(“192.168.1.1”);
При записи адресов нужно учитывать, что в некоторых реализациях TCP/IP принято стандартное для языка С соглашение о том, что числа начинающиеся с нуля, записываются в восьмеричной системе. В таком случае 17.52.86.120 – это не то же самое, 017.52.86.120. В первом примере адрес сети равен 17, а во втором – 15.
В случае ошибки функция возвращает значение INADDR_NONE, что можно использовать для проверки.
Обратное преобразование адреса IP в текстовую строку можно при необходимости легко выполнить с помощью функции inet_ntoa, имеющей следующий прототип:
char FAR * inet_ntoa(struct in_addr in);
При ошибке эта функция возвращает NULL.
Однако чаще всего пользователь работает с доменными именами, применяя сервер DNS или файл HOSTS(пример файла приведен ниже).
# (C) Корпорация Майкрософт (Microsoft Corp.), 1993-1999 # # Это образец файла HOSTS, используемый Microsoft TCP/IP для #Windows. # Этот файл содержит сопоставления IP-адресов именам узлов. # Каждый элемент должен располагаться в отдельной строке. IP-#адрес должен # находиться в первом столбце, за ним должно следовать #соответствующее имя. # IP-адрес и имя узла должны разделяться хотя бы одним пробелом. # Кроме того, в некоторых строках могут быть вставлены #комментарии # (такие, как эта строка), они должны следовать за именем узла и #отделяться # от него символом '#'. # Например: # # 102.54.94.97 rhino.acme.com # исходный сервер # 38.25.63.10 x.acme.com # узел клиента x 127.0.0.1 localhost 172.16.1.3 node |
В этом случае вначале нужно воспользоваться функцией gethostbyname, возвращающей IP адрес, а затем записать полученный IP адрес в структуру sin_addr:
SOCKADDR_IN dest_sin; 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_lenght); |
В случае ошибки функция gethostbyname возвращает NULL. При этом причину ошибки можно выяснить, проверив код возврата функции WSAGetLastError.
Если же указанный узел найден в базе DNS или в файле HOSTS, функция gethostbyname возвращает указатель на структуру hostent, описанную ниже:
struct hostent { char FAR * h_name; //имя узла char FAR * FAR * h_aliases; //список //альтернативных имён short h_addrtype; //тип адреса узла short h_length; //длина адреса char FAR * FAR * h_addr_list //список указателей // на IP-адреса }; typedef struct hostent *PHOSTENT; typedef struct hostent FAR *LPHOSTENT; |
Как и в случае с in_addr, во множестве программ и прилагаемых к Winsock SDK примерах активно используется недокументированное поле структуры h_addr. Например, вот строка из файла "simplec.c":
memcpy(&(server.sin_addr),hp->h_addr,hp->h_length);
В "winsock2.h", можно найти, что обозначает h_addr:
#define h_addr h_addr_list[0]
Т.е. искомый адрес находиться в h_addr_list[0]. А для чего нужны остальные адреса в списке? Дело в том, что с некоторыми доменными именами связано сразу несколько IP-адресов. В случае неработоспособности одного узла, клиент может попробовать подключиться к другому, или просто выбрать узел с наибольшей скоростью обмена. Но в приведенном примере клиент использует только первый IP-адрес в списке и игнорирует все остальные! Конечно, это не важно в данной лабораторной работе, но использование такой возможности может заметно повысить производительность приложения.
Привязка адреса к сокету.
После инициализации сокета следует произвести привязку адреса интерфейса и номера порта к сокету при помощи функции bind:
int bind(SOCKET s, const struct sockaddr FAR *name, int namelen);
При отсутствии ошибки возращаемое значение 0 иначе SOCKET_ERROR (для Windows) или -1 (для UNIX).
Параметр s – это дескриптор сокета. С помощью параметров name и namelen передаются порт и сетевой интерфейс. Обычно в качестве адреса задаётся константа INADDR_ANY. Это означает, что будет принято соединение, запрашиваемое по любому интерфейсу. Если хосту с несколькими сетевыми адресами нужно принимать соединения только по одному интерфейсу, то следует указать IP-адрес этого интерфейса. Как обычно namelen – длина структуры SOCKADDR_IN. Если ваше приложение использовало bind для привязки порта к сокету, то ни одно другое приложение не сможет прослушивать этот порт.
Пример вызова функции bind:
if (bind(srv_socket,(LPSOCKADDR)&srv_address, sizeof(srv_adress))==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, порт). Если указать значение backlog, больше максимально допустимого, то система уменьшит его, не сообщив об ошибке.
Пример вызова функции listen:
if (listen(srv_socket, 1) == SOCKET_ERROR) { closesocket(srv_socket); MessageBox(NULL, “listen Error”, “Error”, MB_OK); return; } |
Далее необходимо выполнить ожидание соединения. Это можно выполнить двумя различными способами:
Первый способ заключается в циклическом вызове функции accept до тех пор, пока не будет установлено соединение. Затем можно будет приступать к обмену данными.
Функция accept имеет следующий прототип:
SOCKET accept(SOCKET s, struct sockaddr FAR *addr, int FAR * addrlen);
Параметр s – это дескриптор прослушиваемого сокета. Функция возвращает адрес приложения на другом конце соединения в структуре SOCKADDR_IN, на которую указывает параметр addr. Целому числу, на которое указывает параметр addrlen, ядро присваивает значение, равное длине этой структуры. Часто нет необходимости знать адрес клиентского приложения, поэтому в качестве addr и addrlen будет передаваться NULL.
Возвращаемое значение в случае ошибки: INVALID_SOCKET (для Windows) или -1 (для UNIX). Если ошибки нет, то функция accept создает сокет с теми же параметрами, что и SOCKET s и возвращает дескриптор созданного сокета. Сокет возвращенный функцией accept связан и может немедленно использоваться для приема и передачи данных.
Так как при вызове accept выполнение программы прекращается до установления соединения целесообразно осуществлять вызов функции из параллельного треда (потока). Для Windows функция создания потока выглядит так:
HANDLE CreateThread( LPSECURITY_ATTRIBUTES LPSA; DWORD cbStack; LPTHREAD_START_ROUTINE lpStartAddr; LPVOID lpvThreadParm; DWORD fdwCreate; LPDWORD lpIDThread; ); |
Подробно о параметрах функции можно узнать в MSDN Library. Для выполнения данной лабораторной работы необходимыми параметрами являются lpStartAddr, lpvThreadParm и lpIDThread. Параметр lpStartAddr определяет адрес функции потока, с которой должен будет начать работу создаваемый поток. Вполне допустимо и даже полезно создавать несколько потоков, у которых в качестве входной точки используется адрес одной и той же стартовой функции. Параметр lpvThreadParm позволяет передавать функции потока какое-либо инициализирующее значение. Оно может представлять собой или просто 32-битное значение, или 32-битный указатель на структуру данных с дополнительной информацией. Последний параметр функции lpIDThread – это адрес переменной типа DWORD, в которой функция вернет идентификатор, приписанный системой новому потоку. В Windows 95 передача NULL вместо этого параметра даст ошибку. В Windows NT версии 4.0 и выше разрешено передавать NULL в параметре lpIDThread. Поэтому для корректной работы вашего приложения на различных платформах необходимо указать функции адрес, для помещения в него идентификатор потока. Для завершения потока необходимо воспользоваться одной из функций:
VOID ExitThread(UINT fuExitCode);
У этой функции нет возвращаемого значения, ведь после ее вызова поток перестает существовать. В параметр fuExitCode она помещает код завершения потока. Функция вызывается из завершаемого потока.
BOOL TerminateThread(HANDLE hThread, DWORD dwExitCode);
Функция завершает поток, идентифицируемый параметром hThread, и помещает код завершения в dwExitCode. Ее используют лишь в крайнем случае, когда управление потоком потеряно. Вызов осуществляется из другого потока.
Пример использования accept:
#define MAXCONNECT 100 SOCKADDR_IN s_addr_out[MAXCONNECT]; DWORD WINAPI ServerAccept(LPVOID); SOCKET sock, socket[MAXCONNECT]; ………………………… int main() { ………………………… DWORD DwThCtrlACC; HANDLE ThreadACC = CreateThread(NULL,0,ServerAccept,0,0,&DwThCtrlACC); return 0; } DWORD WINAPI ServerAccept(LPVOID) { int numconnect = 0; while(numconnect<MAXCONNECT) { int nm = sizeof(s_addr_out[numconnect]); socket[numconnect]= accept(sock,&s_addr_out[numconnect],&nm); numconnect++; } return 0; } |
Второй способ основан на использовании расширения интерфейса Windows Sockets, предназначенного для выполнения асинхронных операций. Вместо того, чтобы ожидать соединение, приложение может вызвать один раз функцию WSAAsyncSelect, указав ей, что при получении запроса на установку соединения функция окна приложения должна получить сообщение:
#define WM_SOCKET (WM_USER + 1) //При попытке установки соединения главное окно приложения получит //сообщение WSA_ACCEPT rc = WSAAsyncSelect(srv_socket, hWnd, WM_SOCKET, FD_ACCEPT); if (rc > 0) { clossesocket(srv_socket); MessageBox(NULL, “WSAAsyncSelect Error”, “Error”,MB_OK); return; } |
В данном случае ожидание соединения выполняется для сокета srv_socket. Последний параметр функции имеет значение FD_ACCEPT. Это означает, что при попытке создания канала связи функция окна с идентификатором hWnd получит сообщение WSA_ACCEPT, определённое приложении.
Обработчик этого сообщения может выглядеть, например, следующим образом:
Void WndProc_OnWSAAccept(HWND hWnd, UNIT msg, WPARAM wParam, LPARAM lParam) { int rc; //При ошибке отменяем поступление извещений в главное //окно приложения if (WSAGETSELECTEDERROR(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,“Invalid socket”,“Error”,MB_OK); return; } //если на данном сокете начинается передача данных от //клиента, в главное окно приложения поступит сообщение //WSA_NETEVENT. Это же сообшение поступит при //разрыве соединения. rc = WSAAsyncSelect(srv_socket, hWnd, WM_SOCKET, FD_READ | FD_CLOSE); if (rc > 0) { closesocket(srv_socket); MessageBox(NULL,“WSAAsyncSelect Error”,“Error”, MB_OK); return; } } |
В данном случае обработчик вызывает функцию accept, выполняющую создание канала передачи данных. После этого функция WSAAsyncSelect вызывается один раз для того, чтобы установить асинхронную обработку приёма данных от удалённого клиента, а также обработку ситуации разрыва канала связи.
Сторона клиента:
Для установки соединения со стороны клиента используется функция connect.
connect (SOCKET s, const struct sockaddr *peer, int peer_len);
Возвращаемое значение: 0 – нормально, -1 (UNIX) или не 0 (Windows) – ошибка. Параметр s – дескриптор сокета, который вернул системный вызов функции socket. Параметр peer указывает на структуру, в которой хранится адрес удаленного хоста и некоторая дополнительная информация (см. выше). Для домена AF_INET – это структура SOCKADDR_IN. Параметр peer_len содержит размер структуры в байтах, на которую указывает peer. При удачном завершении функции connect можно немедленно приступать к обмену данными с сервером.
Пример использования функции connect для установления соединения с сервером.
void SetConnection(…) { SOCKADDR_IN dest_sin; SOCKET clnt_socket; // создаем сокет clnt_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // устанавливаем адрес и порт запрашиваемого сервера dest_sin.sin_family = AF_INET; dest_sin.sin_addr.s_addr = inet_addr(“127.0.0.1”); dest_sin.sin_port = htons(1024); // посылаем запрос на соединение с сервером if(connect(clnt_sock,(PSOCKADDR)&dest_sin,sizeof(dest_sin)) == SOCKET_ERROR) MessageDlg("Failed",mtError,TMsgDlgButtons()<<mbOK, 0); ……………………………………. } |
Использование IP-адреса 127.0.0.1 делает возможным запускать и клиент, и сервер на одном компьютере (что очень удобно для тестирования приложения). Для эффективной и надежной работы соединения типа клиент-сервер, необходимо сигнализировать одну из сторон при разрыве соединения, а также в определенном порядке запускать приложения(первым запускается приложение-сервер).