Московский физико-технический институт
Опубликован: 16.09.2004 | Доступ: свободный | Студентов: 7771 / 1592 | Оценка: 4.44 / 4.28 | Длительность: 15:33:00
Лекция 10:

Семейство протоколов TCP/IP. Сокеты (sockets) в UNIX и основы работы с ними

< Лекция 9 || Лекция 10: 1234567891011

Системные вызовы sendto() и recvfrom()

Для отправки датаграмм применяется системный вызов sendto() . В число параметров этого вызова входят:

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

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

Для чтения принятых датаграмм и определения адреса получателя (при необходимости) служит системный вызов recvfrom() . В число параметров этого вызова входят:

  • Дескриптор сокета, через который принимается датаграмма.
  • Адрес области памяти, куда следует положить данные, составляющие содержательную часть датаграммы.
  • Максимальная длина, допустимая для датаграммы. Если количество данных датаграммы превышает заданную максимальную длину, то вызов по умолчанию рассматривает это как ошибочную ситуацию.
  • Флаги, определяющие поведение системного вызова (в нашем случае они будут полагаться равными 0 ).
  • Указатель на структуру, в которую при необходимости может быть занесен адрес сокета отправителя. Если этот адрес не требуется, то можно указать значение NULL.
  • Указатель на переменную, содержащую максимально возможную длину адреса отправителя. После возвращения из системного вызова в нее будет занесена фактическая длина структуры, содержащей адрес отправителя. Если предыдущий параметр имеет значение NULL, то и этот параметр может иметь значение NULL.

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

Системные вызовы sendto и recvfrom

Прототипы системных вызовов

#include <sys/types.h>
#include <sys/socket.h> 
int sendto(int sockd, char *buff, 
    int nbytes, int flags, 
    struct sockaddr *to, int addrlen);
int recvfrom(int sockd, char *buff, 
    int nbytes, int flags, 
    struct sockaddr *from, int *addrlen);

Описание системных вызовов

Системный вызов sendto предназначен для отправки датаграмм. Системный вызов recvfrom предназначен для чтения пришедших датаграмм и определения адреса отправителя. По умолчанию при отсутствии пришедших датаграмм вызов recvfrom блокируется до тех пор, пока не появится датаграмма. Вызов sendto может блокироваться при отсутствии места под датаграмму в сетевом буфере. Данное описание не является полным описанием системных вызовов, а предназначено только для использования в нашем курсе. За полной информацией обращайтесь к UNIX Manual.

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

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

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

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

Параметр addrlen для системного вызова sendto должен содержать фактическую длину структуры, адрес которой передается в качестве параметра to. Для системного вызова recvfrom параметр addrlen является ссылкой на переменную, в которую будет занесена фактическая длина структуры адреса сокета отправителя, если это определено параметром from. Заметим, что перед вызовом этот параметр должен указывать на переменную, содержащую максимально допустимое значение такой длины. Если параметр from имеет значение NULL, то и параметр addrlen может иметь значение NULL.

Параметр flags определяет режимы использования системных вызовов. Рассматривать его применение мы в данном курсе не будем, и поэтому берем значение этого параметра равным 0.

Возвращаемое значение

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

Определение IP-адресов для вычислительного комплекса

Для определения IP-адресов на компьютере можно воспользоваться утилитой /sbin/ifconfig. Эта утилита выдает всю информацию о сетевых интерфейсах, сконфигурированных в вычислительной системе. Пример выдачи утилиты показан ниже:

eth0     Link encap:Ethernet HWaddr 00:90:27:A7:1B:FE 
    inet addr:192.168.253.12 Bcast:192.168.253.255 
    Mask:255.255.255.0
    UP BROADCAST NOTRAILERS RUNNING MULTICAST MTU:1500 
    Metric:1 RX packets:122556059 errors:0 dropped:0 
    overruns:0 frame:0 TX packets:116085111 errors:0 
    dropped:0 overruns:0 carrier:0 collisions:0 
    txqueuelen:100 RX bytes:2240402748 (2136.6 Mb) 
    TX bytes:3057496950 (2915.8 Mb) Interrupt:10 
    Base address:0x1000 
lo     Link encap:Local Loopback 
    inet addr:127.0.0.1 Mask:255.0.0.0
    UP LOOPBACK RUNNING MTU:16436 Metric:1
    RX packets:403 errors:0 dropped:0 overruns:0 frame:0
    TX packets:403 errors:0 dropped:0 overruns:0 
    carrier:0 collisions:0 txqueuelen:0 
    RX bytes:39932 (38.9 Kb) TX bytes:39932 (38.9 Kb)
Пример 15-16.0. Информация о сетевых интерфейсах.

Сетевой интерфейс eth0 использует протокол Ethernet. Физический 48-битовый адрес, зашитый в сетевой карте, – 00:90:27:A7:1B:FE. Его IP-адрес192.168.253.12.

Сетевой интерфейс lo не относится ни к какой сетевой карте. Это так называемый локальный интерфейс, который через общую память эмулирует работу сетевой карты для взаимодействия процессов, находящихся на одной машине по полным сетевым адресам. Наличие этого интерфейса позволяет отлаживать сетевые программы на машинах, не имеющих сетевых карт. Его IP-адрес обычно одинаков на всех компьютерах – 127.0.0.1.

Пример программы UDP-клиента

Рассмотрим, наконец, простой пример программы 15–16-1.с. Эта программа является UDP-клиентом для стандартного системного сервиса echo. Стандартный сервис принимает от клиента текстовую датаграмму и, не изменяя ее, отправляет обратно. За сервисом зарезервирован номер порта 7. Для правильного запуска программы необходимо указать символьный IP-адрес сетевого интерфейса компьютера, к сервису которого нужно обратиться, в качестве аргумента командной строки, например:

a.out 192.168.253.12

Ниже следует текст программы

/* Простой пример UDP клиента для сервиса echo */
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
int main(int argc, char **argv)
{
    int sockfd; /* Дескриптор сокета */
    int n, len; /* Переменные для различных длин и 
        количества символов */
    char sendline[1000], recvline[1000]; /* Массивы 
        для отсылаемой и принятой строки */
    struct sockaddr_in servaddr, cliaddr; /* Структуры для
    адресов сервера и клиента */
    /* Сначала проверяем наличие второго аргумента в 
    командной строке. При его отсутствии ругаемся и прекращаем 
    работу */
    if(argc != 2){
        printf("Usage: a.out <IP address>\n");
        exit(1);
    }
    /* Создаем UDP сокет */
    if((sockfd = socket(PF_INET, SOCK_DGRAM, 0)) < 0){
        perror(NULL); /* Печатаем сообщение об ошибке */
        exit(1);
    }
    /* Заполняем структуру для адреса клиента: семейство 
    протоколов TCP/IP, сетевой интерфейс – любой, номер порта 
    по усмотрению операционной системы. Поскольку в структуре
    содержится дополнительное не нужное нам поле, которое 
    должно     быть нулевым, перед заполнением обнуляем ее всю */
    bzero(&cliaddr, sizeof(cliaddr));
    cliaddr.sin_family = AF_INET;
    cliaddr.sin_port = htons(0);
    cliaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    /* Настраиваем адрес сокета */
    if(bind(sockfd, (struct sockaddr *) &cliaddr, 
    sizeof(cliaddr)) < 0){
        perror(NULL);
        close(sockfd); /* По окончании работы закрываем 
        дескриптор сокета */
        exit(1);
    }
    /* Заполняем структуру для адреса сервера: 
семейство протоколов TCP/IP, сетевой интерфейс – из аргумента
командной строки, номер порта 7. Поскольку в 
структуре содержится дополнительное не нужное нам
поле, которое должно быть нулевым, перед заполнением
обнуляем ее всю */
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(7);
    if(inet_aton(argv[1], &servaddr.sin_addr) == 0){
        printf("Invalid IP address\n");
        close(sockfd); /* По окончании работы закрываем 
            дескриптор сокета */
        exit(1);
    }
    /* Вводим строку, которую отошлем серверу */
    printf("String => ");
    fgets(sendline, 1000, stdin);
    /* Отсылаем датаграмму */
    if(sendto(sockfd, sendline, strlen(sendline)+1, 
    0, (struct sockaddr *) &servaddr, 
    sizeof(servaddr)) < 0){
    perror(NULL);
    close(sockfd);
    exit(1);
    }
    /* Ожидаем ответа и читаем его. Максимальная 
допустимая длина датаграммы – 1000 символов, 
адрес отправителя нам не нужен */
    if((n = recvfrom(sockfd, recvline, 1000, 0, 
    (struct sockaddr *) NULL, NULL)) < 0){
        perror(NULL);
        close(sockfd);
        exit(1);
    }
    /* Печатаем пришедший ответ и закрываем сокет */
    printf("%s\n", recvline);
    close(sockfd);
    return 0;
}
Листинг 15-16.1. Программа 15–16-1.c . Простой пример UDP клиента для сервиса echo.

Наберите и откомпилируйте программу. Перед запуском "узнайте у своего системного администратора", запущен ли в системе стандартный UDP-сервис echo и если нет, попросите стартовать его. Запустите программу с запросом к сервису своего компьютера, к сервисам других компьютеров. Если в качестве IP-адреса указать несуществующий адрес, адрес выключенной машины или машины, на которой не работает сервис echo, то программа бесконечно блокируется в вызове recvfrom() , ожидая ответа. Протокол UDP не является надежным протоколом. Если датаграмму доставить по назначению не удалось, то отправитель никогда об этом не узнает!

Пример программы UDP-сервера

Поскольку UDP-сервер использует те же самые системные вызовы, что и UDP-клиент, мы можем сразу приступить к рассмотрению примера UDP-сервера (программа 15–16-2.с ) для сервиса echo.

/* Простой пример UDP-сервера для сервиса echo */
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
int main()
{
    int sockfd; /* Дескриптор сокета */
    int clilen, n; /* Переменные для различных длин 
        и количества символов */
    char line[1000]; /* Массив для принятой и 
        отсылаемой строки */
    struct sockaddr_in servaddr, cliaddr; /* Структуры 
        для адресов сервера и клиента */
    /* Заполняем структуру для адреса сервера: семейство
    протоколов TCP/IP, сетевой интерфейс – любой, номер порта 
    51000. Поскольку в структуре содержится дополнительное не
    нужное нам поле, которое должно быть нулевым, перед 
    заполнением обнуляем ее всю */
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(51000);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    /* Создаем UDP сокет */
    if((sockfd = socket(PF_INET, SOCK_DGRAM, 0)) < 0){
        perror(NULL); /* Печатаем сообщение об ошибке */
        exit(1);
    }
    /* Настраиваем адрес сокета */
    if(bind(sockfd, (struct sockaddr *) &servaddr, 
    sizeof(servaddr)) < 0){
        perror(NULL);
        close(sockfd);
        exit(1);
    }
    while(1) {
        /* Основной цикл обслуживания*/
        /* В переменную clilen заносим максимальную длину
        для ожидаемого адреса клиента */
        clilen = sizeof(cliaddr);
        /* Ожидаем прихода запроса от клиента и читаем его. 
        Максимальная допустимая длина датаграммы – 999 
        символов, адрес отправителя помещаем в структуру 
        cliaddr, его реальная длина будет занесена в 
        переменную clilen */
        if((n = recvfrom(sockfd, line, 999, 0, 
        (struct sockaddr *) &cliaddr, &clilen)) < 0){
            perror(NULL);
            close(sockfd);
            exit(1);
        }
        /* Печатаем принятый текст на экране */
        printf("%s\n", line);
        /* Принятый текст отправляем обратно по адресу 
        отправителя */
        if(sendto(sockfd, line, strlen(line), 0, 
        (struct sockaddr *) &cliaddr, clilen) < 0){
            perror(NULL);
            close(sockfd);
            exit(1);
        } /* Уходим ожидать новую датаграмму*/
    }
    return 0;
}
Листинг 15-16.2. Программа 15–16-2.c . Простой пример UDP-сервера для сервиса echo.

Наберите и откомпилируйте программу. Запустите ее на выполнение. Модифицируйте текст программы UDP-клиента (программа 15–16-1.c ), заменив номер порта с 7 на 51000. Запустите клиента с другого виртуального терминала или с другого компьютера и убедитесь, что клиент и сервер взаимодействуют корректно.

< Лекция 9 || Лекция 10: 1234567891011
лия логовина
лия логовина

организовать двустороннюю поочередную связь процесса-родителя и процесса-ребенка через pipe, используя для синхронизации сигналы sigusr1 и sigusr2.

Макар Оганесов
Макар Оганесов