TCP/IP網絡編程_第4章基於TCP的服務器端/客戶端(1) 4.1-4.2

在這裏插入圖片描述

4.1 理解 TCP 和 DUP

根據數據傳輸方式的不同, 基於網絡協議的套接字一般分爲 TCP 套接字和 UDP 套接字. 因爲 TCP 套接字是面向連接的, 因此又基於流(stream) 的套接字.

TCP 是 (傳輸控制協議)的簡寫, 意爲"對數據傳輸過程的控制". 因此, 學習控制方法及範圍有助於正確理解 TCP 套接字.

TCP/IP 協議棧

講解TCP前先介紹 TCP 所屬的 TCP/IP 協議棧(Stack, 層), 如圖4-1所示.
在這裏插入圖片描述
從圖4-1可以看出, TCP/IP 協議棧共分4層, 可以理解爲數據收發分成了 4 個層次過程. 也就是說, 面對 “基於互聯網的有效數據傳輸” 的命題, 並非通過1個龐大協議解決問題, 而是化整數爲零, 通過層次化方案-- TCP/IP協議棧解決. 通過 TCP 套接字收發數據時需要藉助這4層, 如圖4-2所示.
在這裏插入圖片描述
反之, 通過 UDP 套接字 收發數據時, 利用圖4-3中的層協議棧完成.
在這裏插入圖片描述
各層可能通過操作系統等軟件實現, 也可能通過類型 NIC 的硬件設備實現.
在這裏插入圖片描述
TCP/IP 協議的誕生背景
“通過因特網完成有效數據傳輸” 這個課題讓很多專家聚集到了一起, 這些人是硬件、系統、路由算法等各領域的頂尖專家. 爲何需要這麼多領域的專家呢?

我們之前只關注套接字創建及應用, 卻忽略了計算機網絡問題並非僅憑軟件就能解決. 編寫軟件前需要構建硬件系統, 在此基礎是上需要通過軟件實現各種算法. 所以才需要衆多領域的專家進行討論, 形成各種規定. 因此, 把這個大問題劃分若干個小問題再逐個攻破, 將大幅度提高效率.

把"通過因特網完成有效數據傳輸" 問題按照不同領域劃分成小問題後, 出現多種協議, 他們通過層級結構建立了緊密聯繫.

在這裏插入圖片描述
把協議分成多個層次具有哪些優點? 協議設計更容易? 當然這也足以成爲優點之一. 但還有更重要的原因是, 爲了通過標準化操作設計開發式系統.

標準本身就在於對外公開, 引導更多的人遵守規則. 以多個標準爲依據設計的系統稱爲開發式系統, 我們現在學習的 TCP/IP 協議 棧也屬於其中之一. 接下來了解一下開發式系統具有哪些優點. 路由器用來完成 IP 層交互任務. 某公司使用 A 公司的路由器, 現要將其替換成 B 公司的路由器, 是否可行? 這並非難事, 並不一定要換成同一公司的同一型號路由器, 因爲所有生產商都會按 IP 層標準制造.

再舉個例子. 各位的計算機是否裝有網絡接口卡, 也就是所謂的網卡? 尚未安裝也無妨, 其實很容易買到, 因爲所有網卡製造商都會遵守鏈路層的協議標準. 這就是開放式系統的優點.

鏈路層

接下來逐層瞭解 TCP/IP 協議棧, 先講解鏈路層. 鏈路層是物理鏈接領域標準化的結果, 也是最基本的領域, 專門定義 LAN WAN MAN 等網絡標準. 若兩臺主機通過網絡進行數據交互, 則需要圖 4-4 所示的物理連接, 鏈路層就負責這些標準.
在這裏插入圖片描述

IP 層

準備好物理連接後就要傳輸數據. 爲了在複雜的網絡中傳輸數據, 首先需要考慮路徑的選擇. 向目標傳輸數據需要經過那條路徑? 解決此問題就是 IP 層, 該層使用的協議就是 IP.

IP 本身是面向消息的, 不可靠的協議. 每次傳輸數據時都會幫助我們選擇路徑, 但並不一致. 如果傳輸中發生路徑錯誤, 則選擇其他路徑; 但如果發生數據丟失或錯誤, 則無法解決. 換言之. IP 協議無法應對數據錯誤.

TCP/UDP 層

IP 層解決數據傳輸中路徑選擇問題, 只需按照路徑傳輸數據即可. TCP 和 UDP 層以IP層提供的路徑信息爲基礎完成實際的數據傳輸, 故該層又稱傳輸層(Transport). UDP 比 TCP 簡單, 我們將在後續展開討論, 現只解釋TCP 可以保證可靠的數據傳輸, 但它發送數據時以IP 層爲基礎(這也是協議棧結構層次化的原因). 那麼該如何理解二者關係呢?

IP層只關注1個數據包(數據傳輸的單位)的傳輸過程. 因此, 即使傳輸多個數據包, 每個數據包也是由IP層實際傳輸的, 也就是說傳輸順序機傳輸本身是不可靠的. 若只利用IP層傳輸數據, 則有可能導致後傳輸的數據包B比先傳輸的數據包A提早到達. 另外, 傳輸的數據包A, B, C 中可能只收到 A和C , 甚至收到的C可能已損毀. 反之, 若添加TCP協議則按照如下對話方式進行數據交換.
在這裏插入圖片描述
這就是TCP 的作用. 如果數據交換過程中可以確認對方已收到數據, 並重傳丟失的數據, 那麼即使IP層不保證數據傳輸, 這類通信也是可靠的, 如圖4-5所示.
在這裏插入圖片描述
圖 4-5簡單描述了 TCP 的功能. 總之, TCP 和 UDP 存在於IP 層之上, 決定主機之間的數據傳輸方式, TCP 協議確認後向不可靠的IP 協議賦予可靠性.

應用層

上述內容是套接字通信過程中自動處理的. 選擇數據傳輸路徑、數據確認過程都被隱藏到套接字內部. 而與其說是"隱藏", 到不如"使程序員從這些細節中解放出來"的表達更爲精確. 程序編程時無需考慮這些過程, 但這並不意味着不用掌握這些知識. 只有掌握了這些理論, 才能編寫出符合需求的網絡程序.

總之, 向各爲提供的工具就是套接字, 大家只需利用套機字編出程序即可. 編寫軟件的過程中, 需要根據程序特點決定服務器端和客戶端之間的數據傳輸規則(規定), 這便是應用層協議. 網絡編程的大部分內容就是設計並實現應用層協議.

4.2 實現基於 TCP 的服務器端/客戶端

本節實現完整的 TCP 服務器端, 在此過程中各位將理解套接字使用方法及數據傳輸方法.

TCP 服務器端的默認函數調用順序
圖 4-6 給出了 TCP 服務器端默認的函數調用順序, 絕大部分 TCP 服務器端安裝該順序調用.
在這裏插入圖片描述
調用socket 函數創建套接字, 聲明並初始化地址信息結構體變量, 調用bind 函數向套接字分配地址. 這2個階段之前都已討論過, 下面講解之後的幾個過程.

進入等待連接請求狀態
我們已調用bind函數給套接字分配了地址, 接下來就要通過調用 listen 函數進入等待連接請求狀態. 只有調用了 listen 函數, 客戶端才能進入可發出連接請求的狀態. 換言之, 這時客戶端才能調用connect 函數(若提起調用將發生錯誤).
在這裏插入圖片描述
先解析一下等待連接請求狀態的含義和連接請求等待隊列. “服務器端處於等待連接請求狀態” 是指, 客戶端請求連接時, 受理連接前一直使請求處於等待狀態. 圖 4-7 給出了這個過程.
在這裏插入圖片描述
由圖4-7可知作爲listen函數的第一個參數傳遞的文件描述符套接字的用途. 客戶端連接請求本身也是從網絡中接受到的一種數據, 而要想接收就需要套機字. 此任務就由服務器端套接字完成. 服務器端套接字是接收連接請求的一名門衛或一扇門.

客戶端如果向服務器端詢問:"請問我是否可以發起連接? ""服務器端套接字就會親切應答: “你好! 當然可以, 但系統正忙, 請到等待室排號等待, 準備好後會立即受理你的連接” 同時將連接請求請到等候室. 調用listen 函數即可生成這種門衛(服務器端套接字), listen函數的第二個參數決定等候室的大小. 等候室稱爲連接請求等待隊列, 準備好服務器端套接字和連接請求等待隊列後, 這種課接收請求的狀態稱爲等待連接狀態.

listen 函數的第二個參數值與服務器的特性有關, 像頻繁接收請求的 Web 服務器端至少應爲 15. 另外, 連接請求隊列的大小始終根據實驗結果而定.

受理客戶端連接請求
調用 listen 函數後, 若有新的連接請求, 則應按序受理. 受理請求意味着進入可接受數據的狀態. 也許各位已經猜到進入這種狀態所需部件-- 當然是套接字! 大家可能認爲可以使用服務器端套接字, 但服務器端套接字是做門衛的. 如果在與客戶端的數據交換中使用門衛, 那誰來守門呢? 因此需要另外一個套機字, 但不必親自創建. 下面這個函數將自動創建套接字, 並連接到發起請求的客戶端.
在這裏插入圖片描述
accept 函數受理連接請求等待隊列中待處理的客戶端連接請求. 函數調用成功時, accept 函數內部將產生用於數據 I/O 的套接字, 並返回文件描述符. 需要強調的是, 套接字是自動創建的, 並自動與發起連接請求的客戶端建立連接. 圖 4-8 展示了 accept 函數調用過程.
在這裏插入圖片描述
圖4-8 展示了"從等待隊列中取出1個連接請求, 創建套接字並完成連接請求"的過程. 服務器端單獨創建的套機字與客戶端建立連接後進行數據交換.

回顧 Hello word 服務器端

/* 20200521 tcpip網絡編程 */

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

void error_handling(char* message);

int main(int argc, char *argv[])
{
    int serv_sock;
    int clnt_sock;

    struct sockaddr_in serv_addr;
    struct sockaddr_in clnt_addr;
    socklen_t clnt_addr_size;

    char message[] = "Hello World!";

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

    /* 服務器端實現過程中要創建套接字. 第29行創建套接字, 但此時的套接字尚非真正的服務器端套接字 */
    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    if (serv_sock == -1)
    {
        error_handling("socket() error");
    }

    /* 36-42 爲了完成套接字地址分配, 初始化結構體變量並用bind函數 */
    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(argv[1]));

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

    /* 調用listen函數進入等待連接請求狀態. 連接請求等待隊列設置爲5. 此時的套接字纔是服務器端套接字 */
    if (listen(serv_sock, 5) == -1)
    {
        error_handling("listen() error");
    }

    clnt_addr_size = sizeof(clnt_addr);
    /* 調用accept函數從隊列取一個連接請求也客戶端建立連接, 並返回創建的套接字文件描述符. 
    另外, 調用accept函數時若等待隊列爲空, 則accept函數不會返回, 直到隊列中出現新的客戶端連接 */
    clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
    if (clnt_sock == -1)
    {
        error_handling("accept() error");
    }

    /* 調用write函數向客戶端傳輸數據. 調用close函數關閉連接 */
    write(clnt_sock, message, sizeof(message));

    close(clnt_sock);
    close(serv_sock);

    return 0;
}

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

TCP 客戶端的默認函數調用順序

接下來講解客戶端實現順序. 如前面所述, 這要比服務器端簡單許多. 因爲創建套接字和請求連接就是客戶端的全部內容, 如圖 4-9 所示.
在這裏插入圖片描述
與服務器端相比, 區別就在於"請求連接", 它是創建客戶端套接字後向服務器端發起的連接請求. 服務器端調用listen函數後創建連接請求等待隊列 之後客戶端即可請求連接. 那如何發起連接請求呢? 通過調用如下函數完成.
在這裏插入圖片描述
客戶端調用connect 函數後, 發生以下情況之一纔會返回(完成函數調用).
在這裏插入圖片描述
需要注意, 所謂的"接收連接"並不是意味着服務器端調用accept 函數, 其實是服務器端把連接請求信息記錄到等待隊列. 因此 conncet 函數返回後並不立即進行數據交換.

在這裏插入圖片描述
實現服務器端必須經過程之一就是給套接字分配IP 和端口號. 但客戶端實現過程中並未出現套接字地址分配, 而是創建套接字後立即調用connect 函數. 難道客戶端套接字無需分配IP和端口號? 當然不是! 網絡數據交換必須分配IP和端口. 既然如此, 那客戶端套接字何時, 何地, 如何分配地址呢?
在這裏插入圖片描述
客戶端的IP地址和端口號在調用connect 函數是自動分配, 無需調用標記的bind函數進行分配.

回顧 Hello word 客戶端

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

void error_handling(char *message);

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

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

    /* 創建準備連接服務器端的套接字,此時創建的是TCP套接字 */
    sock = socket(PF_INET, SOCK_STREAM, 0);
    if (sock == -1)
    {
        error_handling("socket() error");
    }

    /* 結構體變量serv_addr中初始化IP和端口號信息. 初始化值爲目標服務器端套接字的IP和端口信息 */
    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]));

    /* 調用connect函數向服務器端發送連接請求 */
    if (connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
    {
        error_handling("connect() error!");
    }

    /* 完成連接後, 接收服務器端傳輸的數據 */
    str_len = read(sock, message, sizeof(message));
    if (str_len == -1)
    {
        error_handling("reand() error!");
    }

    printf("Message from server : %s \n", message);

    /* 接收數據後調用close函數關閉套接字, 結束與服務器端的連接 */
    close(sock);

    return 0;
}

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

基於 TCP 的服務器端/客戶端函數調用關係

前面講解了 TCP 服務器端/客戶端的實現順序, 實際上二者並非相互獨立, 各位應該可以勾勒出它們之間的交互過程, 如圖 4-10 所示. 之前詳細討論過
在這裏插入圖片描述
圖 4-10 的總體流程整理如下: 服務器創建套接字後連續調用bind, listen函數進入等待狀態, 客戶端通過調用connect 函數發起連接請求. 需要注意的是, 客戶端只能等到服務器端調用listen 函數後才能調用connect函數. 同時要清楚在調用accept 函數時進入阻塞(blocking) 狀態, 直到客戶端調用 connect 函數爲止.

結語:

你可以在下面網站下載這本書:
https://www.jiumodiary.com/

時間2020:05:26

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