TCP/IP網絡編程系列之三-地址族與數據序列
分配給套接字的IP地址和端口
IP是Internet Protocol (網絡協議)的簡寫,是為首發網絡數據而分配給計算機的值。端口號並非賦予計算機值,而是為了區分程序中創建的套接字而分配給套接字的序號。
網絡地址
網絡地址分為IPV4和IPV6,分別你別為4個字節地址簇和6個字節地址簇。IPV4標准的4個字節的地址分為網絡地址和主機地址,且分為A、B、C、D、E 等類型。一般很少用到E類型。如下圖所示:net-id指網絡ID,host-id指主機ID
說明:
A類地址的首字節范圍是:0-127
B類地址的首字節范圍是:128-191
C類地址的首字節范圍是:192-223
或者也可以這樣分
A類地址的首位是以0開頭
B類地址的前兩位是以10開頭
C類地址的前三位是以110開頭
概述
網絡ID是為區分網絡而設置的一部分IP地址。比如你向www.baidu.com公司傳輸數據,該公司內部構建了局域網,把所有的計算機連接起來。因此,首相向baidu.com網絡傳輸數據,也就是說,並非一開始就瀏覽所有4字節的IP地址,進而找到目標主機;而是僅瀏覽4字節IP地址的網絡地址,先把數據傳到baidu.com的網絡。baidu.com網絡接收到數據之后,瀏覽傳輸數據的主機地址並將數據傳輸給目標地址。一般的網絡都會有路由器和交換機,所以實際上是向路由器或交換機傳遞數據,由接收數據的路由器根據數據中的主機地址向目標主機傳送數據。
用於區分套接字的端口號
IP用於區分計算機,只要有IP地址就能想目標主機傳輸數據,但是僅憑這些數據無法傳輸給最終的應用程序。假設在欣賞音樂的同時在聽音樂或者上網瀏覽網頁,這時至少需要1個接受視頻數據的套接字和1個接受網頁信息的套接字。但是如何區分它們呢,也就是說傳輸到計算機的網絡數據是發送給視頻播放器還是音樂播放器?假設我們開發了迅雷等應用程序,該程序用塊單位分割一個文件,從多台計算機接受數據。那如何區分這些套接字呢?
計算機中一般都會有NIC(NetWork Interface Card,網絡接口卡)數據傳輸設備。通過NIC向計算機內部傳輸數據時會用到IP。OS負責把傳遞到內部的數據適當的分配給套接字,這時就要利用端口號。通過NIC接受的數據內有端口號,操作系統正是參考此端口號把數據傳輸給相應端口的套接字。端口號就是在同一操作系統內為區分不同套接字而設置的,因此無法將一個端口號分配給不同的套接字。並且,端口號由16位構成,可分配的端口號的范圍是0-65535,0-1023是知名端口號(Well-Know PORT),一般分配給特定應用程序,所以應當分配此范圍之外的值。端口號是不能重復,但TCP套接字和UDP套接字不會公用端口號,所以允許重復。比如:某TCP套接字使用9190端口號,則其他TCP無法就無法使用端口號,但是UDP套接字就可以使用。
總之,數據傳輸目標地址同時包含IP地址和端口號,只有這樣,數據才會被傳輸到最總的目的應用程序(應用程序套接字)。
地址信息的表示
應用程序中使用的IP地址和端口號以結構體的形式給出了定義,我們主要以IPV4為中心。
struct sockaddr_in { sa_family_t sin_family;//地址族 uint16_t sin_port;//16位TCP/UDP端口號 struct in_addr sin_addr;//32位IP地址 char sin_zero[8];//不使用 }; 該結構體中的 in_addr用來存放32位的IP地址,定義如下 struct in_addr { In_addr_t s_addr;//32位IP地址 };
uint16_t in_addr_t等類型可以參考POSIX,我這邊簡單說一下
數據類型 | 數據類型說明 | 聲明的頭文件 |
int8_t | signed 8-bit int | sys/types.h |
uint8_t | unsigned 8-bit int | sys/types.h |
int16_t | signed 16-bit int | sys/types.h |
uint16_t | unsigned 16-bit int | sys/types.h |
int32_t | signed 32-bit int | sys/types.h |
這主要是考慮到擴展性,如果使用int32_t類型的數據,就能保證在任何時候都占用4字節,即使將來用64位的來存儲也是一樣。
結構體sockaddr_in的成員分析
- 成員sin_family
每種協議族適用的地址族不同,比如IPV4使用4個字節地址族,IPV6使用16字節地址簇
地址簇 含義
AF_INET IPV4網絡協議中使用的地址族
AF_INET6 IPV6網絡協議中使用的地址族
AF_LOCAL 本地通信中采用的UNIX協議的地址族
- 成員sin_port
該成員保存16位端口號,具體在下面講解。
- sin_addr
保存32位的IP地址信息,且以網絡字節序保存,結構體in_addr聲明為uint32_t,一次只需要保存32位整數類型即可。
- sin_zero
無特殊含義,只是未使用結構體sockaddr_in的大小與sockaddr結構體保持一致而插入的成員,必須填充為0,否則無法得到想要的結果。之前在服務端bind函數的時候,
struct sockaddr_in serv_addr;
if(bind(serv_sock,(struct sockaddr *)&serv_addr,sizeof(serv_addr)==-1),其中第二個參數,是由sockaddr_in結構體轉化而來的,並且希望得到sockaddr結構體變量地址,包括地址簇、端口號、IP地址等。但是我們直接向sockaddr結構體填充這些信息會帶來麻煩。
struct sockaddr
{
sa_family_t sin_family;//地址族
char sa_data[14];//地址信息
};
sa_data保存地址信息,包括IP地址和端口號,剩余部分應填充為0,這也是bind函數的要求。這對於包含地址信息來講非常麻煩,從而有了新的結構體sockaddr_in。
網絡字節序和地址轉換
-
字節序和網絡字節序
cpu向內存保存數據有兩種,這意味着cpu解析數據的方式也是兩種分別為大端序和小端序。
- 大端序(Big Endian):高位字節存放到低位地址。
- 小端序(Little Endian):高位字節存放到高位地址。
比如0x00000001
大端序:內存低比特位 00000000 00000000 00000000 00000001 內存高比特位
小端序:內存低比特位 10000000 00000000 00000000 00000000 內存高比特位
還可以如下圖表示:
所以出現了一個問題如果兩台計算機的cpu的數據保存方式不同,但是他們是如何傳送數據的呢?如何進行網絡傳輸的呢?所以就規定了一個標准,在通過網絡傳輸的過程中統一按照大端序。即先把數據數組轉化為大端序格式然后進行傳輸。因此,所有計算機接受數據時應識別該數據的字節格式,小端序系統傳輸數據時應該轉化為大端字節序的排列方式。
-
字節序轉換(Endian Conversions)
所以我們懂應該知道為何在填充sockadr_in結構提前將數據轉化為網絡字節序。轉換字節的函數:
unsigned short htons (unsigned short);
unsigned short ntohs(unsigned short);
unsigned long htons (unsigned long);
unsigned long ntohs (unsigned long);
通過函數名可以知道他的功能,只要了解一下細節。htons中的h代表主機(host)字節序,n代表網絡字節序。s指的是short,l指的是long(linux 中long類型占用4個字節),可以解釋為"把short類型數據從主機字節序轉化為網絡字節序"。所以ntohs也就知道了吧。我們通過例子來看一下效果:
運行結果之后會看到如下圖結果:
這就是小端序cpu運行結果,如果在大端值中是不會發生變化的。Intel和AMD系列的cpu都采用小端序標准。數據在傳輸過程中需要經過轉換嗎?實際上沒有必要,這個過程是自動的。除了想sockaddr_in結構體變量填充數據外,其他情況不需要考慮字節序問題。
網絡地址的初始化與分配
sockaddr_in 中保存地址信息的成員為32位整數型。因此,為了分配IP地址將其轉化為32位整數型數據。對我們而言並非易事。對於IP地址的的表示,我們熟悉的是點分十進制,而非整型數據表示法。幸運的是,有個函數可以幫我們將字符串形式的IP地址轉化為32位的整型數據。此函數在轉換類型的同時進行網絡字節序轉換。
#include <arpa/inet.h> in_addr_t inet_addr(const char* string) 成功時返回32位大端序整型數據,失敗時返回INADDR_NONE
下面是測試代碼:
運行結果如下圖所示:
從運行結果可以看出,inet_addr函數不僅可以把ip地址轉換為32位整數,而且還可以檢測無效的ip地址。並且輸出的確實是網絡字節序。還有一個函數與inet_addr函數功能完全相同,只不過該函數利用了in_addr結構體,且使用頻率更高。
#include <arpa/inet.h> int inet_aton(const char *string,struct in_addr *addr) 成功時返回1,失敗時返回0;
實際編程中若要調用inet_addr函數,需要將轉化后的IP地址信息代入sockaddr_in結構體中聲明的in_addr結構體變量。而inet_aton函數不需要此過程。原因在於,若傳遞in_addr結構體變量地址值,函數會自動把結果填入該結構體變量。ok,下面再講解一個把網絡字節序整數型IP地址轉換成我們熟悉的字符串形式。
#include <arpa/inet> char *inet_ntoa(struct in_addr adr); 成功時返回轉換的字符串地址,失敗時返回-1。
但在調用時小心,返回值是char類型的指針。返回字符串地址意味着字符串已保存到內存空間,但該函數未向程序員要求分配內存,而是在內部申請了內存並保存了字符串。也就是說,調用完函數后,應該立即將字符串信息復制到其他內存空間。因為在此調用該函數,則有可能覆蓋之前保存的字符串信息。總之,再次調用該函數前返回的字符串地址值是有效的。如要長期保存,則應將字符串復制到其他內存空間。示例:
運行結果如下:
下面我把之前的代碼完全重新組合一下。
服務端代碼:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 #include <unistd.h> 5 #include <arpa/inet.h> 6 #include <sys/socket.h> 7 8 void ErrorMessage(char *message); 9 10 int main(int argc,char *argv[]) 11 { 12 int serv_sock; 13 int client_sock; 14 struct sockaddr_in serv_addr; 15 struct sockaddr_in client_addr; 16 char *serverIP= "127.0.0.1"; 17 char *servPort = "9190"; 18 char message[]="Hi,TCPIP"; 19 socklen_t clnt_addr_size; 20 serv_sock = socket(PF_INET,SOCK_STREAM,0); 21 if(serv_sock==-1) 22 { 23 ErrorMessage("Sock Error!"); 24 } 25 memset(&serv_addr,0,sizeof(serv_addr)); 26 serv_addr.sin_family=AF_INET; 27 serv_addr.sin_addr.s_addr=inet_addr(serverIP); 28 serv_addr.sin_port = htons(atoi(servPort)); 29 if(bind(serv_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==-1) 30 { 31 ErrorMessage("Bind() Error"); 32 } 33 if(listen(serv_sock,5)==-1) 34 { 35 ErrorMessage("listen() error"); 36 } 37 clnt_addr_size = sizeof(client_addr); 38 client_sock = accept(serv_sock,(struct sockaddr*)&client_addr,&clnt_addr_size); 39 if(client_sock==-1) 40 { 41 ErrorMessage("accept() error"); 42 } 43 write(client_sock,message,sizeof(message)); 44 close(client_sock); 45 close(serv_sock); 46 return 0; 47 } 48 void ErrorMessage(char *message) 49 { 50 fputs(message,stderr); 51 fputc('\n',stderr); 52 exit(1); 53 }
客戶端代碼:

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> void ErrorMessage(char *message); int main(int argc,char* argv[]) { int sock; struct sockaddr_in serv_addr; char message[30]; char *serv_port = "9190"; int str_len; sock = socket(PF_INET,SOCK_STREAM,0); if(sock==-1) { ErrorMessage("socket() error"); } memset(&serv_addr,0,sizeof(serv_addr)); serv_addr.sin_family=AF_INET; serv_addr.sin_addr.s_addr=htonl(INADDR_ANY); serv_addr.sin_port=htons(atoi(serv_port)); if(connect(sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==-1) { ErrorMessage("connect() error"); } str_len = read(sock,message,sizeof(message)-1); if(str_len==-1) { ErrorMessage("read() error"); } printf("Message from server:%s\n",message); close(sock); return 0; } void ErrorMessage(char *message) { fputs(message,stderr); fputc('\n',stderr); exit(1); }
運行結果如下圖所示: