TCP/IP網絡編程_第2章套接字類型與協議

第2章套接字類型與協議

因爲涉及套機字編程的基本內容, 所以第2章和第3章顯得相對枯燥一些. 但本章內容是第4章介紹的實際網絡編程基礎, 希望各位反覆精讀.

大家已經對套接字的概念有所理解, 本章將講解套接字創建方法及不同套接字的特性. 在本章僅需瞭解創建套機字調用的socket 函數, 所以希望大家以輕鬆的心態開始學習.

2.1 套接字協議及其數據傳輸特性

“協議” 這個詞給人的第一印象總是相當困難, 我在學生時代也是這麼想. 但各位要慢慢熟悉"協議", 因爲它幾乎是網絡編程的全部內容. 首先解析其定義.

關於協議(Protocol)
如果相隔很遠的兩個人想展開對話, 必須先決定對話的方式. 如果一方使用電話, 那麼另一方也只能使用電話, 而不是書信. 可以說, 電話就是兩個人對話的協議. 協議是對話中使用的通訊規則, 把上述概念擴展到計算機領域可整理爲"計算機之間對話必備通信規則".

各位是否已理解了協議的含義? 簡而言之, 協議就是爲了完成數據交換而定好的約定.

創鍵套機字
創建套機字所用的socket函數已經在第1章中簡單介紹過. 但爲了完全理解該函數, 此處將再次展開討論, 本章的目的也是於此.
在這裏插入圖片描述
第一章並未提及該函數的參數, 但他們對創建套機字來說是不可或缺的. 下面給出詳細說明.

協議族(Protocol Family)
奶油意大利和番茄醬意大利麪均屬於意大利麪的一種, 與之類似, 套機字通信中的協議也具有一些分類. 通過socket 函數的第一個參數傳遞套機字中使用的協議分類信息. 次協議分類信息稱爲協議族, 可分成如下幾類.
在這裏插入圖片描述
本書將着重講解表 2-1 中 PF_INET 對應的IPV4 互聯網協議族. 其他協議族並不常用或未普及, 因此本書將重點放在 PF_INTE 協議族上, 另外, 套機字中實際採用的最終協議信息是通過 socket 函數的第三個參數傳遞的. 指定的協議族範圍內通過第一個參數決定第三個參數.

套機字類型(Type)
套機字類型指的是套機字的數據傳輸方式, 通過socket 函數的第二個參數傳遞, 只有這樣才能決定創建的套機字是數據傳輸方式. 這種說法可能會使各位感到疑惑. 已通過第一個參數傳遞了協議信息, 還有決定數據傳輸方式? 問題就在於, 決定了協議族並不能同時決定數據傳輸方式, 換言之, socket 含數第一個參數 PF_INET 協議中也存在多種數據傳輸方式.

下面介紹2種具有代表性的數據傳輸方式. 這是理解好套接字的重要前提. 請各位務必掌握.

套接字類型 1: 面向連接的套接字(SOCK_STREAM)
如果向socket 函數的第二個參數傳遞 SOCK_STREAM , 將創建面向連接的套接字. 面向連接的套接字到底具有哪些特點呢? 右圖的數據(糖果)傳輸方式特徵整理如下.
在這裏插入圖片描述
(1) 傳輸過程中數據不會消息.
(2) 按序傳輸數據
(3) 傳輸數據不存在數據邊界(Boundary)
圖中通過獨立的傳輸數據(糖果), 只要傳送帶本身沒有問題, 就能保證數據不會丟失. 同時, 較晚傳遞的數據不會先到達, 因爲傳送帶保證了數據的按序傳遞, 最後, 下面這句話說明的確不存在數據邊界:

“100個糖果是分批傳遞的, 但接收者湊齊100個裝袋.”

這種情況可以適用到之前說過的write 和 read 函數.

"傳遞數據的計算機通過3次調用write 函數傳遞了 100 字節的數據, 但接收數據的計算機僅通過1次read 函數調用就接收了全部100個字節. "

收發數據的套機字內部有緩存(buffer), 簡而之就是字節數組. 通過套機字傳輸的數據將保存到該數組. 因此, 收到數據並意味着馬上調用read 函數. 只要不超過數組容量, 則有可能在數據填充滿緩衝後通過1次read 函數調用讀取全部, 也有可能分成多次 read 函數調用進行讀取. 也就是說, 在面向連接的套機字中, read函數和 write 函數的調用次數並無太大意義. 所以說面向連接的套接字不存在數據邊界. 稍後將給出示例以查看該特性.

在這裏插入圖片描述
之前講過, 爲了接受數據, 套機字內部有一個由字節數組構成的緩衝. 如果這個緩衝被接受的數據填滿會發生什麼事情? 之後傳遞的數據是否會丟棄?

首先調用 read 函數從緩衝讀取部分數據, 因此, 緩衝並不總是滿的. 但如果 read 函數讀取速度比接受數據的速度慢, 則緩衝有可能被填滿. 此時套接字無法再接受數據, 但即使這樣也不會發生數據丟失, 因爲傳輸端套接字將停止傳輸. 也就是說, 面向連接的套接字會根據接收端的狀態傳輸數據, 如果傳輸出錯還會提供重傳服務. 因此, 面向連接的套機字除特殊情況外不會發生數據丟失.

還有一點需要說明. 上面中傳輸和接收端各有一名工人, 這說明面向連接的套接字還有如下特點:

“套接字連接必須一一對應.”

面向連接的套接字只能與另外一個同樣特性的套接字連接. 用一句話概括面向連接的套接字如下:

“可靠的, 按序傳遞的, 基於字節的面向連接的數據傳輸方式的套機字”

這是我自己總結, 希望各位深入理解其含義, 不要僅停留於字面表達.

套接字類型2: 面向消息的套機字(SOCK_DGRAM)
如果向 socket 函數的第二個參數傳遞 SOCK_DGRAM, 則將創建面向消息的套機字. 面向消息的套接字可以比喻成高速移動的摩托車快遞. 右圖中摩托車快遞的包裹(數據)傳輸方式如下.
(1) 強調快速傳輸而非傳輸順序.
(2) 傳輸的數據可能丟失也可能損毀.
(3) 傳輸的數據有數據邊界.
(4) 限制每次傳輸的數據大小.

總所周知, 快遞行業的速度就是生命. 用摩托車發往同一目標地的2件包裹無需保證順序, 只有以最快速度交給客戶即可. 這種方式存在損壞或丟失的風險, 而且包裹大小有一定限制. 因此, 若要傳遞大量包裹, 則需分批發送. 另外用2輛摩托車分別發送2件包裹, 則接收者也需要分2次接收. 這種特性就是"傳輸的數據具有數據邊界".

以上就是面向消息的套接字具有的特性. 即, 面向消息的套機字比面向連接的套接字具有更快的傳輸速度, 但無法避免數據丟失或損失. 另外, 每次傳輸的數據大小具有一定限制, 並存在數據邊界. 存在數據邊界意味着接收次數的和傳輸次數相同. 面向消息的套接字特性總結如下:

不可靠的, 不按序傳遞的, 以數據的高速傳輸爲目的的套接字

另外, 面向消息的套接字不存在連接的概念, 這一點將在以後章節介紹.

協議的最終選擇
下面講解 socket 函數的第三個參數, 該參數決定最終採用的協議. 各位是否覺有些困惑?

前面已經通過 socket 函數的前兩個參數 傳遞協議信息套接字數據傳遞方式, 這些信息還不足以決定採用的協議嗎? 爲什麼還需要第3個參數呢?

正如各位所想, 傳遞前兩個參數即可創建所需套機字. 所以大部分情況下可以向第三個參數傳遞0, 除非遇到以下情況:

“同一協議族中存在多個數據傳輸方式相同的協議”

數據傳輸方式相同, 但協議不同. 此時需要通過第三個參數具體指定協議信息.

下面以前講解內容爲基礎, 構建向 socket 函數傳遞的參數. 首先創建滿足如下要求的的套接字:

“IPV4 協議族中面向連接的套接字”

IPV4 與網絡地址系統相關, 關於這一點將給出單獨說明, 目前: 本書是基於 IPV4 展開的. 參數 PF_INET 值IPV4 網絡協議族, SOCKET__STREAM 是面向連接的數據傳輸. 滿足這2條件的協議 只有IPPROTO_TCP, 因此可以如下調用 socket 函數創建套接字, 這種套接字稱爲TCP 套接字.
在這裏插入圖片描述
下面創建滿足如下求的套接字:

“IPV4協議中面向消息的套接字”

SOCK_DGRAM 指的是面向消息的數據傳輸方式, 滿足上述條件的協議只有 IPPROTO_UDP. 因此, 可以如下調用 socket 函數創建套接字, 這種套接字稱爲 UDP 套接字.
在這裏插入圖片描述
前面進行了大量描述以解釋着兩行代碼, 這是爲了讓大家理解他們創建的套接字的特性.

面向連接的套接字: TCP 套機字實例

其他章節將講解 UDP 套接字, 此處只給出面向連接的TCP 套機字示例. 本示例是第 1 章的如下2 個源文件基礎進行修改的而成的.
在這裏插入圖片描述
之前的hello_server.c 和 hello_client.c 是基於TCP 套接字的實例. 先調整其中一部分代碼, 以驗證 TCP 套接字的如下特性:

“傳輸的數據不存在數據邊界.”

爲了驗證這一點, 需要讓write 函數的調用次數不同於read 函數的調用次數. 因此, 在客戶端中分多次調用 read 函數以接收服務器端發送的全部數據.

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

void error_handling(const char* message);

int main(int argc, char *argv[])
{
    int sock;
    struct sockaddr_in serv_addr;
    char message[30];
    int str_len = 0;
    int idx = 0, read_len = 0;

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

    sock = socket(PF_INET, SOCK_STREAM, 0);
    if (sock == -1)
    {
        error_handling("socket() error!");
    }

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

    if (connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
    {
        error_handling("connect() error!");
    }

    /* while循環中反覆調用read函數, 每次讀取1個字節. 如果read返回0, 則循環條件爲假, 跳出while循環. */
    while (read_len = read(sock, &message[idx++], 1))
    {
        if (read_len == -1)
        {
            error_handling("read() error!");
        }
        str_len += read_len;
    }

    printf("Message from server: %s \n", message);
    printf("Function read call count : %d \n", str_len);
    close(sock);
    
    return 0;
}

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

運行結果:
服務端:
在這裏插入圖片描述
客戶端:
在這裏插入圖片描述
與該實例配套使用的服務端tcp_server.c與hello_server.c 完全相同, 故省略源代碼. 執行方式也與hello_server.c 和 hello_client.c 相同, 因此只能給出最終運行的結果.

從運行結果可以看出, 服務器端發送了13 字節的數據, 客戶端調用13次 read 函數進行讀取. 希望各位通過該實例深入理解TCP 套接字的數據傳輸方式.

2.2 Windows 平臺下的實現及驗證

前面講過的套接字類型及傳輸特性與操作系統無關. Windows 平臺下的實現方式也類似, 不需要過多說明, 只需稍加了解 socket 函數返回類型即可.

Windows 操作系統的 socket 函數
Windows 的函數名和參數名都與 Linux 平臺相同, 只是返回值類型稍有不同. 再次給出 socket 函數的聲明.
在這裏插入圖片描述
該函數的參數種類及含義與 Linux 的 socket 函數完全相同, 故略過, 只討論返回值類型. 可以看出返回類型 爲 SOCKET , 此結構體用來保存整數型套接字句柄值. 實際上, socket 函數返回整數型數據, 因此可以通過 int 型變量接收, 就像在 Linux 中做的一樣. 但考慮到以後的擴展性, 定義 SOCKET 數據類型, 希望各位也使用 SOCKET 結構體變量保存 套接字句柄, 這也是微軟希望看到的. 以後即可將 SOCKET 視作保存套接字句柄的一個數據類型.

同樣, 發生錯誤時返回 INVALID_SOCKET , 只需將其理解爲提示錯誤的常數即可. 實際值爲-1, 但值是否爲-1 並不重要, 除非編寫如下代碼.
在這裏插入圖片描述
如果這樣編寫代碼, 那麼微軟定義的 INVALID_SOCKET 常數將失去意義! 應該如下編寫, 這樣, 即使日後微軟更改 INVALID_SOCKET 常量值, 也不會發生問題.
在這裏插入圖片描述
這些問題雖然瑣碎卻非常重要.

基於 Windows 的 TCP 套機字實例
把之前的tcp_server.c, tcp_client.c 如下改爲 基於 Windows 的程序.
在這裏插入圖片描述
與之前一樣, 只給出tcp_client_win.c 源代碼及運行結果. 各位若想請自查看 tcp_server_win.c 的代碼, 可以參考第1章的hello_server_win.c, 或到 OrangeMedia 主頁(http://www.orentec.co.kr/)

#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 = 0;
	int idx = 0, readLen = 0;

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

	if (WSAStartup(MAKEWORD(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)) == INVALID_SOCKET)
	{
		ErrorHandling("connect() error!");
	}

	while (readLen = recv(hSocket, &message[idx++], 1, 0))
	{
		if (readLen == -1)
		{
			ErrorHandling("read() error!");
		}
		strLen += readLen;
	}

	printf("Message from server : %s \n", message);
	printf("Function read call count : %d \n", strLen);

	closesocket(hSocket);
	WSACleanup();
	return 0;
}

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

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

客戶端:
在這裏插入圖片描述

該實例的運行方式與第1章的hello_server_win.c , hello_client_win.c 相同, 因此只給出客戶端的運行結果. 以上就是第2章全部內容, 相信各位對服務器端和客戶端有了更深入的理解.

你們可以從: https://www.jiumodiary.com/
下載本書: TCP IP網絡編程

2.3 習題

(1) 什麼是協議? 在收發數據中定義協議有何意義?

(2) 面向連接的TCP 套接字傳輸特性有3點, 請分別說明.

(3)

時間: 20200523

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