How to use 'getaddrinfo' to choose default free port for all interfaces?

生来就可爱ヽ(ⅴ<●) 提交于 2020-01-13 17:58:32

问题


I'm trying to make a server to listen on both IPv4 and IPv6 in dual stack mode.

I want the same port number for both IPv4 and IPv6 servers, and I want it to be on a random selection of port (using port "0")

when I bind, each server get different port, and I want it to be the same.

so I thought it should be done by the getaddrinfo function.

But when I give it the "0" port it stays "0" in the addrinfo results, what cause each bind to give me different port number.

My question: Is there a way to tell the getaddrinfo to select a single free port which is free on all interfaces, then bind the given address to all interfaces?

if there isn't, is there other way to find a free port number? (without binding and stop when fail)

please refer to the following code:

EDIT: now the code can fully compiled on VS 10.

#ifdef WIN32
#include <WS2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
#else
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <unistd.h>
#define closesocket close
#endif
#include <iostream>
#include <vector>  
#include <stdio.h>
#include <string>

int GetAddressFamily()
{
    return AF_UNSPEC;
}

std::string ipaddress(addrinfo* info)
{
    std::string retval;   
    char temp[260];
    socklen_t addrlen = (socklen_t)info->ai_addrlen;
    int res = ::getnameinfo(info->ai_addr, addrlen, temp, 256, NULL, 0, NI_NUMERICHOST);
    if(res){
        std::cout<<gai_strerrorA(res)<<std::endl;
    }else{
        retval = temp;
    }
    return retval;
}

int getport(addrinfo* info)
{
    int retval=0;

    if (info->ai_family == AF_INET) {
        retval = htons(((struct sockaddr_in*)(info->ai_addr))->sin_port);
    }else{
        retval = htons(((struct sockaddr_in6*)(info->ai_addr))->sin6_port);
    }
    return retval;
}

int main()
{
    char *hostName = NULL; //GetHostName();
    int portNum = 0;
#ifdef WIN32
    WSADATA w;
    if(0 != WSAStartup(MAKEWORD(2, 2), &w))
    {
       std::cerr<<" WSAStartup() failed \n";
       return -1;
    } 
#endif
    addrinfo hints,*results,*tmp;
    memset(&hints,0,sizeof(hints));
    hints.ai_family = GetAddressFamily();
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_flags = AI_NUMERICSERV;
    if(hostName){
        hints.ai_flags |= AI_CANONNAME; 
        //AI_CANONNAME - fills ai_cannonname in address.
    }else{
        hints.ai_flags |= AI_PASSIVE;
        //AI_PASSIVE - give ADDR_ANY and IN6ADDR_ANY_INIT when hostName is NULL 
    }
    char portbuff[40];
    sprintf(portbuff,"%u",portNum);

    int res = ::getaddrinfo(hostName, portbuff,&hints, &results); 
    if(res){
        std::cerr<<gai_strerrorA(res)<<std::endl;
    }else{
        std::vector<int> sockets;
        for(tmp = results; tmp ; tmp=tmp->ai_next){
            std::cout<<ipaddress(tmp).c_str()<<" : "<<port(tmp)<<std::endl;
            int s = socket(tmp->ai_family,tmp->ai_socktype,tmp->ai_protocol);
            if(s != -1){
                res = bind(s, tmp->ai_addr, (int)tmp->ai_addrlen);
                if(res != -1){
                    sockaddr_storage addr;
                    socklen_t len =sizeof(addr);
                    int res = getsockname(s, (struct sockaddr *)&addr, &len);
                    std::cout<<"Bound to port: ";
                    if(addr.ss_family == AF_INET){
                        std::cout<<htons(((sockaddr_in*)&addr)->sin_port)<<std::endl;
                    }else{
                        std::cout<<htons(((sockaddr_in6*)&addr)->sin6_port)<<std::endl;
                    }
                    sockets.push_back(s);
                }
            }
        }
        for(int i=0;i<sockets.size();i++){
            closesocket(sockets[i]);
        }
    }
    ::freeaddrinfo(results);
    return 0;
}

EDIT2: My solution for now:

I added the following function to be called after first successful bind, and will set the given port to addrinfo list:

void setport(addrinfo* info,int port)
{
   for(addrinfo* tmp = info; tmp ; tmp=tmp->ai_next){
   if (tmp->ai_family == AF_INET) {
     ((struct sockaddr_in*)(tmp->ai_addr))->sin_port = htons(port);
   }else{
     ((struct sockaddr_in6*)(tmp->ai_addr))->sin6_port = htons(port);
   }
}

It should be called after successful bind:

port = getport(result)
//...after bind:   
if(port == 0) {
   port = printed value after succesful bind
   setport(result, port)
}

回答1:


I think your best option is to use an IPv6 socket with IPV6_V6ONLY disabled. Then that single socket will acccept both IPv6 and IPv4 connections. IPv4 clients will have the address set to the mapped address ::ffff:a.b.c.d.

There's not really any reason to use getaddrinfo here, because you're not looking anything up. Something like this should do it (untested):

int s = socket(AF_INET6, SOCK_STREAM, 0);
if (s < 0)
    throw std::system_error(errno, generic_category());

const int off = 0;
if (setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY, &off, sizeof(off)) {
    close(s);
    throw std::system_error(errno, generic_category());
}

struct sockaddr_in6 addr{};
socklen_t alen = sizeof(addr);
if (bind(s, static_cast<struct sockaddr*>(&addr), alen)) {
    close(s);
    throw std::system_error(errno, generic_category());
}

getsockname(s, static_cast<struct sockaddr*>(&addr), &alen);
int port = ntohs(addr.sin6_port);

PS. It's a good idea to always set the IPV6_V6ONLY option to whichever value you wish, because default varies between OS.



来源:https://stackoverflow.com/questions/19431370/how-to-use-getaddrinfo-to-choose-default-free-port-for-all-interfaces

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!