Протоколы с установлением соединения — КиберПедия 

Археология об основании Рима: Новые раскопки проясняют и такой острый дискуссионный вопрос, как дата самого возникновения Рима...

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

Протоколы с установлением соединения

2021-03-17 92
Протоколы с установлением соединения 0.00 из 5.00 0 оценок
Заказать работу

Основы Winsock

В этом разделе описаны основные методики и API-вызовы, необходи­мые для написания сетевых приложений. Здесь рассмотриваются способы установления соединения между дву­мя компьютерами в сети и механизмы обмена данными. К методичке прилагается набор примеров клиент-серверных приложений. Единственная зависящая от протокола операция — это создание сокета. Большинство остальных вызовов функций Winsock, ответственных за установление соединения, отправку и прием дан­ных, не зависят от протокола. Примеры этого раздела помогут вам лучше понять вызо­вы Winsock, необходимые для установления соединений и обмена данными. Наша цель — изучить эти вызовы, поэтому в примерах используются прямые блокирующие вызовы Winsock. Другие модели ввода-вывода, реализованные в Winsock, обсуждаются в следующем разделе.

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

Любое Winsock-приложение перед вызовом функции должно загрузить со­ответствующую версию библиотеки Winsock. Если этого не сделать, функция вернет значение SOCKET_ERROR и выдаст ошибку WSANOTINITIALISED. Загруз­ку библиотеки Winsock выполняет функция WSAStartup:

 

int WSAStartup(

WORD wVersionRequested,

LPWSADATA lpWSAData

);

 

Параметр wVersionRequested задает версию загружаемой библиотеки Winsock. Старший и младший байты определяют дополнительный и основной номер версии библиотеки соответственно. Для получения значения парамет­ра wVersionRequested можно использовать макрос MAKEWORD(x, у), где х — старший байт, а у — младший.

Параметр lpWSAData — указатель на структуру LPWSADATA, которая при вызове функции WSAStartup заполняется сведениями о версии загружаемой библиотеки:

 

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*  lpVendorlnfo;

} WSADATA, *LPWSADATA;

 

WSAStartup присваивает параметру wVersion значение загружаемой версии. Параметр wHighVersion содержит номер последней доступной версии Winsock. Помните, что в обоих полях старший байт определяет дополнительный, а младший — основной номер версии. Поля szDescription и szSystemStatus запол­няются не во всех реализациях Winsock и практически не применяются.

Не используйте и поля iMaxSockets и iMaxUdpDg. Предполагается, что в них заданы максимальное количество одновременно открытых сокетов и максимальный размер дейтаграммы. Макси­мальное количество одновременно открытых сокетов зависит от свободной физической памяти. Наконец, поле lpVendorlnfo зарезервировано для инфор­мации изготовителя реализации Winsock и не используется ни на одной из платформ Win32.

Для использования в приложении Winsock 1 необходимо под­ключить файл Winsock.h, а для Winsock 2 — Winsock2.h.

Даже если платформа поддерживает Winsock 2, не обязательно использовать самую последнюю версию. Напротив, если необходимо, чтобы приложение поддерживалось несколькими платформами, возьмите за основу Winsock 1.1. Такое приложение будет отлично работать на платформе Windows NT 4-0, по­тому что все вызовы Winsock 1.1 имеются в Winsock 2 DLL.

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

В большинстве случаев при написании новых приложений следует загру­жать последнюю доступную версию библиотеки Winsock. Если будет выпуще­на версия 3, приложение, использующее версию 2.2, должно выполняться кор­ректно. При запросе более поздней версии Winsock, не поддерживаемой ва­шей платформой, WSAStartup вернет ошибку, а в поле wHighVersion структуры WSADATA появится номер последней версии библиотеки, поддерживаемой данной системой.

Проверка и обработка ошибок

Проверка и обработка ошибок играют весомую роль при написании Winsock-приложения. Функции Winsock достаточно часто возвращают ошибки, но как правило, не критические — передачу информации можно продол­жать. Большинство функций Winsock при ошибке вызова возвращают зна­чение SOCKET_ERROR, но так происходит не всегда. При подробном рассмот­рении API-вызовов мы обратим внимание на возвращаемые значения, соот­ветствующие ошибкам. Константа SOCKET_ERROR на самом деле равна -1. Для получения более информативного кода ошибки, возникшей после одно­го из вызовов Winsock, задействуйте функцию WSAGetLastError.

 

int WSAGetLastError (void);

 

Эта функция возвращает код последней ошибки. Всем кодам ошибок, воз­вращаемым WSAGetLastError, соответствуют стандартные константные значе­ния. Они описаны в Winsockh или в Winsock2.h (в зависимости от версии Winsock). Единственное различие этих заголовочных файлов — Winsock2.h содержит больше кодов ошибок новых API-функций. Константы, определенные для кодов ошибок директивой #define, обычно начинаются с префикса WSAE.

Серверные API-функции

Сервер — это процесс, который ожидает подключения клиентов для обслу­живания их запросов. Сервер должен прослушивать соединения на стандар­тном имени. В TCP/IP таким именем является IP-адрес локального интерфей­са и номер порта. У каждого протокола своя схема адресации, а потому и свои особенности именования. Первый шаг установления соединения — привязка сокета данного протокола к его стандартному имени функцией bind. Второй — перевод сокета в режим прослушивания функцией listen. И наконец, сервер должен принять соединение клиента функцией accept.

Рассмотрим каждый API-вызов, необходимый для привязки, прослушива­ния и установления соединения с клиентом. Базовые вызовы, которые кли­ент и сервер должны сделать для установления канала связи, иллюстрирует рис. 3.

Рис. 3. Основные этапы работы клиента и сервера Winsock

Функция bind

После создания сокета определенного протокола следует связать его со стан­дартным адресом, вызвав функцию bind:

 

int bind(

SOCKET             s,

const struct sockaddr* name,

int                namelen

};

 

Параметр s задает сокет, на котором вы ожидаете соединения клиентов. Второй параметр с типом struct sockaddr — просто универсальный буфер. Фактически, в этот буфер вы должны поместить адрес, соответствующий стандартам используемого протокола, а затем при вызове bind привести его к типу struct sockaddr. В заголовочном файле Winsock определен тип SOCK­ADDR, соответствующий структуре struct sockaddr. Далее в главе этот тип бу­дет использоваться для краткости. Последний параметр задает размер пере­данной структуры адреса, зависящей от протокола. Например, следующий код иллюстрирует привязку при ТСР-соединении:

 

SOCKET             s;

struct sockaddr_in tcpaddr;

int                port = 5150;

 

s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

tcpaddr.sin_family = AF_INET;

tcpaddr.sin_port = htons(port);

tcpaddr.sin_addr.s_addr = htonl(INADDR_ANY);

bind(s, (SOCKADDR *)&tcpaddr, sizeof(tcpaddr));

 

Подробнее о структуре sockaddr_in — в предыдущем разделе, посвященном адресации TCP/IP. Там приведен пример создания потокового сокета и после­дующей настройки структуры адреса TCP/IP для приема соединений клиен­тов. В данном случае сокет указывает на IP-интерфейс по умолчанию с но­мером порта 5150. Формально вызов bind связывает сокет с IP-интерфейсом и портом.

При возникновении ошибки функция bind возвращает значение SOCK­ET_ERROR. Самая распространенная ошибка при вызове bind — WSAEADDRINUSE. В случае использования TCP/IP это означает, что с локальным IP-интерфейсом и номером порта уже связан другой процесс, или они нахо­дятся в состоянии TIME_WAIT. При повторном вызове bind для уже связанного сокета возвращается ошибка WSAEFAULT.

Функция listen

Теперь нужно перевести сокет в режим прослушивания. Функция bind толь­ко ассоциирует сокет с заданным адресом. Для перевода сокета в состояние ожидания входящих соединений используется API-функция listen.

int listen(

SOCKET s,

int    backlog

);

Первый параметр — связанный сокет. Параметр backlog определяет мак­симальную длину очереди соединений, ожидающих обработки, что важно при запросе нескольких соединений сервера. Пусть значение этого парамет­ра равно 2, тогда при одновременном приеме трех клиентских запросов первые два соединения будут помещены в очередь ожидания, и приложение сможет их обработать. Третий запрос вернет ошибку WSAECONNREFUSED. После того как сервер примет соединение, запрос удаляется из очереди, а другой — занимает его место. Значение backlog зависит от поставщика про­токола. Недопустимое значение заменяется ближайшим разрешенным. Стан­дартного способа получить действительное значение backlog нет.

Ошибки, связанные с listen, довольно просты. Самая частая из них — WSAEINVAL, обычно означает, что перед listen не была вызвана функция bind. Иногда при вызове listen возникает ошибка WSAEADDRINUSE, но чаще она происходит при вызове bind.

Функции accept

Итак, все готово к приему соединений клиентов. Теперь вызовем функцию accep. Прототип accept:

SOCKET accept(

SOCKET s,

struct sockaddr* addr,

int* addrlen

);

Параметр s — связанный сокет в состоянии прослушивания. Второй па­раметр — адрес действительной структуры SOCKADDR_IN, a addrlen — ссыл­ка на длину структуры SOCKADDR_IN. Для сокета другого протокола замените SOCKADDR_IN на структуру SOCKADDR, соответствующую этому протоколу. Вызов accept обслуживает первый находящийся в очереди запрос на соеди­нение. По его завершении структура addr будет содержать сведения об IP-адресе клиента, отправившего запрос, а параметр addrlen — размер струк­туры.

Кроме того, accept возвращает новый дескриптор сокета, соответствую­щий принятому клиентскому соединению. Для всех последующих операций с этим клиентом должен применяться новый сокет. Исходный прослуши­вающий сокет используется для приема других клиентских соединений и продолжает находиться в режиме прослушивания.

API-функции клиента

Клиентская часть значительно проще и для установления соединения тре­буется всего три шага: создать сокет функцией socket; разре­шить имя сервера (зависит от используемого протокола); инициировать соединение функцией connect.

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

Функции connect

Нам осталось обсудить собственно установление соединения. Оно осуществ­ляется вызовом connect. Сначала версию этой функции:

 

int connect(

SOCKET s,

const struct sockaddr FAR* name,

int namelen

);

 

Параметры практически не требуют пояснений: s — действительный ТСР-сокет для установления соединения, пате — структура адреса сокета (SOCKADDR_IN) для TCP, описывающая сервер к которому подключаются, namelen — длина переменной пате.

Если на компьютере, к которому вы подключаетесь, не запущен процесс, прослушивающий данный порт, функция connect вернет ошибку WSAECONNREFUSED. Другая ошибка — WSAETIMEDOUT, происходит, когда вызываемый адресат недоступен, например, из-за отказа коммуникационного оборудова­ния на пути к узлу или отсутствия узла в сети.

Передача данных

По сути, в сетевом программировании самое главное — уметь отправлять и принимать данные. Для пересылки данных по сокету используются функции send. Аналогично, для приема данных существуют функции recv.

Заметьте, что все буферы, используемые при отправке и приеме данных, состоят из элементов типа char. To есть эти функции не предназначены для работы с кодировкой UNICODE. Это особенно важно для Windows СЕ, так как она использует UNICODE по умолчанию. Отправить строку символов UNICODE можно несколькими способами: например, в исходном виде или привести к типу char*. Тон­кость в том, что при указании количества отправляемых или принимаемых символов результат функции, определяющей длину строки, нужно умножить на 2, так как каждый UNICODE-символ занимает 2 байта строкового масси­ва.

Все функции приема и отправки данных при возникновении ошибки возвращают код SOCKET_ERROR. Для получения более подробной информа­ции об ошибке вызовите функцию WSAGetLastError. Самые распространен­ные ошибки — WSAECONNABORTED и WSAECONNRESET. Обе возникают при закрытии соединения: либо по истечении времени ожидания, либо при зак­рытии соединения партнерским узлом. Еще одна типичная ошибка — WSAEWOULDBLOCK, обычно происходит при использовании неблокирующих или асинхронных сокетов. По существу, она означает, что функция не может быть выполнена в данный момент.

Функция send

API-функция send для отправки данных по сокету определена так:

 

int send(

SOCKET s,

const char* buf,

int len,

int flags

);

 

Параметр s определяет сокет для отправки данных. Второй параметр — buf, указывает на символьный буфер, содержащий данные для отправки. Тре­тий — len, задает число отправляемых из буфера символов. И последний па­раметр — flags, может принимать значения 0, MSG_DONTROUTE, MSG_OOB, или результат логического ИЛИ над любыми из этих параметров. При ука­зании флага MSG_DONTROUTE транспорт не будет маршрутизировать от­правляемые пакеты. Обработка этого запроса остается на усмотрение базо­вого протокола (например, если транспорт не поддерживает этот параметр - запрос игнорируется). Флаг MSG_OOB указывает, что данные должны быть отправлены вне полосы (out of band), то есть срочно.

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

Функции recv и WSARecv

Функция recv — основной инструмент приема данных по сокету. Она опре­делена так:

 

int recv(

SOCKET s,

char* buf,

int len,

int flags

);

 

Параметр s определяет сокет для приема данных. Второй параметр — buf является символьным буфером и предназначен для полученных данных, a len указывает число принимаемых байт или размер буфера buf. Последний па­раметр — flags, может принимать значения 0, MSG_PEEK, MSG_OOB или ре­зультат логического ИЛИ над любыми из этих параметров. Разумеется, 0 оз­начает отсутствие особых действий. Флаг MSG_PEEK указывает, что доступ­ные данные должны копироваться в принимающий буфер и при этом оста­ваться в системном буфере. По завершении функция также возвращает ко­личество ожидающих байт.

Считывать сообщения таким образом не рекомендуется. Мало того, что из-за двух системных вызовов (одного — для считывания данных, и друго­го, без флага MSG_PEEK — для удаления данных), снижается производитель­ность. В ряде случаев этот способ просто не надежен. Объем возвращаемых данных может не соответствовать их суммарному доступному количеству. К тому же, сохраняя данные в системных буферах, система оставляет все мень­ше памяти для размещения входящих данных. В результате уменьшается раз­мер окна TCP для всех отправителей, что не позволяет приложению достичь максимальной производительности. Лучше всего скопировать все данные в собственный буфер и обрабатывать их там. Флаг MSG_OOB уже обсуждался ранее при рассмотрении отправки данных.

Использование recv в сокетах, ориентированных на передачу сообщений или дейтаграмм, имеет несколько особенностей. Если при вызове recv раз­мер ожидающих обработки данных больше предоставляемого буфера, то после его полного заполнения возникает ошибка WSAEMSGSIZE. Заметьте: ошибка превышения размера сообщения происходит только при использо­вании протоколов, ориентированных на передачу сообщений. Потоковые протоколы буферизируют поступающие данные и при запросе приложени­ем предоставляют их в полном объеме, даже если количество ожидающих обработки данных больше размера буфера. Таким образом, ошибка WSAEMS­GSIZE не может произойти при работе с потоковыми протоколами.

Потоковые протоколы

Большинство протоколов с установлением соединения являются потоковы­ми. Важно учитывать, что при использовании любой функции отправки или приема данных через потоковый сокет нет гарантии, что вы прочитаете или запишете весь запрошенный объем данных. Скажем, требуется отправить 2048 байт из символьного буфера функцией send:

 

char sendbuff[2048];

int nBytes = 2048;

// Заполнение буфера sendbuff 2048 байтами данных

// Присвоение s значения действительного потокового сокета соединения

ret = send(s, sendbuff, nBytes, 0);

 

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

В TCP/IP также существует так называемый размер окна. Принимающая сторона регулирует его, указывая количество данных, которое способна при­нять. При переполнении данными получатель может задать нулевой размер окна, чтобы справиться с поступившими данными. Это приведет к приоста­новке отправки данных, пока размер окна не станет больше 0. В нашем слу­чае размер буфера может оказаться равным 1024 байтам, следовательно, по­требуется повторно отправить оставшиеся 1024 байта. Отправку всего содер­жимого буфера обеспечит следующий фрагмент программы:

 

char sendbuff[2048];

int nBytes = 2048, nLeft, idx;

 

// Заполнение буфера sendbuff 2048 байтами данных

// Присвоение s значения действительного потокового сокета соединения

nLeft = nBytes;

idx = 0;

while (nLeft > 0)

{

ret = send(s, &sendbuff[idx], nLeft, 0);

if (ret == SOCKET_ERROR)

{

// Ошибка

}

nLeft -= ret;

idx += ret;

 

Все, сказанное далее справедливо и для приема данных на потоковом сокете, но для нас это не очень важно. Приложение не знает, сколько дан­ных оно в очередной раз прочтет на потоковом сокете. Если вам нужно от­править дискретные сообщения по потоковому протоколу, это не составит труда. Например, если у всех сообщений одинаковый размер (512 байт), то прочитать их можно так:

 

char recvbuff[1024];

int ret, nLeft, idx;

while (nLeft > 0)

{

ret = recv(s, &recvbuff[idx], nLeft, 0);

if (ret == SOCKET_ERROR)

{

// Ошибка

}

idx += ret;

nLeft -= ret;

}

 

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

Завершение сеанса

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

Функция shutdown

Правильно написанное приложение уведомляет получателя об окончании отправки данных. Так же должен поступить и узел. Такое поведение называ­ется корректным завершением сеанса и осуществляется с помощью функции shutdown.

 

int shutdown(SOCKET s, int how);

 

Параметр how может принимать значения SD_RECEIVE, SD_SEND или SD_ BOTH. Значение SD_RECEIVE запрещает все последующие вызовы любых функций приема данных, на протоколы нижнего уровня это не действует. Если в очереди TCP-сокета есть данные, либо они поступают позже, соеди­нение сбрасывается. UDP-сокеты в аналогичной ситуации продолжают при­нимать данные и ставить их в очередь. SD_SEND запрещает все последующие вызовы функций отправки данных. В случае ТСР-сокетов после подтвержде­ния получателем приема всех отправленных данных передается пакет FIN. Наконец, SD_BOTH запрещает как прием, так и отправку.

Функция closesocket

Эта функция закрывает сокет. Она определена так:

 

int closesocket(SOCKET s);

 

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

Ожидающие асинхронные вызовы, исходящие от любого потока данно­го процесса, отменяются без уведомления. Ожидающие операции перекры­того ввода-вывода также аннулируются. Все выполняющиеся события, про­цедура и порт завершения, связанные с перекрытым вводом-выводом, завер­шатся ошибкой WSA_OPERATION_ABORTED. (Асинхронные и неблокирующие модели ввода-вывода более подробно обсуждаются далее).

Приемник

Процесс получения данных на сокете, не требующем соединения, прост. Сначала создают сокет функцией socket. Затем выполняют привязку сокета к интерфейсу, на котором будут принимать данные, функ­цией bind (как и в случае протоколов, ориентированных на сеансы). Разни­ца в том, что нельзя вызвать listen или accept: вместо этого нужно просто ожидать приема входящих данных. Поскольку в этом случае соединения нет, принимающий сокет может получать дейтаграммы от любой машины в сети. Простейшая функция приема — recvform:

 

int recvfrom(

SOCKET s,

char* buf,

int len

int flags,

struct sockaddr* from,

int* fromlen

);

 

Первые четыре параметра такие же, как и для функции recv, включают до­пустимые значения для flags: MSG_OOB и MSG_PEEK. Параметр from — струк­тура SOCKADDR для данного протокола слушающего сокета, на размер струк­туры адреса ссылается fromlen. После возврата вызова структура SOCKADDR будет содержать адрес рабочей станции, которая отправляет данные.

Отправитель

Есть два способа отправки данных через сокет, не требующий соединения. Первый и самый простой — создать сокет и вызвать функцию sendto или WSASendTo. Рассмотрим сначала функцию sendto:

 

int sendto(

SOCKET s

const char* buf,

int len,

int flags,

const struct sockaddr * to,

int tolen

);

 

Параметры этой функции такие же, как и у recvfrom, за исключением buf— буфера данных для отправки, и len — показывающего сколько байт от­правлять. Параметр to — указатель на структуру SOCKADDR с адресом при­нимающей рабочей станции.

Дополнительные функции API

Рассмотрим API-функции Winsock, которые пригодятся вам при создании сетевых приложений.

Функция getpeername

Эта функция возвращает информацию об адресе сокета партнера на под­ключенном сокете:

 

int getpeername(

SOCKET s,

struct sockaddr* name,

int* namelen

);

 

Первый параметр — сокет для соединения, два последних — указатель на структуру SOCKADDR базового протокола и ее длина. Для сокетов дейтаграмм данная функция возвращает адрес, переданный вызову соединения (за исключением адресов, переданных в вызов sendto).

Функция getsockname

Эта функция противоположна getpeername и возвращает адресную инфор­мацию для локального интерфейса определенного сокета:

 

int getsockname(

SOCKET s,

struct sockaddr* name,

int* namelen

);

 

Используются те же параметры, что и для getpeername, однако возвраща­ется информация о локальном адресе. В случае TCP адрес совпадает с соке­той сервера, слушающим на заданном порте и IP-интерфейсе.

 

Ввод-вывод в Winsock

Эта глава посвящена управлению вводом-выводом через сокеты в Windows-приложениях. В Winsock такое управление реализовано с помощью режимов работы и моделей ввода-вывода. Режим (mode) сокета определяет поведение функций, работающих с сокетом. Модель (model) сокета описывает, как при­ложение производит ввод-вывод при работе с сокетом. Модели не зависят от режима работы и позволяют обходить их ограничения.

Winsock поддерживает два режима: блокирующий (blocking) и неблоки­рующий (nonblocking). Эти режимы подробно описаны в начале главы, здесь же демонстрируется их использование в приложениях для управления вво­дом-выводом. Далее приведен ряд интересных моделей, которые помогают приложению управлять несколькими сокетами одновременно в асинхрон­ном режиме: select, WSAAsyncSelect, WSAEventSelect, перекрытый ввод-вывод (overlapped I/O) и порт завершения (completion port).

Режимы работы сокетов

В блокирующем режиме функции ввода-вывода, такие как send и recv, перед завершением ожидают окончания операции. В неблокирующем — работа функций завершается немедленно. Приложения, выполняемые на платфор­мах Windows СЕ и Windows 95 (в случае Winsock 1), поддерживают очень мало моделей ввода-вывода и требуют от программиста описать блокирова­ние и разблокирование сокетов в разных ситуациях.

Блокирующий режим

При блокировке сокета необходима осторожность, так как этом режиме лю­бой вызов функции Winsock именно блокирует сокет на некоторое время. Большинство приложений Winsock следуют модели «поставщик — потреби­тель», в которой программа считывает или записывает определенное количе­ство байт и затем выполняет с ними какие-либо операции.

 

SOCKET sock;

char buff[256];

int done = 0;

while(!done) {

nBytes = recv(sock, buff, 65);

if (nBytes == SOCKET_ERROR) {

printf("recv failed with error %d\n", WSAGetLastError());

return;

}

DoComputationOnData(buff);

}

 

Проблема в том, что функция recv может не завершиться никогда, так как для этого нужно считать какие-либо данные из буфера системы. В такой си­туации некоторые программисты могут соблазниться «подглядыванием» данных (чтение без удаления из буфера), используя флаг MSG_PEEK в recv или вызывая ioctlsocket с параметром FIONREAD. Подобный стиль програм­мирования заслуживает резкой критики. Издержки, связанные с «подгляды­ванием», велики, так как необходимо сделать один или более системных вы­зовов для определения числа доступных байт, после чего все равно прихо­дится вызывать recv для удаления данных из буфера.

Чтобы этого избежать, следует предотвратить замораживание приложе­ния из-за недостатка данных (из-за сетевых проблем или проблем клиента) без постоянного «подглядывания» в системные сетевые буферы. Один из ме­тодов — разделить приложения на считывающий и вычисляющий потоки, совместно использующие общий буфер данных. Доступ к буферу регулиру­ется синхронизирующим объектом, таким как событие или мьютекс. Задача считывающего потока — постоянно читать данные из сети и помещать их в общий буфер. Считав минимально необходимое количество данных, этот поток инициирует сигнальное событие, уведомляющее вычисляющий поток, что можно начинать вычисления. Затем вычисляющий поток удаля­ет часть данных из буфера и производит с ними необходимые операции. В следующем листинге реализованы две функции: для приема данных (ReadThread) и их обработки (ProcessThread).

 

// Перед созданием двух потоков,

// инициализируется общий буфер (data)

// и создается сигнальное событие (hEvent)

CRITICAL_SECTION data;

HANDLE   hEvent;

TCHAR    buff[MAX_BUFFER_SIZE];

int    nbytes;

// Считывающий поток void ReadThread(void) {

int nTotal = 0, nRead = 0, nLeft = 0 nBytes = 0;

while (!done)

{

nTotal = 0;

nLeft = NUM_BYTES_REQUIRED;

while (nTotal!= NUM_BYTES_REQUIRED)

{

EnterCriticalSection(&data);

nRead = recv(sock, &(buff[MAX_BUFFER_SIZE - nBytes]), nLeft);

if (nRead == -1) {

printf("error\n"); ExitThread();

nTotal += nRead;

nLeft -= nRead;

 

nBytes += nRead;

LeaveCriticalSection(&data);

}

SetEvent(hEvent); } }

// Вычисляющий поток void ProcessThread(void)

WaitForSingleObject(hEvent);

EnterCriticalSection(&data);

DoSomeComputationOnData(buff);

// Удаление обработанных данных из буфера

// и сдвиг оставшихся в начало массива

nBytes -= NUM BYTES REQUIRED;

}

LeaveCriticalSection(&data);

}

 

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

Неблокирующий режим

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

SOCKET s;

unsigned long ul = 1;

int nRet;

s = socket(AF_INET, SOCK_STREAM, 0);

nRet = ioctlsocket(s, FIOBIO, (unsigned long *)&ul);

if (nRet == SOCKET ERROR)

{

// Не удалось перевести сокет в неблокирующий режим

}

Если сокет находится в неблокирующем режиме, функции Winsock завер­шаются немедленно. В большинстве случаев они будут возвращать ошибку

Основы Winsock

В этом разделе описаны основные методики и API-вызовы, необходи­мые для написания сетевых приложений. Здесь рассмотриваются способы установления соединения между дву­мя компьютерами в сети и механизмы обмена данными. К методичке прилагается набор примеров клиент-серверных приложений. Единственная зависящая от протокола операция — это создание сокета. Большинство остальных вызовов функций Winsock, ответственных за установление соединения, отправку и прием дан­ных, не зависят от протокола. Примеры этого раздела помогут вам лучше понять вызо­вы Winsock, необходимые для установления соединений и обмена данными. Наша цель — изучить эти вызовы, поэтому в примерах используются прямые блокирующие вызовы Winsock. Другие модели ввода-вывода, реализованные в Winsock, обсуждаются в следующем разделе.

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

Любое Winsock-приложение перед вызовом функции должно загрузить со­ответствующую версию библиотеки Winsock. Если этого не сделать, функция вернет значение SOCKET_ERROR и выдаст ошибку WSANOTINITIALISED. Загруз­ку библиотеки Winsock выполняет функция WSAStartup:

 

int WSAStartup(

WORD wVersionRequested,

LPWSADATA lpWSAData

);

 

Параметр wVersionRequested задает версию загружаемой библиотеки Winsock. Старший и младший байты определяют дополнительный и основной номер версии библиотеки соответственно. Для получения значения парамет­ра wVersionRequested можно использовать макрос MAKEWORD(x, у), где х — старший байт, а у — младший.

Параметр lpWSAData — указатель на структуру LPWSADATA, которая при вызове функции WSAStartup заполняется сведениями о версии загружаемой библиотеки:

 

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*  lpVendorlnfo;

} WSADATA, *LPWSADATA;

 

WSAStartup присваивает параметру wVersion значение загружаемой версии. Параметр wHighVersion содержит номер последней доступной версии Winsock. Помните, что в обоих полях старший байт определяет дополнительный, а младший — основной номер версии. Поля szDescription и szSystemStatus запол­няются не во всех реализациях Winsock и практически не применяются.

Не используйте и поля iMaxSockets и iMaxUdpDg. Предполагается, что в них заданы максимальное количество одновременно открытых сокетов и максимальный размер дейтаграммы. Макси­мальное количество одновременно открытых сокетов зависит от свободной физической памяти. Наконец, поле lpVendorlnfo зарезервировано для инфор­мации изготовителя реализации Winsock и не используется ни на одной из платформ Win32.

Для использования в приложении Winsock 1 необходимо под­ключить файл Winsock.h, а для Winsock 2 — Winsock2.h.

Даже если платформа поддерживает Winsock 2, не обязательно использовать самую последнюю версию. Напротив, если необходимо, чтобы приложение поддерживалось несколькими платформами, возьмите за основу Winsock 1.1. Такое приложение будет отлично работать на платформе Windows NT 4-0, по­тому что все вызовы Winsock 1.1 имеются в Winsock 2 DLL.

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

В большинстве случаев при написании новых приложений следует загру­жать последнюю доступную версию библиотеки Winsock. Если будет выпуще­на версия 3, приложение, использующее версию 2.2, должно выполняться кор­ректно. При запросе более поздней версии Winsock, не поддерживаемой ва­шей платформой, WSAStartup вернет ошибку, а в поле wHighVersion структуры WSADATA появится номер последней версии библиотеки, поддерживаемой данной системой.

Проверка и обработка ошибок

Проверка и обработка ошибок играют весомую роль при написании Winsock-приложения. Функции Winsock достаточно часто возвращают ошибки, но как правило, не критические — передачу информации можно продол­жать. Большинство функций Winsock при ошибке вызова возвращают зна­чение SOCKET_ERROR, но так происходит не всегда. При подробном рассмот­рении API-вызовов мы обратим внимание на возвращаемые значения, соот­ветствующие ошибкам. Константа SOCKET_ERROR на самом деле равна -1. Для получения более информативного кода ошибки, возникшей после одно­го из вызовов Winsock, задействуйте функцию WSAGetLastError.

 

int WSAGetLastError (void);

 

Эта функция возвращает код последней ошибки. Всем кодам ошибок, воз­вращаемым WSAGetLastError, соответствуют стандартные константные значе­ния. Они описаны в Winsockh или в Winsock2.h (в зависимости от версии Winsock). Единственное различие этих заголовочных файлов — Winsock2.h содержит больше кодов ошибок новых API-функций. Константы, определенные для кодов ошибок директивой #define, обычно начинаются с префикса WSAE.

Протоколы с установлением соединения

Сначала мы рассмотрим функции Winsock, необходимые для приема и ус­тановления соединений: обсудим, как слушать соединения клиентов, и изу­чим процесс принятия или отклонения соединения. Затем поговорим о том, как инициировать соединение с сервером. В заключение будет описан про­цесс передачи данных в ходе сеанса связи.

Серверные API-функции

Сервер — это процесс, который ожидает подключения клиентов для обслу­живания их запросов. Сервер должен прослушивать соединения на стандар­тном имени. В TCP/IP таким именем является IP-адрес локального интерфей­са и номер порта. У каждого протокола своя схема адресации, а потому и свои особенности именования. Первый шаг установления соединения — привязка сокета данного протокола к его стандартному имени функцией bind. Второй — перевод сокета в режим прослушивания функцией listen. И наконец, сервер должен принять соединение клиента функцией accept.

Рассмотрим каждый API-вызов, необходимый для привязки, прослушива­ния и установления соединения с клиентом. Базовые вызовы, которые кли­ент и сервер должны сделать для установления канала связи, иллюстрирует рис. 3.

Рис. 3. Основные этапы работы клиента и сервера Winsock

Функция bind

После создания сокета определенного протокола следует связать его со стан­дартным адресом, вызвав функцию bind:

 

int bind(

SOCKET             s,

const struct sockaddr* name,

int                namelen

};

 

Параметр s задает сокет, на котором вы ожидаете соединения клиентов. Второй параметр с типом struct sockaddr — просто универсальный буфер. Фактически, в этот буфер вы должны поместить адрес, соответствующий стандартам используемого протокола, а затем при вызове bind привести его к типу struct sockaddr. В заголовочном файле Winsock определен тип SOCK­ADDR, соответствующий структуре struct sockaddr. Далее в главе этот тип бу­дет использоваться для краткости. Последний параметр задает размер пере­данной структуры адреса, зависящей от протокола. Например, следующий код иллюстрирует привязку при ТСР-соединении:

 

SOCKET             s;

struct sockaddr_in tcpaddr;

int                port = 5150;

 

s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

tcpaddr.sin_family = AF_INET;

tcpaddr.sin_port = htons(port);

tcpaddr.sin_addr.s_addr = htonl(INADDR_ANY);

bind(s, (SOCKADDR *)&tcpaddr, sizeof(tcpaddr));


Поделиться с друзьями:

Археология об основании Рима: Новые раскопки проясняют и такой острый дискуссионный вопрос, как дата самого возникновения Рима...

Наброски и зарисовки растений, плодов, цветов: Освоить конструктивное построение структуры дерева через зарисовки отдельных деревьев, группы деревьев...

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

Биохимия спиртового брожения: Основу технологии получения пива составляет спиртовое брожение, - при котором сахар превращается...



© cyberpedia.su 2017-2024 - Не является автором материалов. Исключительное право сохранено за автором текста.
Если вы не хотите, чтобы данный материал был у нас на сайте, перейдите по ссылке: Нарушение авторских прав. Мы поможем в написании вашей работы!

0.26 с.