ГЛАВА 4 СОКЕТЫ


4.1. ВВЕДЕНИЕ


Версия BSD 4.2. системы UNIX была первой версией, в которой TCP/IP был включен в состав ядра операционной системы, и в которой был предложен программный интерфейс этого протокола : сокеты (sockets). Сокеты, таким образом, представляют собой API (Application Program Interface), то есть интерфейс между прикладными программами и сетевыми уровнями.

4.2. ФУНКЦИОНАЛЬНЫЕ ВОЗМОЖНОСТИ

4.2.1. Предоставляемые услуги

Термин "сокет" (socket) обозначает одновременно библиотеку сетевых интерфейсов и оконечное устройство канала связи (точку связи), через которое процесс может передавать или получать данные. Эта точка связи представлена переменным целым значением, аналогичным дескриптору файла. Сокет-интерфейс представляет собой совокупность примитивов, позволяющих управлять обменом данными между процессами, независимо от того, протекают эти процессы на одной машине или нет. Сокет-библиотека маскирует интерфейс и механизмы транспортного уровня: один сокет-вызов преобразуется в несколько транспортных запросов. Сокеты позволяют осуществить доступ к сети как к файлу.

4.2.2. Механизм реализации

Модель "клиент-сервер"

Сокеты позволяют,помимо всего прочего, конструировать распределенные прикладные программы в соответствии с моделью "клиент-сервер" (рис. 4.1.).

Рис. 4.1. Модель "клиент-сервер" и сокеты.

Использование буферов TCP

Как мы видели в главе 1, механизм буферизации реализуется посредством сокетов TCP (рис. 4.2.). Для каждого подключения существует один буфер передачи (размером 4 Кб на компьютерах Sun и HP, 16 Кб на компьютере Cray) и один буфер приема.

Рис. 4.2. Сокеты и буферы.

Считывание с сокета блокируется до тех пор, пока в приемном буфере ничего нет (если сокет объявлен не-блокирующим, операция чтения возвращает сообщение об ошибке). Сразу же после получения, данные пересылаются в прикладную программу (даже если число полученных байтов меньше необходимого). Запись в сокет блокируется только в том случае,если буфер передачи переполнен (если сокет объявлен не-блокирующим, операция записи возвращает сообщение об ошибке). Байты, хранящиеся в буфере, не пересылаются в сеть до тех пор пока буфер не переполнится. Можно осуществить запись экспресс данных ("out of band"): в этом случае не используются механизмы буферизации и данные принимаются программой-получателем перед обычным потоком.

4.2.3. Реализация сокетов

В версии BSD 4.3. системы UNIX сокеты реализованы в ядре, как показано на рис. 4.3.

Рис. 4.3. Реализация сокетов в BSD.

В версии System V Release 4, сокеты реализованы в форме библиотеки-надстройки над TLI (Transport Level Interface). Сокет-интерфейс, таким образом, сохраняется для существующих прикладных программ.

4.3. ИСПОЛЬЗОВАНИЕ

4.3.1. Текущее применение

Принципы применения

При создании сокета, указывается в какой области происходит работа: UNIX (AF_UNIX), TCP/IP (AF_INET), X25, DECNET, APPLETALK... Каждой области соответствует свой тип протокола. Кроме того, задается тип, определяющий свойства коммуникации:
- SOCK_DGRAM: сообщения посылаются в форме дейтаграмм. Связанный с ним протокол связи нe является таким надежным (нарушается последовательность, возможны потери данных) в реэиме без установления логического соединения, как UDP в области AF_INET;
- SOCK_STREAM: посылаются потоки байтов, понятие "сообщения" не вводится. Используемый протокол связи надежен, с установлением виртуального соединения, как TCP в области AF_INET;
- SOCK_ RAW: обеспечивает доступ к протоколам самого низкого уровня, таким как IP в области AF_INET, либо реализует новые протоколы.

Если используются сокеты над UDP размер передаваемых данных ограничен несколькими килобайтами, от 2 Кб до 8 Кб, в зависи-мости от системы. Сокет-интерфейс можно использовать для связи между двумя процессами на одной машине. В этом случае необходимо указать, что работа производится в области AF_UNIX. Вызовы сокетов для области AF_UNIX те же, что и для области AF_INET; меняются только структуры, связанные с адресами. Данное сходство вызо-вов позволяет достаточно легко переходить от локальных задач к сетевым и обратно.

Использование в режиме с установлением виртуального соединения

Клиент :
- создает сокет;
- подсоединяется к серверу, предоставляя адрес удаленного сокета (адрес Internet сервера и номер сервисного порта). Это соединение автоматически присваивает клиенту номер порта;
- осуществляет считывание или запись на сокет;
- закрывает сокет.
Сервер:
- создает сокет;
- связывает сокет-адрес (адрес Internet и номер порта) с сервисной программой: "binding";
- переводит себя в состояние "прослушивания" входящих соединений;
- для каждого входящего соединения:
- принимает соединение (создается новый сокет с теми же характеристиками, что и исходный;
- считывает и записывает на новый сокет;
- закрывает новый сокет.
На рис. 4.4. показаны примитивы, используемые для сокетов типа SOCK_STREAM.

Рис. 4.4. Использование сокетов с установлением логического соединения.

Некоторые вызовы способные заблокировать программу : Клиент:
- connect () до того, как сервер осуществит accept ();
- write () при переполнении буфера передачи;
- read () до того, как будет получен хотя бы один символ вследствие операции записи, осуществленной сервером.
Сервер:
- accept () до того, как клиент осуществит connect ();
- read () до того, как будет получен хотя бы один символ, вследствие операции записи, осуществленной клиентом;
- write () при переполнении буфера передачи.

Использование в режиме дейтаграмм

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


Сервер:
- создает сокет;
- связывает сокет-адрес с сервисной программой: "binding" (операция необходима только в случае, если процесс должен получить данные);
- считывает или осуществляет запись на сокет. На рис. 4.5. показаны примитивы, используемые для сокетов типа SOCK_DGRAM.

Адресация

Много вызовов, связанных с сокетами требуют в качестве аргу-мента указатель структуры, содержащий сокет-адрес, общая структура которого определена в файле <sys/socket.h>:
struct sockaddr {
u_short sa_family; /*AF_UNIX или AF_INET*/
char sa_data [14];/*абсолютный адрес протокола*/
};
Тип u_short, также как и определенное число других используемых типов, определены в файле <sys/types.h>.

Рис. 4.5. Использование сокетов в режиме дейтаграмм.

Адресация в области AF_INET

В области AF_INET клиент и сервер явно указывают используемый транспортный протокол (TCP или UDP). Сервер связывает свою сервисную программу с сокет-адресом (адрес Internet и номер порта), затем переходит в состояние ожидания запросов от кли-ентов. Клиент адресует свои запросы, предоставляя серверу адрес Internet и номер сервисного порта.
В файле <netinet/in.h> определены следующие структуры:
struct in_addr {
u_long s_addr;
};
struct sockaddr_in {
short sin_family; /*AF_INET*/
u_short sin_port; /*номер порта*/
struct in_addr sin_addr; /*машинный адрес Internet*/
char sin_zero [8]; /*не использован*/
};

Адресация в области AF_UNIX

В области AF_UNIX обмен между клиентами осуществляется через файл UNIX типа сокет. В файле <sys/un.h> определена следующая структура:
struct sockaddr_un {
short sun_family; /*AF_UNIX*/
char sun_path [108]; /*полное имя файла*/
};

Включаемые файлы

Два нижеследующих файла являются включаемыми файлами для программировании сокетов в областях AF_INET и AF_UNIX.

ПРОГРАММА 21

/* файл soct.h для сокетов TCP/IP *******/

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <errno.h>
/*специфическая опция прикладной программы*/
#define PORT 6315 /* задание номера порта */

ПРОГРАММА 22
/* файл socu.h для сокетов UNIX *******/
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <errno.h>
/*специфическая опция прикладной программы*/
#define SPATH "/tmp/usocket" /* полное имя файла сокета */

4.3.2. Примитивы

Основные примитивы

Создание сокета
int socket (домен, тип, протокол)
int domain; /*AF_UNIX или AF_INET*/
int type; /*SOCK_STREAM или SOCK_DGRAM*/
int protocole; /*поставить ноль*/

Примитив возвращает используемый сокет-дескриптор для следующих вызовов.
- Binding (связывание)
int bind (sock, localaddr, addrlen)
int sock;/*сокет-дескриптор*/
struct sockaddr *localaddr; /*локальный сокет-адрес*/
int addrlen; /*длина адреса*/

Данный вызов позволяет связать локальный адрес с сокет-дескриптором, создаваемым socket (). Эта операция является необязательной для клиентов, использующих сокет с установлением логического (виртуального) соединения, так как адрес присваивается в момент соединения с сервером в случае, если связыва-ние не произошло. Для сокетов в режиме дейтаграмм, эта операция является необходимой только в случае, если процесс должен получить данные. Для сокетов TCP/IP возможно присвоение номеру порта нулевого значения. Система присваивает номер, который можно получить посредством примитива getsockname ().

- Соединение клиента c сервером

int connect (sock, servaddr, addrlen)
int sock; /*сокет-дескриптор*/
struct sockaddr *servaddr; /*адрес сервера*/
int addrlen; /*длина адреса*/

- установка сервера в режим "прослушивания"
int listen (sock, qlen)
int sock; /*сокет-дескриптор*/
int qlen; /*макс. число необработ. подсоединений*/
Этот примитив указывает, что сервер готов к получению запросов на соединение. Параметр qlen указывает на максимальное число запросов, которое может быть установлено в режим ожидания обработки.
- Согласие сервера на соединение
int accept (sock, addrdistant, addrlen)
int sock; /*сокет-дескриптор*/
struct sockaddr *addrdistant; /*адрес телекоммуник.*/
int *addrlen; /*длина адреса*/

Этот вызов используется сервером для ожидания запросов клиентов. Два последних параметра могут быть установлены в 0, кроме случаев, когда необходимо проверить идентичность клиента. Благодаря этому примитиву сервер дает клиенту понять, что его запрос принят. Примитив accept () возвращает новый сокет-дескриптор, который будет использован для обмена данными с клиентом. Для сервера имеется возможность создания порожденного процесса, кото-рый воспользуется вновь созданным дескриптором, в то время как порождающий процесс вновь перейдет в состояние ожидания соединения (accept ()) на сокете, открытом функцией socket ().
- Примитивы считывания и записи read(), write(), send(), recv(), sendto(), recvfrom()
Вызовы read () и write () используются также, как и для дескриптора файла. Вызовы send () и recv () имеют дополнительный аргумент, позволяющий, кроме всего прочего, посылать экспресс-данные. Вызовы send to () и recv from () используются для сокетов типа SOCK_DGRAM. Два дополнительных параметра позволяют уточ-нить адрес удаленного компьютера в случае sendto() и восстановить этот адрес в случае recvfrom().
- Закрытие соединения int close (sock) int sock; /*сокет-дескриптор*/ Перед закрытием ядро пытается переслать еще не посланные данные.

Некоторые вспомогательные программы

Номер порта кодируется в двух байтах, адреса Internet в 4-х. Важно, чтобы система-клиент и система-сервер пришли к соглашению по поводу порядка передаваемых байтов. Для этого имеется 4 программы, преобразующие короткие или длинные целые в кодиро-ванные значения по стандартному формату. Две первых программы используются для перехода от локального формата к сетевому, а остальные две - для обратной операции.
#include <sys/types.h>
#include <netinet/in.h>
u_long htonl (hostlong) u_long hostlong;
u_short htons (hostshort) u_short hostshort;
u_long ntohl (netlong) u_long netlong;
u_short ntohs (netshort) u_short netshort;
- Операции с данными
Три программы: bzero (), bcopy () и bcmp () позволяют инициализировать символьную строку нулями, скопировать одну строку в другую и сравнить две строки, соответственно.. В данном случае, речь идет о примитивах BSD. Подобные же примитивы существуют и системах на основе System V: memset (), memcpy () и memcmp ().
В Приложении А.3 показано средство моделирования примитивов BSD посредством примитивов System V.
- Преобразования форматов адресации
Две программы позволяют перейти от адреса Internet в форме символов (состояние,в котором он находился в файле /etc/hosts) к адресу в форме 4-х байтов и наоборот.
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
u_long inet_addr (адрес) char *адрес /*адрес в ASCII-символах*/
char *inet_ntoa (inadresse) struct in_addr inadresse; /*адрес в форме целого*/

- Построение сетевых адресов

Для конструирования адреса Internet по имени машины,можно ис- пользовать функцию gethostbyname (). #include <netdb.h>
struct hostent *gethostbyname (hostname) char *hostname
Функция возвращает указатель структуры hostent:
struct hostent {
char *h_name; /*имя машины*/
char **h_aliases; /*список псевдоимен*/
int h_addrtype; /*AF_INET*/
int h_length; /*4 байта*/
char **h_addr_list;
/*список адресов Internet*/
};
Эта функция находит адрес в файле /etc/hosts, или же использует сервисные программы сервера имен. Сервером имен является демон (следящая программа) UNIX, который обладает или может найти имя и адрес всех машин сети. Наиболее распространенными серверами имен являются NIS (Network Information Service), который ранее назывался YP (Yellow Pages) и BIND (Berkeley Internet Name Service), иначе называемый DNS (Domain Name Service).

ПРОГРАММА 23
 /* программа, выдающая на экран адреса Internet, соответст-

   вующие именам машин, указанных в качестве параметра */   #include "soct.h"
#include <arpa/inet.h>

main(argc,  argv)
int argc;
char **argv;
{
   struct hostent *hostp;   /* структура адреса   */
   long int i;              /* счетчик цикла      */

/* цикл по аргументам = именам машин   */
   for (i = 1; i<argc; i++)  {
   if ( (hostp = (structhostent *) gethostbyname(argv[i]))       == NULL)
                          printf("**host %s non trouve**\n", argv[i]);
     else {
/* считается, что у машины только один адрес Internet */
                          printf("host %s adresse : %s \n", hostp->h_name,
                                     inet_ntoa(*hostp->h_addr_list));
     }
  }
}

Одна из функций позволяет найти имя машины по ее адресу Internet: gethostbyaddr (). Наконец, один из примитивов позволяет найти номер порта сервисной программы, определенной в файле /etc/services: getservbyname ().
#include <netdb.h>
struct servent *getservbyname (servname, protoname) char *servname; /*имя услуги*/
char *protoname; /*TCP или UDP*/
Функция возвращает указатель на структуру servent:
struct servent {
char *s_name; /*имя услуги*/
char **s_aliases; /*список псевдоимен*/
int s_port; /*номер порта*/
char *s_proto; /*используемый протокол*/
};

ПРОГРАММА 24  /* программа, выдающая на экран номера портов, соответствующих именам служб, указанных в качестве параметра  */
#include "soct.h"
#include  <arpa/inet.h>

main(argc,  argv)
int argc;
char **argv;
{
   struct servent *servp;   /* структура службы   */
   long int i;              /* счетчик цикла      */
/* цикл по аргументам = именам служб    */
   for (i = 1; i<argc; i++)  {
    if ( (servp = getservbyname(argv[i],  "tcp")) == NULL) && (servp = getservbyname(argv[i],  "udp")) == NULL)
    printf("**service %s non trouve**\n", argv[i]);
    else {
/* обратите внимание на номер порта : значение считывается     в формате сети ; используйте htonl()  */
                          printf("service %s port: %d protocole: %s \n",
                                    servp->s_name, htonl(servp->s_port),
                                    servp->s_proto);
                 }
  }
}

- Адрес, связанный с сокетом
int getsockname (sock, адрес, addrlen) int sock; /*сокет-дескриптор*/
struct sockaddr *адрес; /*указатель адреса*/
int *addrlen; /*длина адреса*/


Этот примитив позволяет найти адрес порта, присвоенный сис- темой.

Другие примитивы

Считывание и запись Примитивы readv() и writev() позволяют осуществлять считывание и запись c помощью нескольких несмежных буферов. Этим можно пользоваться, например, для записи общего заголовка, находящегося в постоянном буфере и данных, находящихся в переменном буфере. Эти два примитива не характерны для сокетов. Примитивы sendmsg() и recvmsg() позволяют осуществлять считывание и запись в самом общем виде и используются со всеми опциями.
- Адрес удаленного процесса
Примитив getpeername() позволяет получить сокетадрес уда-ленного процесса.
- Управление окончанием соединения Функция shutdown () позволяет управлять окончанием соединения.
int shutdown () (sock, controle) int sock;/*сокет-дескриптор*/ int controle; Аргумент controle может принимать следующие значения:
0: больше нельзя получать данные на сокете;
1: больше нельзя посылать данные на сокет;
2: больше нельзя ни посылать, ни принимать данные на сокет.
- Определение параметров сокета
Параметры сокетов устанавливаются примитивом setsockopt(). Для установки некоторых опций можно использовать также функции fcntl () или ioctl (). Текущие значения параметров можно определить с помощью примитива getsockopt ().

4.3.3. Рабочие характеристики

Присвоение номеров портов

Зарезервировано определенное число номеров; это номера в диапазоне от 1 до 1023, отведенные для стандартных служб. Для сервера номер порта либо закреплен в программном коде, либо считывается в файле /etc/services, либо присваивается системой в момент bind (). Для клиента номер порта либо зафиксирован в его коде, либо считывается из файла /etc/services, либо присваивается систе-мой в момент connect (). Если номер порта сервера хранится в /etc/services, файл /etc /services клиента должен содержать идентичный номер, что при-водит к возникновению проблемы соответствия. Можно дать системе возможность выбора номера порта для сер-вера и восстановить этот номер в процессе-клиенте. Для этого можно использовать функции popen () или rexec (). Пример такого решения мы увидим в разделе 4.3.5. Другое решение состоит в том, что в программах фиксируется номер порта, идентичный для клиента и для сервера, при условии, что этот номер не используется уже другой сервисной программой. Проверить, что был ли присвоен номер, можно справив-шись в таблице /etc/services (номера портов, установленных для определенного числа служб ) и в результате выполнения команды rpcinfo -p (номера портов, динамически присвоенных серверам RPC).

Считывание и запись в сокет в режиме виртуального соединения

Считывание с сокета с установлением логического (виртуального) соединения обладает следующей особенностью: примитив возв-ращает результат после считывания, по крайней мере, одного байта; в результате этого, число считанных байтов часто меньше требуемого числа. Следовательно, необходимо зациклить примитив считывания, чтобы добиться продолжения... Точнее, вот что возвращает вызов примитива read () (или recv (), или recvfrom ()):
-1: если соединение разорвано или не существует;
>0: было получено определенное число байтов; необходимо организовать цикл считывания для продолжения работы (конечно же, нет необходимости в организации цикла, если ожидаемое количество байтов уже получено);
0: удаленная программа выполнила close (), на чем и остановилась. Функция записи возвращает результат после того, как буфер выходных данных быдет передан через TCP. Программа предупреждается о возможной ошибке сигналом SIGPIPE, о котором речь пойдет далее.
Ниже в данной главе читатель найдет две процедуры: reads () и writes (), позволяющие читать и записывать n-байтов в сокет. Для этого организуется цикл по числу считанных или записанных байтов, до получения необходимого их количества. Две эти процедуры, применяемые повсеместно, использованы во многих примерах в данной главе.

ПРОГРАММА 25
/* процедуры reads() и writes() для чтения и записи в сокет    в режиме виртуального соединения  */

#include   <stdio.h>


/* запись в сокет буфера, состоящего из пос байт */
int writes(sock,  pbuf,  noc)
register int sock;       /* дескриптор сокета */
register char *pbuf;     /* буфер   */
register int noc;        /* число записываемых байт */
{
   int nreste, necrit;
   nreste = noc;
   while (nreste > 0)  {
      necrit = write(sock, pbuf, nreste);
      if (necrit < 0) return(necrit);
                        nreste -= necrit;
                        pbuf += necrit;
   }
   return(noc-nreste);
}
/* считывание в буфер пос байт из сокета     */
int reads(sock, pbuf, noc)
register int sock;         /* дескриптор сокета */
register char *pbuf;       /* буфер  */
register int noc;          /* число считываемых байт */
{
   int nreste, nlit;

   nreste = noc;
   while (nreste > 0)  {
                        nlit = read(sock, pbuf, nreste);
                        if (nlit < 0) return(nlit);
                        else if (nlit == 0) break;
                        nreste -= nlit;
                        pbuf += nlit;
   }
   return(noc-nreste);
}

Управление сигналами

С сокетами связаны 3 сигнала:
- SIGIO: указывает на то, что сокет готов к асинхронному вводу-выводу. Сигнал посылается процессу или группе про-цессов, связанных с сигналом;
- SIGURG: указывает на то, что на сокете получены экспресс-данные. Посылается процессу или группе процессов, связанных с сигналом;
- SIGPIPE: указывает на то, что запись на сокете более невозможна. Посылается процессу, связанному с сокетом. Более подробно использование данных сигналов объясняется в нижеследующих разделах.

Управление ошибками

Как уже было указано, сигналы об ошибках реализуются следую- щим образом:
- read () возвращает ноль, если удаленный процесс разрушен или -1, если прервана связь по сети;
- write () вызывает посылку сигнала SIGPIPE, если удаленный процесс разрушен или если связь по сети прервана.
Для проверки работоспособности удаленного процесса можно добавить управление с помощью реле времени (темпоризатора) по вызовам read () или write (). Возможно, что read () никогда не кончит свою работу, если удаленная программа зациклилась, или что вызов займет слишком много времени, если удаленная машина сильно загружена. Возможен аварийный останов программы-клиента. Сервер в этом случае должен контролировать присутствие клиента. Следует установить опцию SO_KEEPALIVE, которую мы рассмотрим в разделе об определении параметров сокетов. Дополнительный контроль можно осуществить следующим образом: сервер периодически осу-ществляет запись байта в контрольный сокет. Таким образом, если клиента больше нет, будет обнаружена ошибка SIGPIPE и сер-вер остановится. Пример использования этого механизма приведен в главе 10 "RPC".

ПРОГРАММА 26  /* Управление сигналом SIGPIPE с помощью опции SO_KEEPALIVE                       и временной задержкой                 ****************/

#include "soct.h"
#include <signal.h>

/* хэндлер сигнала SIGPIPE        */
tsigpipe()
{
  err_quit("SIGPIPE recu \n");
}

/* хэндлер сигнала SIGALRM    */
tsigalarm()
{
  err_quit("time out sur lecture ou ecriture \n");
}
main()
{
  int sock;         /* дескриптор сокета */
  int optval;       /* значение опции  */
  int optlen;       /* длина optval    */
/* установка опции SO_KEEPALIVE     */
  optlen = sizeof(optval);
  optval = 1;
  setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &optval,
                                    optlen);
/* установка обработчика сигнала SIGPIPE      */
  signal(SIGPIPE, tsigpipe);
/* установка обработчика сигнала SIGALRM      */
  signal(SIGALRM, tsigalarm);
}


Безопасность

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

Отладка программ

Отладка программ осуществляется следующим образом:
- надо сначала запрограммировать и проверить клиента и сервер на одной и той же машине, используя локальные средства и полиэкранное отображение;
- затем перенести сервер на другую машину. Кроме того, сервер можно проверить независимо от клиента, используя сервисную программу telnet с опцией номера порта; любой введенный с клавиатуры символ, в этом случае, посылается серверу.
Команда netstat -an позволяет получить информацию о состоянии сокетов, а также номера используемых портов.

Суперсервер Internet

Суперсервер Internet (демон inetd) находит список серверов в файле /etc/inetd.conf и номера портов в файле /etc/services. Cуперсервер мультиплексирует запросы от клиентов посредством функции select (). Для каждой запрашиваемой услуги создается порожденный процесс, реализующий ее, в то время как суперсервер возвращается к select () для обработки следующего запроса (рис. 4.6.).

Рис. 4.6. Функционирование демона inetd (по [STEVENS 90])
Для того, чтобы добавить сервер, исполняемый посредством демона inetd, необходимо осуществить следующие операции:
- вывести из программы примитивы, реализованные в демоне inetd: socket (), bind (), listen (), accept (). Использовать операции считывания и записи с дескрипторами 0 и 1 (вместо сокет-дескриптора). Выйти из программы посредством exit () после завершения сервисной программы.
- составить конфигурацию сервисной программы в файлах /etc/services и /etc/inetd.conf;
- уничтожить демона inetd;
- вновь запустить демона inetd.

4.3.4. Используемые опции

Определение параметров сокета

Наиболее интересными опциями являются:
- TCP_NODELAY: запрещает хранение данных в буферах TCP;
- SO_ERROR: пересылает значение переменной so_error, определяемой в файле <sys/socketvar.h>;
- SO_KEEPALIVE: периодическая передача контрольных сообщений на сокет в режиме установления соединения.
Если один из процессов не отвечает, соединение считается прерванным и в переменной so_error возвращается ошибка ETIMEDOUT (см. оп-цию SO_ERROR); - SO_RCVBUF и SO_SNDBUF: определяет размер буферов TCP. Характеристики можно улучшить, взяв буферы большего размера;
- SO_REUSEADDR: позволяет повторно использовать уже использованный сокет-адрес (в частности, номер порта).

ПРОГРАММА 27  /* изменение размера буферов (tampon) TCP ***************/   #include "soct.h"
int buffsize = 8192;

main()
{
  int sock;             /* дескриптор сокета */
  int optval;           /* значение опции */
  int optlen;           /* длина optval    */

/* создание сокета  */
  sock = socket(AF_INET, SOCK_STREAM, 0);

/* считывание и модификация длины буфера TCP */
  optlen = sizeof(optval);

  getsockopt(sock, SOL_SOCKET, SO_SNDBUF, &optval, &optlen);   printf("recvbuffsize %d\n", optval);
  getsockopt(sock, SOL_SOCKET, SO_RCVBUF, &optval, &optlen);   printf("sendbuffsize %d\n", optval);
  setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &buffsize,
                                  sizeof(buffsize));
  setsockopt(sock, SOL_SOCKET, SO_SNDBUF, &buffsize,
                                  sizeof(buffsize));
}


Мультиплексирование с помощью select ()

Здесь мы приводим пример использования сокетов с примитивом select (), рассмотренным в главе 1.

ПРОГРАММА 28
/* использование функции select() для определения, готовыли данные на сокете сервера TCP    */
#include "soct.h"
#include <sys/time.h>
main()
{
  int sock;                 /* дескриптор исходного сокета */    int nsock;                /* дескриптор, полученный с по-                                                  мощью accept  */
  int retour;               /* возвращаемое значение  */
  struct sockaddr_in server;/* адрес сокета  */
  fd_set readf;             /* переменная для select */
  fd_set writef;            /* переменная для select */
  struct timeval to;        /* тайм-аут для select  */

/* бесконечный цикл ожидания  */
  for (;;)  {
/* процесс ждет операцию ввода-вывода на сокете ; одновременно                  можно ждать и другие операции */
                                FD_ZERO(&readf);
                                FD_ZERO(&writef);
                                FD_SET(sock, &readf);
                                FD_SET(sock, &writef);
                                to.tv_sec = 10;

                                retour = select(sock+1, &readf, &writef, 0, &to);
/* тайм-аут, если select возвращает нулевое значение */
                                if (retour == 0)  {
                                   err_ret("timeout");
                                   continue;
                                }
/* в противном случае, ищем соответствующий дескриптор */
                                if ( (FD_ISSET(sock, &readf)) || (FD_ISSET(sock,
                                                                     &writef))) {
/* прием связи с сокета           */
                                   nsock = accept(sock, (struct sockaddr *) 0, (int *)0);
/* обращение к соответствующей службе  */
                                   serveur(nsock);
/* закрытие текущей связи   */
                                   close (nsock);

                                }
                                else
                                {
/* это не сокет; надо проверить все дескрипторы ввода-вывода*/   err_ret("autre descripteur");
                                }
  }
}

Не-блокирующие операции

Отдельные вызовы сокетов блокируют программу в случае , если удаленный процесс не осуществил ожидаемую операцию: connect (), accept (), write (), read ()... Этой блокировки можно избежать, если объявить сокеты неблокирующими , посредством примитива ioctl () (флаг FIONBIO) или примитива fcntl () (флаг FNDELAY, если речь идет о системе BSD и флаг O_NDELAY в случае System V). В этом случае работа осуществляется следующим образом:
* accept () завершает работу сразу же с ошибкой EWOULDBLOCK;
* connect () завершает работу сразу же немедленно с ошибкой EINPROGRESS;
* recv () или read () или recvfrom возвращают -1 (FIONBIO или FNDELAY) или 0 (O_NDELAY) при отсутствии считываемых данных; в EWOULDBLOCK или EAGAIN выставляется ошибка. Использование неблокирующих операций целесообразно в том случае, когда нет необходимости довести ввода-вывода до конца. В этом случае, конец можно периодически проверять и осущест-влять другую обработку данных до окончания ввода-вывода.

ПРОГРАММА 29  /* неблокирующие операции на сокете клиента  */


#include "soct.h"
#include <sys/ioctl.h>
#include <fcntl.h>
#define TAILLEMAXI 1024
client(sock)
int sock;           /* дескриптор сокета   */
{
   char buf[TAILLEMAXI];      /* буфер  */
   int i;                     /* счетчик цикла    */
   int on = 1, off = 0;       /* значение для ioctl()  */

/* неблокирующий сокет создается с помощью ioctl() или
   fcntl()   */
                        /* ioctl(sock, FIONBIO,&on);   */
   fcntl(sock, F_SETFL, FNDELAY|fcntl(sock, F_GETFL, 0));
/* цикл передачи буферов   */
/* обработка ошибок EWOULDBLOCK производится в процедурах

   записи  */
   for (i = 0; i<5; i++)  {

                         writes(sock, buf, TAILLEMAXI);
   }
}

/* запись в сокет буфера, занимающего пос байт ; процедура
   корректируется, чтобы обеспечить обработку
   ошибок EWOULDBLOCK     */
int writes(sock, pbuf, noc)
register int sock;         /* дескриптор сокета  */
register char *pbuf;       /* буфер  */
register int noc;          /* число байт    */
{
  int nreste, necrit;
  nreste = noc;
  while (nreste > 0) {
refecriture:
                        necrit = write (sock, pbuf, nreste);
                        if ( (necrit < 0) && (errno = EWOULDBLOCK)) {
                           err_ret("EWOULDBLOCK");

/* повторение при выполнении блокирующей операции   */
                                /*                      */

                           fcntl(sock, F_SETFL, ~FNDELAY&fcntl(sock, F_GETFL,
                                  0));
                           goto refecriture;
                        }
                        if (necrit < 0) return(necrit);
                        nreste -= necrit;
                        pbuf += necrit;
  }
  return(noc-nreste);
}

Асинхронные сокеты

Идеи (рассмотренные в гл. 1), позволяющие реализовать асинхронный ввод-вывод, можно применить и к сокетам. Таким образом, можно избежать попадания процесса в состояние ожидания ввода-вывода, управление которым будет осуществляться по получению сигнала SIGIO.

Экспресс-данные

Этот тип пересылки возможен только для сокетов в режиме логического (виртуального) соединения с помощью TCP. По умолчанию можно послать только один байт. Для передачи большего количества, необходимо использовать опцию SO_OOBINLINE, сохраняющие экспресс-данные в обычном потоке, затем вызвать ioctl () с флагом SIOCATMARK для выявления этих экспресс-данных в полученном потоке. Процесс уведомляется о прибытии экспресс-данных сигналом SIGURG.

Широковещательная передача

Данные могут передаваться всем сокетам сети в режиме дейтаграмм. Необходимо установить опцию SO_BROADCAST посредством примитива setsockopt ().

Параллельный сервер

Для создания параллельного сервера достаточно создать порожденный процесс, который берет эту услугу на себя. Новый сокет-дескриптор, возвращеный функцией accept (), управляется порожденным процессом. В нижеследующих примерах мы показываем принципы программирования процесса-сервера параллельного типа, реализованного в форме демона (следящей программы). Отметим, в частности, управление сигналом SIGCHLD.

Запуск и останов сервера клиентом

Если запуск сервера демоном inetd не предусмотрен, можно использовать функцию REXEC (), позволяющую осуществлять следующие операции:
- запустить сервер;
- восстановить в процессе-клиенте номер порта, присвоенного системой серверу. Для этого клиент читает номер из сокет-дескриптора, возвращенного rexec (). Сервер должен за-писать присвоенный номер порта в стандартный вывод ;
- остановить сервер посредством посылки некоторой информации, которую он интерпретирует соответствующим образом, или послать сигнал прерывания SIGINT на контрольный сокет. Пример показан в разделе 4.3.5. (выполнение удаленной процедуры).


4.3.5. Другие примеры


- Эхофункция цепочки символов Здесь мы вновь рассматриваем пример эхофункции, описанный в разделе 3.1. Программы работают в области AF_INET в режиме соединения и дейтаграмм, и в области AF_UNIX в тех же двух режимах. Читатель, таким образом, располагает примером реализации сокетов всех типов, легко распространяемым для любых других случаев. В примерах с установлением соединения используются процедуры reads () и writes (), определяемые в разделе "Считывание и за-пись на сокете с установлением соединения, с использованием TCP".

- Имитация телеуправления В нижеследующем примере моделируется осуществление телеуправления. Клиент запускает сервер с помощью функции rexec (). Сервер позволяет системе присвоить номер порта, а клиент восс-танавливает связанное с этим значение посредством считывания из сокета, созданного с помощью rexec ().

4.4. УТИЛИТЫ, ОБЛЕГЧАЮЩИЕ ИСПОЛЬЗОВАНИЕ СОКЕТОВ

4.4.1. Принципы

Вызовы сокетов заменяются примитивами более высокого уровня, доступным, в частности, без адаптации, из программ,написанных на Фортране. Несмотря на свое сходство с доступом к файлам, сокет-интерфейс требует манипуляций со структурой данных, которые непривычны для программистов, работающих на Фортране. Обмен двоичными данными требует использования идентичного формата данных на всех машинах. Проще всего здесь использовать XDR (eXternal Data Representation), о котором мы поговорим в главе 9.

4.4.2. Реализация

Библиотека утилит должна обеспечивать следующие функции:
- запускать программы на удаленной машине (сервер);
- обмен данными (целые, вещественные, строки символов, массивы) между локальной программой и сервером, даже если внутренние представления на двух ЭВМ различны (например, Cray и рабочая станция Sun);
- контроль взаимодействия между программой и сервером, с управлением ошибками и тайм-аутами. Совокупность процедур, осуществляющих эти операции, реализованных на языках Си и Фортран, поставляется разработчиками программ.

4.4.3. Преимущества и недостатки такого подхода

Преимущество подобной библиотеки утилит заключается в легкости реализации поставляемых программ (применяемых непосредственно на Фортране). Это позволяет осуществить распределенную обработку данных, не прибегая к манипулированию сокетами и XDR. Однако, использование подобной библиотеки становится весьма проблематичным, как только появляется необходимость в переносе более сложных типов данных (структур, например). Кроме того, существует ограничение и по характеристикам: действительно, передача данных эффективна при каждом вызове процедуры из библиотеки (то есть, не существует механизма буферизации).

4.5. ПРОГРАММНЫЕ ПРОДУКТЫ SQL

4.5.1. Функциональные возможности и механизмы

Необходимо отметить особую роль распределенной обработки данных при работе с базами данных. СУБД, как бы она ни называлась: Oracle, Informix, Ingres или Sybase, должна опираться на мощный компьютер. Она связывается с рабочими станциями или даже микро-компьютерами, обеспечивающими пользовательский интерфейс. ЭВМ-клиент управляет этим интерфейсом и посылает запросы (большей частью, это информационный поиск) серверу базы данных. Сервер обрабатывает запрос и передает результат клиенту. Язык запросов баз данных стандартизован: речь идет об SQL (Structured Query Language). Команды SQL позволяют осуществлять манипуляции информацией баз данных (выбор, ввод, удаление ...).
Архитектура "клиент-сервер" для баз данных передает эти SQL-команды клиента серверу. В ответ, сервер посылает искомую информацию или протокол выполненной операции. При взаимодействии между клиентом и сервером, в основном, используются протоколы TCP/IP, сокет-интерфейс которых скрыт разработчиком. Процесс берет на себя взаимодействие, если сер-вер не находится на том же компьютере, что и клиент. Так, например, для серверов данных Oracle, управление коммуникацией обеспечивает SQL*NET. SQL*Net является, в сущности, одним из UNIX-процессов. Разработчику, пишущему интерфейс, нет нужды заниматься сетевыми взаимодействиями. Программы Oracle связываются с процессом SQL*Net, используя механизм IPC UNIX, если при первом обращении к базе данных указывается, что администратор базы находится на удаленной машине. SQL*Net, таким образом, передает запросы и ответы по сети (рис. 4.7.).

Две машины: взаимодействие через сеть

Рис. 4.7. Взаимодействие между программой и администратором баз данных.

Большая часть администраторов реляционных баз данных обеспе-чены (или претендуют на обеспеченность) средствами такого рода в среде TCP/IP (о программных средствах SQL речь пойдет далее в этой главе и в главе 13 "Синтез"). Такой режим работы имеет свои недостатки:
- передача SQL-последовательностей по сети достаточно громоздка;
- прием и дешифровка SQL-команд отнимает много времени. Вот почему эта архитектура ориентирована на передачу упрощенных команд по сети, соответствующих запросам SQL, из-вестным серверу. Таким образом добиваются уменьшения размера передаваемых данных и времени расшифровки на сервере (рис. 4.8.). Sybase выбрал этот тип архитектуры, другие поставщики администраторов баз данных также должны были бы им вдохновиться.

Рис. 4.8. Архитектур SQL: классическая и упрощенная.

4.5.2. Применение

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

4.6. ИТОГИ

Сокеты представляют собой интерфейс входа в сеть - надстройку транспортной службы. Термин "сокет" обозначает одновременно библиотеку функций и точку входа в канал связи, то есть дескриптор, полученный посредством примитива socket (). Программирование сокетов заключается в комбинировании определенного числа примитивов для считывания или записи потока байтов или сообщений. Сокеты позволяют входить в сеть, как в файл. Этот интерфейс гибкий, но достаточно низкого уровня. Существуют два режима его применения, в зависимости от использования сокетов типа SOCK_STREAM или SOCK_DGRAM. В первом случае устанавливается соединение с TCP, во втором, работа идет с UDP в режиме дейтаграмм. Для упрощения программирования сокетов можно создать библиотеку более высокого уровня, позволяющего передавать простые типы данных (целые, с плавающей запятой, символы, массивы...). Можно обеспечить доступ к базе данных посредством программных продуктов, использующих сокеты (программные средства SQL), причем применение этих продуктов прозрачно для пользователя.