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

СПО_1 / СПО / Sozdanie.setevyh.prilojenii.v.srede.Linux

.pdf
Скачиваний:
79
Добавлен:
11.04.2015
Размер:
2.94 Mб
Скачать

Функция bind() пытается зарезервировать для серверного сокета указанное имя файла или порт (список доступных или стандартных портов содержится в файле /etc/services). Клиенты подключаются к данному порту, посылая и при нимая через него данные.

Создание очереди ожидания

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

Очередь сокета активизируется при вызове функции listen(). Когда сервер вызывает эту функцию, он указывает число позиций в очереди. Кроме того, сокет переводится в режим "только прослушивание". Это очень важно, так как позво ляет впоследствии вызывать функцию accept().

#include <sys/socket.h> linclude <resolv.h>

int listen(int sd, int numslots);

Параметр sd является дескриптором сокета, полученным в результате вызова функции socket(). Параметр numslots задает число позиций в очереди ожидания. Приведем пример (листинг 6.3).

Листинг 6.3. Пример функции listen() /****************************************************************/

/***

Пример функции listen(): перевод сокета

***/

/***

в режим прослушивания клиентских подключений

***/

у****************************************************************/

int sd;

sd = socket(PF_INET, SOCK_STREAM, 0);

/***

Привязка

к порту

***/

 

 

if (

listen(sd,

20)

!=

0 )

/* перевод сокета в режим

*/

 

perror("Listen");

/*

прослушивания очереди с 20 ю позициями

*/

Как правило, размер очереди устанавливается равным от 5 до 20. Больший размер оказывается избыточным в современной многозадачной среде. Если мно гозадачный режим не поддерживается, может потребоваться увеличить размер очереди до величины периода тайм аута (например, 60, если тайм аут составляет

60секунд).

Функция listen() может генерировать следующие ошибки.

EBADF. Указан неверный дескриптор сокета.

EOPNOTSUPP. Протокол сокета не поддерживает функцию listen(). В TCP (SOCK_STREAM) очередь ожидания поддерживается, а в протоколе UDP (SOCK_DGRAM) — нет.

После перевода сокета в режим ожидания необходимо организовать цикл по лучения запросов на подключение.

Глава 6. Пример сервера

121

Ⱦɚɧɧɚɹ ɜɟɪɫɢɹ ɤɧɢɝɢ ɜɵɩɭɳɟɧɚ ɷɥɟɤɬɪɨɧɧɵɦ ɢɡɞɚɬɟɥɶɫɬɜɨɦ %RRNV VKRS Ɋɚɫɩɪɨɫɬɪɚɧɟɧɢɟ ɩɪɨɞɚɠɚ ɩɟɪɟɡɚɩɢɫɶ ɞɚɧɧɨɣ ɤɧɢɝɢ ɢɥɢ ɟɟ ɱɚɫɬɟɣ ɁȺɉɊȿɓȿɇɕ Ɉ ɜɫɟɯ ɧɚɪɭɲɟɧɢɹɯ ɩɪɨɫɶɛɚ ɫɨɨɛɳɚɬɶ ɩɨ ɚɞɪɟɫɭ piracy@books-shop.com

Прием запросов от клиентов

На данный момент программа создала сокет, назначила ему номер порта и организовала очередь ожидания. Теперь она может принимать запросы на под ключение. Функция accept () делает указанный сокет диспетчером соединений. Здесь привычный ход событий нарушается. Когда сокет переводится в режим прослушивания, он перестает быть двунаправленным каналом передачи данных. Программа не может даже читать данные из него. Она может только принимать запросы на подключение. Функция accept() блокирует программу до тех пор, по ка не поступит такой запрос.

Когда клиент устанавливает соединение с сервером, сокет, находящийся в ре жиме прослушивания, организует новый двунаправленный канал между клиентом и своим собственным портом. Функция accept)) неявно создает в программе но вый дескриптор сокета. По сути, при каждом новом подключении создается вы деленный канал между клиентом и сервером. С этого момента программа взаи модействует с клиентом через новый канал.

Можно также узнать, кто устанавливает соединение с сервером, поскольку в функцию accept() передается информация о клиенте. Аналогичный процесс рас сматривался в главе 4, "Передача сообщений между одноранговыми компьютера ми", когда функция recvfrom() получала не только данные, но и указатель на ад рес отправителя.

#include <sys/socket.h> #include <resolv.h>

int accept(int sd, sockaddr *addr, int *addr_size);

Как всегда, параметр sd является дескриптором сокета. Во втором параметре возвращается адрес клиента и номер порта, а в третьем — размер структуры sockaddr. В отличие от функции recvfrom(), последние два параметра являются необязательными. Если в программе не требуется знать адрес клиента, задайте эти параметры равными нулю.

Необходимо убедиться, что размер буфера адреса достаточен для размещения в нем полученной адресной структуры. Беспокоиться о повреждении данных из за переполнения буфера не стоит: функция задействует ровно столько байтов, сколько указано в третьем параметре. Параметр addr_size передается по ссылке, поэтому программа может легко узнать реальный размер полученной структуры (листинг 6.4).

Листинг6.4.Примерфункцииaccept()

/****************************************************************/

/***

Пример функции accept(): ожидание и принятие запросов

***/

/***

на подключение от клиентов

***/

/****************************************************************/

int sd;

struct sockaddr_in addr;

/*** Создание сокета, привязка его к порту и

 

перевод в режим прослушивания ***/

for

(;;)

/* цикл повторяется бесконечно */

{

int clientsd;

/* новый дескриптор сокета */

int size = sizeof(addr);

/* вычисление размера структуры */

122

Часть П. Создание серверных приложений

www.books-shop.com

clientsd = accept(sd, &addr, &size); /* ожидание подключения */

if { clientsd > 0 )

/* ошибок нет */

{

 

 

/*** Взаимодействие с клиентом ***/

 

close(clientsd);

/* очистка и отключение

*/

}

 

 

else

/* произошла ошибка

*/

perror("Accept");

Взаимодействие с клиентом

Обратите внимание на то, что в приведенном выше фрагменте программы за крывался дескриптор clientsd, который отличается от основного дескриптора со кета. Это очень важный момент, поскольку для каждого соединения создается отдельный дескриптор. Если забыть их закрыть, лимит дескрипторов может со временем исчерпаться.

Повторноеиспользованиеадреснойструктуры

В функции accept() можно использовать адресную структуру, инициализированную еще при вызове функции bind( ). По завершении функции bind( ) хранящаяся в этой структуре инфор мация больше не нужна серверу.

Помните, что большинство полей структуры имеет сетевой порядок следова ния байтов. Извлечь адрес и номер порта из переменной addr можно с помощью функций преобразования (листинг 6.5).

Листинг 6.5. Пример функции accept( ) с регистрацией подключений

/****************************************************************/

/***

 

Расширенный пример функции accept(): информация

***/

/***

 

о каждом новом подключении отображается на экране

***/

/****************************************************************/

/***

(Внутри цикла) ***/

 

 

 

client = accept(sd, saddr, &size);

 

 

 

if ( client > 0 )

 

 

 

{

 

 

 

 

 

if (

addr.sin_family == AF_INET)

 

 

 

printf( "Connection [%s]: %s:%d\n",

/* регистрация */

 

 

ctime(time(0) ) ,

 

/* метка

времени */

 

 

ntoa(addr.sin_addr),

ntohs(addr.sin_port));

 

/* —

взаимодействие с клиентом

— */

 

 

Если в процессе выполнения функции accept() происходит ошибка, функция возвращает отрицательное значение. В противном случае создается новый деск риптор сокета. Ниже перечислены коды возможных ошибок.

Глава 6. Пример сервера

123

www.books-shop.com

EBADF. Указан неверный дескриптор сокета.

EOPNOTSUPP. При вызове функции accept() сокет должен иметь тип

SOCK_STREAM.

EAGAIN. Сокет находится в режиме неблокируемого ввода вывода, а оче редь ожидания пуста. Функция accept() блокирует работу программы, если не включен данный режим.

Настало время вернуться к эхо серверу, который возвращает клиенту получен ное сообщение до тех пор, пока не поступит команда bye (листинг 6.6).

Листинг 6.6. Пример эхо сервера

/***

Пример эхо сервера: возврат полученного сообщения

***/

/***

до тех пор пока не поступит команда "bye<ret>"

***/

/*** (Внутри цикла после функции accept()) ***/

•••

if ( client > 0 )

{char buffer[1024]; int nbytes;

do

{

nbytes recv(client, buffer, sizeof(buffer), 0);

if ( nbytes > 0 ) /* если получены данные, возвращаем их */ send(client, buffer, nbytes, 0);

}

while ( nbytes > 0 && strncmp("bye\r", buffer, 4) 1=0); close(client);

}

Заметьте, что признаком окончания сеанса является строка "bye\r", а не "bye\n". В общем случае это зависит от того, как выполняется обработка входного потока. Из соображений надежности следует проверять оба случая. Попробуйте протестировать данную программу, использовав в качестве клиента утилиту Telnet.

Общие правила определения

протоколов

Взаимодействуя с другими компьютерами, программа должна следовать опре деленным правилам общения. Два основных вопроса, на которые необходимо от ветить: "Кто начинает первым?" и "Когда мы закончим?"

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

124

Часть //. Создание серверных приложений

www.books-shop.com

Какая программа должна начинать передачу

первой?

БОЛЬШИНСТВО серверов первыми начинают сеанс. Но в некоторых системах с высоким уровнем безопасности предполагается, что клиент должен отправить первое сообщение. Сервер может заставить клиента идентифицировать себя (указать не только адрес узла и порт).

Следует избегать ненужного взаимодействия. Если сервер начинает первым, он, как правило, выдает одну и ту же информацию при каждом подключении. Нужно ли это клиенту? Не замедлит ли это работу?

Какая программа должна управлять диалогом?

Чаше всего диалогом с сервером управляют клиенты. Клиент подключается к серверу и посылает запросы. Сервер, в свою очередь, обрабатывает запросы и вы дает ответ.

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

Какой уровень сертификации требуется?

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

Процесс сертификации включает передачу имени пользователя и пароля. Кроме того, может потребоваться наличие цифрового сертификата. В главе 16, "Безопасность сетевых приложений", описывается протокол SSL (Secure Sockets Layer — протокол защищенных сокетов), а также рассматриваются вопросы безо пасности.

С другой стороны, сертификация не всегда нужна, а вместо этого необходимо регистрироваться в системе. Как часто пользователи посещают сервер? Требуется ли настраивать работу сервера в соответствии с предпочтениями отдельных поль зователей? Как незаметно собрать информацию о посетителе? Ответы на эти во просы важны при создании серверных приложений, особенно Web серверов.

Какой тип данных используется?

БОЛЬШИНСТВО серверов использует кодировку ASCII, а большинство Web страниц представлено в формате текст/HTML. Полезно задать себе вопросы: "Является ли это наиболее эффективной формой представления данных?" и "Поддерживает ли клиент сжатие данных?" Подумайте, как можно уменьшить задержки на сервере, в сети и в клиентской системе.

Сжатие данных имеет существенные преимущества. Компрессированные ASCII потоки уменьшаются в размере на 50 80%.

Глава 6. Пример сервера

125

www.books-shop.com

Как следует обрабатывать двоичные данные?

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

Случается ли так, что программа начинает передачу данных в текстовом виде, а затем переключается в двоичный режим? В этом случае необходимо, чтобы клиент или сервер посылал соответствующее уведомление.

Как обнаружить взаимоблокировку?

Бывает, что клиент и сервер ждут друг друга (это называется взаимоблокиров! кой, или тупиковой ситуацией). При этом бесцельно расходуются ценные ресурсы и испытывается терпение пользователей. Когда возникает тупик, единственное решение заключается в том, чтобы разорвать соединение и подключиться заново, смирившись с возможной потерей данных.

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

Необходима ли синхронизация по таймеру?

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

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

Одно из решений проблемы синхронизации заключается в том, чтобы прово дить все транзакции на сервере (максимально снимая ответственность с клиента). Когда сервер завершает транзакцию, он сообщает клиенту дату и время ее окон чания.

Как и когда переустанавливать соединение?

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

126

Часть II. Создание серверных приложений

www.books-shop.com

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

Повторное открытие соединения позволяет начать сеанс сначала. Посредством приоритетного сообщения клиент или сервер уведомляется о том, что необходи мо закрыть соединение. Затем соединение снова открывается, при этом часть информации теряется и происходит откат к предыдущему состоянию.

Когда завершать работу?

Итак, между клиентом и сервером установлено соединение и передаются па кеты. Пришло время прощаться. Определить конец сеанса может быть не так просто, как кажется. Например, при взаимодействии с HTTP сервером сеанс за вершается в момент получения двух символов новой строки. Но иногда запроса от клиента можно ждать бесконечно, если чтение данных осуществляется с по мощью функции read() или recv(), а буфер имеет недостаточный размер. В этом случае сервер превысит время ожидания и объявит о разрыве соединения.

Кроме того, бывает трудно определить, какая программа должна прервать со единение первой. Клиент получит сообщение о разрыве канала (EPIPE), если сер вер закроет соединение до того, как клиент закончит передачу данных.

Более сложный пример: сервер HTTP

Эхо сервер представляет собой отличный отправной пункт для создания раз личных видов серверов. Одним из них является HTTP сервер. Полная его реали зация выходит за рамки данной книги, но можно создать уменьшенный вариант сервера, который отвечает на запросы любого броузера. Текст этого примера име ется на Web узле (файл html ls server.c).

Сервер генерирует HTML код динамически, а не загружает его из файла. Это упрощает программу (листинг 6.7).

Листинг6.7.ПримерпростогоНИР сервера

/**************************************************************/

/*** Простой HTTP сервер ***/

/**************************************************************/

while(1)

{int client;

int size = sizeof(addr);

client = accept(sd, &addr, &size); if ( client > 0 )

{char buffer[1024];

/* Сообщение клиенту */

char *reply = "<html><body>Hello!</body></html>/n";

Глава 6. Пример сервера

127

www.books-shop.com

bzero(buffer,

sizeof(buffer) );

/*

очистка буфера */

recv(client, buffer,

sizeof(buffer), 0);

/* получение

 

 

 

 

сообщения */

send(client,

reply,

strlen(reply), 0);

/*

ответ клиенту*/

/* — отображение клиентского сообщения — */ fprintf(stderr, "%s", buffer);

close(client);

}

else perror("Accept");

Каждый раз, когда клиент подключается, он посылает запрос примерно такого вида:

GET /dir /document HTTP/ 1.0 (определение протокола)

Первая строка представляет собой запрос. Все последующие сообщения ин формируют сервер о том, какого рода данные готов принимать клиент. В первую строку может входить конфигурационная информация, позволяющая серверу оп ределить, как следует взаимодействовать с клиентом. Метод GET принимает два параметра: собственно запрос и используемый протокол. Сервер может анализи ровать запрос, выделяя имя каталога и имя документа (естественно, запросы бы вают гораздо более сложными). Протокол HTTP 1.0 допускает наличие пробелов в путевом имени, поэтому запрос включает в себя все, что находится от началь ного символа косой черты до строки HTTP/.

При ответе сервер может дополнительно передавать MIME заголовок, указы вая на статус сообщения и тип возвращаемого документа:

НТТР/1.1 200 ОК Content Type: text/html

(пустая строка) (пустая строка) <html>

<head>

Первая строка является статусной. Она информирует клиента о том, насколь ко успешно выполнен запрос. Именно здесь передается печально известное со общение об ошибке 404 ("Not Found"). Полный список кодов завершения HTTP 1.1 представлен в приложении А, "Информационные таблицы".

В действительности этот этап можно пропустить, поскольку по умолчанию клиент ожидает поступления HTML документа. Таким образом, достаточно про сто послать сгенерированный HTML код.

При написании HTTP сервера следует учитывать ряд моментов. В частности, сервер не знает заранее, насколько большим получится результирующий документ, поэтому дескриптор сокета необходимо привести к типу FILE* (листинг 6.8).

128

Часть II. Создание серверных приложений

www.books-shop.com

Листинг 6.8. Расширенный алгоритм HTTP сервера

/**************************************************************/

/***

Пример сервера HTTP 1.0: устанавливаем соединение,

***/

/***

принимаем запрос, открываем каталог и создаем

***/

/***

для него список файлов в формате HTML

***/

/**************************************************************/

/*** Создание сокета, привязка его к порту и перевод в режим прослушивания ***/

for(;;)

{int client;

int size = sizeof(addr);

client = accept(sd, &addr, &size);

/* ожидание запроса

 

 

 

 

 

 

на

подключение */

 

if ( client > 0 )

 

 

 

 

 

{ char

buf[1024];

 

 

 

 

 

FILE

*clientfp;

 

 

 

 

 

bzero(buf,

sizeof(buf));

 

/* очистка буфера

*/

recv(client, buf, sizeof(buf),

0 ) ;

/* получение

 

 

 

 

 

 

 

сообщения */

clientfp = fdopen(client, "w");

/* приведение к типу

 

 

 

 

 

 

 

FILE*

*/

if (

clientfp != NULL

)

 

/* если

преобразование

 

 

 

 

 

 

 

прошло успешно

*/

{

 

 

 

 

 

 

 

 

/**** Извлекаем путевое имя из сообщения ****/

 

 

/**** открываем каталог ****/

 

 

 

/****

для каждого файла... ****/

 

 

 

/****

Читаем имя файла ****/

 

 

 

/****

Генерируем HTML таблицу ****/

 

 

fclose(clientfp);

/*

закрываем указатель на файл

*/

}

 

 

 

 

 

 

 

else

 

 

 

 

 

 

 

perror("Client FILE");

/*

приведение

к типу FILE*

 

 

 

 

 

 

 

невозможно

*/

close(client);

 

/*

закрываем клиентский сокет */

}

 

 

 

 

 

 

 

else

 

 

 

 

 

 

 

perror ("Accept") ;

 

/* ошибка в функции accept () */

Эту программу можно улучшить, сортируя список файлов по именам, распо знавая тип каждого файла, добавляя коды ошибок HTTP 1.1 и т.д.

Глава 6. Пример сервера

129

www.books-shop.com

Резюме: базовые компоненты

сервера

Вэтой главе рассматривались основы создания серверных приложений. Серве ры позволяют централизованно управлять данными и операциями, обслуживать множество клиентов и распределять нагрузку между ними.

Всерверных приложениях используются три новые функции: bindf), listen()

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

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

130

Часть II. Создание серверных приложений

www.books-shop.com

Соседние файлы в папке СПО
  • #
    11.04.201527.19 Mб69Cpp4Unix.pdf
  • #
    11.04.201516.44 Mб52IP Arhitektura, protokoly, realizatsiya (vklyuchaya IP versii s IP Security).djvu
  • #
  • #
    11.04.201510.72 Mб51Стивенс. UNIX. Разработка сетевых приложений.djvu