如何实现在一个 Socket 应用程序中同时支持 IPv4 和 IPv6

如何实现在一个 Socket 应用程序中同时支持 IPv4 和 IPv6

如何巧妙地设计代码结构

陈 鲁, 软件工程师, IBM
孙 妍, 软件工程师, IBM

 

简介: 当今的网络主流是 IPv4 网络,但随着 IP 地址的日益短缺,IPv6 网络开始渐渐盛行,因此传统的网络编程也需要做一些改进来适应 IPv6 和 IPv4 共存的网络环境。 本文介绍了一种设计模式来根据用户输入的地址或者域名建立合适的网络连接,并且屏蔽了网络连接细节,提供给用户一个统一的接口进行二次开发。 在文中还给出了一个基于 OpenSSL https 安全连接的应用来说明该方法的使用细节。

 

现代网络中,IPv4, IPv6 共存的情况日益增加,而这两种协议的地址格式,地址解析的 API 各不同,程序员必须面对如下两个问题并且合理地解决这些问题。

  1. 怎么准确识别用户输入的地址或者域名是属于 IPv4 网络还是 IPv6 网络? 
  2. 怎么屏蔽网络连接细节,提供给用户一个统一的接口? 

目前我们使用的第二代互联网 IPv4 技术,它的最大问题是网络地址资源有限,IPv6 是“Internet Protocol Version 6”的缩写,它是 IETF 设计的用于替代现行版本 IP 协议 -IPv4- 的下一代 IP 协议。与 IPV4 相比,IPv6 具有更大的地址空间。IPv4 中规定 IP 地址长度为 32 位;而 IPv6 中 IP 地址的长度为 128 位。

在 IPv4 网络下,网络编程主要依靠的是 socket 连接。在客户端,其基本步骤如下,创建一个 socket,使用 socket 连接服务器,最后通过 TCP 或者 UDP 协议进行数据读写。如果把这套方法移植到 IPv6 网络下,就需要在原来的基础上引入新的协议族、新的数据结构以及新的地址域名转换函数等。具体的一些差异如 图 1所示:



IPv4 与 IPv6 区别 

在这里要稍微介绍下 getaddrinfo()函数,它提供独立于协议的名称解析。函数的前两个参数分别是节点名和服务名。节点名可以是主机名,也可以是地址串 (IPv4 的点分十进制数表示或 IPv6 的十六进制数字串 )。服务名可以是十进制的端口号,也可以是已定义的服务名称,如 ftp、http 等。函数的第三个参数 hints 是 addrinfo 结构的指针,由调用者填写关于它所想返回的信息类型的线索。函数的返回值是一个指向 addrinfo 结构的链表指针 res。详见 图 2



getaddrinfo 函数说明 

getaddrinfo 函数之前通常需要对以下 6 个参数进行以下设置:nodename、servname、hints 的 ai_flags、ai_family、ai_socktype、ai_protocol。在 6 项参数中,对函数影响最大的是 nodename,sername 和 hints.ai_flag。而 ai_family 只是有地址为 v4 地址或 v6 地址的区别。而 ai_protocol 一般是为 0 不作改动。其中 ai_flags、ai_family、ai_socktype。说明如 图 3所示:



getaddrinfo 参数说明 

getaddrinfo 函数在 IPv6 和 IPv4 网络下都能实现独立于协议的名称解析,而且它返回的指向 addrinfo 结构的链表中会存放所有由输入参数 nodename 解析出的所有对应的 IP 信息,包括 IP 地址,协议族信息等。所以只要对 const struct addrinfo* hints 进行一些配置,就可以利用这个函数来识别连接目标的网络协议属性,进而根据其网络协议族而进行准确的连接操作。这样就解决了我们提出的第一个问题。具体的函数实现如下 清单 1所示:



				
 BaseSocket* BaseSocket::CreateSmartSocket(char* ipaddr) 
 { 		
    struct addrinfo *answer, hint, *curr; 
    bzero(&hint, sizeof(hint)); 
    hint.ai_family = AF_UNSPEC; 
    hint.ai_socktype = SOCK_STREAM; 
    char ipstr2[128]; 
    struct sockaddr_in  *sockaddr_ipv4; 
    struct sockaddr_in6 *sockaddr_ipv6; 
    	    
    int ret = getaddrinfo(ipaddr, NULL,&hint, &answer); 
    if (ret != 0) { 
        return NULL; 
    } 
    DeleteSmartSocket(); 

    for (curr = answer; curr != NULL; curr = curr->ai_next) { 
        switch (curr->ai_family){ 
            case AF_UNSPEC: 
                //do something here 
                break; 
            case AF_INET: 
                sockaddr_ipv4 = reinterpret_cast<struct sockaddr_in *>( curr->ai_addr); 
                inet_ntop(AF_INET, &sockaddr_ipv4->sin_addr, ipstr2,sizeof(ipstr2)); 
                smartSocketmap[typeIPv4]=new  SocketV4(ipstr2); 
                break; 
            case AF_INET6: 
                sockaddr_ipv6 = reinterpret_cast<struct sockaddr_in6 *>( curr->ai_addr); 
                inet_ntop(AF_INET6, <sockaddr_ipv6->sin6_addr, ipstr2,sizeof(ipstr2)); 
                smartSocketmap[typeIPv6]=new  SocketV6(ipstr2); 
                break; 
        } 
    } 	    
    freeaddrinfo(answer); 
    if(!smartSocketmap.empty()) 
    { 
        smartSocket=new BaseSocket(); 
    } 

    return smartSocket; 
 } 
 

对于用户来说,他们只想实现网络连接,而并不希望了解太多网络连接上冗繁的细节。如何屏蔽 IPv4 和 IPv6 网络的差异性,让用户使用统一的函数接口来完成操作,就成为我们的第二个课题。 程序中申明了一个基类叫 BaseSocket,继承于它的两个子类 SocketV4 和 SocketV6 分别负责有关 IPv4、IPv6 网络环境下的各种操作。详见 图 4



类继承图 

在设计 BaseSocket 类的时候,并没有把它作为一个单纯的基类来使用,而是把它设计成了一个 SocketV4 和 SocketV6 的代理类。我们都知道,C++ 支持向上类型转换(取一个对象的地址,并将其作为基类的地址使用),结合虚函数能够实现多态性,我们就在这里使用一个基类的指针使其指向不同的子类实例,并把这些指针放到一个容器内。这样设计的初衷是希望外部使用者只使用类的公共接口,享受类的服务,而无需关注类的内部实现细节。具体来说,就是在 IPv4、IPv6 同时存在的网络环境下,用户只需要享用 BaseSocket 类提供出的公共服务,而无需关注具体网络环境下网络操作的差异性。 为了达到上述的目的,BaseSocket 类的设计主要做了了以下几点处理:

  1. 把构造和析构函数隐藏起来(单件模式)

    在 BaseSocket 的函数申明中,通常作为类公共成员的构造函数和析构函数被塑造成了 protected 成员函数。而开放给用户创建真正操作对象的函数却是 CreateSmartSocket()。CreateSmartSocket() 函数会动态地根据网络环境创建合适的子类 SocketV4 或者 SocketV6,使用的方法就是调用上文中提到的 getaddrinfo() 函数。生成的子类对象都存储在静态 smartSocketmap 中。这样设计的原因是在于如果不这样做,用户就必须根据不同的网络来创建属于 IPv4 或者 IPv6 网络的 socket 子类,然后分别调用他们的成员函数,这样繁琐又不利于用户代码的维护和扩展。smartSocketmap 以这样的设计为用户构建对象创作的一个统一的接口,在不同网络下,只需要维护一套统一的代码,而无需为不同网络下的实现细节而费神。

  2. 向用户提供代理对象

    在 BaseSocket 类中申明了两个 BaseSocket 类型的指针,一个是 smartSocket,另外一个是 m_cursocket。BaseSocket* smartSocket 是一个静态的全局代理指针,用户只通过它来进行网络操作。在客户程序中,只存在一份由 CreateSmartSocket() 函数创建的 smartSocket 的副本,这是因为在每次需要网络连接时网络环境相应是固定的,不会由 v4 网络突然转变成 v6 网络,一个副本在运行时已经满足使用需求。CreateSmartSocket() 函数会先去侦测存储空间堆上是否已经存在 smartSocket 指针,存在的话就会调用 DeleteSmartSocket() 删除之前创建的副本,然后再创建一个新的 smartSocket 指针,提供给用户使用。而 m_cursocket 是指向真正操作对象(子类)的指针。值得注意的是,m_cursocket 指针是隐藏在 BaseSocket 类中的,而 smartSocket 正是 BaseSocket 类为 m_cursocket 封装的一层代理指针。用户所知的仅仅是调用了 smartSocket 的某个成员函数,而实际上,程序通过把 m_cursocket 定位到 map smartSocketmap 中的某一项,获得了真正的 SocketV4 或者 SocketV6 对象。




    对象结构图 




    获取 smartSocket 对象流程图 

    图 5和 图 6就展示了程序如何根据用户输入的地址信息判断网络类型,继而创建 smartSocket 对象的过程。

  3. 使用虚函数(实现多态性)

    在基类中,主要操作的函数都被申明为虚函数。如果编译器发现一个类中有被声明为 virtual 的函数,就会为其生成一个虚函数表,也就是 VTABLE。VTABLE 实际上是一个函数指针的数组,每个虚函数占用这个数组的一个位置。派生类有自己的 VTABLE,但是派生类的 VTABLE 与基类的 VTABLE 有相同的函数排列顺序,同名的虚函数被放在两个数组的相同位置上。在创建类实例的时候,编译器还会在每个实例的内存布局中增加一个 vptr 字段,该字段指向本类的 VTABLE。C++ 对于虚函数的调用采用晚捆绑,从而能够实现多态性。 在程序中,m_cursocket 虽然是一个基类指针,但它指向的却是一个子类对象地址。由于这样的转换是子类向上转换,所以是安全的。指向正确的子类对象后,如果需要调用成员函数,就能通过本实例中的 vptr 指针指向本类的 VTABLE,由此获得正确的子类成员函数的地址来进行操作。




    利用 smartSocket 对象进行网络连接流程图 

图 7描述了 m_cursocket 如何进行类型转换,获得准确的子类对象,并且调用子类 Connect 函数的过程。

综上所述,通过以上三点,就可以降低用户程序和网络操作 Socket 部分的耦合性。让用户容易地实现他们所需要的网络连接,而不必要太关注网络环境的细节。同样,因为耦合性降低,有关 Socket 代码部分的更新和维护也相对方便,不会牵一发而动全身。


下面我们展示一个网络连接实例,在这个实例中,我们会使用到 SSL 连接。众所周知,有些 server 或者网站会启用 SSL 进行安全连接,那么对于这一类的网络连接就不是简单的使用 socket 可以解决的,我们必须借用 OpenSSL 来帮助我们实现。通常我们的底层数据是用 OpenSSL 的 BIO 对象来处理的,借助 BIO_new_ssl(), BIO_new_accept() 等函数轻松实现 IPv4 环境下的网络安全连接。然而这些方法在 IPv6 的环境下却没有实现很好的支持。为此,我们需要另辟蹊径来达成我们的目标。经过一段时间对 OpenSSL 文档的研究,我们发现以下方法既可以实现我们安全连接的目的,又可以同时支持 IPv4 和 IPv6 两种网络环境,具有比较好的可扩展性。这个方法十分简单,那就是手工创建一个 socket,该 socket 连接了一个 IPv6 或者 IPv4 地址,然后将 socket 绑定到某个 SSL 对象上就可以实现 SSL 的连接了。



  1. 网络连接

    这个部分首先调用 BaseSocket::GetSmartSocket()创建了一个 HTTPsSocket,HTTPsSocket 得到的就是代理指针 smartSocket 的地址,然后试图调用 HTTPsSocket->sockConnect() 连接 SSL 端口。从调用过程可见用户根本不用关心他现在所处的网络是 IPv4 还是 IPv6 网络,只要调用统一的函数接口就可以了。如 清单 2所示。




    						
     
     /* Create a HTTPsSocket */ 
     if((HTTPsSocket=BaseSocket::GetSmartSocket())==NULL) 
        HTTPsSocket =BaseSocket::CreateSmartSocket(const_cast<char *>(SPName.c_str())); 
     if(HTTPsSocket==NULL) 
     { 
        return -1; 
     } 
    
     /* Connect the HTTPsSocket to SSLport */ 
     if (HTTPsSocket->isConnected()) 
     { 
        HTTPsSocket->disconnect(); 
     } 
     if(HTTPsSocket->sockConnect(SSLport)) 
     { 
        return SOCKETERR; 
     } 
     // END OF CONNECT TO THE SSLport 
    			
  2. 初始化 SSL

    这里是为了实现安全连接,对 openSSL 对象进行创建和初始化配置。如下 清单 3所示。




    						
     /* Lets get nice error messages */ 
     SSL_load_error_strings(); 
    
     /* Setup all the global SSL stuff */ 
     SSL_library_init(); 	
     ctx=SSL_CTX_new(SSLv23_client_method()); 
     if(ctx==NULL) 
     { 
        return -1; 
     } 
    
     /* Lets make a SSL structure */ 
     if (!(ssl = SSL_new(ctx))) 
     { 
        return -1; 
     } 
    			
  3. 绑定 smartSocket 到 SSL

    通过把 smartSocket 的系统 fd 绑定到生成的 ssl 对象上,就可以在 HTTPsSocket 上加载 openSSL 协议了。最后使用 SSL_connect() 函数进行连接。如 清单 4所示。




    						
     /* Lets bind socket fd to the SSL structure */ 
     int fd=HTTPsSocket->getSocketFD(); 
     SSL_set_fd(ssl, fd); 
    
     /* Lets use a SSL connect */ 
     if (SSL_connect (ssl) <= 0 ) 
     { 
        return -1; 
     } 
    			
  4. 接收数据

    利用先建立起的 smartSocket 连接进行数据接收。如 清单 5所示。




    						
     int each = 0; 	
     /*If it is openSSL enabled connection, then use SSL_read to get the message, 
     otherwise use  default HTTPSocket->sockRecv() function*/ 	
     if(SSLenabled) 
     { 	
        each = SSL_read(ssl, buff, len ); 
     } 
     else 
     { 
        each = HTTPSocket->sockRecv((unsigned char *)buff, len, 0 ); 
     } 
     return each; 	
    			
  5. 发送数据

    利用先建立起的 smartSocket 连接进行数据发送。如 清单 6所示。




    						
     /*If it is openSSL enabled connection, then use SSL_write to send the message, 
     otherwise use  default HTTPSocket->sockSend() function*/ 	
     int trans = 0, each = 0; 
     if(SSLenabled) 
     { 
        while (trans < len) 
        { 
            if((each = SSL_write(ssl, buf + trans, len - trans)) == 0) 
            { 
                return SOCKETERR ; 
            } 
            trans += each ; 
        } 
     } 
     else 
     { 
        while (trans < len) 
        { 
            if((each=HTTPSocket->sockSend((unsigned char *)buf+trans,len-trans,0))==0) 
            { 
                return SOCKETERR ; 
            } 
            trans += each ; 
        } 
     } 
     return trans; 
    			

本文介绍了一种屏蔽 socket 网络连接细节的代码设计,该方法可以很好地适应 IPv4 和 IPv6 的网络环境,而且它提供给用户一个统一的接口,把用户从 v4 或者 v6 网络的连接细节中解放出來。


学习

获得产品和技术

  • 下载 IBM 软件试用版,体验强大的 DB2®,Lotus®,Rational®,Tivoli®和 WebSphere®软件。

讨论

 

 

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个简单的示例代码,用于在同一个 socket 上绑定 IPv4IPv6 地址: ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netdb.h> #include <arpa/inet.h> int main(int argc, char *argv[]) { int sockfd, status; struct addrinfo hints, *res, *p; char ipstr[INET6_ADDRSTRLEN]; memset(&hints, , sizeof hints); hints.ai_family = AF_UNSPEC; // IPv4 or IPv6 hints.ai_socktype = SOCK_STREAM; // TCP stream sockets if ((status = getaddrinfo("localhost", "808", &hints, &res)) != ) { fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(status)); exit(1); } // loop through all the results and bind to the first we can for (p = res; p != NULL; p = p->ai_next) { if ((sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1) { perror("socket"); continue; } // set socket options to allow both IPv4 and IPv6 int yes = 1; if (setsockopt(sockfd, IPPROTO_IPV6, IPV6_V6ONLY, &yes, sizeof(yes)) == -1) { perror("setsockopt"); exit(1); } if (bind(sockfd, p->ai_addr, p->ai_addrlen) == -1) { close(sockfd); perror("bind"); continue; } break; } if (p == NULL) { fprintf(stderr, "failed to bind socket\n"); exit(1); } // convert the IP address to a string and print it void *addr; if (p->ai_family == AF_INET) { struct sockaddr_in *ipv4 = (struct sockaddr_in *)p->ai_addr; addr = &(ipv4->sin_addr); } else { struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)p->ai_addr; addr = &(ipv6->sin6_addr); } inet_ntop(p->ai_family, addr, ipstr, sizeof ipstr); printf("bound to %s\n", ipstr); freeaddrinfo(res); // free the linked list return ; } ``` 这个示例代码使用了 `getaddrinfo()` 函数来获取一个或多个与指定主机名和服务名匹配的地址信息。然后,它循环遍历这些地址信息,尝试在每个地址上绑定一个 socket。在绑定之前,它还设置了一个 socket 选项,以允许同时支持 IPv4IPv6。最后,它将绑定的 IP 地址转换为字符串,并打印出来。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值