использование функции Unix getaddrinfo C для запуска установки сервера - PullRequest
0 голосов
/ 29 декабря 2018

Я создаю клиент-серверное приложение на C с исходным кодом, взятым из книги «Расширенное программирование в среде Unix».

На сервере выполняется следующее:

struct addrinfo hint;
memset(&hint, 0, sizeof(hint));
hint.ai_flags = AI_CANONNAME;
hint.ai_socktype = SOCK_STREAM;
hint.ai_addr = NULL;
hint.ai_next = NULL;
....
if ((n = sysconf(_SC_HOST_NAME_MAX))<0)
{
    n = HOST_NAME_MAX;
}
if((host = malloc(n)) == NULL)
{
    printf("malloc error\n");
    exit(1);
}
if (gethostname(host, n)<0)
{
    printf("gethostname error\n");
    exit(1);
}
...
if((err = getaddrinfo(host, "ruptime", &hint, &ailist))!=0)
{
    syslog(LOG_ERR, "ruptimed: getaddrinfo error %s", gai_strerror(err));
    exit(1);
}
for (aip = ailist; aip!=NULL; aip = aip->ai_next)
{
    if ((sockfd = initserver(SOCK_STREAM, aip->ai_addr, aip->ai_addrlen, QLEN))>=0)
    {
        //printf("starting to serve\n");
        serve(sockfd);
        exit(0);
    }
}

Насколько я понял, функция getaddrinfo используется для просмотра на хосте структур адреса адреса сокета, на которых запущена служба с именем ruptime и типа SOCK_STREAM.

Хотя она не была указана в книгеДля работы мне пришлось запустить новую запись в файле /etc/services/ с неиспользуемым портом и указанным именем ruptime:

ruptime         49152/tcp #ruptime Unix System Programming
ruptime         49152/udp #ruptime Unix System Programming

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

Однако в документации сказано:

Если в hints.ai_flags указан флаг AI_PASSIVE, а узел NULL, то возвращенные адреса сокетов будут подходящими для привязки(2) использование сокета, который будет принимать (2) соединения.Возвращенный адрес сокета будет содержать «подстановочный адрес» (INADDR_ANY для адресов IPv4, IN6ADDR_ANY_INIT для адресов IPv6).Подстановочный адрес используется приложениями (обычно серверами), которые намереваются принимать соединения по любому из сетевых адресов хоста.

Итак, отсюда и из других обсуждений SO что-то вроде:

hint.ai_flags |= AI_PASSIVE
...
getaddrinfo(NULL, myserviceport, &hint, &aihint)

кажется более подходящим.

В чем разница между этими двумя методами?Второй ищет также SOCK_DGM?Есть ли причина, по которой в книге был выбран первый метод?Вторым способом, так как я указываю порт в коде, это позволяет избежать добавления новой записи в /etc/services/?

Еще один вопрос.Клиенту я должен был передать имя хоста.Я думал, что адрес loopback (клиент и сервер работают на одной машине) будет в порядке.Вместо этого имя хоста выглядит как ./client MBPdiPippo.lan.Что определяет тот факт, что соединение может быть создано с именем хоста, но не с адресом обратной связи?Неужели я передаю host в качестве первого параметра на getaddrinfo на сервере?

ПОЛНЫЙ КОД

server.c

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h> //_SC_HOST_NAME_MAX
#include<string.h>
#include<netdb.h> //Here are defined AF_INET and the others of the family
#include<syslog.h> //LOG_ERR
#include<errno.h> //errno
#include <sys/types.h>

#include"utilities.h"
#include "error.h"

#define BUFLEN 128
#define QLEN 10

#ifndef HOST_NAME_MAX
#define HOST_NAME_MAX 156
#endif

int initserver(int type, const struct sockaddr *addr, socklen_t alen, int qlen);
void serve(int sockfd);

int main(int argc, char* argv[])
{
    printf("entered main\n");
    struct addrinfo *ailist, *aip, hint;
    int sockfd, err, n;
    char *host;
    if (argc != 1)
    {
        printf("usage: ruptimed\n");
        exit(1);
    }
    if ((n=sysconf(_SC_HOST_NAME_MAX))<0)
    {
        n = HOST_NAME_MAX;
    }
    if((host = malloc(n)) == NULL)
    {
        printf("malloc error\n");
        exit(1);
    }
    if (gethostname(host, n)<0)
    {
        printf("gethostname error\n");
        exit(1);
    }
    printf("host: %s\n", host);
    printf("Daemonizing\n");
    int res = daemonize("ruptimed");
    printf("%d\n", res);
    printf("Daemonized\n");
    memset(&hint, 0, sizeof(hint)); //set to 0 all bytes
    printf("hint initialized\n");
    hint.ai_flags = AI_CANONNAME;
    hint.ai_socktype = SOCK_STREAM;
    hint.ai_canonname = NULL;
    hint.ai_addr = NULL;
    hint.ai_next = NULL;
    printf("getting addresses\n");
    if((err = getaddrinfo(host, "ruptime", &hint, &ailist))!=0)
    {
        printf("error %s\n", gai_strerror(err));
        syslog(LOG_ERR, "ruptimed: getaddrinfo error %s", gai_strerror(err));
        exit(1);
    }
    printf("Got addresses\n");
    for (aip = ailist; aip!=NULL; aip = aip->ai_next)
    {
        if ((sockfd = initserver(SOCK_STREAM, aip->ai_addr, aip->ai_addrlen, QLEN))>=0)
        {
            printf("starting to serve\n");
            serve(sockfd);
            exit(0);
        }
    }
    exit(1);
}

void serve(int sockfd)
{
    int clfd;
    FILE *fp;
    char buf[BUFLEN];
    set_cloexec(sockfd);
    for(;;)
    {
        /*After listen, the socket can receive connect requests. accept
        retrieves a connect request and converts it into a connection.
        The file returned by accept is a socket descriptor connected to the client that
        called connect, haing the same coket type and family type. The original
        soket remains available to receive otherconneion requests. If we don't care
        about client's identity we can set the second (struct sockaddr *addr)
        and third parameter (socklen_t *len) to NULL*/
        if((clfd = accept(sockfd, NULL, NULL))<0)
        {
            /*This generates a log mesage.
            syslog(int priority, const char *fformat,...)
            priority is a combination of facility and level. Levels are ordered from highest to lowest:
            LOG_EMERG: emergency system unusable
            LOG_ALERT: condiotin that must be fied immediately
            LOG_CRIT: critical condition
            LOG_ERR: error condition
            LOG_WARNING
            LOG_NOTICE
            LOG_INFO
            LOG_DEBUG
            format and other arguements are passed to vsprintf function forf formatting.*/
            syslog(LOG_ERR, "ruptimed: accept error: %s", strerror(errno));
            exit(1);
        }
        /* set the FD_CLOEXEC file descriptor flag */
        /*it causes the file descriptor to be automatically and atomically closed
         when any of the exec family function is called*/
        set_cloexec(clfd);
        /**pg. 542 Since a common operation is to create a pipe to another process
        to either read its output or write its input Stdio has provided popen and
        pclose: popen creates pipe, close the unused ends of the pipe,
        forks a child and call exec to execute cmdstr and
        returns a file pointer (connected to std output if "r", to stdin if "w").
        pclose closes the stream, waits for the command to terminate*/
        if ((fp = popen("/usr/bin/uptime", "r")) == NULL)
        {
            /*sprintf copy the string passed as second parameter inside buf*/
            sprintf(buf, "error: %s\n", strerror(errno));
            /*pag 610. send is similar to write. send(int sockfd, const void *buf, size_t nbytes, it flags)*/
            send(clfd, buf, strlen(buf),0);
        }
        else
        {
            /*get data from the pipe that reads created to exec /usr/bin/uptime */
            while(fgets(buf, BUFLEN, fp)!=NULL)
            {
                /* clfd is returned by accept and it is a socket descriptor
                connected to the client that called connect*/
                send(clfd, buf, strlen(buf), 0);
            }
            /*see popen pag. 542*/
            pclose(fp);
        }
        close(clfd);
    }
}


int initserver(int type, const struct sockaddr *addr, socklen_t alen, int qlen)
{
    int fd, err;
    int reuse = 1;
    if ((fd = socket(addr->sa_family, type, 0))<0)
    {
        return (-1);
    }
    if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(int))<0)
    {
        goto errout;
    }
    if(bind(fd, addr, alen)<0)
    {
        goto errout;
    }
    if (type == SOCK_STREAM || type == SOCK_SEQPACKET)
    {
        if(listen(fd, qlen)<0)
        {
            goto errout;
        }
    }
    return fd;
    errout:
        err = errno;
        close (fd);
        errno = err;
        return(-1);
}

utilities.c: содержит функции demonize и setcloexec.В функции daemonize я не закрывал файловые дескрипторы для отладки.

#include "utilities.h"
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <syslog.h>
#include <sys/time.h>//getrlimit
#include <sys/resource.h>//getrlimit
#include <signal.h> //sigempyset , asigcation (umask?)
#include <sys/resource.h>
#include <fcntl.h> //O_RDWR
#include <stdarg.h>

#include "error.h"
int daemonize(const char *cmd)
{
    int fd0, fd1, fd2;
    unsigned int i;
    pid_t pid;
    struct rlimit       rl;
    struct sigaction    sa;
    /* *Clear file creation mask.*/
    umask(0);
    /* *Get maximum number of file descriptors. */
    if (getrlimit(RLIMIT_NOFILE, &rl) < 0)
    {
        err_quit("%s: can’t get file limit", cmd);
    }
    /* *Become a session leader to lose controlling TTY. */
    if ((pid = fork()) < 0)
    {
        err_quit("%s: can’t fork", cmd);
    }
    else if (pid != 0) /* parent */
    {
        exit(0); //the parent will exit
    }
    setsid();
    /* *Ensure future opens won’t allocate controlling TTYs. */
    sa.sa_handler = SIG_IGN;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    if (sigaction(SIGHUP, &sa, NULL) < 0)
    {
        err_quit("%s: can’t ignore SIGHUP", cmd);
    }
    if ((pid = fork()) < 0)
    {
        err_quit("%s: can’t fork", cmd);
    }
    else if (pid != 0) /* parent */
    {
        exit(0);
    }
    /*
    *Change the current working directory to the root so
    * we won’t prevent file systems from being unmounted.
    */
    if (chdir("/") < 0)
    {
        err_quit("%s: can’t change directory to /", cmd);
    }
    /* Close all open file descriptors. */
    if (rl.rlim_max == RLIM_INFINITY)
    {
        rl.rlim_max = 1024;
    }
    printf("closing file descriptors\n");
    /*for (i = 0; i < rl.rlim_max; i++)
    {
        close(i);
    }*/
    /* *Attach file descriptors 0, 1, and 2 to /dev/null.*/
    //printf not working
    /*printf("closed all file descriptors for daemonizing\n");*/
    /*fd0 = open("/dev/null", O_RDWR);
    fd1 = dup(0);
    fd2 = dup(0);*/
    /* *Initialize the log file. Daemons do not have a controlling terminal so
    they can't write to stderror. We don't want them to write to the console device
    because on many workstations the control device runs a windowing system. They can't
    write on separate files either. A central daemon error-logging facility is required.
    This is the BSD. 3 ways to generate log messages:
    1) kernel routines call the log function. These messages can be read from /dev/klog
    2) Most user processes (daemons) call syslog to generate log messages. This causes
    messages to be sent to the UNIX domain datagram socket /dev/log
    3) A user process on this host or on other host connected to this with TCP/ID
    can send log messages to UDP port 514. Explicit network programmin is required
    (it is not managed by syslog.
    The syslogd daemon reads al three of log messages.

    openlog is optional since if not called, syslog calls it. Also closelog is optional
    openlog(const char *ident, int option, int facility)
    It lets us specify ident that is added to each logmessage. option is a bitmask:
        LOG_CONS tells that if the log message can't be sent to syslogd via UNIX
        domain datagram, the message is written to the console instead.
    facility lets the configuration file specify that messages from different
    facilities are to be handled differently. It can be specified also in the 'priority'
    argument of syslog. LOG_DAEMON is for system deamons
    */
    /*
    openlog(cmd, LOG_CONS, LOG_DAEMON);
    if (fd0 != 0 || fd1 != 1 || fd2 != 2)
    {*/
        /*This generates a log mesage.
        syslog(int priority, const char *fformat,...)
        priority is a combination of facility and level. Levels are ordered from highest to lowest:
        LOG_EMERG: emergency system unusable
        LOG_ALERT: condiotin that must be fied immediately
        LOG_CRIT: critical condition
        LOG_ERR: error condition
        LOG_WARNING
        LOG_NOTICE
        LOG_INFO
        LOG_DEBUG

        format and other arguements are passed to vsprintf function forf formatting.*/
        /*syslog(LOG_ERR, "unexpected file descriptors %d %d %d", fd0, fd1, fd2);
        exit(1);
    }*/
    return 0;
}

/*The function set the FD_CLOEXEC flag of the file descriptor already open that
is passed to as parameter. FD_CLOEXEC causes the file descriptor to be
automatically and atomically closed when any of the exec family function is
called*/
int set_cloexec(int fd)
{
    int val;
    /* retrieve the flags of the file descriptor */
    if((val = fcntl(fd, F_GETFD, 0))<0)
    {
        return -1;
    }
    /* set the FD_CLOEXEC file descriptor flag */
    /*it causes the file descriptor to be automatically and atomically closed
     when any of the exec family function is called*/
    val |= FD_CLOEXEC;
    return (fcntl(fd, F_SETFD, val));
}

функции ошибок Я использовал

/* Fatal error unrelated to a system call.
* Print a message and terminate*/
void err_quit (const char *fmt, ...)
{
    va_list ap;
    va_start (ap, fmt);
    err_doit (0, 0, fmt, ap);
    va_end (ap);
    exit(1);
}

/*Print a message and return to caller.
*Caller specifies "errnoflag"*/
static void err_doit(int errnoflag, int error, const char *fmt, va_list ap)
{
    char buf [MAXLINE];
    vsnprintf (buf, MAXLINE-1, fmt, ap);
    if (errnoflag)
    {
        snprintf (buf+strlen(buf), MAXLINE-strlen(buf)-1, ": %s",
            strerror (error));
    }
    strcat(buf, "\n");
    fflush(stdout); /*in case stdout and stderr are the same*/
    fputs (buf, stderr);
    fflush(NULL); /* flushes all stdio output streams*/
}

1 Ответ

0 голосов
/ 01 января 2019

Во-первых, придурок.Код getaddrinfo() должен быть включен в функцию initserver(), а связанный список структур сокетов должен быть освобожден (используя freeaddrinfo()) после цикла.Это делает код намного более понятным;вы хотите держать тесно связанные реализации близко друг к другу.

В чем разница между этими двумя методами?

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

На практике это означает, что если дополнительные сетевые интерфейсы станут доступными во время выполнения, ядро ​​будет учитывать их при маршрутизации пакетов в / изсокеты, связанные с подстановочным адресом.

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

Лично я бы переписал функцию OP initServer(), чтобы она выглядела примерно так:

enum {
    /* TCP=1, UDP=2, IPv4=4, IPv6=8 */
    SERVER_TCPv4 = 5,   /* IPv4 | TCP */
    SERVER_UDPv4 = 6,   /* IPv4 | UDP */
    SERVER_TCPv6 = 9,   /* IPv6 | TCP */
    SERVER_UDPv6 = 10,  /* IPv6 | UDP */
    SERVER_TCP   = 13,  /* Any  | TCP */
    SERVER_UDP   = 14   /* Any  | UDP */
};

int initServer(const char *host, const char *port,
               const int type, const int backlog)
{
    struct addrinfo  hints, *list, *curr;
    const char      *node;
    int              family, socktype, result, fd;

    if (!host || !*host || !strcmp(host, "*"))
        node = NULL;
    else
        node = host;

    switch (type) {
    case SERVER_TCPv4: family = AF_INET;   socktype = SOCK_STREAM; break;
    case SERVER_TCPv6: family = AF_INET6;  socktype = SOCK_STREAM; break;
    case SERVER_TCP:   family = AF_UNSPEC; socktype = SOCK_STREAM; break;
    case SERVER_UDPv4: family = AF_INET;   socktype = SOCK_DGRAM;  break;
    case SERVER_UDPv6: family = AF_INET6;  socktype = SOCK_DGRAM;  break;
    case SERVER_UDP:   family = AF_UNSPEC; socktype = SOCK_DGRAM;  break;
    default:
        fprintf(stderr, "initServer(): Invalid server type.\n");
        return -1;
    }
    memset(&hints, 0, sizeof hints);
    hints.ai_flags = AI_PASSIVE;
    hints.ai_family = family;
    hints.ai_socktype = socktype;
    hints.ai_protocol = 0;
    hints.ai_canonname = NULL;
    hints.ai_addr = NULL;
    hints.ai_next = NULL;
    result = getaddrinfo(node, port, &hints, &list);
    if (result) {
        /* Fail. Output error message to standard error. */
        fprintf(stderr, "initServer(): %s.\n", gai_strerror(result));
        return -1;
    }

    fd = -1;
    for (curr = list; curr != NULL; curr = curr->ai_next) {
        int  reuse = 1;

        fd = socket(curr->ai_family, curr->ai_socktype, curr->ai_protocol);
        if (fd == -1)
            continue;

        if (bind(fd, curr->ai_addr, curr->ai_addrlen) == -1) {
            close(fd);
            fd = -1;
            continue;
        }

        if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR,
                        &reuse, sizeof (int)) == -1) {
            close(fd);
            fd = -1;
            continue;
        }

        if (listen(fd, backlog) == -1) {
            close(fd);
            fd = -1;
            continue;
        }

        break;
    }
    freeaddrinfo(list);
    if (fd == -1) {
        fprintf(stderr, "initServer(): Cannot bind to a valid socket.\n");
        return -1;
    }

    return fd;
}

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

Таким образом, вы можете прочитать host и port из файла конфигурации.Если host равно "*", пусто или NULL, функция попытается привязаться к подстановочному адресу.(Кстати, это должно быть по умолчанию; если администратор сервера хочет ограничиться определенным интерфейсом, он может предоставить либо IP-адрес, либо имя хоста, соответствующее этому интерфейсу.)

Аналогично,системный администратор может использовать файл конфигурации для указания port в качестве любой строки, определенной в базе данных services (getent services), или в виде строки десятичного числа;в случае с OP оба "49152" и "ruptime" будут работать.

Поскольку я указываю порт в коде, позволяет ли он добавить новую запись в / etc / services?/?

База данных services (запустите getent services, чтобы увидеть ее на своем компьютере) содержит только сопоставление между именами служб и номерами портов для TCP (SOCK_STREAM) и/ или протоколы UDP (SOCK_DGRAM).

Единственный способ избежать добавления записи ruptime 49152/tcp в базу данных ваших служб - указать порт в виде строки десятичного числа, вместо этого "49152"имени "ruptime".Это влияет как на серверы, так и на клиентов.(То есть, даже если ваш сервер знает, что ruptime - это порт 49152 для сокетов TCP, клиенты не узнают об этом, если они не имеют его в своей базе данных сервисов.)

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

Iсам использовал бы номер порта.

Клиенту, работающему на той же машине, я должен был передать имя хоста.Я думал, что адрес обратной связи будет работать.Что определяет тот факт, что соединение может быть создано с именем хоста, но не с адресом обратной связи?Это то, что я передаю хост в качестве первого параметра для getaddrinfo на сервере?

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

Если вы привязываетесь к определенному имени хоста, он будет отвечать только на запросы к этому конкретному сетевому интерфейсу.

(Это делается ядром ОС и является частью того, как сетевые пакеты направляются в пространство пользователяприложения.)

Это также означает, что «правильная» интернет-служба, которая связывается с конкретными именами хостов (а не с подстановочным адресом), должна действительно иметь возможность прослушивать входящие соединения через несколько сокетов, а не толькоодин.Это может не быть абсолютно необходимым или даже необходимым в большинстве случаев использования, но я могу вам сказать, что это действительно пригодится, когда служба запускается на машине, расположенной в нескольких разных сетях, и вы хотите предоставитьСервис только для некоторых из них.К счастью, вы можете отключить блокировку прослушивающего сокета (используя fcntl(fd, F_SETFL, O_NONBLOCK) - я также рекомендую использовать fcntl(fd, F_SETFD, O_CLOEXEC) в системах, которые определяют O_CLOEXEC, чтобы прослушивающие сокеты случайно не передавались дочерним процессам, выполняющим внешние двоичные файлы), изатем используйте select() или poll(), чтобы ожидать accept() способных соединений;каждый сокет становится читаемым, когда приходит соединение.

...