Как я могу использовать один и тот же сокет UDP для отправки и получения пакетов? Чего мне не хватает в этом коде? - PullRequest
0 голосов
/ 25 января 2019

Я пытаюсь написать приложение, в котором клиент должен отправлять UDP-пакеты на сервер, который затем должен отвечать клиенту через все беспроводные интерфейсы. И клиент, и сервер реализованы в одном двоичном файле, и пользователь может выбрать режим, используя соответствующие параметры командной строки.

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

Тогда это соответствующий код для клиента и сервера:

        struct sockaddr_in inaddr;

        fd=socket(AF_INET,SOCK_DGRAM,0);

        if(fd==-1) {
            perror("socket() error");
            exit(EXIT_FAILURE);
        }

        // Prepare sockaddr_in structure
        bzero(&inaddr,sizeof(inaddr));
        inaddr.sin_family=AF_INET;
        inaddr.sin_port=htons(opts.port); // opts.port is parsed from the command line
        inaddr.sin_addr.s_addr=opts.destIPaddr.s_addr; // opts.destIPaddr is parsed from the command line and already in the correct format

        // Bind to the wireless interface (devname is previusly obtained in a tested piece of code)
        if(setsockopt(sData.descriptor,SOL_SOCKET,SO_BINDTODEVICE,devname,strlen(devname))==-1) {
            perror("setsockopt() for SO_BINDTODEVICE error");
            close(sData.descriptor);
            exit(EXIT_FAILURE);
        }

Оба будут читать и писать, используя:

sendto(fd,packet,packetsize,0,(struct sockaddr *)&(inaddr),sizeof(inaddr))

И

struct sockaddr_in srcAddr;
socklen_t srcAddrLen=sizeof(srcAddr);

// .....

recvfrom(fd,packet,MAX_PACKET_SIZE,0,(struct sockaddr *)&srcAddr,&srcAddrLen);

Проблема в том, что клиент и сервер не могут обмениваться данными, и клиент, для каждого отправленного пакета, кажется, всегда получает ICMP-пакет «недоступен через порт» (я ясно вижу, что в Wireshark клиент отправляет правильные пакеты UDP и сервер отказывая им с «портом недоступен»).

Возможно, я не правильно использую сокет UDP: вы знаете, что мне здесь не хватает? Моей конечной целью было бы:

  • Привязать сокет к определенному порту как на стороне клиента, так и на стороне сервера, т. Е. Клиент должен отправлять пакеты с указанным портом в качестве пункта назначения, а сервер должен получать их, прослушивая точно на том же порту
  • Свяжите сокет только с беспроводным интерфейсом (имя которого в данный момент хранится в devname - но получить его IP-адрес или MAC-адрес также не должно быть проблемой)
  • Заставить сервер и клиент взаимодействовать через UDP, при этом клиент отправляет запросы, сервер получает их и отвечает клиенту, который должен получить все ответы

Ответы [ 2 ]

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

Я бы лично связал сервер UDP с подстановочным адресом и конкретным портом и использовал опцию сокета IP_PKTINFO для получения интерфейса и адреса назначения в качестве вспомогательного сообщения для каждого пакета.

По сути, включение опций сокета IP_PKTINFO означает, что вы получаете IPPROTO_IP уровень IP_PKTINFO тип ancillary message с каждым полученным вами пакетом, используя recvmsg().

Аналогично, при отправке ответа вы можете использовать элементы ipi_ifindex или ipi_spec_dst во вспомогательном сообщении IP_PKTINFO, чтобы сообщить ядру, как маршрутизировать сообщение.

Таким образом, выможет связываться только с одним (или двумя, если вы используете как IPv4, так и IPv6) подстановочным сокетом и использовать его для приема и отправки пакетов UDP через любой интерфейс, который вы хотите;в частности, используя тот же интерфейс и IP-адрес источника, который клиент использовал в качестве пункта назначения.Всякий раз, когда становятся доступными новые интерфейсы, ваша серверная сторона также немедленно реагирует на них (хотя, очевидно, она может просто отбрасывать нежелательные клиентские запросы на уровне, основываясь на интерфейсе, с которого они пришли).Простой и достаточно надежный.

Возможно, следующий пример server.c лучше иллюстрирует это:

#define _POSIX_C_SOURCE  200809L
#define _GNU_SOURCE
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/udp.h>
#include <net/if.h>
#include <netdb.h>
#include <signal.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>

static volatile sig_atomic_t  done = 0;

static void handle_done(int signum)
{
    if (!done)
        done = signum;
}

static int install_done(int signum)
{
    struct sigaction  act;

    memset(&act, 0, sizeof act);
    sigemptyset(&act.sa_mask);

    act.sa_handler = handle_done;
    act.sa_flags   = 0;

    return sigaction(signum, &act, NULL);
}

static inline const char *ip4_address(const struct in_addr addr)
{
    static char    buffer[32];
    char          *p = buffer + sizeof buffer;
    unsigned char  octet[4];

    /* in_addr is in network byte order. */
    memcpy(octet, &addr, 4);

    /* We build the string in reverse order. */
    *(--p) = '\0';
    do {
        *(--p) = '0' + (octet[3] % 10);
        octet[3] /= 10;
    } while (octet[3]);
    *(--p) = '.';
    do {
        *(--p) = '0' + (octet[2] % 10);
        octet[2] /= 10;
    } while (octet[2]);
    *(--p) = '.';
    do {
        *(--p) = '0' + (octet[1] % 10);
        octet[1] /= 10;
    } while (octet[1]);
    *(--p) = '.';
    do {
        *(--p) = '0' + (octet[0] % 10);
        octet[0] /= 10;
    } while (octet[0]);

    return p;
}

int main(int argc, char *argv[])
{
    int   ip4fd, ip4port;
    char  dummy;

    if (argc < 2 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
        fprintf(stderr, "\n");
        fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv[0]);
        fprintf(stderr, "       %s UDP-PORT-NUMBER\n", argv[0]);
        fprintf(stderr, "\n");
        return EXIT_FAILURE;
    }
    if (sscanf(argv[1], " %d %c", &ip4port, &dummy) != 1 || ip4port < 1 || ip4port > 65535) {
        fprintf(stderr, "%s: Invalid UDP port number.\n", argv[1]);
        return EXIT_FAILURE;
    }

    if (install_done(SIGHUP) ||
        install_done(SIGINT) ||
        install_done(SIGTERM)) {
        fprintf(stderr, "Cannot install signal handlers: %s.\n", strerror(errno));
        return EXIT_FAILURE;
    }

    ip4fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (ip4fd == -1) {
        fprintf(stderr, "Cannot create an UDP socket: %s.\n", strerror(errno));
        return EXIT_FAILURE;
    }

    /* Set the IP_PKTINFO socket option, so each received datagram has an
       ancillary message containing a struct in_pktinfo. */
    {
        int  option = 1;
        if (setsockopt(ip4fd, IPPROTO_IP, IP_PKTINFO, &option, sizeof option) == -1) {
            fprintf(stderr, "Cannot set IP_PKTINFO socket option: %s.\n", strerror(errno));
            close(ip4fd);
            return EXIT_FAILURE;
        }
    }

    /* Bind to the wildcard address, to receive packets using any network interface. */
    {
        struct sockaddr_in  ip4addr;

        ip4addr.sin_family = AF_INET;
        ip4addr.sin_port   = htons(ip4port);
        ip4addr.sin_addr.s_addr = htonl(INADDR_ANY);

        if (bind(ip4fd, (const struct sockaddr *)(&ip4addr), sizeof ip4addr) == -1) {
            fprintf(stderr, "Cannot bind to port %d: %s.\n", ip4port, strerror(errno));
            close(ip4fd);
            return EXIT_FAILURE;
        }
    }

    printf("Now listening on UDP port %d.\n", ip4port);
    printf("Press CTRL+C, or send HUP, INT, or TERM (pid %ld) to exit.\n",
           (long)getpid());
    fflush(stdout);

    /* Receive UDP messages, and describe them. */
    {
        unsigned char        payload[4096], ancillary[1024];
        char                *iface, ifacebuf[IF_NAMESIZE + 1];
        unsigned int         iface_index;
        struct in_addr       iface_addr, dest_addr;
        struct iovec         iov;
        struct msghdr        hdr;
        struct cmsghdr      *cmsg;
        struct sockaddr_in   from;
        struct in_pktinfo   *info;
        ssize_t              len;
        size_t               i;

        while (!done) {

            iov.iov_base = payload;
            iov.iov_len = sizeof payload;

            hdr.msg_name = &from;
            hdr.msg_namelen = sizeof from;

            hdr.msg_iov = &iov;
            hdr.msg_iovlen = 1;

            hdr.msg_control = ancillary;
            hdr.msg_controllen = sizeof ancillary;

            hdr.msg_flags = 0;

            /* Receive a new datagram. */
            len = recvmsg(ip4fd, &hdr, 0);
            if (len < 0) {
                if (len == -1) {
                    if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK)
                        continue;
                    fprintf(stderr, "Error receiving data: %s.\n", strerror(errno));
                } else
                    fprintf(stderr, "recvmsg() error: Unexpected return value, %zd.\n", len);
                close(ip4fd);
                return EXIT_FAILURE;
            }

            /* Report. */
            printf("Received %zu bytes from %s port %d:\n",
                   (size_t)len, ip4_address(from.sin_addr), ntohs(from.sin_port));

            /* Check the ancillary data for the pktinfo structure. */
            info = NULL;
            for (cmsg = CMSG_FIRSTHDR(&hdr); cmsg != NULL; cmsg = CMSG_NXTHDR(&hdr, cmsg))
                if (cmsg->cmsg_level == IPPROTO_IP && cmsg->cmsg_type == IP_PKTINFO)
                    info = (void *)CMSG_DATA(cmsg);

            if (!info) {
                fprintf(stderr, "Error: Packet is missing the IP_PKTINFO ancillary information!\n");
                close(ip4fd);
                exit(EXIT_FAILURE);
            }

            /* info may be unaligned. */
            memcpy(&iface_index, &(info->ipi_ifindex),   sizeof info->ipi_ifindex);
            memcpy(&iface_addr,  &(info->ipi_spec_dst),  sizeof info->ipi_spec_dst);
            memcpy(&dest_addr,   &(info->ipi_addr),      sizeof info->ipi_addr);

            iface = if_indextoname(info->ipi_ifindex, ifacebuf);

            /* Report the IP_PKTINFO information. */
            if (iface)
                printf("  Interface: %u (%s)\n", iface_index, iface);
            else
                printf("  Interface: %u\n", iface_index);
            printf("  Local address: %s port %d\n", ip4_address(iface_addr), ip4port);
            printf("  Real destination: %s port %d\n", ip4_address(dest_addr), ip4port);

            for (i = 0; i < (size_t)len; i++) {
                if (i == 0)
                    printf("  Data: 0x%02x", payload[i]);
                else
                if ((i & 15) == 0)
                    printf("\n        0x%02x", payload[i]);
                else
                    printf(" 0x%02x", payload[i]);
            }

            if (len > 0)
                printf("\n");

            fflush(stdout);

            /*
             * Construct a response.
             */
            payload[0] = 'O';
            payload[1] = 'k';
            payload[2] = '!';
            payload[3] = '\n';
            iov.iov_base = payload;
            iov.iov_len = 4;

            /* Keep hdr.msg_name and hdr.msg_namelen intact. */

            hdr.msg_iov = &iov;
            hdr.msg_iovlen = 1;

            /* Prep the ancillary data. */
            hdr.msg_control = ancillary;
            hdr.msg_controllen = CMSG_SPACE(sizeof (struct in_pktinfo));

            cmsg = CMSG_FIRSTHDR(&hdr);
            cmsg->cmsg_level = IPPROTO_IP;
            cmsg->cmsg_type = IP_PKTINFO;
            cmsg->cmsg_len = CMSG_LEN(sizeof (struct in_pktinfo));
            info = (void *)CMSG_DATA(cmsg);
            /* info may be unaligned. */
            memcpy(&(info->ipi_ifindex), &iface_index, sizeof info->ipi_ifindex);
            memcpy(&(info->ipi_spec_dst), &iface_addr, sizeof info->ipi_spec_dst);
            memcpy(&(info->ipi_addr), &from.sin_addr,  sizeof info->ipi_addr);

            hdr.msg_flags = 0;

            /* Send the response. */
            do {
                len = sendmsg(ip4fd, &hdr, MSG_NOSIGNAL);
            } while (len == -1 && errno == EINTR);
            if (len == -1) {
                fprintf(stderr, "Cannot send a response message: %s.\n", strerror(errno));
                close(ip4fd);
                return EXIT_FAILURE;
            }

            printf("  %zd-byte response sent successfully.\n", len);
            fflush(stdout);
        }
    }

    close(ip4fd);
    return EXIT_SUCCESS;
}

Компиляция с использованием, например, gcc -Wall -O2 server.c -o server и запуск с указанием портачисло в качестве параметра командной строки.Например, ./server 4044.

Для тестирования на стороне клиента я использовал netcat: echo 'Hello!' | nc -q 1 -u theipaddress 4044.

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

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

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

Хотя из самого вопроса и не ясно, ваши комментарии, похоже, указывают на то, что вы не bind() адресуете сокет, как предполагает @Someprogrammerdude. В этом случае важно понимать, что bind() ing служит иным и в значительной степени ортогональным целям, чем тот, который используется опцией сокета SO_BINDTODEVICE, несмотря на использование «BIND» в имени опции.

Функция bind() предназначена для ассоциирования сокета с адресом , который для TCP и UDP включает номер порта. SO_BINDTODEVICE означает ограничение сокета для данных, проходящих через определенное устройство. Хотя на практике обычно существует взаимно-однозначное соответствие между IP-адресами и сетевыми интерфейсами,

  1. Системные интерфейсы POSIX не являются специфическими для набора протоколов IP, и они стараются избегать предположения, что все семейства адресов имеют характеристики, аналогичные характеристикам IP.

  2. Даже для IP один сетевой интерфейс может иметь несколько адресов.

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

В комментариях вы спрашиваете

мне всегда нужно использовать разные struct sockaddr_in, один для bind и один за sendto? Я не могу получить тот же результат, используя только один структура

Вы можете повторно использовать структуру адреса сокета, но помните, что его содержимое должно быть другим для bind(), чем для sendto(). Первому требуется привязка к локальному адресу, тогда как второму требуется адрес remote , на который должно быть отправлено сообщение. Я думаю, что использовать отдельные объекты для этих разных целей немного чище, но в этом нет необходимости.

Что касается разрешения клиенту выбирать свой собственный порт, как я уже говорил в комментариях, это обычный режим работы для UDP. Фактически, настолько обычно, что система позаботится о вас при первом вызове sendto(), если вы еще не связали сокет. Вы уже используете recvfrom(), с помощью которого сервер (и клиент тоже) могут получить адрес партнера, который отправил каждое сообщение. Вы должны быть в состоянии передать этот адресный объект обратно sendto(), чтобы отправить ответ. Таким образом, это должно быть так же просто, как обеспечить, чтобы сервер bind() s прослушивал общеизвестный номер порта, а клиент - нет, чтобы использовать номер порта, автоматически назначаемый системой.

...