Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Скачиваний:
34
Добавлен:
05.06.2015
Размер:
282.18 Кб
Скачать

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

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

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

Функция bind

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

// Code 3.04

int bind(

SOCKET s,

const struct sockaddr FAR * name,

int namelen

)

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

// Code 3.05

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. Формально вызов blind связывает сокет с IP-интерфейсом и портом.

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

^ Функция listen

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

// Code 3.06

int listen(

SOCKET s,

Int backlog

);

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

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

^ Функции accept и WSAAccept

Все готово к приему соединений клиентов и можно вызвать функцию accept или WSAAccept. Прототип accept:

// Code 3.07

SOCKET accept(

SOCKET s,

struct sockaddr FAR * addr,

int FAR * addrlen

);

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

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

В Winsock 2 есть функция WSAAccept, способная устанавливать соединения в зависимости от результата вычисления условия:

// Code 3.08

SOCKET WSAAccept(

SOCKET s,

struct sockaddr FAR * addr,

LPINT addrlen,

LPCONDITIONPROC lpfnCondition,

DWORD dwCallbackData

);

Первые три параметра — те же, что и в accept для Winsock 1. Параметр lpfnCondition — указатель на функцию, вызываемую при запросе клиента. Она определяет возможность приема соединения и имеет следующий прототип:

// Code 3.09

int CALLBACK ConditionFunc(

LPWSABUF lpCallerId,

LPWSABUF lpCallerData,

LPQOS lpSQOS,

LPQOS lpGQOS,

LPWSABUF lpCalleeId,

LPWSABUF lpCalleeData,

GROUP FAR * g,

DWORD dwCallbackData

);

Передаваемый по значению параметр lpCallerId содержит адрес соединяющегося объекта. Структура WSABUF используется многими функциями Winsock 2 и определена так:

// Code 3.10

typedef struct _WSABUF

{

u_long len;

char FAR * buf;

} WSABUF, FAR * LPWSABUF;

В зависимости от ее использования, поле len определяет размер буфера, на который ссылается поле buf, или количество данных в буфере buf.

Для lpCallerId параметр buf указывает на структуру адреса протокола, по которому осуществляется соединение. Чтобы получить корректный доступ к информации, просто приведите указатель buf к соответствующему типу SOCKADDR. При использовании протокола TCP/IP это должна быть структура SOCKADDR_IN, содержащая IP-адрес подключающегося клиента. Большинство сетевых протоколов удаленного доступа поддерживают идентификацию абонента на этапе запроса.

Параметр lpCallerData содержит данные, отправленные клиентом в ходе запроса соединения. Если эти данные не указаны, он равен NULL. Большинство сетевых протоколов, таких как TCP/IP, не используют данные о соединении. Чтобы узнать, поддерживает ли протокол эту возможность, можно обратиться к соответствующей записи в каталоге Winsock путем вызова функции WSAEnumProtocols (см. первую главу).

Следующие два параметра — lpSQOS и lpGQOS, задают уровень качества обслуживания, запрашиваемый клиентом. Оба параметра ссылаются на структуру, содержащую сведения о требованиях пропускной способности для приема и передачи. Если клиент не запрашивает параметры качества обслуживания (quality of service, QoS), то они равны NULL. Разница между ними в том, что lpSQOS используется для единственного соединения, a IpGQOS — для групп сокетов. Группы сокетов не реализованы и не поддерживаются в Winsock 1 и 2. Подробнее о QoS — в последующих главах.

Параметр lpCalleeId — другая структура WSABUF, содержащая локальный адрес, к которому подключен клиент. Поле buf указывает на объект SOCKADDR соответствующего семейства адресов. Эта информация полезна, если сервер запущен на многоадресной машине. Если сервер связан с адресом INADDR_ANY, запросы соединения будут обслуживаться на любом сетевом интерфейсе, а параметр — содержать адрес интерфейса, принявшего соединение.

Параметр lpCalleeData дополняет lpCallerData. Он ссылается на структуру WSABUF, которую сервер может использовать для отправки данных клиенту в ходе установления соединения. Если поставщик услуг поддерживает эту возможность, поле len указывает максимальное число отправляемых байт. В этом случае сервер копирует некоторое, не превышающее это значение, количество байт, в блок buf структуры WSABUF и обновляет поле len, чтобы показать, сколько байт передается. Если сервер не должен возвращать данные о соединении, то перед возвращением условная функция приема соединения присвоит полю len значение 0. Если поставщик не поддерживает передачу данных о соединении, поле len будет равно 0. Большинство протоколов: фактически, все, поддерживаемые платформами Win32 — не поддерживают обмен данными при установлении соединения.

Обработав переданные в условную функцию параметры, сервер должен решить: принимать, отклонять или задержать запрос соединения. Если соединение принимается, условная функция вернет значение СF_АССЕРТ, если отклоняется — CF_REJECT. Если по каким-либо причинам на данный момент решение не может быть принято, возвращается CF_DEFER.

Как только сервер готов обработать запрос, он вызывает функцию WSAAccept. Условная функция выполняется в одном процессе с WSAAccept и должна работать как можно быстрее. В протоколах, поддерживаемых платформами Win32, клиентский запрос задерживается, пока не будет вычислено значение условной функции. В большинстве случаев базовый сетевой стек ко времени вызова условной функции уже может принять соединение. При возвращении значения CF_REJECT стек просто закрывает его (см. последующие главы).

При возникновении ошибки возвращается значение INVALID_SOCKET, чаще всего — WSAEWOULDBLOCK. Оно возникает, если сокет находится в асинхронном или неблокирующем режиме и нет соединения для приема. Если условная функция вернет CF_DEFER, WSAAccept генерирует ошибку WSATRY_AGAIN, если CF_REJECT — WSAECONNREFUSED.

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

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

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

^ Состояния TCP

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

Начальное состояние любого сокета — CLOSED. Как только клиент инициирует соединение, серверу отправляется пакет SYN и клиентский сокет переходит в состояние SYN_SENT. Получив пакет SYN, сервер отправляет пакет SYN-and-ACK, а клиент отвечает на него пакетом АСК. С этого момента клиентский сокет переходит в состояние ESTABLISHED. Если сервер не отправляет пакет SYN-ACK, клиент по истечении времени ожидания возвращается в состояние CLOSED.

Если сокет сервера связан и прослушивает локальный интерфейс и порт, то он находится в состоянии LISTEN. При попытке клиента установить соединение сервер получает пакет SYN и отвечает пакетом SYN-ACK. Состояние сокета сервера меняется на SYN_RCVD. Наконец, после отправки клиентом пакета АСК сокет сервера переводится в состояние ESTABLISHED.

Существует два способа закрыть соединение. Если этот процесс начинает приложение, то закрытие называется активным, иначе — пассивным. На рис. 3.2 изображены оба вида закрытия. При активном закрытии соединения приложение отправляет пакет FIN. Если приложение вызывает closesocket или shutdown (со вторым аргументом SD_SEND), оно отправляет узлу пакет FIN, и состояние сокета меняется на FIN_WAIT_1. Обычно узел отвечает пакетом АСК и сокет переходит в состояние FIN_WAIT_2. Если узел тоже закрывает соединение, он отправляет пакет FIN, а компьютер отвечает пакетом АСК и переводит сокет в состояние TIME_WAIT.

Состояние TIME_WAIT также называется состоянием ожидания 2*MSL MSL — максимальное время жизни сегмента (Maximum Segment Lifetime), иными словами, время существования пакета в сети перед его отбрасыванием. У каждого IP-пакета есть поле времени жизни (time-to-live, TTL). Если оно равно 0, значит, пакет можно отбросить. Каждый маршрутизатор, обслуживающий пакет, уменьшает значение TTL на 1 и передает пакет дальше. Перейдя в состояние TIME_WAIT, приложение остается в нем на протяжении двух периодов времени, равных MSL. Это позволяет TCP в случае потери заключительного пакета АСК послать его заново, с последующей отправкой FIN. По истечении 2*MSL сокет переходит в состояние CLOSED.

Результат двух других способов активного закрытия — состояние TIME_WAIT. В предыдущем случае только одна сторона отправляла FIN и получала ответ АСК, а узел оставался свободным для передачи данных до момента своего закрытия. Здесь возможны и два других способа. В первом случае, при одновременном закрытии, компьютер и узел одновременно запрашивают закрытие: компьютер отправляет узлу пакет FIN и получает от него пакет FIN.

Затем в ответ на пакет FIN компьютер отправляет пакет АСК и изменяет состояние сокета на CLOSING. После получения компьютером пакета АСК от узла сокет переходит в состояние TIME_WAIT.

Второй случай активного закрытия является вариацией одновременного закрытия: сокет из состояния FIN_WAIT_1 сразу переходит в состояние TIME_WAIT. Это происходит, если приложение отправляет пакет FIN и тут же после этого получает от узла пакет FIN-ACK. В таком случае узел подтверждает пакет FIN приложения отправкой своего, на которое приложение отвечает пакетом АСК.

Основной смысл состояния TIME_WAIT заключается в том, что пока соединение ожидает истечения 2*MSL, сокетная пара, участвующая в соединении, не может быть использована повторно. Сокетная пара — это комбинация локального и удаленного IP-портов. Некоторые реализации TCP не позволяют повторно использовать любой из портов сокетной пары, находящейся в состоянии TIME_WAIT. В реализации Microsoft этого дефекта нет. При попытке соединения с сокетной парой, находящейся в состоянии TIME_WAIT, произойдет ошибка WSAEADDRINUSE. Одно из решений проблемы (кроме ожидания окончания состояния TIME_WAIT пары сокетов, использующей локальный порт) состоит в использовании параметра сокета SO_REUSEADDR. Более подробно SO_REUSEADDR рассматривается в последующих главах.

Наконец, целесообразно рассмотреть пассивное закрытие. По этому сценарию приложение получает от узла пакет FIN и отвечает пакетом АСК. В этом случае сокет приложения переходит в состояние CLOSE_WAIT. Так как узел закрыл свою сторону, он больше не может отправлять данные, но приложение вправе это делать, пока не закроет свою сторону соединения. Для закрытия своей стороны приложение отправляет пакет FIN, после чего ТСР-сокет приложения переводится в состояние LAST_ACK. После получения от узла пакета АСК сокет приложения возвращается в состояние CLOSED.

^ Функции connect и WSAConnect

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

// Code 3.11

int connect(

SOCKET s,

const struct sockaddr FAR * name,

int namelen

);

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

// Code 3.12

int WSAConnect(

SOCKET s,

const struct sockaddr FAR * name,

int namelen,

LPWSABUF lpCallerData,

LPWSABUF lpCalleeData,

LPQOS lpSQOS,

LPQOS lpGQOS

);

Первые три параметра такие же, как и в функции connect. Следующие два: lpCallerData и lpCalleeData, — это строковые буферы, используемые для приема и отправки данных в момент установления соединения. Параметр lpCallerData указывает на буфер, содержащий данные, отправляемые клиентом серверу вместе с запросом на соединение; lpCallerData — на буфер с данными, возвращаемыми сервером в ходе установления соединения. Обе переменные являются структурами WSABUF, и для lpCallerData поле len должно указывать длину данных передаваемого буфера buf. В случае lpCalleeData поле len определяет размер буфера buf, куда принимаются данные от сервера. Два последних параметра: lpSQOS и lpGQOS, — ссылаются на структуры QoS, определяющие требования пропускной способности отправки и приема данных устанавливаемого соединения. Параметр lpSQOS указывает требования к сокету s, a lpGQOS — к группе сокетов. На данный момент группы сокетов в полной мере не поддерживаются. Нулевое значение lpSQOS означает, что приложение не предъявляет требований к качеству обслуживания.

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

Соседние файлы в папке Сетевое программирование от Ивана Ерохина