TCP/IP實現(四) IP編址
一.IP地址
一個IP地址是被指派給一個系統中的網路介面的而不是系統本身,就如在《TCP/IP實現(二) 介面層資料結構》中描述的那樣,每一個用於儲存IP協議地址資訊的in_ifaddr結構都和一個描述介面資訊的ifnet(或者是他的專用化)相關聯。
IPv4地址分為5類:
二.地址指派
三.ioctl系統呼叫
ioctl函式可以用於獲取介面資訊,訪問路由表,訪問ARP快取記憶體等,一般將那些不適合歸於其它類別的特性介面放入該函式中。在本節中我們只討論與介面資訊相關的使用與實現。
1.ioctl函式的使用
1)獲取所有介面的所有地址資訊和介面名SIOCGIFCONF
通過給ioctl函式傳遞引數SIOCGIFCONF可以獲取介面的名稱和地址資訊,此外還需要一個套接字描述符和一個ifconf結構體,該結構體的定義如下:
// 該結構體其實是對一個緩衝區的封裝,該緩衝區用於存放ifreq結構 struct ifconf { int ifc_len; // 初始化時設定為緩衝區buf的長度,當呼叫完ioctl函式後為緩衝區中的資料長度 union { char* buf; // 在開闢空間時使用 struct ifreq *ifcu_req; // 在遍歷緩衝區時使用 }ifc_ifcu; #define ifc_buf ifc_ifcu.ifcu_buf #define ifc_req ifc_ifcu.ifcu_req }; struct ifreq { #define IFHWADDRLEN 6 #define IFNAMSIZ IF_NAMESIZE union { char ifrn_name[IFNAMSIZ]; //介面名稱,比如:le0 } ifr_ifrn; union { // 注意以下都是共用體,因此一個ifreq只能儲存以下資訊中的一個 struct sockaddr ifru_addr; //介面地址資訊 struct sockaddr ifru_dstaddr; struct sockaddr ifru_broadaddr; struct sockaddr ifru_netmask; struct sockaddr ifru_hwaddr; short int ifru_flags; int ifru_ivalue; int ifru_mtu; struct ifmap ifru_map; char ifru_slave[IFNAMSIZ]; /* Just fits the size */ char ifru_newname[IFNAMSIZ]; __caddr_t ifru_data; } ifr_ifru; };
使用方法如下:
void get_ifi_info(){ int sockfd; int lastlen = 0, len; char *buf; ifconf ifc; ifreq* ifrp; sockfd = socket(AF_INET, SOCK_DGRAM, 0); len = 100 * sizeof(struct ifreq); // 獲取介面列表 // - 由於起初並不知道要為接收介面資訊的快取ifconf 開闢多大的空間,因此只能採用探測的方法 // - 先呼叫ioctl,記錄下緩衝區中的資料長度,再次用更大的緩衝區去呼叫ioctl,只有兩次資料長度相等才說明緩衝區夠大 while(1) { buf = new char[len]; ifc.ifc_len = len; ifc.ifc_buf = buf; if(ioctl(sockfd, SIOCGIFCONF, &ifc) < 0) { // 呼叫ioctl將介面資訊存入ifconf指示的快取 if(errno != EINVAL || lastlen != 0) { exit(errno); } } else { if(ifc.ifc_len == lastlen) break; lastlen = ifc.ifc_len; } len += 10 * sizeof(struct ifreq); delete buf; } // 遍歷存有介面資訊的緩衝區 int addrlen; sockaddr_in *addr4; sockaddr_in6 *addr6; char addrBuf[30]; for(ifrp = ifc.ifc_req; (char*)ifrp < buf + ifc.ifc_len; ifrp++){ switch(ifrp->ifr_addr.sa_family ) { // 判斷地址簇型別 case AF_INET6: addr6 = reinterpret_cast<sockaddr_in6 *>(&ifrp->ifr_addr); inet_ntop(AF_INET6, &addr6->sin6_addr, addrBuf,30); addrlen = sizeof(struct sockaddr_in6); break; case AF_INET: addr4 = reinterpret_cast<sockaddr_in *>(&ifrp->ifr_addr); inet_ntop(AF_INET, &addr4->sin_addr, addrBuf,30); default: addrlen = sizeof(struct sockaddr_in); break; } std::cout<<ifrp->ifr_name << " " << addrBuf <<std::endl; // 列印介面名和介面地址 } }
2)獲取及設定介面標誌(SIOCGIFFLAGS,SIOCSIFFLAGS)
介面標誌ifnet中的if_flag用於表明介面的操作狀態和屬性,一個程序可以檢視所有標誌,但下圖中標為核心專用的不可改變
檢視介面標誌的命令是:SIOCGIFFLAGS
設定介面標誌的命令是:SIOCSIFFLAGS
對於這兩個命令需要通過ifreq引數傳入介面名來指定介面,從而獲取指定介面的標誌,若介面不存在則將errno設定為ENXIO。
使用方式如下:
void get_if_flags()
{
struct ifreq ifq;
strcpy(ifq.ifr_name, "ens33"); // 設定要查詢的介面名,如“lo”
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(ioctl(sockfd, SIOCGIFFLAGS, &ifq) < 0) { // 將設定了介面名的ifreq 作為引數傳入ioctl
exit(errno);
}
int flags = ifq.ifr_flags;
if( (flags & IFF_UP) == 0) { // 該介面是否開啟
std::cout<<"IFF DOWN";
return;
}
if(flags & IFF_BROADCAST) { // 該介面是否用於廣播網
std::cout<<"ifnet be used to broadcast"<<std::endl;;
}
if(flags & IFF_LOOPBACK) { // 該介面是否用於環回網路
std::cout<<"ifnet be used to LOOPBACK"<<std::endl;;
}
}
3)獲取和設定介面的IP地址,網路掩碼等(SIOCGIFADDR,SIOCSIFADDR,...)
意該命令所指定的socket必須支援傳入地址的地址簇類,即你不能通過一個UDP套接字配置一個OSI地址。
設定介面IP(SIOCSIFADDR):
若該介面還未設定IP地址,則為其分配一個新的IP地址,若已有IP地址則將其第一個IP地址改變為使用者設定的IP。若該過程中IP設定失敗,則將介面回滾回原先的IP。
獲取介面IP(SIOCGIFADDR):
該命令將IP地址存入使用者指定的ifreq中,以便返回給使用者。
這裡只對設定介面IP進行舉例說明:
void set_ifi_addr()
{
struct ifreq ifq;
strcpy(ifq.ifr_name,"ens33"); // 通過介面名指定介面
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
std::string ip = "10.1.16.9";
sockaddr_in *addr = (sockaddr_in*)&(ifq.ifr_addr);
addr->sin_family = AF_INET;
::inet_pton(AF_INET, ip.c_str(), &addr->sin_addr);
if(ioctl(sockfd, SIOCSIFADDR, &ifq) < 0) { // 設定指定介面的IP
std::cout<<"errno"<<errno<<std::endl;
}
}
2.ioctl系統呼叫的實現
1)SIOCGIFCONF(獲取各個介面的名稱和全部地址)命令的實現
ioctl函式轉而呼叫ifconf函式來獲取介面名和為其配置的地址資訊。該函式首先遍歷介面連結串列ifnet,將介面的名稱複製到一個區域性的ifreq中,接著遍歷該介面ifnet的地址連結串列,將地址資訊也儲存到區域性ifreq中,若無地址資訊則將ifreq的ifru_addr成員清0,最後將該區域性ifreq複製到使用者提供的快取中。其簡要虛擬碼如下:
ifreq ifr; // 區域性ifreq
// 遍歷介面連結串列,並判斷使用者提供的剩餘空間是否足夠大
for(ifnet *ifp = ifnet; ifp && space > sizeof(ifreq); ifp = ifp->if_next) {
拷貝介面名到ifr ->ifr_name
if(ifp->if_addrlist) {// 若介面的地址連結串列為空
bzero(ifr->ifru_adr);//清空區域性ifreq的地址資訊
將ifr拷貝至使用者快取;
}
else {
for(遍歷介面的地址連結串列) {
拷貝地址資訊到區域性ifreq;
將ifr拷貝至使用者快取;
}
}
}
參照圖:
2)SIOCGIFFLAGS(獲取介面標誌)和SIOCSIFFLAGS(設定介面標誌)命令的實現
對於獲取介面標誌的命令SIOCGIFFLAGS,ioctl函式根據提供的介面名查詢到相應介面,並將介面中的標誌資訊ifnet的if_flags複製到使用者提供的ifreq中。
而對於設定介面標誌的命令SIOCSIFFLAGS,ioctl函式還會呼叫suser函式檢查程序的許可權,只有程序擁有超級使用者許可權才可以修改介面標誌,之後將使用者傳入的flags和~IFF_CANTCHANCE進行與操作,其意圖在於去除那些不能被程序改變的標誌,最後呼叫相應函式設定介面標誌。
3) SIOCSIFADDR(設定介面IP)等命令的實現
在呼叫ioctl函式後回進行如下虛擬碼操作:
for(in_ifaddr* ia = in_ifaddr; ia; ia = ia->ia->next){ //遍歷整個IP地址連結串列(in_ifaddr結構)
// 若該IP地址關聯的介面是要找的介面
if(ia->ia_ifp == ifp) // ia_ifp 為協議地址結構對介面資訊結構ifnet的回指
break;
}
switch(cmd) // 命令分類
{
case SIOCSIFADDR:
case SIOCSIFNETMASK:
case SIOCSIFDSTADDR:
if( (so->so_state & SS_PRIV) == 0) // 若不是超級使用者程序建立的套接字
return EPERM; // 程序能夠建立一個套接字,並放棄它的超級使用者許可權,仍可傳送有特權的ioctl命令。
if(ia == (struct in_ifaddr *)0) {
分配一個新的in_ifaddr結構,並進行初始化,但不填入地址資訊
}
};
switch(cmd)
{
case SIOCSIFADDR:
呼叫in_ifinit函式將地址資訊填入找到的第一個IP地址結構或是上面新建立的地址結構中
若失敗則回滾到舊值
break;
case SIOCDIFNETMASK:
...
break;
...
}
注意設定IP地址時,最後會執行ifp->if_flags |= IFF_UP。即將介面設為執行狀態。