Археология об основании Рима: Новые раскопки проясняют и такой острый дискуссионный вопрос, как дата самого возникновения Рима...
Опора деревянной одностоечной и способы укрепление угловых опор: Опоры ВЛ - конструкции, предназначенные для поддерживания проводов на необходимой высоте над землей, водой...
Топ:
Организация стока поверхностных вод: Наибольшее количество влаги на земном шаре испаряется с поверхности морей и океанов...
Особенности труда и отдыха в условиях низких температур: К работам при низких температурах на открытом воздухе и в не отапливаемых помещениях допускаются лица не моложе 18 лет, прошедшие...
Теоретическая значимость работы: Описание теоретической значимости (ценности) результатов исследования должно присутствовать во введении...
Интересное:
Что нужно делать при лейкемии: Прежде всего, необходимо выяснить, не страдаете ли вы каким-либо душевным недугом...
Отражение на счетах бухгалтерского учета процесса приобретения: Процесс заготовления представляет систему экономических событий, включающих приобретение организацией у поставщиков сырья...
Инженерная защита территорий, зданий и сооружений от опасных геологических процессов: Изучение оползневых явлений, оценка устойчивости склонов и проектирование противооползневых сооружений — актуальнейшие задачи, стоящие перед отечественными...
Дисциплины:
2021-03-17 | 92 |
5.00
из
|
Заказать работу |
|
|
Основы 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 определен тип SOCKADDR, соответствующий структуре 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 возвращает значение SOCKET_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. Заметьте: ошибка превышения размера сообщения происходит только при использовании протоколов, ориентированных на передачу сообщений. Потоковые протоколы буферизируют поступающие данные и при запросе приложением предоставляют их в полном объеме, даже если количество ожидающих обработки данных больше размера буфера. Таким образом, ошибка WSAEMSGSIZE не может произойти при работе с потоковыми протоколами.
Потоковые протоколы
Большинство протоколов с установлением соединения являются потоковыми. Важно учитывать, что при использовании любой функции отправки или приема данных через потоковый сокет нет гарантии, что вы прочитаете или запишете весь запрошенный объем данных. Скажем, требуется отправить 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 определен тип SOCKADDR, соответствующий структуре 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 - Не является автором материалов. Исключительное право сохранено за автором текста.
Если вы не хотите, чтобы данный материал был у нас на сайте, перейдите по ссылке: Нарушение авторских прав. Мы поможем в написании вашей работы!