如何从UDP套接字(C/C++)中获取本地IP地址

11
  1. 您有多个网络适配器。
  2. 将UDP套接字绑定到本地端口,而不指定地址。
  3. 在其中一个适配器上接收数据包。

如何获取接收数据包的适配器的本地IP地址?

问题是,“接收方适配器的IP地址是什么?” 而不是我们在中获取的发送方地址。

receive_from( ..., &senderAddr, ... );

调用。

7个回答

4
您可以枚举所有的网络适配器,获取它们的IP地址,并将子网掩码覆盖的部分与发送者的地址进行比较。
例如:
IPAddress FindLocalIPAddressOfIncomingPacket( senderAddr )
{
    foreach( adapter in EnumAllNetworkAdapters() )
    {
        adapterSubnet = adapter.subnetmask & adapter.ipaddress;
        senderSubnet = adapter.subnetmask & senderAddr;
        if( adapterSubnet == senderSubnet )
        {
            return adapter.ipaddress;
        }
    }
}

仅当数据包起源于适配器子网时才执行操作,若是通过路由到达则不执行。 - Len Holgate
没错,Len说得对。如果数据包通过本地路由器上的NAT到达,你只会看到本地IP(类似于10.0.0.2)。 - Eli Bendersky
广播不是路由(只在本地发送),这不是事实吗?如果我没记错的话,该方法应该可以工作。 - Luis Soeiro
它适用于广播,但广播远非服务器可能接收到的唯一数据包类型。对于拥有多个 IP 地址的服务器,任何一个地址都可能从互联网的任何地方接收数据包,因此它并不适用于通用情况。 - plugwash

4
timbo 提供的解决方案假定地址范围是唯一且不重叠的。虽然这通常是正确的,但并不是一般性的解决方案。
在 Steven 的书 "Unix network programming" (第20.2节)中提供了一个出色的函数实现,可以完全满足你的需求。该函数基于 recvmsg(),而不是 recvfrom()。如果你的套接字启用了 IP_RECVIF 选项,那么 recvmsg() 将返回接收数据包的接口索引。这可以用来查找目标地址。
源代码可以在这里找到。所涉及的函数名为 'recvfrom_flags()'.

我会研究一下。也许那是一个更好的解决方案。 - Christopher

3

你好,

我假设你已经使用INADDR_ANY进行了绑定以指定地址。

如果是这样的话,那么INADDR_ANY的语义是创建一个UDP套接字,并在所有接口上指定端口。该套接字将接收发送到指定端口的所有接口的数据包。

在使用此套接字发送数据时,将使用编号最低的接口。发件人地址字段设置为使用的第一个出站接口的IP地址。

首个出站接口定义为执行ifconfig -a时的序列。它可能是eth0。

希望对你有所帮助。

祝好, 罗布


1
很不幸,当使用绑定到“任意IP”的套接字时,sendto和recvfrom API调用在根本上是有问题的,因为它们没有本地IP信息的字段。
那么你该怎么办呢?
你可以猜测(例如基于路由表);你可以获取本地地址列表并将每个本地地址绑定到分离的套接字;你可以使用支持此信息的新API。这包括两部分:首先,您必须使用相关套接字选项(对于IPv4的ip_recvif,对于IPv6的ipv6_recvif)告诉协议栈你需要这些信息。然后,您必须使用不同的函数(在Linux和其他类Unix系统上为recvmsg,在Windows上为WSArecvmsg)来接收数据包。
这些选项都不太理想。猜测显然会有时产生错误答案。绑定单独的套接字增加了软件的复杂性,并且如果本地地址列表在程序运行时发生更改,则会导致问题。新的API是正确的技术解决方案,但可能会降低可移植性,并且可能需要修改您正在使用的套接字包装库。
编辑看起来我错了,似乎微软的文档是误导性的,WSArecvmsg 在 Windows XP 上是可用的。请参见 https://stackoverflow.com/a/37334943/5083516

0
在Linux环境下,您可以使用recvmsg来获取本地IP地址。
    //create socket and bind to local address:INADDR_ANY:
    int s = socket(PF_INET,SOCK_DGRAM,0);
    bind(s,(struct sockaddr *)&myAddr,sizeof(myAddr)) ;
    // set option
    int onFlag=1;
    int ret = setsockopt(s,IPPROTO_IP,IP_PKTINFO,&onFlag,sizeof(onFlag));
    // prepare buffers
    // receive data buffer
    char dataBuf[1024] ;
    struct iovec iov = {
        .iov_base=dataBuf,
        .iov_len=sizeof(dataBuf)
    } ;
    // control buffer
    char cBuf[1024] ;
    // message
    struct msghdr  msg = {
        .msg_name=NULL, // to receive peer addr with struct sockaddr_in
        .msg_namelen=0, // sizeof(struct sockaddr_in)
        .msg_iov=&iov,
        .msg_iovlen=1,
        .msg_control=cBuf,
        .msg_controllen=sizeof(cBuf)
    } ;
    while(1) {
        // reset buffers
        msg.msg_iov[0].iov_base = dataBuf ;
        msg.msg_iov[0].iov_len = sizeof(dataBuf) ;
        msg.msg_control = cBuf ;
        msg.msg_controllen = sizeof(cBuf) ;
        // receive
        recvmsg(s,&msg,0);
        for( struct cmsghdr* pcmsg=CMSG_FIRSTHDR(&msg);
             pcmsg!=NULL; pcmsg=CMSG_NXTHDR(&msg,pcmsg) ) {
            if(pcmsg->cmsg_level==IPPROTO_IP && pcmsg->cmsg_type==IP_PKTINFO) {
                struct in_pktinfo * pktinfo=(struct in_pktinfo *)CMSG_DATA(pcmsg);
                printf("ifindex=%d ip=%s\n", pktinfo->ipi_ifindex, inet_ntoa(pktinfo->ipi_addr)) ;
            }
        }
    }

在非对称路由环境下,以下内容无法正常工作。

您可以先将SO_REUSEADDR设置为true。

BOOL bOptVal = 1;
setsockopt(so, SOL_SOCKET, SO_REUSEADDR, (char *)&boOptVal, sizeof(bOptVal));

receive_from( ..., &remoteAddr, ... );之后,创建另一个套接字,并连接到remoteAddr。然后调用getsockname可以获取IP地址。

SOCKET skNew = socket( )
// Same local address and port as that of your first socket 
// INADDR_ANY
bind(skNew, ,  )
// set SO_REUSEADDR to true again
setsockopt(skNew, SOL_SOCKET, SO_REUSEADDR, (char *)&boOptVal, sizeof(bOptVal));

// connect back  
connect(skNew, remoteAddr)

// get local address of the socket
getsocketname(skNew, )

这不起作用,新创建的套接字通过路由表检测到的路由出去。 - Ferrybig

-3
ssize_t
     recvfrom(int socket, void *restrict buffer, size_t length, int flags,
         struct sockaddr *restrict address, socklen_t *restrict address_len);
ssize_t recvmsg(int socket, struct msghdr *message, int flags);
[..] 如果address不是空指针且socket不是面向连接的,则填充消息的源地址。

实际代码:

int nbytes = recvfrom(sock, buf, MAXBUFSIZE, MSG_WAITALL, (struct sockaddr *)&bindaddr, &addrlen);

fprintf(stdout, "读取 %d 字节,本地地址为 %s\n", nbytes, inet_ntoa(bindaddr.sin_addr.s_addr));

希望这有所帮助。


4
源地址即为远程地址。 - Remember Monica

-4

试试这个:

gethostbyname("localhost");

1
这只会获取本地主机配置为解析的内容(应该是127.0.0.1)...而不是UDP数据包的接收端。 - Urkle

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接