TCP/IP網絡編程之地址族與數據序列


分配IP地址和端口號

IP是Internet Protocol(網絡協議)的簡寫,是為收發網絡數據而分配給計算機的值。端口號並非賦予計算機的值,而是為區分程序中創建的套接字而分配給套接字的序號

網絡地址(Internet Address)

為使計算機連接到網絡並收發數據,必須向其分配IP地址。IP地址分為兩類:

  • IPv4(Internet Protocol version 4):4字節地址族
  • IPv6(Internet Protocol version 6):16字節地址族

IPv4和IPv6的差別主要是IP地址所用的字節數,目前通用的地址族為IPv4,IPv6是為了應對2010年前后IP地址耗盡的問題而提出的標准,即便如此,現在還是主要使用IPv4,IPv6的普及需要更長的時間

IPv4標准的4字節IP地址分為網絡地址和主機(計算機)地址,且分為A、B、C、D、E等類型,圖1-1展示了IPv4地址族,一般不會使用已被預約的E類地址,故省略

圖1-1   IPv4地址族

網絡地址(網絡ID)是為區分網絡而設置的一部分IP地址。假設向WWW.SEMI.COM公司傳輸數據,該公司內部構建了局域網,把所有計算機連起來。因此,首先應向SEMI.COM網絡傳輸數據,也就是說,並非一開始就瀏覽所有4字節IP地址,進而找到目標主機;而是僅瀏覽4字節IP地址的網絡地址,先把數據送到SEMI.COM網絡,SEMI.COM網絡(構成網絡的路由器)接收到數據后,瀏覽傳輸數據的主機地址(主機ID)並將數據傳給目標主機。圖1-2展示了數據傳輸過程

圖1-2   基於IP地址的數據傳輸過程

某主機向203.211.172.103和203.211.217.202傳輸數據,其中203.211.172和203.211.217為該網絡的網絡地址。所以,“向相應網絡傳輸數據”實際上是向構成網絡的路由器(Route)或交換機(Switch)傳遞數據,由接收數據的路由器根據數據中的主機地址向目標主機傳遞數據

若想構建網絡,需要一種物理設備完成外網與本網主機之間的數據交換,這種設備便是路由器或交換機。它們實際上也是一種計算機,只不過是為特殊目的而設計運行的,因此有了別名。所以,如果在我們使用的計算機上安裝適當的軟件,也可以將其作為交換機。另外,交換機比路由器功能要簡單一些,但實際用途差別不大

網絡地址分類與主機地址邊界

只需通過IP地址的第一個字節即可判斷網絡地址占用的字節數,因為我們根據IP地址的邊界區分網絡地址,如下所示:

  • A類地址的首字節范圍:0~127
  • B類地址的首字節范圍:128~191
  • C類地址的首字節范圍:192~223

還有如下這種表述方式:

  • A類地址的首位以0開始,0000 0000為0,0111 111為127,與上面的0~127對應
  • B類地址的前2位以10開始,1000 0000為128,1011 1111為191,與上面的128~191對應
  • C類地址的前3位以110開始,1100 0000為192,1101 1111為223,與上面的192~223對應

正因如此,通過套接字收發數據時,數據傳到網絡后即可輕松找到正確主機

用於區分套接字的端口號

IP用於區分計算機,只要有IP地址就能找到目標主機,但僅憑這些無法傳輸給目標主機中的應用程序,畢竟處理數據靠的還是目標主機中的程序。假設用戶在上網的同時,一邊欣賞視頻,一邊瀏覽網頁,這里至少需要兩個套接字,一個接收視頻數據,一個接收網頁數據,那么問題來了,怎么區分這兩個套接字呢?或者說,怎么區分到達的數據是正在觀看的視頻,還是正在瀏覽的網頁呢?這里就需要用到端口號了

計算機中一般配有NIC(Network Interface Card,網絡接口卡)數據傳輸設備。通過NIC向計算機內部傳輸數據時會用到IP,操作系統負責把傳遞到內部的數據適配給套接字,這時就要利用端口號了。也就是說,通過NIC接收的數據內有端口號,操作系統正是參考此端口號把數據傳輸給相應端口的套接字,如圖1-3所示

圖1-3   數據分配過程

端口號就是同一操作系統內區分不同套接字而設置的,因此無法將一個端口號分配給不同套接字。另外,端口號由16位構成,可分配的端口號范圍是0~65535。但0~1023是知名端口(Well-known PORT),一般分配給特定應用程序,所以應當分配此范圍之外的端口。另外,雖然端口號不能重復,但TCP套接字和UDP套接字不會共用端口號,所以允許重復。例如:如果某TCP套接字使用8500號端口,則其他TCP套接字就無法使用該端口號,但UDP套接字可以使用

總之,數據傳輸目標地址同時包含IP地址和端口號,只有這樣,數據才會被傳輸到最終的目的應用程序

地址信息的表示

應用程序中使用IP地址和端口號以結構體的形式給出了定義。這里將以IPv4為中心,圍繞此結構體討論目標地址的表示方法

struct sockaddr_in
{
	short sin_family;			//協議族Address family
	unsigned short sin_port;	//16位TCP/UDP端口號
	struct in_addr sin_addr;	//32位IP地址
	unsigned char sin_zero[8];	//沒有實際意義,只是為了跟SOCKADDR結構在內存中對齊
};

  

該結構體中提到另一結構體in_addr定義如下,它用來存放32位IP地址

struct in_addr
{
	in_addr_t s_addr;			//32位IPv4地址
};

  

講解以上兩個結構體前觀察一些數據類型。uint16_t、int_addr_t等類型可以參考POSIX(Portable Operating System Interface,可移植操作系統接口)。POSIX是為Unix系列操作系統設立的標准,它定義了一些其他數據類型,如表1-1

表1-1   POSIX中定義的數據類型
數據類型名稱 數據類型說明 聲明的頭文件
int8_t signed 8-bit int sys/types.h
uint8_t unsigned 8-bit int(unsigned char)
int16_t signed 16-bit int
uint16_t unsigned 16-bit int(unsigned short)
int32_t signed 32-bit int
uint32_t unsigned 32-bit int(unsigned long)
sa_family_t  地址族(address family)   sys/socket.h
socklen_t  長度(length of struct)
in_addr_t  IP地址,聲明為uint32_t netinet/in.h  
in_port_t  端口號,聲明為uint16_t

從這些數據類型聲明也可掌握之前結構體的含義,那為什么需要額外定義這些數據類型呢?這是考慮到擴展性的結果。如果使用int32_t類型的數據,就能保證在任何時候都占用4個字節,即時將來使用64位表示int類型也是如此

結構體sockaddr_in的成員分析

成員sin_family:

每種協議適用的地址族均不同。比如,IPv4使用4字節地址族,IPv6使用16字節地址族,可以參考表1-2保存的sin_family地址信息

表1-2   地址族
地址族(Adddress Family) 含義
AF_INET IPv4網絡協議中使用的地址族
AF_INET6 IPv6網絡協議中使用的地址族
AF_LOCAL 本地通信中采用的Unix協議的地址族

AF_LOCAL只是為了說明具有多種地址族而添加的

成員sin_port:

該成員保存16位端口號,且以網絡字節序保存(后續還會說明何為網絡字節序)

成員sin_addr:

該成員保存32位IP地址信息,且也以網絡字節序保存。為理解好該成員,應同時觀察結構體in_addr。但結構體in_addr聲明為uint32_t,因此只需當做32位整數即可

成員sin_zero:

無特殊含義,只是為了結構體sockaddr_in的大小與sockaddr結構體保持一致而插入的成員。必須填充為0,否則無法得到想要的結果,后續還會介紹sockaddr

從之前介紹的代碼也可看出,sockaddr_in結構體變量地址將以如下方式傳遞給bind函數,后續還會介紹到bind函數,現在來看下下面參數傳遞和類型轉換的代碼

struct sockaddr_in serv_addr;
……
if (bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) 
        error_handling("bind() error");
……

  

此處重要的是第二個參數的傳遞,實際上,bind函數的第二個參數期望得到sockaddr結構體變量地址值,包括地址族、端口號、IP地址等。從下列代碼也可看出,直接向sockaddr結構體填充這些信息會帶來麻煩

struct sockaddr 
{
	unsigned short sa_family; 		//地址族(Address Family)
	char sa_data[14];				//地址信息
};

  

此結構體成員要求sa_data保存的信息需包含IP地址和端口號,剩余部分應填充0,這也是bind函數要求的。而這對於包含地址信息來講非常麻煩,繼而就有了新的結構體sockaddr_in。若按照之前的講解填寫sockaddr_in結構體,則將生成符合bind函數要求的字節流。最后轉換為sockaddr型的結構體變量,再傳遞給bind函數即可

sockaddr_in是保存IPv4地址信息的結構體,那為何還需要通過sin_family單獨指定地址族信息呢?這還是與sockaddr結構體有關,結構體sockaddr並非只為IPv4設計,這從保存地址信息的數組sa_data長度為14字節也可看出。因此,結構體sockaddr要求在sin_family中指定地址族信息,是為了與sockaddr保持一致,sockaddr_in結構體中也有地址族信息

網絡字節序與地址變換

不同CPU中,4字節整數值1在內存空間的保存方式是不同的。4字節整數型值1可用二進制表示如下:

00000000   00000000   00000000   00000001

有些CPU以這種順序保存到內存,另一些CPU則以倒序保存

00000001   00000000   00000000   00000000

若不考慮這些就收發數據則會發生問題,因為保存順序的不同意味着對接收數據的解析順序也不同

字節序(Order)與網絡字節序

CPU向內存保存數據的方式有兩種,這意味着CPU解析數據的方式也有兩種:

  • 大端序:高位字節存放到低位地址
  • 小端序:高位字節存放到高位地址

用下面的例子進行說明,假設在0x20號開始的地址中保存4字節int類型數0x12345678,大端序CPU保存如圖1-4所示:

圖1-4   大端序字節表示

整數0x12345678,0x12是最高位字節,0x78是最低位字節。因此,大端序先保存最高位字節0x12(最高位字節0x12存放到低位地址),小端序保存方式如圖1-5所示:

圖1-5   小端序字節表示

先保存的是最低位字節0x78,從以上分析可以看出,每種CPU的數據保存方式均不同。因此,代表CPU數據保存方式的主機字節序(Host Byte Order)在不同CPU中也各不相同。目前主流的Intel系列CPU以小端序方式保存數據。那么,如果兩台字節序不同的計算機之間交換數據,勢必會出現這樣的問題,大端序計算機傳輸數據0x1234時未考慮字節序問題,直接以0x12、0x34的順序發送,結果接收端以小端序方式保存數據,因此小端序接收到的數據則變為0x3412,而非0x1234。正因如此,在通過網絡傳輸數據時約定統一使用大端序傳輸數據

字節序轉換

既然我們明白了在填充sockaddr_in結構體前將數據轉換成網絡字節序。接下來,我們就來了解一下關於轉換字節序的函數:

  • unsigned short htons(unsigned short);
  • unsigned short ntohs(unsigned short);
  • unsigned long htonl(unsigned long);
  • unsigned long ntohl(unsigned long);

htons中的h代表主機(host)字節序,n代表網絡(network)字節序。另外,s指的是short,l指的是long(Linux中long類型占用4個字節)。因此,htons是h、to、n、s的組合,也可以解釋為“把short型數據從主機字節序轉化為網絡字節序”。而ntohs可以解釋為“把short類型數據從網絡字節序轉化為主機字節序”。通常,以s作為后綴的函數中,s代表兩個字節short,因此用於端口號轉換,以l作為后綴的函數中,1代表4個字節,因此用於IP地址轉換

下面通過示例說明以上函數的調用過程

endian_conv.c

#include <stdio.h>
#include <arpa/inet.h>

int main(int argc, char *argv[])
{
    unsigned short host_port = 0x1234;
    unsigned short net_port;
    unsigned long host_addr = 0x12345678;
    unsigned long net_addr;

    net_port = htons(host_port);
    net_addr = htonl(host_addr);
    printf("Host ordered port:%#x \n", host_port);
    printf("Network ordered port:%#x \n", net_port);
    printf("Host ordered address:%#lx \n", host_addr);
    printf("Network ordered address:%#lx \n", net_addr);
    return 0;
}

  

  • 第6、8行:各保存2個字節、4個字節的數據。當然,若運行的CPU不同,則保存的字節序也不同
  • 第11、12行:變量host_port、host_addr中的數據轉化為網絡字節序。若運行環境為小端序CPU,則改變之后的字節序保存

編譯並運行endian_conv.c

# gcc endian_conv.c -o endian_conv
# ./endian_conv 
Host ordered port:0x1234 
Network ordered port:0x3412 
Host ordered address:0x12345678 
Network ordered address:0x78563412 

  

這就是小端序CPU中運行的結果。如果在大端序CPU中運行,則變量值不會改變

網絡地址的初始化與分配

前面已討論過網絡字節序列,接下來介紹bind函數為代表的結構體的應用

將字符串信息轉換為網絡字節序的整型

sockaddr_in中保存地址信息的成員為32位整數型,因此,為了分配IP地址,需要將其表示為32位整數型數據。這對於只熟悉字符串信息的我們並非易事,對於IP的表示,我們熟悉點分十進制法,而非整數型數據表示法。幸運的是,有個函數會幫我們將字符串形式的IP轉換為32位整數型數據

#include <arpa/inet.h>
in_addr_t inet_addr(const char* strptr);//成功時返回32位大端序整數型值,失敗時返回INADDR_NONE

  

如果向該函數傳遞“211.214.107.99”的點分十進制格式的字符串,它會將其轉換為32位整數型數據並返回。當然,該整數型值滿足網絡字節序。另外,該函數的返回值類型in_addr_t在內部聲明為32位整數型。下面示例表示該函數的調用過程

inet_addr.c

#include <stdio.h>
#include <arpa/inet.h>
int main(int argc, char *argv[])
{
    char *addr1 = "1.2.3.4";
    char *addr2 = "1.2.3.256"; 

    unsigned long conv_addr = inet_addr(addr1); 
    if (conv_addr == INADDR_NONE)
        printf("Error occured!\n");
    else
        printf("Network ordered integer addr:%#lx\n", conv_addr);
    conv_addr = inet_addr(addr2); 
    if (conv_addr == INADDR_NONE)
        printf("Error occured!\n");
    else
        printf("Network ordered integer addr:%#lx\n", conv_addr);
    return 0;
}

  

  • 第6行:一個字節能表示的最大整數為255,也就是說,它是錯誤的IP地址。利用該錯誤地址檢驗inet_addr函數的錯誤檢測能力
  • 第8、13行:通過運行結果驗證第8行的函數能正常調用,而第13行的函數調用出現異常

編譯並運行inet_addr.c

# ./inet_addr 
Network ordered integer addr:0x4030201
Error occured!

  

從運行結果上來看,inet_addr函數不僅可以把IP地址轉換為32位整數型,而且可以檢測無效的IP地址。另外,從輸出結果可以驗證確實轉換為網絡字節序

inet_aton函數與inet_addr函數在功能上完全相同,也將字符串形式IP地址轉換為32位網絡字節序整數並返回,只不過該函數利用了in_addr結構體

#include <arpa/inet.h>
int inet_aton(const char *string, struct in_addr*addr);//成功時返回1,失敗時返回0

  

  • string:含有需轉換的IP地址信息的字符串地址
  • addr:將保存轉件結果的in_addr結構體變量的地址值

實際編程中若要調用inet_addr函數,需將轉換后的IP地址信息代入sockaddr_in結構體中聲明的in_addr結構體變量。而inet_aton函數則不需此過程,原因在於,若傳遞in_addr結構體變量地址值,函數會自動把結果填入該結構體變量。通過示例了解一下inet_aton函數調用過程

inet_aton.c

#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
void error_handling(char *message);

int main(int argc, char *argv[])
{
    char *addr = "127.232.124.79";
    struct sockaddr_in addr_inet;
    if (!inet_aton(addr, &addr_inet.sin_addr))
        error_handling("Conversion error");
    else
        printf("Network ordered integer addr:%#x\n", addr_inet.sin_addr.s_addr);
    return 0;
}
void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

  

  • 第9、10行:轉換后的IP地址信息需保存到sockaddr_in的in_addr型變量才有意義。因此,inet_aton函數的第二個參數要求得到in_addr型的變量地址值。這就省去了手動保存IP地址信息的過程

 

編譯並運行inet_aton.c

# gcc inet_aton.c -o inet_aton
# ./inet_aton 
Network ordered integer addr:0x4f7ce87f

  

 最后再介紹一個與inet_aton函數相反的函數,此函數可以把網絡字節序整數型IP轉換成我們熟悉的字符串形式

#include <arpa/inet.h>
char *inet_ntoa(struct in_addr in);//成功時返回轉換的字符串地址值,失敗時返回-1

  

該函數將通過參數傳入的整數型IP地址轉換為字符串格式並返回。但調用時需小心 ,返回值類型為char指針,返回字符串地址意味着字符串已保存到內存空間了,但該函數未向程序員要求分配內存,而是在其函數內部申請內存並保存字符串。也就是說,調用完函數后,應立即將字符串復制到其他的內存空間。因為,若再次調用inet_ntoa函數,則有可能覆蓋之前保存的字符串信息。總之,再次調用inet_ntoa函數前返回的字符串地址值是有效的,若需長期保存,則應將字符串復制到其他內存空間。下面給出該函數調用示例:

inet_ntoa.c

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <arpa/inet.h>

int main(int argc, char *argv[])
{
    struct sockaddr_in addr1, addr2;
    char *str_ptr;
    char str_arr[20];

    addr1.sin_addr.s_addr = htonl(0x1020304);
    addr2.sin_addr.s_addr = htonl(0x1010101);

    str_ptr = inet_ntoa(addr1.sin_addr);
    strcpy(str_arr, str_ptr);
    printf("Dotted-Decimal notation1:%s\n", str_ptr);

    inet_ntoa(addr2.sin_addr);
    printf("Dotted-Decimal notation2:%s\n", str_ptr);
    printf("Dotted-Decimal notation3:%s\n", str_arr);
    return 0;
}

  

  • 第15行:向inet_ntoa函數傳遞結構體變量addr1中的IP地址信息並調用該函數,返回字符串形式的IP地址
  • 第16行:瀏覽並復制第15行中返回的IP地址信息
  • 第19、20行:再次調用inet_ntoa函數。由此得出,第15行中返回的地址已覆蓋了新的IP地址字符串,可通過第20行的輸出結果進行驗證
  • 第21行:第16行中復制了字符串,因此可以正確輸出第15行中返回的IP地址字符串

編譯運行inet_ntoa.c

# gcc inet_ntoa.c -o inet_ntoa
# ./inet_ntoa 
Dotted-Decimal notation1:1.2.3.4
Dotted-Decimal notation2:1.1.1.1
Dotted-Decimal notation3:1.2.3.4

  

網絡地址初始化

結合前面所學的內容,現在介紹創建套接字過程中常見的網絡地址信息初始化方法

struct sockaddr_in addr;
char *serv_ip = "211.217.168.13";          //聲明IP地址字符串
char *serv_port = "8500";                  //聲明端口號字符串
memset(&addr, 0, sizeof(addr));            //結構體變量addr的所有成員初始化為0
addr.sin_family = AF_INET;                 //指定地址族
addr.sin_addr.s_addr = inet_addr(serv_ip); //基於字符串的IP地址初始化
addr.sin_port = htons(atoi(serv_port));    //基於字符串的端口號初始化

  

上述代碼中,memset函數將每一個字節初始化為同一個值:第一個參數為結構體變量addr的地址值,即初始化為addr,第二個參數為0,因此初始化為0;最后一個參數中傳入addr的長度。因此addr的所有字節均初始化為0。這么做是為了將sockaddr_in結構體成員sin_zero初始化為0,另外,最后一行代碼調用atoi函數把字符串類型的值轉換成整數型。總之,上面的代碼利用字符串格式的IP地址和端口號初始化了sockaddr_in結構體變量

另外,代碼中的IP地址和端口號采用了硬編碼,這並不是一個好的寫法,因為運行環境改變就得更改代碼。因此,我們運行示例main函數時傳入IP地址和端口號

請求方法不同意味着調用的函數也不同,服務器端的准備工作通過bind函數完成,而客戶端則通過connect函數完成。因此,函數調用前需要的地址值類型也不同,服務端聲明sockaddr_in結構體變量,將其初始化為賦予服務器端IP和套接字的端口號,然后調用bind函數;而客戶端則聲明sockaddr_in結構體,並初始化為要與之連接的服務器端套接字的IP和端口號,然后調用connect函數

INADDR_ANY

每次創建服務器端套接字都要輸入IP地址會有些繁瑣,此時可如下初始化地址信息

struct sockaddr_in addr;
char *serv_port = "8500";       			//聲明端口號字符串
memset(&addr, 0, sizeof(addr)); 			//結構體變量addr的所有成員初始化為0
addr.sin_family = AF_INET;      			//指定地址族
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(atoi(serv_port)); 	//基於字符串的端口號初始化

    

與之前方式最大的區別在於,利用常數INADDR_ANY分配服務器端的IP地址。若是采用這種方式,則可自動獲取運行服務端的計算機IP地址,不必親自輸入。而且,若同一計算機中已分配多個IP地址(多宿主計算機),則只要端口號一致,就可以從不同IP地址接收數據。因此,服務端中優先考慮這種方式

向套接字分配網絡地址

既然已討論了sockaddr_in結構體的初始化方法,接下來就把初始化的地址信息分配給套接字,bind函數負責這項操作:

#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);

  

  • sockfd:要分配地址信息(IP地址和端口號)的套接字文件描述符
  • myaddr:存有地址信息的結構體變量地址值
  • addrlen:第二個結構體變量的長度

如果此函數調用成功,則將第二個參數指定的地址信息分配給第一個參數中的相應套接字。下面給出服務器端常見的套接字初始化過程:

int serv_sock;
struct sockaddr_in serv_addr;
char *serv_port = "9190";
/*創建服務器端套接字(監聽套接字)*/
serv_sock = socket(PF_INET, SOCK_STREAM, 0);

/*地址信息初始化*/
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADRR_ANY);
serv_addr.sin_port = htons(atoi(serv_port));
/*分配地址信息*/
bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr));

  

服務端代碼結構以上,當然還有未顯示的異常處理代碼

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM