设置静态ip手动代理
当今的网络主流是 IPv4 网络,但随着 IP 地址的日益短缺,IPv6 网络开始渐渐盛行,因此传统的网络编程也需要做一些改进来适应 IPv6 和 IPv4 共存的网络环境。 本文介绍了一种设计模式来根据用户输入的地址或者域名建立合适的网络连接,并且屏蔽了网络连接细节,提供给用户一个统一的接口进行二次开发。 在文中还给出了一个基于 OpenSSL https 安全连接的应用来说明该方法的使用细节。
现代网络中,IPv4, IPv6 共存的情况日益增加,而这两种协议的地址格式,地址解析的 API 各不同,程序员必须面对如下两个问题并且合理地解决这些问题。
在 IPv4 网络下,网络编程主要依靠的是 socket 连接。在客户端,其基本步骤如下,创建一个 socket,使用 socket 连接服务器,最后通过 TCP 或者 UDP 协议进行数据读写。如果把这套方法移植到 IPv6 网络下,就需要在原来的基础上引入新的协议族、新的数据结构以及新的地址域名转换函数等。具体的一些差异如图 1所示:
在这里要稍微介绍下 getaddrinfo()函数,它提供独立于协议的名称解析。函数的前两个参数分别是节点名和服务名。节点名可以是主机名,也可以是地址串 (IPv4 的点分十进制数表示或 IPv6 的十六进制数字串 )。服务名可以是十进制的端口号,也可以是已定义的服务名称,如 ftp、http 等。函数的第三个参数 hints 是 addrinfo 结构的指针,由调用者填写关于它所想返回的信息类型的线索。函数的返回值是一个指向 addrinfo 结构的链表指针 res。详见图 2。
getaddrinfo 函数在 IPv6 和 IPv4 网络下都能实现独立于协议的名称解析,而且它返回的指向 addrinfo 结构的链表中会存放所有由输入参数 nodename 解析出的所有对应的 IP 信息,包括 IP 地址,协议族信息等。所以只要对 const struct addrinfo* hints 进行一些配置,就可以利用这个函数来识别连接目标的网络协议属性,进而根据其网络协议族而进行准确的连接操作。这样就解决了我们提出的第一个问题。具体的函数实现如下清单 1所示:
对于用户来说,他们只想实现网络连接,而并不希望了解太多网络连接上冗繁的细节。如何屏蔽 IPv4 和 IPv6 网络的差异性,让用户使用统一的函数接口来完成操作,就成为我们的第二个课题。 程序中申明了一个基类叫 BaseSocket,继承于它的两个子类 SocketV4 和 SocketV6 分别负责有关 IPv4、IPv6 网络环境下的各种操作。详见图 4。
在设计 BaseSocket 类的时候,并没有把它作为一个单纯的基类来使用,而是把它设计成了一个 SocketV4 和 SocketV6 的代理类。我们都知道,C++ 支持向上类型转换(取一个对象的地址,并将其作为基类的地址使用),结合虚函数能够实现多态性,我们就在这里使用一个基类的指针使其指向不同的子类实例,并把这些指针放到一个容器内。这样设计的初衷是希望外部使用者只使用类的公共接口,享受类的服务,而无需关注类的内部实现细节。具体来说,就是在 IPv4、IPv6 同时存在的网络环境下,用户只需要享用 BaseSocket 类提供出的公共服务,而无需关注具体网络环境下网络操作的差异性。 为了达到上述的目的,BaseSocket 类的设计主要做了了以下几点处理:
在 BaseSocket 的函数申明中,通常作为类公共成员的构造函数和析构函数被塑造成了 protected 成员函数。而开放给用户创建真正操作对象的函数却是 CreateSmartSocket()。CreateSmartSocket() 函数会动态地根据网络环境创建合适的子类 SocketV4 或者 SocketV6,使用的方法就是调用上文中提到的 getaddrinfo() 函数。生成的子类对象都存储在静态 smartSocketmap 中。这样设计的原因是在于如果不这样做,用户就必须根据不同的网络来创建属于 IPv4 或者 IPv6 网络的 socket 子类,然后分别调用他们的成员函数,这样繁琐又不利于用户代码的维护和扩展。smartSocketmap 以这样的设计为用户构建对象创作的一个统一的接口,在不同网络下,只需要维护一套统一的代码,而无需为不同网络下的实现细节而费神。
图 5图 6就展示了程序如何根据用户输入的地址信息判断网络类型,继而创建 smartSocket 对象的过程。
在基类中,主要操作的函数都被申明为虚函数。如果编译器发现一个类中有被声明为 virtual 的函数,就会为其生成一个虚函数表,也就是 VTABLE。VTABLE 实际上是一个函数指针的数组,每个虚函数占用这个数组的一个位置。派生类有自己的 VTABLE,但是派生类的 VTABLE 与基类的 VTABLE 有相同的函数排列顺序,同名的虚函数被放在两个数组的相同位置上。在创建类实例的时候,编译器还会在每个实例的内存布局中增加一个 vptr 字段,该字段指向本类的 VTABLE。C++ 对于虚函数的调用采用晚捆绑,从而能够实现多态性。 在程序中,m_cursocket 虽然是一个基类指针,但它指向的却是一个子类对象地址。由于这样的转换是子类向上转换,所以是安全的。指向正确的子类对象后,如果需要调用成员函数,就能通过本实例中的 vptr 指针指向本类的 VTABLE,由此获得正确的子类成员函数的地址来进行操作。
图 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 的连接了。
本文介绍了一种屏蔽 socket 网络连接细节的代码设计,该方法可以很好地适应 IPv4 和 IPv6 的网络环境设置静态ip手动代理,而且它提供给用户一个统一的接口,把用户从 v4 或者 v6 网络的连接细节中解放出來。