IPV4的C++代码如何升迁到IPV6,以及如何优雅的进行IPV4,IPV6兼容性编码

如果单纯的从代码角度来看,IPV4和IPV6的变化基本不大,主要是围绕着sockaddr系列结构体的变化,API层面基本没变化,变化最大的就是将字符串解析成地址结构体的那几个函数,为了解析字符串中的IPV4地址,IPV4使用 inet_addr()函数,在升级之后,更改为inet_pton函数,这个函数除了地址字符串,还需要指定IP协议版本,另外,对域名的解析也有变化,IPV4使用gethostbyname()函数,而新的协议采用getaddrinfo()函数,只要处理好了这几个函数,IPV4 V6混合编码就基本OK了。

IPV4 IPV6混合编码最重要的就是理清sockaddr系列结构体的关系,主要有这几个结构体:

sockaddr   // 最基本的结构体,所有的API都是用这个结构体指针,用结构体长度来判断使用哪个结构体;

sockaddr_in // IPV4使用的地址结构;

sockaddr_in6 // IPV6使用的地址结构;

sockaddr_storage  // 通用的地址存储结构体,建议尽可能的用这个结构体来编码;

这儿给出一个解析地址的函数实现:

 

bool CSocketUtility::DomainAnalyze(tagDomain & domain)
{
    bool result = false;
    if( !domain.host.empty() && domain.data.GetCapacity() )
    {
        if( IsIPV4Address( domain.host.c_str() ) )
        {
            if( domain.data.GetCapacity() >= sizeof(sockaddr_in) &&
                inet_pton( AF_INET, domain.host.c_str(), & (
                domain.data.Query<sockaddr_in>() -> sin_addr ) ) > 0 )
            {
                // return 1, succeed.
                domain.data.SetLength( sizeof(sockaddr_in) );
                domain.data.Query<sockaddr_in>() ->  sin_family  = AF_INET;
                domain.data.Query<sockaddr_in>() ->  sin_port     = htons( domain.port );
                result = true;
            }
        }
        else
        if( IsIPV6Address( domain.host.c_str() ) )
        {
            if( domain.data.GetCapacity() >= sizeof(sockaddr_in6) &&
                inet_pton( AF_INET6, domain.host.c_str(), & (
                domain.data.Query<sockaddr_in6>() -> sin6_addr ) ) > 0 )
            {
                // return 1, succeed.
                domain.data.SetLength( sizeof(sockaddr_in6) );
                domain.data.Query<sockaddr_in6>() -> sin6_family = AF_INET6;
                domain.data.Query<sockaddr_in6>() -> sin6_port     = htons( domain.port );
                result = true;
            }
        }
        else
        {
            addrinfo hints;
            memset( & hints, 0, sizeof( hints ) );
            hints.ai_family = AF_UNSPEC;

            addrinfo * retval = NULL;
            if( 0 == getaddrinfo( domain.host.c_str(), NULL, & hints, & retval ) )
            {
                // return 0, succeeded.
                if( retval )
                {
                    for( addrinfo * current = retval;
                        !result && current != NULL; current = current -> ai_next )
                    {
                        switch( current -> ai_family )
                        {
                        case AF_UNSPEC:
                            // nothing to do here;
                            break;
                        case AF_INET:
                            if( current -> ai_addrlen <= domain.data.GetCapacity() ) {
                                memcpy( domain.data, current -> ai_addr, current -> ai_addrlen );
                                domain.data.SetLength( current -> ai_addrlen );
                                domain.data.Query<sockaddr_in>()  -> sin_port  = htons( domain.port );
                                result = true;
                            }
                            break;
                        case AF_INET6:
                            if( current -> ai_addrlen <= domain.data.GetCapacity() ) {
                                memcpy( domain.data, current -> ai_addr, current -> ai_addrlen );
                                domain.data.SetLength( current -> ai_addrlen );
                                domain.data.Query<sockaddr_in6>() -> sin6_port = htons( domain.port );
                                result = true;
                            }
                            break;
                        }
                    }

                    freeaddrinfo( retval );
                    retval = NULL;
                }
            }
        }
    }
    return result;
}

 

其中,tagDomain就是地址的结构体,包含以下几个数据:

    struct tagDomain
    {

        public:
            a_string host;

            U16 port;

            CMemory data;

            tagDomain();

            tagDomain(const tagDomain & value);

            virtual ~tagDomain();

            tagDomain & operator=(const tagDomain & value);

    };  //end struct tagDomain

其中,host是主机地址,port就是端口,而data,就是存储的sockaddr_storage,结构体初始化时,需要为data分配好内存;

在解析好地址之后,host, port, data内,都存储好了数据,并在sockaddr中,存储了地址所使用的协议版本,地址,端口,这个结构体如果是监听器就必须一直保存,如果是连接器,在连接之后就可以丢弃了。接下来的代码和原来几乎一样,只需做少量改变。

首先是创建SOCKET,需要根据协议版本创建对应的SOCKET类型。

SOCKET CTcpSelectModelImplement::Create(tagDomain & domain)
{
    SOCKET result = INVALID_SOCKET;
    if( domain.data.Alloc(
        sizeof(sockaddr_storage) ) &&
        CSocketUtility::DomainAnalyze(domain) )
    {
        result = socket(
            domain.data.Query<sockaddr>() -> sa_family,
            SOCK_STREAM, 0 );
    }
    return result;
}

注意这个函数,创建socket时用的是sockaddr结构体,无论sockaddr_in还是sockaddr_in6,结构体开头的版本成员变量都是一样的。可以用sockaddr这个结构体来取。

另外在开始的时候,解析地址之前,tagDomain 有一次内存的分配行为,分配的大小时sockaddr_storage的大小。

创建socket之后,就是不同的流程,监听器是绑定+监听,而连接器就是连接主机:

 

bool CTcpSelectModelImplement::Listen(U64 name, const tagDomain & domain)
{
    bool result = false;
    if( name && !domain.host.empty() &&
        domain.port && domain.data.GetLength() )
    {
        CObjectPtr listener = Obtain( name );
        if( listener )
        {
            SOCKET hs = INVALID_SOCKET;

            CInterface<ITcpConnectionController> segment;
            if( segment.Mount(listener, IID_TCP_CONNECTION_CONTROLLER) ) {
                hs = segment -> GetSocket();
            }

            if( hs != INVALID_SOCKET )
            {
                // 绑定该地址上的该端口给hs。
                if( SOCKET_ERROR != ::bind(
                    hs, domain.data.Query<sockaddr>(), domain.data.GetLength() ) ) {
                    result = ( listen(hs, 5) != SOCKET_ERROR );
                }

                if( result )
                {
                    CCriticalSectionScope scope( region );
                    server.insert( make_pair(hs, name) );
                    server_update = true;
                }
            }
        }
    }
    return result;
}

可以注意到在bind函数中,使用的就是sockaddr结构体指针,并且通过结构体的长度来判断采用那个协议版本。

连接函数如下:

bool CTcpSelectModelImplement::Connect(U64 name, const tagDomain & domain)
{
    bool result = false;
    if( name && domain.host.length() &&
        domain.port && domain.data.GetLength() )
    {
        CObjectPtr connector = Obtain( name );
        if( connector )
        {
            SOCKET hs = INVALID_SOCKET;

            CInterface<ITcpConnectionController> segment;
            if( segment.Mount(connector, IID_TCP_CONNECTION_CONTROLLER) ) {
                hs = segment -> GetSocket();
            }

            if( hs != INVALID_SOCKET )
            {
                // connect to domain;
                result = ( 0 == connect( hs,
                    domain.data.Query<sockaddr>(), domain.data.GetLength() ) );
                if( result )
                {
                    CCriticalSectionScope scope( region );
                    client.insert( make_pair(hs, name) );
                    client_update = true;
                }
            }
        }
    }
    return result;
}

这个处理与监听函数基本一样,而接受,发送消息的函数更是没有什么需要更改的地方。

到此,IPV4 IPV6的混合编码基本就完成了,IPV4和IPV6最大的变化就是地址解析,而IO模型层面更是没有什么改变,没有必要为IPV4和IPV6分别写一个类来实现不同的协议版本,只需要在原类上稍微修改一下结构即可。

 

另外,需要注意的是,对IOCP这个IO模型,使用的不是标准的socket函数,有些函数需要注意:

    result = ( AcceptEx( listener, hs, wb -> buf, 0,
        sizeof(sockaddr_storage), sizeof(sockaddr_storage), & bytes, ol ) == TRUE );

在AcceptEx函数中,原来使用的是 sizeof(sockaddr_in),为了兼容IPV6,全部改为sizeof(sockaddr_storage),否则使用IPV6的时候,会得到内存不足的错误。

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章