TCP/IP網絡編程_第3章地址族與數據序列 3.4-3.6

3.4 網絡地址的初始化與分配

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

將字符串信息轉換爲網絡字節序的整型數
sockaddr_in 中保存地址信息的成員爲32位整數型. 因此, 爲了分配IP 地址, 需要將其表示爲 32 位整數型數據. 這對於只熟悉字符串信息的我們來說實非易事. 各位可以嘗試將 IP 地址 201.211.214.36 轉換爲4字節整數型數據.

對於IP 地址的表示, 我們熟悉的是分十進制表示法(Dotted Decimal Notation), 而非整數型數據表示法. 幸運的是, 有一個函數會幫我們將字符串形式的 IP 地址轉換成32位整數. 此函數在轉換類型的同時進行網絡字節序轉換.
在這裏插入圖片描述
如果向該函數傳遞類似 “211.214.107.99” 的點分十進制格式的字符串, 它會將其轉換爲32位整數型數據並返回. 當然, 該整數型值滿足網絡字節序. 另外, 該函數的返回值類型in_addr_t 在內部聲明爲32位整數型. 下列實例表示該函數的調用過程.

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

int main(int argc, char *argv[])
{
    char *addr1 = "1.2.3.4";
    /* 1個字節能表示的最大整數爲255, 也就是說, 它是錯誤的IP地址. 
    利用該錯誤地址驗證inet_addr函數的錯誤檢驗能力 */
    char *addr2 = "1.2.3.256";

    /* 通過運行結果驗證第9行的函數正常調用 */
    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 occureded \n");
    }
    else
    {
        printf("Network ordered integer addr: %#lx \n\n", conv_addr);
    }
    
    return 0;
}

運行結果:
在這裏插入圖片描述
從運行結果可以看出, inet_addr函數不僅可以把IP地址轉成32位整數型, 而且可以檢測無效的IP地址. 另外, 從輸出結果可以驗證確實轉換爲網絡字節序.

inet_aton函數與inet_addr函數在功能上完全相同, 也將字符串形式IP地址轉換爲32位網絡字節序整數並返回. 只不過該函數利用了 in_addr 結構體, 且其使用頻率更高.
在這裏插入圖片描述
實際編程中若要調用inet_addr 函數, 需將轉換後的 IP 地址信息代入sockaddr_in結構體中聲明的 in_addr 結構體變量. 而inet_aton函數則不需此過程. 原因在於, 若傳遞 in_addr結構體變量地址值, 函數會自動把結果填入該結構體變量. 通過實例瞭解 inet_aton 函數調用過程.

#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;

    /* 轉換後的IP地址信息需保存到sockaddr_in的in_addr變量纔有意義. 
    因此, inet_aton函數的第二個參數要求得到in_addr型變量地址值. 這就
    省去了手動保存IP地址信息的過程. */
    if (inet_aton(addr, &addr_inet.sin_addr) == 0)
    {
        error_handling("Conversion error");
    }
    else
    {
        printf("Network ordered intager addr: %#x \n",
        addr_inet.sin_addr.s_addr);
    }
    
    return 0;
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

運行結果:
在這裏插入圖片描述
上述結果無關緊要, 更重要的是大家要熟練掌握該函數的調用方法. 最後再介紹一個與inet_aton 函數正好相反的函數, 此函數可以把網絡字節序整數型IP地址轉換成我們熟悉的字符串形式.
在這裏插入圖片描述
該函數將通過參數傳入的整數型 IP 地址轉換爲字符串格式並返回. 但調用時需小心, 返回值類型爲char 指針. 返回字符串地址意味着字符串已保存到內存空間, 但該函數未向程序員要求分配內存, 而是在內部申請了內存並保存了字符串. 也就是說, 調用完該函數後, 應立即將字符串信息複製到其他內存空間. 因爲, 若再次調用inet_ntoa 函數, 則有可能覆蓋之前保存的字符串信息. 總之, 再次調用inet_ntoa 函數前返回的字符串地址值是有效的. 若需要長期保存, 則應將字符串複製到其他內存空間. 下面給出該函數調用實例.

#include <stdio.h>
#include <string.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);

    /* 向inet_ntoa函數傳遞結構體變量addr1中的IP地址信息並調用該函數, 返回字符串型的IP地址. */
    str_ptr = inet_ntoa(addr1.sin_addr);
    /* 瀏覽並複製第15行中返回的IP地址信息 */
    strcpy(str_arr, str_ptr);
    printf("Dotted-Decimal notationl: %s \n", str_ptr);

    /* 再次調用inet_ntoa函數. 由此得出, 第15行中返回的地址已覆蓋了新的IP地址字符串, 
    可通過第23行的輸出結果進行驗證. */
    inet_ntoa(addr2.sin_addr);
    printf("Dotted-Decimal ntoation2: %s \n", str_ptr);
    /* 第17行中複製了字符串, 因此可以正確輸出第15行中返回的IP地址字符串. */
    printf("Dotted-Decimal notation3: %s \n", str_arr);
    
    return 0;
}

運行結果:
在這裏插入圖片描述

網絡地址初始化

結合前面所學的內容, 現在介紹套接字創建過程中常見的網絡地址信息初始化方法.
在這裏插入圖片描述
上述代碼中, memset 函數將每個字節初始化爲同一值: 第一個參數爲結構體變量 addr 的地址值, 即初始化對象爲addr; 第二個參數爲0, 因此初始化爲0; 最後一個參數中傳入 addr 的長度, 因此addr的所有字節均初始化0, 這麼做是爲了將 sockaddr_in 結構體的成員 sin_zero 初始化爲0. 另外, 最後一行代碼調用的 atoi 函數把字符串類型的值轉換成整數型. 總之, 上述代碼利用字符串格式的IP地址和端口號初始化了 sockaddr_in 結構體變量.

另外, 代碼中對IP地址和端口號進行了硬編碼, 這並非良策, 因爲運行的環境改變就得更改代碼. 因此, 我們運行實例main函數時傳入IP地址和端口號.

客戶端地址信息初始化

上述網絡地址信息初始化過程主要針對服務器端而非客戶端 .給套接字分配IP地址和端口號主要是爲下面這件事做準備:

在這裏插入圖片描述
反觀客戶端中的連接請求如下:
在這裏插入圖片描述
請求方法不同意味這調用的函數也不同. 服務器端的準備工作通過bind函數完成, 而客戶端則通過 connect 函數完成. 因此, 函數調用前需要準備的地址值類型也不同. 服務器端聲明sockaddr_in 結構體變量, 將其初始化爲賦予服務器端 IP 和套接字的端口號, 然後調用bind 函數; 而客戶端則聲明sockaddr_in 結構體, 並初始化要爲之連接的服務器端套接字的IP 和端口號, 然後調用connect 函數.

INADDR_ANY

每次創建服務器套接字都要輸入IP地址會有些繁瑣, 此時可如下初始化地址信息.
在這裏插入圖片描述
與之前方式最大的區別在於, 利用常數INADDR_ANY 分配服務器端的IP地址. 若採用這種方式, 則可以自動獲取運行的服務器端的計算機IP地址, 不必親自輸入. 而且, 若同一計算機中已分配多個IP地址(多宿主(Multi-homed) 計算機, 一般路由器屬於這一種), 則只要端口號一致, 就可以從不同IP地址接收數據. 因此, 服務器端優先考慮這種方式. 而客戶端中除非帶有一部分服務器端功能, 否則不會採用.
在這裏插入圖片描述
初始化服務器端套機字是應分配所屬計算機的IP地址, 應爲初始化時使用的IP地址非常明確, 那爲何還要進行IP地址初始化呢? 如前所述, 同一計算機中可以分配多個IP地址, 實際IP地址的個數與計算機中安裝的NIC數量相等. 即使是服務器端套機字, 也需要決定應接收那個IP 傳來的(哪個NIC 傳來的) 數據. 因此, 服務器端套接字初始化過程中要求IP地址信息. 另外, 若只有一個NIC, 則直接使用INADDR_ANY.

第一章的 Hello_server.c Hello_client.c 運行過程
第一章中執行以下命令以運行相當於服務器端的hello_server.c
在這裏插入圖片描述
通過代碼可知, 先main 函數傳遞的9190 爲端口號. 通過此端口創建服務器端套接字並運行程序, 但未傳遞IP地址, 因爲可以通過INADDR_ANY指定IP地址. 相信各位現在再去讀代碼會感到簡單很多.

執行下列命令以運行相當於客戶端的hello_client.c. 與服務器端運行方式相比, 最大的區別是傳遞了IP地址信息.
在這裏插入圖片描述
127.0.0.1 是回送地址(loopback addrress), 是指計算機自身IP地址. 在第1章的實例中, 服務器端和客戶端在同一計算機中運行, 因此, 連接目標服務器端的地址爲127.0.0.1. 當然, 若用實際IP地址代替此地址也能正常運行. 如果服務器端和客戶端分別在2臺計算機中運行, 則可以輸入服務器端 IP 地址.

向套接字分配網絡地址

既然已討論了 sockaddr_in 結構體的初始化方法, 接下來就把初始化的地址信息分配給套機字. bind 函數負責這項操作.
在這裏插入圖片描述
在這裏插入圖片描述
如果此函數調用成功, 則將第二個參數指定的地址信息分配給第一個參數中相應套接字.下面給出服務器端常見套接字初始化過程.
在這裏插入圖片描述
服務器端代碼結構默認如上, 當然還有未顯示的異常處理代碼.

3.5 基於Windows 的實現

windows 中同樣存在 sockaddr_in 結構體及各種變換函數, 而且名稱, 使用方法及含義都相同. 也就無需針對Windows 平臺進行太多修改或改用其他函數. 接下來將前面幾個程序改成 Windows 版本.

函數 htons, htonl 在 Windows 中的使用
首先給出 Windows 平臺下調用 htons 函數的實例. 這兩個函數的用法與 Linux 平臺下的使用並無區別, 故省略.

#include <stdio.h>
#include <WinSock2.h>

void ErrorHandling(const char* message);

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

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		ErrorHandling("WSAStartup() erro");
	}

	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);

	WSACleanup();
	return 0;
}

void ErrorHandling(const char* message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

運行結果:
在這裏插入圖片描述
該程序多了進行庫初始化的 WSAStartup 函數調用和 winsock2.h 頭文件的#include 語句, 其他部分沒有區別.

函數 inet_addr 、inet_ntoa 在 Windows 中的使用

下列實例給出了 inet_addr 函數和 inet_ntoa函數的調用過程. 前面分別給出了 Linux 中這兩個函數的調用實例, 而在 Windows 中不存在 inet_aton 函數, 故省略.

#include <stdio.h>
#include <string.h>
#include <WinSock2.h>

void ErrorHandling(const char* message);

int main(int argc, char* argv[])
{
	WSADATA wsaData;
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		ErrorHandling("WSAStartup() error");
	}
	
	/* inet_addr函數調用示例 */
	{
		const char* addr = "127.212.124.78";
		unsigned long conv_addr = inet_addr(addr);
		if (conv_addr == INADDR_ANY)
		{
			printf("Error occurd! \n");
		}
		else
		{
			printf("Network ordered integer addr: %#lx \n", conv_addr);
		}
	}
	

	/* inet_ntoa函數調用示例 */
	{
		struct sockaddr_in addr;
		char* strPtr;
		char strArr[20];

		addr.sin_addr.s_addr = htonl(0x1020304);
		strPtr = inet_ntoa(addr.sin_addr);
		strcpy(strArr, strPtr);
		printf("Dotted-Decimal notation3 %s \n", strArr);
	}

	WSACleanup();
	return 0;
}

void ErrorHandling(const char* message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

運行結果:
在這裏插入圖片描述
上述實例在main函數體內使用括號增加變量聲明, 同時區分各函數的調用過程. 添加中括號可以在相應區域的初始化部分聲明局部變量. 當然, 此類局部變量跳出括號則消失.

在 Windows 環境下向套接字分配網絡地址

Windows 中向套接字分配網絡地址的過程與 Linux 中完全相同, 因爲bind 函數的含義, 參數及返回類型完全一致.
在這裏插入圖片描述
這與Linux 平臺下套接字初始化及地址分配過程基本一致, 只不過改了一些變量名.

WSAStringToAddress & WSAAddressToString

下面介紹 Winsock2中增加的2個轉換函數. 它們在功能上與 inet_ntoa 和 inet_addr 完全相同, 但優點在於支持多種協議, 在 IPV4 和 IPV6 中均可適用, 當然它們也是有缺點的, 適用 inet_ntoa、inet_addr 可以很容易地在 Linux 和 Windows 之間切換程序. 而將要介紹的這2個函數則依賴於特定平臺, 會降低兼容性. 因此本書不會使用它們, 介紹的目的僅在瞭解更多函數.

先介紹WSAStringToAddress 函數, 它將地址信息字符串適當填入結構體變量.
在這裏插入圖片描述
在這裏插入圖片描述
下面給出這兩個函數的使用示例.

書本給的代碼我無法實現報錯, 也不會改, 代碼如下, 等到我以後功力行了再回來改吧!

#undef UNICODE
#undef _UNICODE
#include <stdio.h>
#include <WinSock2.h>

int main(int argc, char* argv[])
{
	const char* strAddr = "203,211,218,102:9190";

	char strAddrBuf[50];
	SOCKADDR_IN servAddr;
	int size;

	WSADATA wsaData;
	WSAStartup(MAKEWORD(2, 2), &wsaData);

	size = sizeof(servAddr);
	WSAStringToAddress((char*)strAddr, AF_INET, NULL, (SOCKADDR*)&servAddr, &size);

	size = sizeof(strAddrBuf);
	WSAAddressToString((SOCKADDR*)&servAddr, sizeof(servAddr), NULL, strAddrBuf, &size);

	printf("Second conv result: %s \n", strAddrBuf);
	WSACleanup();
	
	return 0;
}

書上的運行結果:
在這裏插入圖片描述

3.6 習題

(1) IP地址族 IPV4 和 IPV6 有何區別? 在何種背景下擔生了 IPV6?

時間: 2020:05:25

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章