TCP/IP網絡編程_1.3基於Windows平臺的實現

1.3基於Windows平臺的實現

Windows 套接字(以下簡稱Winsocket) 大部分是參考BSD系列UNIX套接字設計的, 所以很多地方都跟Linux 套接字類型. 因此, 只需更改Linux 環境下編寫好的一部分網絡編程內容, 就能在Windows 平臺下運行. 本書也會同時講解Linux 和 Windows兩大平臺, 這不會給大家增加負擔, 反而會減輕負擔.

同時學習Linux 和 Windows 的原因

大多數項目都在Linux 系列的操作系統下開發服務端, 而大多數客戶端是在Windows平臺下開發的. 不僅如此, 有時應用程序還需要兩個平臺之間相互切換. 因此, 學習套接字編程的過程中 , 有必要兼顧Windows 和 Linux 兩大平臺. 另外, 這兩大平臺下的套接字編程非常類似, 如果把其中相似的一部分放在一起講解, 將大大提高學習效率. 這會不會增加學習負擔? 一點也不. 只有理解好其中一個平臺下的網絡編程方法, 就很容易通過分析差異掌握另一平臺.

爲Windows 套接字編程設置頭文件和庫

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

Winsock 的初始化

進行Winsock 編程是, 首先必須調用 WSAStarup 函數, 設置程序中用到的Winsock 版本, 並初始化相應版本的庫.
在這裏插入圖片描述
有必要給出上述兩個參數的詳細說明. 先說第一個, Winsock 中存在多個版本, 應準備 WORD 類型的( WORD 是通過typedef 聲明定義的 unsigned short 類型) 套接字版本信息, 並傳遞給該函數的第一個參數w VersionRequested. 若版本爲1.2 則其中1 是主版本號, 2 是副版本號, 應傳遞0x0201.

如前所述, 高8位爲副版本號, 低8位主版本號, 以此進行傳遞. 本書主要使用2.2版本, 古應傳遞0x0202. 不過, 以字節爲單位手動構造版本信息有些麻煩, 藉助MAKEWORD 宏函數則能輕鬆構建WORD 型版本信息.
在這裏插入圖片描述
接下來講解第二個參數lpWSADATA, 次參數中需要傳入 WSADATA 型結構體變量地址(LPWSADATA是WSADATA的指針類型). 調用完函數後, 相應參數中將填充已初始化的庫信息. 雖無特殊含義, 但爲了調用函數, 必須傳遞WSADATA 結構體變量地址. 下面給出WSAStartup 函數調用過程, 這段代碼幾乎已成爲Winsock 編程的公式.
在這裏插入圖片描述
前面已經介紹了Winsock相關庫的初始化方法, 接下來講解如何註銷該庫–利用下面給出的函數.
在這裏插入圖片描述
調用該函數時, Winsock 相關庫將歸還Windows 操作系統, 無法再調用Winsock相關函數. 從原則上講 , 無需再使用Winsock函數時才調用該函數, 但通常都在結束之前調用.

1.4 基於Windows 的套接字相關函數及示例

本節介紹的Winsock 函數與之前的Linux 套接字相關函數對應. 既然只是介紹, 就不做詳細說明了, 目的只在於讓各位體會基於Linux 和 Windows 的套接字函數之間的相似性.

基於 Windows 的套機字相關函數

首先介紹的函數與 Linux 下的 socket 含數提供相同功能. 稍後講解返回值類型SOCKET.
在這裏插入圖片描述
下列函數與 Linux 的 bind函數相同, 調用其分配IP地址和端口號.
在這裏插入圖片描述
下列函數與Linux 的 listen 函數相同, 調用其使套接字可接收客戶端的連接.
在這裏插入圖片描述
下列函數與 Linux 的 accept 函數相同, 調用其受理客戶端連接請求.
在這裏插入圖片描述
下列函數與Linux 的 connect 函數相同, 調用其從客戶端發送連接請求.
在這裏插入圖片描述
最後這個函數在關閉套機字時調用. Linux 中, 關閉文件可套接字是都會調用close 函數; 而Windows 中有專門用來關閉套接字的函數.
在這裏插入圖片描述
以上就是基於 Windows 的套接字相關函數, 雖然返回值和參數與 Linux 函數有所區別, 但具有相同功能的函數名是一樣的, 正是這些特點時跨越兩大操作系統平臺的網絡編程更加簡單.

Windows 中的文件句柄和套接字句柄

Linux 內部也將套機字當做文件, 因此, 不管創建文件還是套機字 都返回文件描述符. 之前也通過實例介紹了文件描述符返回及編號的過程. Windosw 中通過調用系統函數創建文件時, 返回"句柄(handle)", 換言之, Windows 中的句柄相當於Linux 中的文件描述符. 只不過Windows 中要區分文件句柄和套接字句柄. 雖然都稱"句柄", 但不想 Linux 那樣完全一致. 文件句柄相關函數與套接字相關函數有區別的, 這一點不同於Linux 文件描述符.

既然對句柄有了一定理解, 接下來再觀察基於Windows 的套接字相關函數, 這將加深各位對SOCKET 類型的參數和返回值的理解. 的確! 這就是爲了保存套接字句柄整型值的新數據類型, 它由typedef 聲明定義. 回顧socket , listen 和 accept 等套接字相關函數, 則更能體會與 Linux 中套接字相關函數的相似性.

有些程序員可能會問: “既然Winsock 是以UNIX, Linux系列的BSD套接字以原始設計的, 爲什麼不照搬過來, 而是存在一定差距呢?” 有人認爲這是微軟爲了防止UNIX, Linux服務器端直接移植到 Windows 而故意爲之. 從網絡程序移植角度上看, 這也是可以理解的. 但我有不同意見. 從本質上說, 這兩種操作系統內核結構上存在巨大差異, 而依賴於操作系統的代碼實現風格也不盡相同, 連 Windows 程序員給變量命名的方式也不同於Linux 程序員. 從各方面考慮, 保存這種差異性就顯得比較自然. 因此我個人認爲, Windows 套接字與BSD系列的套接字編程方式有所不同是爲了保持這種自然差異性.

創建基於 Windows 的服務器端和客戶端

接下來將之前基於Linux 的服務器端與客戶端實例轉換到 Windows 平臺. 目前想完全理解這些代碼有些困難, 我們只需驗證套接字相關函數的調用過程, 套接字庫的初始化與註銷過程即可.
先介紹服務器實例.

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

void ErrorHandling(const char* message);

int main(int argc, char* argv[])
{
	WSADATA wsaData;
	SOCKET hServSock, hClntSock;
	SOCKADDR_IN servAddr, clntAddr;

	int szClntAddr;
	char message[] = "Hello World!";
	if (argc != 2)
	{
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}

	/* 初始化套接字 */
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		ErrorHandling("WSAStartup() error!");
	}

	/* 創建套接字 */
	hServSock = socket(PF_INET, SOCK_STREAM, 0);
	if (hServSock == INVALID_SOCKET)
	{
		ErrorHandling("socket() error!");
	}

	/* 給該套機字分配IP地址與端口號. */
	memset(&servAddr, 0, sizeof(servAddr));
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servAddr.sin_port = htons(atoi(argv[1]));

	if (bind(hServSock, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
	{
		ErrorHandling("bind() error!");
	}

	/* 調用listen函數使第28行創建的套機字成爲服務器端套機字 */
	if (listen(hServSock, 5) == SOCKET_ERROR)
	{
		ErrorHandling("listen() error!");
	}

	szClntAddr = sizeof(clntAddr);
	/* 調用accept函數受理客戶端連接請求 */
	hClntSock = accept(hServSock, (SOCKADDR*)&clntAddr, &szClntAddr);
	if (hClntSock == INVALID_SOCKET)
	{
		ErrorHandling("accept() error");
	}

	/* 調用send函數向第53行連接的客戶端傳輸數據 */
	send(hClntSock, message, sizeof(message), 0);

	closesocket(hClntSock);
	closesocket(hServSock);
	WSACleanup();

	return 0;
}

void ErrorHandling(const char* message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}
#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>

void ErrorHandling(const char* message);

int main(int argc, char* argv[])
{
	WSADATA wsaData;
	SOCKET hSocket;
	SOCKADDR_IN servAddr;

	char message[30];
	int strLen;
	if (argc != 3)
	{
		printf("Usage : %s <IP> <port>\n", argv[0]);
		exit(1);
	}

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

	hSocket = socket(PF_INET, SOCK_STREAM, 0);
	if (hSocket == INVALID_SOCKET)
	{
		ErrorHandling("socket() error!");
	}

	memset(&servAddr, 0, sizeof(servAddr));
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.s_addr = inet_addr(argv[1]);
	servAddr.sin_port = htons(atoi(argv[2]));

	if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
	{
		ErrorHandling("connect() error");
	}

	strLen = recv(hSocket, message, sizeof(message) - 1, 0);
	if (strLen == -1)
	{
		ErrorHandling("recv() error");
	}
	printf("Message from server: %s\n", message);

	closesocket(hSocket);
	WSACleanup();

	return 0;
}

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

運行環境: vs 2019
運行結果:
在這裏插入圖片描述
在這裏插入圖片描述
shift + 右鍵 -> 有一個 : 在此處打開powershell窗口

基於 Windows 的I/O 函數

Linux 中套接字也是文件, 因而可以通過文件I/O函數read和write進行數據傳輸. 而Windows 中則不同. Windows 嚴格區分文件I/O 函數和套接字I/O函數. 下面介紹Winsock數據傳輸函數.
在這裏插入圖片描述

此函數與Linux 的write函數相比, 只是多出了最後的flags參數. 後續章節中將會給出該參數的詳細說明, 在此之前需要注意, send 函數並非Windows 獨有. Linux 中也有同樣的函數, 它也是來自BSD 套接字. 只不過我在 Linux 相關實例中暫時只使用 read, write函數, 爲了強調 Linux 環境下文件I/O 和 套接字I/O相同. 下面介紹與 send 函數對應的recv函數.
在這裏插入圖片描述
我只是在 Windows 環境下提前介紹了 send, recv 函數, 以後的 Linux 實例中也會涉及. 請不要誤認爲 Linux 中的read , write 函數就是對應於Windows 的send, recv 函數. 另外, 之前的程序代碼中也給出了 send, recv 函數調用過程, 故不再另外給出相關實例.
在這裏插入圖片描述
1.5 習題
(1)套接字在網絡編程的作用是什麼? 爲何稱它爲套接字?

P2,網絡編程就是編寫程序讓兩臺聯網的計算機相互交換數據。在我們不需要考慮物理連接的情況下,我們只需要考慮如何編寫傳輸軟件。操作系統提供了名爲“套接字”,套接字是網絡傳輸傳輸用的軟件設備

socket英文原意是插座:我們把插頭插到插座上就能從電網獲得電力供給,同樣,爲了與遠程計算機進行數據傳輸,需要連接到Internet,而變成中的“套接字”就是用來連接該網絡的工具

(2)在服務器端創建套接字後,會依次調用listen函數和accept函數。請比較並說明兩者作用
listen:將套接字轉爲可接受連接方式

accept:受理連接請求,並且在沒有連接請求的情況調用該函數,不會返回。直到有連接請求爲止。二者存在邏輯上的先後關係

(3)Linux中,對套接字數據進行I/O時可以直接使用I/O相關函數;而在Windows中則不可以。原因爲何?
Linux把套接字也看作是文件,所以可以用文件I/O相關函數;而Windows要區分套接字和文件,所以設置了特殊的函數

(4)創建套接字後一般會給它分配地址,爲什麼?爲了完成地址分配需要調用哪些函數?
要在網絡上區分來自不同機器的套接字,所以需要地址信息。分配地址是通過bind()函數實現

(5)Linux中的文件描述符與Windows的句柄實際上非常類似。請以套接字爲對象說明他們的含義。
Linux的文件描述符是爲了區分指定文件而賦予文件的整數值(相當於編號)。Windows的文件描述符其實也是套接字的整數值,其目的也是區分指定套接字。

(6)底層文件I/O函數與ANSI標準定義的文件I/O函數之間有何區別?

ANSI標準定義的輸入、輸出函數是與操作系統(內核)無關的以C標準寫成的函數。相反,底層文件I/O函數是直接提供的。理論上ANSI標準I/O提供了某些機制,性能上由於底層I/O

時間: 2020_05_22

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