Загрузка...



  • 21.1 Введение
  • 21.1.1 Программный интерфейс Berkeley
  • 21.1.2 Ориентация на Unix
  • 21.2 Службы socket
  • 21.3 Блокированные и неблокированные вызовы
  • 21.4 Вызовы socket
  • 21.5 Программирование работы TCP socket
  • 21.5.1 Модель сервера TCP
  • 21.5.2 Пассивное открытие сервера TCP
  • 21.5.3 Активное открытие клиента TCP
  • 21.5.4 Другие запросы
  • 21.6 Серверная программа TCP
  • 21.6.1 Вызовы в серверной программе TCP
  • 21.7 Клиентская программа TCP
  • 21.7.1 Вызовы в клиентской программе TCP
  • 21.8 Более простой сервер
  • 21.9 Интерфейс программирования socket для UDP
  • 21.10 Программа сервера UDP
  • 21.10.1 Вызовы в серверной программе UDP
  • 21.11 Клиентская программа UDP
  • 21.11.1 Запросы в клиентской программе UDP
  • 21.12 Дополнительная литература
  • Глава 21

    Программный интерфейс socket

    21.1 Введение

    Коммуникационные стандарты определяют все правила для обмена информацией в сети. Однако до некоторого момента игнорировалась необходимость стандартизации интерфейса программирования приложений (Application Programming Interface — API). Как же тогда программист должен создавать приложения клиент/сервер, если программы на каждом из компьютеров совершенно различны?

    21.1.1 Программный интерфейс Berkeley

    К счастью, большинство реализаций TCP/IP обеспечивает программный интерфейс, следующий очень простой модели программного интерфейса socket, который впервые был предложен в 1982 г. в версии 4.1c операционной системы Unix университета Беркли (Berkeley Software Distribution — BSD). Co временем в исходный интерфейс было внесено несколько усовершенствований.

    Программный интерфейс socket разрабатывался для применения с различными коммуникационными протоколами, а не только для TCP/IP. Однако, когда была закончена спецификация транспортного уровня OSI, стало ясно, что этот интерфейс не согласуется с требованиями OSI.

    В 1986 г. компания AT&T предложила спецификацию протокола интерфейса транспортного уровня (Transport Layer Interface — TLI) для операционной системы Unix System V. Интерфейс TLI мог применяться для транспортного уровня OSI, TCP и других протоколов.

    Еще одним важным событием в истории socket стал программный интерфейс socket для Windows (WinSock), позволивший приложениям Windows функционировать поверх стеков TCP/IP, созданных разными производителями. В Windows 95 обеспечивается поддержка многопротокольного интерфейса.

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

    21.1.2 Ориентация на Unix

    Исходный вариант интерфейса socket был разработан для Unix. Архитектура этой операционной системы позволяет единообразно обращаться к файлам, терминалам и вводу/выводу. Операции с файлами предполагают использование одного из следующих вызовов:

    descriptor = open(filename, readwritemode)

    read(descriptor, buffer, length)

    write(descriptor, buffer, length)

    close(descriptor)

    Когда программа открывает файл, вызов создает в памяти область, называемую управляющим блоком файла (file control block) и содержащую сведения о данном файле (например, имя, атрибуты и место размещения).

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

    Похожие методы используются в socket для TCP/IP. Главным отличием между программным интерфейсом socket и файловой системой Unix является то, что в socket применяется несколько дополнительных предварительных вызовов, необходимых для сбора всех сведений перед формированием соединения. Не считая дополнительной работы при запуске, для чтения или записи, в сети применяются те же самые операции.

    21.2 Службы socket

    Программный интерфейс socket обеспечивает работу трех служб TCP/IP: потокового обмена, обмена датаграммами в UDP и пересылки необработанных данных непосредственно на уровень IP. Все эти службы показаны на рис. 21.1.

    Рис. 21.1. Программный интерфейс socket

    Вспомним, что API интерфейса socket разрабатывался не только для TCP/IP. Исходная цель заключалась в создании единого интерфейса для различных коммуникационных протоколов, в том числе и для XNS (Xerox Network Systems).

    Результат получился несколько странным. Например, некоторые вызовы socket содержат необязательные параметры, не имеющие никакого отношения к TCP/IP — они необходимы в других протоколах. Кроме того, иногда программист обязан указывать длину для параметров фиксированного размера, например для адресов IP версии 4. Смысл этого в том, что, хотя длина адреса в IP версии 4 всегда равна 4 байт, в программных интерфейсах для других протоколов могут использоваться адреса другой длины.

    21.3 Блокированные и неблокированные вызовы

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

    ■ Вызов с последующим ожиданием называется блокированным (blocking) или синхронным (synchronous).

    ■ Вызов с переходом на выполнение других операций называется неблокированным (nonblocking) или асинхронным (asynchronous).

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

    21.4 Вызовы socket

    Вызовы socket подготавливают сетевое взаимодействие путем создания блоков управления пересылкой (Transmission Control Block — TCB). В некоторых изданиях процесс создания TCB называется созданием socket. Вызов socket возвращает небольшое целое число, называемое дескриптором и используемое для идентификации соединения во всех последующих запросах.

    В TCB используется множество параметров. Перечисленные ниже параметры предоставляют информацию, необходимую для создания сеанса TCP:

    ■ Локальный IP-адрес

    ■ Локальный порт

    ■ Протокол (например, TCP или UDP)

    ■ Удаленный IP-адрес

    ■ Удаленный порт

    ■ Размер выходного буфера

    ■ Размер приемного буфера

    ■ Текущее состояние TCP

    ■ Усредненное время цикла пересылка-получение

    ■ Отклонение от усредненного времени цикла пересылка-получение

    ■ Текущее время тайм-аута повторной пересылки

    ■ Количество выполняемых повторных пересылок

    ■ Текущий размер окна отправки

    ■ Максимальный размер отправляемого сегмента

    ■ Порядковый номер последнего подтвержденного по ACK байта

    ■ Максимальный размер получаемого сегмента

    ■ Порядковый номер следующего отправляемого байта

    ■ Разрешение/запрещение отслеживания

    21.5 Программирование работы TCP socket

    Рассмотрим вызовы из программ к socket, используемые при взаимодействии с TCP. Для упрощения не будем указывать в вызовах параметры ввода/вывода и сконцентрируемся на более важных функциях и их взаимоотношениях. Детали формирования параметров описаны ниже.

    21.5.1 Модель сервера TCP

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

    Иногда клиенты появляются быстрее, чем их может обслужить главный процесс. Как поступить в этом случае? Стандартный механизм заключается в том, что при запуске главного процесса в TCP создается очередь, которая способна хранить несколько запросов на соединение. Запросы клиентов, которые нельзя обслужить сразу, помешаются в очередь и обрабатываются в порядке этой очереди. Предположим, что очередь заполнена до конца и поступает запрос от очередного клиента. В этом случае соединение с новым клиентом не будет создано.

    21.5.2 Пассивное открытие сервера TCP

    Сервер готовится к принятию запроса на соединение и пассивно ожидает обращения клиентов. При подготовке он выполняет ряд запросов:

    socket() Сервер идентифицирует тип связи (в данном случае TCP). Локальная система создает соответствующую структуру данных TCB для взаимодействия с сервером и возвращает дескриптор socket.
    bind() Сервер устанавливает локальный IP-адрес и порт, которыми он будет пользоваться. Вспомним, что хост может иметь несколько IP-адресов. Сервер может применять один IP-адрес или указать, что желает принимать соединения от любого локального IP-адреса. Он может запросить определенный порт или разрешить связывание запроса с одним из доступных свободных портов.
    listen() Сервер устанавливает длину очереди для клиентов.
    accept() Сервер готов принимать соединения от клиентов. Если очередь не пуста, принимается первый полученный клиентский запрос. Запрос accept() создает новый TCB, который будет использоваться для соединения этого клиента и возвращать новый дескриптор соединения серверу.

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

    21.5.3 Активное открытие клиента TCP

    Открытый клиент активно запрашивает соединение через два запроса:

    socket() Клиент идентифицирует тип связи (в данном случае TCP). Локальная система создает соответствующую структуру данных TCB для соединения и возвращает локальный дескриптор socket.
    connect() Клиент указывает IP-адрес и порт сервера. TCP попытается установить соединение с сервером.

    Если клиент желает явно определить применяемый далее локальный порт, он должен вызвать bind() перед выдачей запроса connect(). Если порт доступен, он присваивается клиенту.

    Если клиент запросил порт не через bind(), ему присваивается один из неиспользованных портов. Номер порта вводится в TCB.

    21.5.4 Другие запросы

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

    send() Запись буфера данных в socket. Как альтернативу можно применить write().
    sendv() Пересылка в socket последовательности буферов. Как альтернативу можно применить writev().
    recu() Получение буфера данных из socket либо из read().
    recvmsg() Получение последовательности буферов из socket либо из readv().

    Иногда программе нужна информация, хранящаяся в TCB:

    getsockopt() Чтение выбранной информации из TCB. Иногда система обеспечивает необязательные системные запросы ввода/вывода, которые позволяют читать различные части TCB.

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

    setsockopt() Устанавливает значения нескольких параметров TCB, например размеры приемного и выходного буферов, пересылку срочных данных в общем порядке оправки информации либо блокировку закрытия соединения до благополучной отправки всех данных.
    iocntl() Устанавливает ввод/вывод в socket в режим блокирования
    или fcntl() или снимает блокирование.

    На рис. 21.2 демонстрируется последовательность вызовов в типичном сеансе TCP. Вызовы socket(), bind() и listen() обрабатываются очень быстро, и на них немедленно возвращается ответ.

    Рис. 21.2. Последовательность программных вызовов в socket TCP

    Вызовы accept(), send() и recv() предполагаются в режиме блокирования (что является их обычным значением по умолчанию). Вызов send блокируется и при переполнении выходного буфера TCP. Вызовы write() и read() можно использовать вместо send() и recv().

    21.6 Серверная программа TCP

    Рассмотрим подробно пример серверной программы. Сервер предназначен для непрерывной работы. Он будет выполнять следующие действия:

    1. Запрашивать у socket создание главного TCB и возвращать значение дескриптора socket, который будет идентифицировать этот TCB в последующих вызовах.

    2. Вводить локальный адрес сервера socket в структуру данных программы.

    3. Запрашивать связывание, при котором в TCB копируется локальный адрес socket.

    4. Создавать очередь, которая сможет хранить сведения о пяти клиентах. Оставшиеся шаги повторяются многократно:

    5. Ожидать запросов от клиентов. Когда появляется клиент, создавать для него новый TCB на основе копии главного TCB и записи в него адреса socket клиента и других параметров.

    6. Создавать дочерний процесс для обслуживания клиента. Дочерний процесс будет наследовать новый TCB и обрабатывать все дальнейшие операции по связи с клиентом

    (ожидать сообщений от клиента, записывать их и завершать работу).

    Каждый шаг в программе объясняется в следующем разделе.

    /* tcpserv.c

     * Для запуска программ ввести "tcpserv". */


    /* Сначала включить набор стандартных заголовочных файлов. */

    #include <sys/types.h>

    #include <sys/socket.h>

    #include <stdio.h>

    #include <netinet/in.h>

    #include <netdb.h>

    #include <errno.h>


    main() {

     int sockMain, sockClient, length, child;

     struct sockaddr_in servAddr;


     /* 1. Создать главный блок управления пересылкой. */

     if ((sockMain = socket(AF_INET, SOCK_STREAM, 0)) < 0) {

      perror("Сервер не может открыть главный socket.");

      exit(1);

     }


     /* 2. Создать структуру данных для хранения локальных IP-адресов

      * и портов, которые будут использованы. Предполагается прием

      * клиентских соединений от любых локальных IP-адресов

      * (INADDR_ANY). Поскольку данный сервер не применяет

      * общеизвестный порт, установить port = 0. Это позволит

      * связать вызов с присвоением порта серверу и записать

      * порт в TCB. */

     bzero((char *)&servAddr, sizeof(servAddr));

     servAddr.sin_family = AF_INET;

     servAddr.sin_addr.s_addr = htonl(INADDR_ANY);

     servAddr.sin_port = 0;


     /* 3. Связать запрос, выбор номера порта и

      * запись его в TCB. */

     if (bind(sockMain, &servAddr, sizeof(servAddr))) {

      perror("Связывание сервера неудачно.");

      exit(1);

     }


     /* Чтобы увидеть номер порта, следует использовать

      * функцию getsockname(), чтобы скопировать порт в servAddr. */

     length = sizeof(servAddr);

     if (getsockname(sockMain, &servAddr, &length)) {

      perror("Вызов getsockname неудачен.");

      exit(1);

     }

     printf("СЕРВЕР: номер порта - %d\n", ntohs(servAddr.sin_port));


     /* 4. Создать очередь для хранения пяти клиентов. */

     listen(sockMain, 5);


     /* 5. Ожидать клиента. При разрешении возвратить новый

      * дескриптор socket, который должен использоваться клиентом. */

     for(;;) {

      if ((sockClient = accept(sockMain, 0, 0)) < 0) {

       perror ("Неверный socket для клиента.");

       exit(1);

      }


      /* 6. Создать дочерний процесс для обслуживания клиента. */

      if ((child = fork()) < 0)
    {

       perror("Ошибка создания дочернего процесса.");

       exit(1);

      }
    else if (child == 0) /* Это код для исполнения дочернего процесса. */

      {

       close(sockMain); /* Дочерний процесс неинтересен для sockMain.*/

       childWork(sockClient);

       close(sockClient);

       exit(0);

      }


      /* 7. Это родительский процесс. Его более не интересует

       * socket клиента, поскольку его обслуживание передано

       * дочернему процессу. Родительский процесс закрывает свой элемент для

       * socket клиента и переходит на цикл приема новых accept(). */

      close(sockClient);

     }

    }


    /* Дочерний процесс читает один поступивший буфер, распечатывает

     * сообщение и завершается. */

    #define BUFLEN 81

    int childWork(sockClient)

    int sockClient;

    {

     char buf[BUFLEN];

     int msgLength;


     /* 8. Опустошить буфер. Затем вывести recv для получения сообщения от клиента. */

     bzero(buf, BUFLEN);

     if ((msgLength = recv(sockClient, buf, BUFLEN, 0)) < 0) {

      perror("Плохое получение дочерним процессом.");

      exit(1);

     }

     printf ("SERVER: Socket для клиента - %d\n", sockClient);

     printf ("SERVER: Длина сообщения - %d\n", msgLength);

     printf ("SERVER: Сообщение: %s\n\n", buf);

    }

    21.6.1 Вызовы в серверной программе TCP

    1. sockMain = socket (AF_INET, SOCK_STREAM, 0); Вызов socket имеет форму:

    дескриптор_socket = socket(адрес_домена, тип_коммуникации, протокол)

    Напомним, что интерфейс socket может использоваться для других видов коммуникаций, например XNS. AF_INET указывает на семейство адресов Интернета. SOCK_STREAM запрашивает socket TCP. Эта переменная должна иметь значение SOCK_DGRAM, чтобы создать socket UDP, a SOCK_RAW служит для непосредственного обращения к IP.

    Не нужно явно определять никакую другую информацию протокола для TCP (или для UDP). Однако параметр protocol необходим для интерфейса с необработанными данными, а также для некоторых протоколов из других семейств, использующих socket.

    2. struct sockaddr_in servAddr;

    ...

    bzero((char *)&servAddr, sizeof(servAddr));

    servAddr.sin_family = AF_INET;

    servAddr.sin_addr.s_addr = htonl(INADDR_ANY);

    servAddr.sin_port = 0;

    Программная структура servAddr используется для хранения адресной информации сервера. Вызов bzero() инициализирует servAddr, помещая нули во все параметры. Первая переменная в структуре servAddr указывает, что остальная часть значений содержит данные семейства адресов Интернета.

    Следующая переменная хранит локальный IP-адрес сервера. Например, если сервер подключен к локальной сети Ethernet и к сети X.25, может потребоваться ограничить доступ клиентов через интерфейс Ethernet. В данной программе об этом можно не беспокоится. INADDR_ANY означает, что клиенты могут соединяться через любой интерфейс.

    Функция htonl() имеет полное название host-to-network-long. Она применяется для преобразования 32-разрядных целых чисел локального компьютера в формат Интернета для 32-разрядного адреса IP. Стандарты Интернета предполагают представление целых чисел с наиболее значимым байтом слева. Такой стиль именуется Big Endian (стиль "тупоконечников"). Некоторые компьютеры хранят данные, располагая слева менее значимые байты, т.е. в стиле Little Endian ("остроконечников"). Если локальный компьютер использует стиль Big Endian, htonl() не будет выполнять никакой работы.

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

    3. bind(sockMain, &servAddr, sizeof(servAddr)); getsockname(sockMain, &servAddr, &length);

    Вызов bind имеет форму:

    возвращаемый_код = bind(дескриптор_socket, адресная_структура, длина_адресной_структуры)

    Если адресная структура идентифицирует нужный порт, bind попытается получить его на сервере. Если переменная порта имеет значение 0, bind получит один из неиспользованных портов. Функция bind позволяет ввести номер порта и IP-адрес в TCB. Вызов getsockname имеет форму:

    возвращаемый_код = getsockname(дескриптор_socket, адресная_структура, длина_адресной_структуры)

    Мы запросили у bind выделение порта, но эта функция не сообщает нам, какой именно порт был предоставлен. Для выяснения этого нужно прочитать соответствующие данные из TCB. Функция getsockname() извлекает информацию из TCB и копирует ее в адресную структуру, где можно будет прочитать эти сведения. Номер порта извлекается и выводится следующим оператором:

    printf("SERVER: Номер порта %d\n", ntohs(servAddr.sin_port))
    ;

    Функция ntohs() имеет полное название network-to-host-short и служит для преобразования номера порта из порядка следования байт в сети в локальный порядок следования байт на хосте.

    4. listen(sockMain, 5);

    Вызов listen применяется для ориентированных на соединение серверов и имеет форму:

    возвращаемый_код = listen(дескриптор_socket, размер_очереди)

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

    5. sockClient = accept(sockMain, 0, 0);

    Вызов accept имеет форму:

    новым_дескриптор_socket = accept(дескриптор_socket,

    клиентская_адресная_структура, длина_клиентской_адресной_структуры)

    По умолчанию вызов блокируется до соединения клиента с сервером. Если указана переменная клиентская_адресная_структура, после соединения клиента в эту структуру будут введены IP-адрес и порт клиента. В этом примере программы не проверяются IP-адрес и номер порта клиента, а просто два последних поля параметра заполняются нулями.

    6. child = fork();

    close(sockMain);

    В языке С команда fork создает новый дочерний процесс, который наследует все дескрипторы ввода/вывода родительской программы, а также имеет доступ к sockMain и sockClient. Операционная система отслеживает количество процессов, имеющих доступ к socket.

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

    7. close(sockClient);

    Этот вызов выполняется из родительской части программы. Когда родительский процесс закрывает sockClient, дочерний процесс все еще имеет доступ к socket.

    8. msgLength = recv(sockClient, buf, BUFLEN, 0));

    close(sockClient);

    Вызов recv имеет форму:

    длина_сообщения = recv(дескриптор_socket, буфер, длина_буфера, флаги)

    По умолчанию вызов recv блокированный. Функции fcntl() или iocntl() позволяют изменить статус socket на неблокированный режим.

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

    21.7 Клиентская программа TCP

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

    tcpclient pltim.cs.yale.edu 1356 hello


    /* tcpclient.с

     * Перед запуском клиента должен быть запущен сервер. Производится

     * поиск порта сервера. Для запуска клиента нужно ввести:

      * tcpclient имя_хоста порт сообщение */

    #include <sys/types.h>

    #include <sys/socket.h>

    #include <netinet/in.h>

    #include <netdb.h>

    #include <stdio.h>

    #include <errno.h>


    main(argc, argv) /* Клиентская программа имеет входные аргументы. */

    int argc;

    char* argv[];

    {

     int sock;

     struct sockaddr_in servAddr;

     struct hostent *hp, *gethostbyname();

     /* Аргументами будут 0:имя_программы, 1:имя_хоста, 2:порт, 3:сообщение */

     if (argc < 4)
    {

      printf("ВВЕСТИ tcpclient имя_хоста порт сообщение\n");

      exit(1);

     }


     /* 1. Создание TCB. */

     if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {

      perror("He могу получить socket\n");

      exit(1);

     }


     /* 2. Заполнить поля адреса и порта сервера в servAddr.

      * Сначала заполнить нулями адресную структуру. Затем получить IP-адрес

      * для данного имени хоста и ввести его в адресную структуру.

      * Наконец ввести номер порта, взяв его из argv[2]. */

     bzero((char *)&servAddr, sizeof(servAddr));

     servAddr.sin_family = AF_INET;

     hp = gethostbyname (argv[1]);

     bcopy(hp->h_addr, &servAddr.sin_addr, hp->h_length);

     servAddr.sin_port = htons(atoi(argv[2]));


     /* 3. Соединиться с сервером. Вызывать bind не нужно.

      * Система присвоит свободный порт во время выполнения соединения. */

     if (connect (sock, &servAddr, sizeof(servAddr)) < 0) {

      perror("Клиент не может соединиться.\n");

      exit(1);

     }


     /* 4. Клиент анонсирует свою готовность послать сообщение.

      * Сообщение отправляется, и распечатывается последняя строка. */

     printf ("CLIENT: Готов к пересылке\n");

     if (send(sock, argv[3], strlen(argv[3]), 0) < 0) {

      (perror("Проблемы с пересылкой.\n");

      exit(1);

     }

     printf ("CLIENT: Пересылка завершена. Счастливо оставаться.\n");

     close(sock);

     exit(0);

    }

    21.7.1 Вызовы в клиентской программе TCP

    1. sock = socket(AF_INET, SOCK_STREAM, 0);

    Клиент создает блок управления пересылкой ("socket") так же, как это делал сервер.

    2. Сервер должен инициализировать адресную структуру для использования в bind.

    Эта структура содержит локальный IP-адрес и номер порта сервера. Клиент также инициализирует адресную структуру, хранящую те же сведения. Эта структура будет использоваться в вызове connect для указания точки назначения.

    Вызов bzero() помещает нули в servAddr — адресную структуру сервера. Еще раз мы трактуем семейство адресов как Интернет.

    Затем нужно преобразовать введенное пользователем имя хоста в IP-адрес. Это делает функция gethostbyname, которая возвращает указатель на структуру hostent, содержащую имя сервера и IP-адрес.

    Функция bcopy применяется для копирования IP-адреса (который находится в hp->h_addr) в servAddr.

    Второй введенный конечным пользователем аргумент определял порт сервера. Он читался как текстовая строка ASCII, поэтому ее сначала нужно преобразовать в целое число через atoi(), а затем изменить порядок следования байт через htons(). Наконец номер порта копируется в адресную переменную из servAddr.

    bzero((char *)&servAddr, sizeof(servAddr));

    servAddr.sin_family = AF_INET;

    hp = gethostbyname(argv[i]);

    bcopy(hp->h_addr, &servAddr.sin_addr, hp->h_length);

    servAddr.sin_port = htons(atoi(argv[2]));

    3. connect(sock, &servAddr, sizeof(servAddr)); Вызов connect имеет форму:

    connect(дескриптор_socket, адресная_структура, длина_адресной_структуры)

    Клиент откроет соединение с сервером, IP-адрес и порт которого хранятся в адресной структуре.

    4. send (sock, argv[3], strlen(argvs[3]), 0); Вызов send имеет форму:

    возвращаемый_код = send(дескриптор_socket, буфер, длина_буфера, флаги)

    Отметим, что введенный конечным пользователем третий аргумент (который появляется в программе как argv[3]) — это текст отправляемого сообщения. Обычно флаги используются для сообщения о срочных данных. В нашем случае параметры флагов установлены в 0.

    5. close(sock);

    Клиент выполняет close для закрытия соединения.

    21.8 Более простой сервер

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

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

    Ниже приведен листинг для более простого сервера. К этому серверу клиенты также могут обращаться через рассмотренную выше программу tcpclient.

    /* tcpsimp.c

    * Для запуска программ ввести "tcpsimp" */


    /* Сначала включить стандартные заголовочные файлы. */

    #include <sys/types.h>

    #include <sys/socket.h>

    #include <stdio.h>

    #include <netinet/in.h>

    #include <netdb.h>

    #include <errno.h>


    main() {

     int sockMain, sockClient, length, child;

     struct sockaddr_in servAddr;


     /* 1. Создать главный socket. */

     if ( (sockMain = socket (AF_INET, SOCK_STREAM, 0)) < 0) {

      perror("Сервер не может открыть главный socket.");

      exit(1);

     }


     /* 2. Ввести информацию в структуру данных, используемую для

      * хранения локального IP-адреса и порта, "sin" в именах

      * переменных — это сокращение от "socket internet". */

     bzero((char *)&servAddr, sizeof(servAddr));

     servAddr.sin_family = AF_INET;

     servAddr.sin_addr.s_addr = htonl(INADDR_ANY);

     servAddr.sin_port = 0;


     /* 3. Вызвать bind, которая запишет используемый номер порта

      * в servAddr. */

     if (bind(sockMain, &servAddr, sizeof(servAddr)) ) {

      perror("Вызов bind от сервера неудачен.");

      exit(1);

     }


     /* 4. Чтобы узнать номер порта, следует использовать функцию

      * getsockname() для копирования порта в servAddr. */

     length = sizeof(servAddr);

     if (getsockname(sockMain, &servAddr, &length)) {

      perror("Вызов getsockname неудачен.");

      exit(1);

     }

     printf ("SERVER: Номер порта %d\n", ntohs(servAddr.sin_port));


     /* 5. Установить очередь на пять клиентов.*/

     listen(sockMain, 5);


     /* 6. Ожидать поступления клиентов. Вызов accept возвратит

      * дескриптор нового socket, который следует использовать клиенту. */

     for(;;) {

      if ((sockClient = accept(sockMain, 0, 0)) < 0) {

       perror("Неверный socket клиента.");

       exit(1);

      }


      /* 7. Обслужить клиента и закрыть соединение с ним. */

      doTask(sockClient);

      close(sockClient);

     }

    }


    /* Читать один поступивший буфер, распечатать некоторую информацию

     * и завершить работу. */

    #define BUFLEN 81

    int doTask(sockClient)

    int sockClient;

    {

     char buf[BUFLEN];

     int msgLength;


     /* 8. Опустошение буфера и вызов recv

     * для получения сообщения от клиента. */

     bzero(buf, BUFLEN);

     if ((msgLength = recv(sockClient,buf, 80, 0)) < 0) {

      perror("Неверное получение." );

      exit(1);

     }

     printf("SERVER: Socket для клиента %d\n", sockClient);

     printf("SERVER: Длина сообщения %d\n", msgLength);

     printf("SERVER: Сообщение: %s\n\n", buf);

    }

    21.9 Интерфейс программирования socket для UDP

    Мы познакомились с наиболее общим интерфейсом программирования TCP. Теперь рассмотрим программирование сервера и клиента UDP. На рис. 21.3 показана схема диалога UDP между клиентом и сервером. Вызовы socket() и bind() быстро выполняются и немедленно возвращают ответ. Вызов recvfrom предполагает режим блокирования по умолчанию, который можно изменить на неблокированный (т.е. асинхронный) режим.

    Рис. 21.3. Типичные программные вызовы в socket UDP

    21.10 Программа сервера UDP

    Показанная ниже программа создает socket для UDP, связывает вызов с портом, а затем получает и распечатывает сообщения, которые посылаются на этот порт:

    /* udpserv.c

     * Для запуска программы ввести "udpserv".

     *

     * Сначала включить стандартные заголовочные файлы. */

    #include <sys/types.h>

    #include <sys/socket.h>

    #include <stdio.h>

    #include <netinet/in.h>

    #include <netdb.h>

    #include <errno.h>

    #define BUFLEN 81


    main() {

     int sockMain, addrLength, msgLength;

     struct sockaddr_in servAddr, clientAddr;

     char buf[BUFLEN];


     /* 1. Создать socket для UDP. */

     if ((sockMain = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {

      perror("Сервер не может открыть socket для UDP.");

      exit(1);

     }


     /* 2. Ввести информацию в структуру данных, используемую для хранения локальных

      * IP-адресов и порта. Возложить на bind получение свободных портов. */

     bzero((char *)&servAddr, sizeof(servAddr));

     servAddr.sin_family = AF_INET;

     servAddr.sin_addr.s_addr = htonl(INADDR_ANY);

     servAddr.sin_port = 0;


     /* 3. Вызвать bind, которая запишет номер используемого порта

      * в TCB. */

     if (bind(sockMain, &servAddr, sizeof(servAddr))) {

      perror("Вызов bind от сервера неудачен.");

      exit(1);

     }


     /* 4. Извлекаем номер порта и используем функцию

      * getsockname() для копирования порта в servAddr. */

     addrLength = sizeof(servAddr);

     if ( getsockname(sockMain, &servAddr, &addrLength)) {

      perror(Вызов getsockname неудачен.");

      exit(1);

     }

     printf("SERVER: Номер порта is %d\n", ntohs(servAddr.sin_port));


     /* 5. Бесконечный цикл ожидания сообщений от клиентов. */

     for (;;) {

      addrLength = sizeof(clientAddr);

      bzero(buf, BUFLEN);

      if ((msgLength = recvfrom(sockMain, buf, BUFLEN, 0, &clientAddr, &addrLength)) < 0) {

       perror("Плохой socket клиента.");

       exit(1);

      }


      /* 6. Распечатать клиентские IP-адрес и порт вместе с сообщением. */

      printf("SERVER: IP-адрес клиента: %s\n",

       inet_ntoa(clientAddr.sin_addr));

      printf("SERVER: Порт клиента: %d\n",

       ntohs(clientAddr.sin_port));

      printf("SERVER: Длина сообщения %d\n", msgLength);

      printf("SERVER: Сообщение: %s\n\n", buf);

     }

    }

    21.10.1 Вызовы в серверной программе UDP

    1. sockMain = socket(AF_NET, SOCK_DGRAM, 0);

    Семейство адресов — снова Интернет.

    2. bzero((char *)&servAddr, sizeof(servAddr));

    servAddr.sin_family = AF_INET;

    servAddr.sin_addr.s_addr = htonl(INADDR_ANY);

    servAddr.sin_port = 0;

    Вызовы инициализации адресной структуры сервера те же, что и в программе для TCP.

    3. bind(sockMain, &servAddr, sizeof(servAddr));

    Как и прежде, bind получает порт для сервера и записывает значения в TCB. Конечно, по сравнению с TCP, UDP содержит очень мало информации.

    4. getsockname(sockMain, &servAddr, &length);

    Использовать getsockname, чтобы извлечь присвоенный socket порт.

    5. msgLength = recvfrom(sockMain, buf, BUFLEN, 0, &clientAddr, &length);

    Вызов recvfrom имеет форму:

    recvfrom(дескриптор_socket, входной_буфер, длина_буфера, флаги, исходная_адресная_структура, указатель_на_длину_исходной_адресной_структуры)

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

    6. inet_ntoa(clientAddr.sin_addr);

    Этот вызов преобразует 32-разрядный адрес Интернета клиента в знакомую нам нотацию этого адреса с точками и десятичными значениями.

    21.11 Клиентская программа UDP

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

    udpclient plum.cs.yale.edu 2315 "Это сообщение."


    /* udpclient.с

     * Перед запуском клиента следует запустить сервер.

     * Далее нужно получить порт сервера.

     * Для запуска клиента ввести:

     * udpclient имя_хоста порт сообщение */

    #include <sys/types.h>

    #include <sys/socket.h>

    #include <netinet/in.h>

    #include <netdb.h>

    #include <stdio.h>

    #include <errno.h>


    main(argc, argv)

     int argc;

     char *argv[]; /* Это вводимые пользователем аргументы. */

                   /* argv[0] - имя программы. argv[1] указывает на имя хоста. */

                   /* argv[2] ссылается на порт, */

                   /* а argv [3] ссылается на текстовое сообщение. */

    {

     int sock;

     struct sockaddr_in, servAddr, clientAddr;

     struct hostent *hp, *gethostbyname();


     /* Должно быть четыре аргумента. */

     if (argc < 4) {

      printf ("ВВЕСТИ udpclient имя_хоста порт сообщение\n");

      exit(1);

     }


     /* 1. Создать socket для UDP. */

     if ((sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {

      perror("He получен socket\n");

      exit(1);

     }


     /* 2. Занести адрес и порт сервера в servAddr.

      * Сначала заполнить адресную структуру нулями.

      * Использовать функцию gethostbyname для получения имени хоста

      * и его IP-адреса. Затем скопировать IP-адрес

      * в servAddr функцией bcopy.

      * Наконец занести номер порта из argv[2]. */

     bzero((char *)&servAddr, sizeof(servAddr));

     servAddr.sin_family = AF_INET;

     hp = gethostbyname(argv[1]);

     bcopy(hp->h_addr, &servAddr.sin_addr, hp->h_length);

     servAddr.sin_port = htons(atoi(argv[2]));


     /* 3. Вызвать bind для получения порта UDP. Система

      * назначает свободный порт. */

     bzero((char *)&clientAddr, sizeof(clientAddr));

     clientAddr.sin_family = AF_INET;

     clientAddr.sin_addr.s_addr = htonl(INADDR_ANY);

     clientAddr.sin_port = 0;

     if (bind(sock, &clientAddr, sizeof(clientAddr)) < 0) {

      perror("Клиент не получил порт.\n");

      exit(1);

     }


     /* 4. Клиент анонсирует свою готовность к приему сообщений.

      * Он посылает сообщение и распечатывает последнюю строку. */

     printf ("CLIENT: Готов к пересылке\n");

     if (sendto(sock, argv[3], strlen(argv[3]), 0, &servAddr, sizeof(servAddr)) < 0) {

      perror "Проблема с sendto.\n");

      exit(1);

     }

     printf ("CLIENT: Пересылка закончена. Счастливо.\n");


     /* Закрытие socket */

     close(sock);

    }

    21.11.1 Запросы в клиентской программе UDP

    1. sock = socket(AF_INET, SOCK_DGRAM, 0); UDP клиента создает socket для UDP.

    2. bzero((char *)&servAddr, sizeof(servAddr));

    servAddr.sin_family = AF_INET;

    hp = gethostbyname(argv[1]);

    bcopy(hp->h_addr, &servAddr.sin_addr, hp->length);

    servAddr.sin_port = htons(atoi(argv[2]));

    Структура servAddr заполнена введенными конечным пользователем значениями, как это делалось и в клиенте для TCP.

    3. bind (sock, &clientAddr, sizeof(clientAddr)); Клиент вызывает bind для получения порта.

    4. sendto(sock, argv[3], strlen(argv[3]), 0, &servAddr, sizeof(servAddr));

    Вызов sendto имеет форму:

    sendto(дескриптор_socket, буфер, длина_буфера, флаги, адресная_структура_назначения, длина адресной_структуры_назначения)

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

    21.12 Дополнительная литература

    Любое техническое руководство по программированию в Unix содержит описания программных вызовов socket. В книге Ричарда Стивенса (Richard Stevens) Unix Network Programming детально обсуждается программирование socket. Руководства программиста TCP/IP для других операционных систем, описывают вызовы socket и часто содержат примеры типичных программ. Следует ознакомиться с подобным руководством, поскольку между операционными системами могут существовать различия.








    Главная | В избранное | Наш E-MAIL | Прислать материал | Нашёл ошибку | Наверх