Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

Интерфейс прикладного программирования Socket API, Часть 4: Датаграммы

Оригинал: "The Socket API, Part 4: Datagrams "
Автор: Pankaj Tanwar
Дата публикации: November 1, 2011
Перевод: Н.Ромоданов
Дата перевода: июль 2012 г.

Начало серии статей о Socket API

Давайте попробуем разработать клиентские программы для серверов, использующие протокол UDP — протокол, лежащий в основе некоторых важных сервисов, таких как DNS, NFS и т.д.

Протокол UDP, User Datagram Protocol — протокол пользовательских датаграмм, является протоколом без установления соединения. Это означает, что вы перед тем, как начать передачу данных (например, тройного подтверждения для TCP), соединение не устанавливаете; вы просто отправляете данные в виде датаграммы на требуемый адрес. Именно поэтому мы называем этот протокол ненадежным, поскольку мы не знаем или нам все равно, достигла ли датаграмма получателя или нет.

Код

Теперь, давайте, как мы обычно это делаем, перейдем к программе. Код в этой статье представляет собой модифицированную версию эхо-программ сервера/клиентов, взятых из книги UNIX Network Programming by Stevens et al (Сетевое программирование для UNIX, Стивенс и др.). Этот файл с именем udpserver.c:

#include <stdio.h>
#include <sys/socket.h>
#include <strings.h>
#include <netinet/in.h>
 
int main()
{
    int sfd, n;
    socklen_t len;
    char line[128];
    struct sockaddr_in saddr, caddr;
 
    sfd = socket(AF_INET, SOCK_DGRAM, 0);   
 
    bzero(&saddr, sizeof(saddr));
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = htonl(INADDR_ANY);
    saddr.sin_port = htons(2910);
 
    bind(sfd, (struct sockaddr *)&saddr, sizeof(saddr));
 
    printf("Server running\n");
    for(;;) {
        len=sizeof(caddr);
        n=recvfrom(sfd, line, 128, 0, (struct sockaddr *)&caddr, &len);
        sendto(sfd, line, n, 0, (struct sockaddr *)&caddr, len);
    }
 
    return 0;
}

Этот код почти похож на предыдущий; давайте рассмотрим различия. Во-первых, вызов socket(): SOCK_STREAM для TCP/SCTP заменяется на SOCK_DGRAM. Затем мы делали связывание bind() с адресом и портом, и сервер был готов к приему данных от клиента (которые в конкретном случае были просто текстом, который помещается в один пакет).

Далее вы видите, что вызов listen() отсутствует. Вместо этого вы с помощью команды recvform() ожидаете приема данных и когда вы их получаете, вы с помощью sendto() отправляете эти данные обратно клиенту в виде эхо-ответа.

Прежде, чем подробнее рассматривать эти функции, обратите внимание на то, что в вашем сервере для обслуживания каждого из запросов не используется функция fork() - запросы обслуживаются в итеративном режиме в бесконечном цикле. Это объясняется тем, что в протоколе TCP вы должны поддерживать связь с каждым клиентом, а для это требуются отдельные потоки; в протоколе UDP вы каждый раз просто отвечаете одному клиенту и вам не нужно на следующей итерации принимать данные от этого клиента.

Таким образом, вы видите, что на сервере нам нужен буфер, в котором данные, поступающие от клиентов, будут запоминаться в виде очереди с обслуживанием вида "первым пришел — первым обслужили". В большинстве серверов TCP используется параллельная обработка, а в серверах UDP - итеративная.

Отсылка и прием данных

Давайте рассмотрим новые функции, используемые для датаграмм; сначала функция recvfrom():

#include <sys/socket.h>
ssize_t recvfrom (int sockfd, void *buf, size_t nbytes,
    int flags, struct sockaddr *from, socklen_t *addrlen);

Эта функция блокирует сервер до тех пор, пока не получит некоторые данные в sockfd из адреса, указанного в аргументе *from и не запишет nbytes байтов в буфер *buf. Аргумент flags формируется с помощью операции ИЛИ (OR) над одним или несколькими значениями, но мы не используем его, так что он равен 0. Последний аргумент, *addrlen, является указателем на размер структуры, содержащей адрес сокета.

Теперь отправим данные с помощью sendto():

#include <sys/socket.h>
ssize_t sendto (int sockfd, const void *buf, size_t nbytes,
    int flags, const struct sockaddr *to, socklen_t addrlen);

Эта функция отправляет данные из буфера *buf в количестве, равном nbytes байтов, через сокет sockfd по адресу, который хранится в структуре *to. Ее размер составляет addrlen, и нужно отметить, что это не ссылка, как это было в recvfrom(). Аргумент flags точно такой же, как и в recvfrom(). Обе функции возвращают количество прочитанных/записанных байтов.

А код для клиентской программы udpclient.c выглядит следующим образом:

/**
 
#include <sys/socket.h>
 
ssize_t recvfrom (int sockfd, void *buff, size_t nbytes,
    int flags, struct sockaddr *from, socklen_t *addrlen);
 
ssize_t sendto (int sockfd, const void *buff, size_t nbytes,
    int flags, const struct sockaddr *to, socklen_t addrlen);
 
**/
 
 
#include <stdio.h>
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#define MAX 100
 
int main(int argc, char** argv)
{
    int sfd, n;
    socklen_t len;
    char sline[MAX], rline[MAX+1];
    struct sockaddr_in saddr;
 
    if(argc!=2) {
        printf("Usage: %s ipaddress\n", argv[0]);
        return -1;
    }
 
    sfd = socket(AF_INET, SOCK_DGRAM, 0);   
 
    bzero(&saddr, sizeof(saddr));
    saddr.sin_family = AF_INET;
    inet_pton(AF_INET, argv[1], &saddr.sin_addr);
    saddr.sin_port = htons(2910);
 
    printf("Client Running\n");
    while(fgets(sline, MAX, stdin)!=NULL) {
        len=sizeof(saddr);
        sendto(sfd, sline, strlen(sline), 0, (struct sockaddr *)&saddr, len);
        n=recvfrom(sfd, rline, MAX, 0, NULL, NULL);
        rline[n]=0;
        fputs(rline, stdout);
    }
 
    return 0;
}

Клиентская программа очень проста - она с помощью команды sendto() просто пересылает строку, прочитанную из стандартного ввода, на сервер и читает ответ от сервера с помощью команды recvfrom().

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

Это может быть проблемой в случае, если ваш сервер находится в нескольких сетях (с несколькими IP-адресами), и сервер не связывается с адресом так, как мы это обычно делали с помощью INADDR_ANY.

Рис.1: Запуск сервера

Рис.2: Работа клиентской программы

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

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

И здесь, я заканчиваю статью, подписавшись с моим обычным "FOSS — это круто!"

Продолжение серии статей о Socket API