如何實現在一個 Socket 應用程式中同時支援 IPv4 和 IPv6
簡介: 當今的網路主流是 IPv4 網路,但隨著 IP 地址的日益短缺,IPv6 網路開始漸漸盛行,因此傳統的網路程式設計也需要做一些改進來適應 IPv6 和 IPv4 共存的網路環境。 本文介紹了一種設計模式來根據使用者輸入的地址或者域名建立合適的網路連線,並且遮蔽了網路連線細節,提供給使用者一個統一的介面進行二次開發。 在文中還給出了一個基於 OpenSSL https 安全連線的應用來說明該方法的使用細節。
現代網路中,IPv4, IPv6 共存的情況日益增加,而這兩種協議的地址格式,地址解析的 API 各不同,程式設計師必須面對如下兩個問題並且合理地解決這些問題。
- 怎麼準確識別使用者輸入的地址或者域名是屬於 IPv4 網路還是 IPv6 網路?
- 怎麼遮蔽網路連線細節,提供給使用者一個統一的介面?
目前我們使用的第二代網際網路 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 網路下,就需要在原來的基礎上引入新的協議族、新的資料結構以及新的地址域名轉換函式等。具體的一些差異如
在這裡要稍微介紹下 getaddrinfo()函式,它提供獨立於協議的名稱解析。函式的前兩個引數分別是節點名和服務名。節點名可以是主機名,也可以是地址串 (IPv4 的點分十進位制數表示或 IPv6 的十六進位制數字串 )。服務名可以是十進位制的埠號,也可以是已定義的服務名稱,如 ftp、http 等。函式的第三個引數 hints 是 addrinfo 結構的指標,由呼叫者填寫關於它所想返回的資訊型別的線索。函式的返回值是一個指向 addrinfo 結構的連結串列指標 res。詳見 圖 2。
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。說明如
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 類的設計主要做了了以下幾點處理:
- 把構造和解構函式隱藏起來(單件模式)
在 BaseSocket 的函式申明中,通常作為類公共成員的建構函式和解構函式被塑造成了 protected 成員函式。而開放給使用者建立真正操作物件的函式卻是 CreateSmartSocket()。CreateSmartSocket() 函式會動態地根據網路環境建立合適的子類 SocketV4 或者 SocketV6,使用的方法就是呼叫上文中提到的 getaddrinfo() 函式。生成的子類物件都儲存在靜態 smartSocketmap 中。這樣設計的原因是在於如果不這樣做,使用者就必須根據不同的網路來建立屬於 IPv4 或者 IPv6 網路的 socket 子類,然後分別呼叫他們的成員函式,這樣繁瑣又不利於使用者程式碼的維護和擴充套件。smartSocketmap 以這樣的設計為使用者構建物件創作的一個統一的介面,在不同網路下,只需要維護一套統一的程式碼,而無需為不同網路下的實現細節而費神。
- 向用戶提供代理物件
在 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 物件。
- 使用虛擬函式(實現多型性)
在基類中,主要操作的函式都被申明為虛擬函式。如果編譯器發現一個類中有被宣告為 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 的連線了。
- 網路連線
這個部分首先呼叫 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
- 初始化 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; }
- 繫結 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; }
- 接收資料
利用先建立起的 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;
- 傳送資料
利用先建立起的 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®軟體。
討論