如何使用相同的UDP套接字发送和接收数据包?我在此代码中缺少什么?

时间:2019-01-25 16:53:21

标签: c sockets networking udp port

我正在尝试编写一个应用程序,其中客户端应将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 :(得分:1)

尽管从问题本身还不清楚,但是您的注释似乎表明您没有bind()将套接字地址{@Someprogrammerdude推断为。在那种情况下,必须了解bind()的作用与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()的内容不同。前者需要绑定 local 地址,而后者则需要将消息发送到的 remote 地址。我认为将单独的对象用于这些不同的目的会更干净一些,但这不是必需的。

正如我在评论中所说,关于让客户端选择自己的端口,这是UDP的通常操作模式。实际上,通常如此,实际上,如果尚未绑定套接字,则在第一次调用sendto()时使用the system will take care of it for you。您已经在使用recvfrom(),服务器(和客户端)也可以通过它们获取发送每条消息的对等方的地址。您应该能够将该地址对象反馈回sendto()以发送响应。因此,应该像确保服务器bind()以便侦听众所周知的端口号一样容易,但确保客户端devtool: "eval"不能使用系统自动分配的端口号。 / p>

答案 1 :(得分:1)

我个人将UDP服务器绑定到通配符地址和特定端口,并使用IP_PKTINFO套接字选项获取接口和目标地址,作为每个数据包的辅助消息。

从本质上讲,启用IP_PKTINFO套接字选项意味着您收到的ancillary message收到的每个数据包都具有IPPROTO_IPIP_PKTINFO类型recvmsg()

类似地,发送响应时,可以在辅助ipi_ifindex消息中使用ipi_spec_dstIP_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

由于我在撰写本文时是星期五晚上,并且我懒于设置其他设备,因此我仅在一台机器上进行了非常轻松的测试。逻辑是合理的;只是我的实现可能会关闭。

如果您有任何疑问或看到错误或明显错误,请在评论中告知我,以便我进行验证和修复。