1. 程式人生 > >TCP/IP實現(四) IP編址

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。即將介面設為執行狀態。