TCP/IP網絡編程系列之四(初級)


TCP/IP網絡編程系列之四-基於TCP的服務端/客戶端

理解TCP和UDP

根據數據傳輸方式的不同,基於網絡協議的套接字一般分為TCP和UDP套接字。因為TCP套接字是面向連接的,因此又稱為基於流的套接字。在了解TCP之前,先了解一下TCP所屬的TCP/IP協議棧。

如圖所示,TCP/IP協議棧共分為4層,可以理解成數據收發分成了4個層次化過程。

鏈路層

它是物理鏈接領域標准化結果,也是最基本的領域,專門定義LAN、WAN、MAN等網絡標准。若兩台計算機通過網絡進行數據交換,鏈路層就負責整個物理連接。

IP層

准備好物理連接后就要傳輸數據,首先要考錄路徑的選擇問題,向目標主機傳輸數據選擇哪條路徑,就是由IP層負責處理的。IP本身是面向消息的、不可靠的協議。每次傳輸數據時會幫我們選擇路徑,但是並不一致,傳輸過程中出現路徑錯誤,則選擇其他路徑。但是如果發生數據丟失或錯誤則無法解決,IP協議無法應對數據錯誤。

TCP/UDP層

IP層負責傳輸路徑的選擇,只需按照路徑傳輸即可。TCP/UDP層以IP層提供的路徑信息為基礎完成實際的數據傳輸,故該層又稱為傳輸層。UDP比TCP簡單,先說一下TCP。TCP可以保證傳輸數據的可靠性。IP層只關注一個數據包的傳輸過程。因此,即使傳輸多個數據包,每個數據包也是由IP層實際傳輸的。也就是說,傳輸的順序及傳輸本身不可靠的。若只利用IP層傳輸數據,則有可能導致后傳輸的數據包B比先傳輸的數據包A先到達目的地。也有可能在傳輸的過程中丟失A,反之如果添加TCP協議的話就不會出現這種情況。數據交換過程中可以確認對方已收到數據,並重傳丟失的數據,那么IP層不保證數據傳輸,這類通信也是可靠的。

總之,TCP和UDP存在於IP層之上,決定主機之間的數據傳輸方式,TCP確認之后向不可靠的IP協議賦予可靠性。

應用層

套接字通信過程是自動處理的。選擇數據傳輸路徑、數據確認過程都被隱藏到套接字內部。總之,向各位提供的工具就是套接字,大家只需利用套接字編寫程序即可。編寫軟件的過程中,需要根據程序特點決定服務端和客戶端之間的數據傳輸規則,這便是應用層的協議。

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

TCP函數的調用順序為下面所示:

socket()->bind()->listen()->accept()->read()/write()->close()   《=》 創建套接字->分配套接字地址->等待連接請求->允許連接->數據交換->斷開連接

進入等待連接請求狀態

我們調用的bind函數分配了地址,下面就要調用listen函數進入等待連接請求狀態。只有調用了listen函數,客戶端才能進入可發出連接請求的狀態。這時的客戶端才可以調用connect函數。(提前調用失敗).

#inlcude <sys/socket.h>

int listen(int sock,int backlog)
成功時返回0,失敗時返回-1
sock:希望進入等待連接請求的套接字文件描述符,傳遞的描述符套接字參數成為服務端套接字(監聽套接字)
backlog:連接請求等待隊列的長度,若為5,則隊列長度為5,表示最多可以連接5個客戶端連接。

"服務端等待連接請求狀態"是指客戶端請求連接時,受理連接前一直使請求狀態處於等待狀態。listen函數的第一個參數的用途,客戶端連接請求本身也是從網絡中接收到的一種數據,而想要接受就需要套接字。此任務就有服務端套接字完成。

受理客戶端的連接請求

調用listen函數之后,若有新的連接請求,則應按順序受理。受理請求意味着進入可接受數據的狀態。

復制代碼
#include <sys/socket.h>

int accept(int sock,struct sockaddr *addr,socklen_t *addrlen)
成功時返回創建的套接字文件描述符,失敗時返回-1

sock:服務端套接字描述符
addr:保存發起連接請求的客戶端地址信息的變量地址值,調用函數后想傳遞來的地址變量參數填充客戶端地址信息。
addrlen:第二個參數addr參數長度,但是存有長度的變量地址。函數調用完成后,該變量即被填入客戶端地址長度。
復制代碼

accept函數受理連接請求等待隊列中待處理的客戶端連接請求。函數調用成功時,accept函數內部將產生用於數據I/O的套接字,並返回文件描述符。需要強調的是,套接字是自動創建的,並自動與發起連接請求的客戶端建立連接。

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

socket()->connect()->read()/write()->close()  ==============  創建套接字->請求連接->交換數據->斷開連接

服務端調用listen函數后創建連接請求等待隊列,之后客戶端即可請求連接。那么如何發起連接請求的呢?就是通過connect函數完成的。

#include <sys/socket.h>

int connect(int sock,struct sockaddr *servaddr,socklen_t addrlen)
sock:客戶端套接字文件描述符
servaddr:保存服務端地址信息的變量地址
addelen:以字節為單位已傳遞給第二個結構體參數servaddr的地址變量長度。

客戶端調用connect函數后,“服務端接收連接請求”或”發生斷網等異常情況而中斷請求“函數才會返回,完成函數調用。需要注意的是,所謂的"接收連接"並不意味着服務端調用accept函數,其實是服務端把連接請求記錄到等待隊列。因此accept函數返回后並不立即進行交換數據。

客戶端實現過程中並未出現套接字地址分配,而是創建完套接字之后立即調用connect函數,客戶端套接字是何時、何地如何分配的呢?何時?調用connect函數時候;何地?操作系統,更准確的說是在內核中;如何?IP用計算機的IP,端口隨機分配的。

總結:

服務端創建套接字后連續調用bind、listen函數進入等待狀態,客戶端通過函數connect請求連接。需要注意的是,客戶端只能等到服務器端調用listen函數后才能調用connect函數。同時清楚,客戶端調用connect函數之前,服務端有可能率先調用accept函數。當然,此時的服務器端在調用accept函數時進入阻塞狀態,直到客戶端調用connect函數為止。

實現迭代服務器端/客戶端

  回聲客戶端存在問題,每次調用read、write函數都會以字符串為單位執行實際的I/O操作。當然,每次調用write函數都會傳遞一個字符串,因此還算合理。但是客戶端是基於TCP的,因此多次調用write函數傳遞的字符串有可能一次傳遞到服務端。此時客戶端有可能從服務器端收到多個字符串,這不是我們希望看到的結果。還需要考慮服務端的情況,就是字符串太長,需要分兩個數據包發送。服務器端希望通過調用1次write函數傳輸數據,但如果數據太大,操作系統就有可能把數據分成多個數據包發送到客戶端。另外,在此過程中,客戶端有可能尚未收到全部數據包時就調用了read函數。

服務端代碼:

#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 serverSocket;
    int clientSocket;
    struct sockaddr_in serverAddr;
    struct sockaddr_in clientAddr;
    socklen_t clientSocketAddrSize;
    char message []="Hello!";
    if(argc != 2)
    {
        printf("Usage:%s <port>\n",argv[0]);
        exit(1);
    }
    serverSocket = socket(PF_INET,SOCK_STREAM,0);
    if(serverSocket == -1)
    {
        Error_Handling("socket() error");
    }
    memset(&serverAddr,0,sizeof(serverAddr));
    serverAddr.sin_family=AF_INET;
    serverAddr.sin_addr.s_addr=htonl(INADDR_ANY);
    serverAddr.sin_port=htons(atoi(argv[1]));
    if(bind(serverSocket,(struct sockaddr*) &serverAddr,sizeof(serverAddr))==-1)
    {
        Error_Handling("bind() error");

    }
    if(listen(serverSocket,5)==-1)
    {
        Error_Handling("listen() error");
    }
    clientSocketAddrSize = sizeof(clientAddr);
    clientSocket = accept(serverSocket,(struct sockaddr*)&clientAddr,&clientSocketAddrSize);
    if(clientSocket == -1)
    {
        Error_Handling("accept() error");

    }
    write(clientSocket,message,sizeof(message));
    close(clientSocket);
    close(serverSocket);
    return 0;
}

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

客戶端代碼:

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

#define MAXSIZE 30
void Error_Handling(char* message);

int main(int argc,char* argv[])
{
    int sock;
    struct  sockaddr_in serverAddr;
    char message[MAXSIZE];
    int str_len;
    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("sock() error");
    }
    memset(&serverAddr,0,sizeof(serverAddr));
    serverAddr.sin_family=AF_INET;
    serverAddr.sin_addr.s_addr=inet_addr(argv[1]);
    serverAddr.sin_port=htons(atoi(argv[2]));
    if(connect(sock,(struct sockaddr*)&serverAddr,sizeof(serverAddr))==-1)
    {
        Error_Handling("connect() error");
    }

    str_len = read(sock,message,sizeof(message)-1);
    if(str_len == -1)
    {
        Error_Handling("read error");
    }
    printf("Message from server : %s \n",message);
    close(sock);
    return 0;
}

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

TCP 原理

TCP套接字中的I/O緩沖

TCP套接字的數據收發無邊界。服務端調用一次write函數傳輸40個字節的數據,客戶端也有可能通過4次read函數調用每次讀取10字節。那么有個問題,客戶端可以緩慢的分批讀取數據,那么剩余的數據都存放在哪?實際上,write函數調用后並非立即傳輸數據,read函數調用后也並非馬上接收數據。write函數調用瞬間,數據將移至輸出緩沖;read函數調用瞬間,從輸入緩沖讀取數據。調用writ函數時,數據將移到輸出緩沖,在適當的時候傳向對方的輸入緩沖。這是對方將調用read函數沖輸入緩沖讀取數據。

I/O緩沖的特點:

  1.I/O緩沖在每個TCP套接字中單獨存在

  2.I/O緩沖在創建套接字過程中自動生成

  3.即使關閉套接字也會繼續傳輸輸出緩沖中的遺留數據

  4.關閉套接字將丟失輸入緩沖中的數據。

那么如果客戶端的輸入緩沖為30個字節,但是服務端傳輸了100個字節,那怎么辦?這根本不會發生的。因為TCP會控制數據流,TCP中有滑動窗口協議(Sliding Window)。

與對方套接字的連接

TCP通信過程中會經過3次對話過程,因此該過程又稱三次握手。套接字是以全雙工方式工作的。也就是說,它可以雙向傳遞數據。因此,收發數據前做些准備,首先,請求連接的主機A向主機B傳遞如下信息:

  [SYN] SEQ: 1000,ACK:-

該消息中,SEQ為1000,ACK為空。現在傳遞的數據包序號為1000,如果接收無誤,請通知我向你傳遞1001號數據包。這是首次請求連接時使用的信息,又稱SYN。SYN是Synchronization的簡寫,表示收發數據前傳輸的同步信息。接下來主機B向A傳遞如下信息:

  [SYN+ACK] SEQ:2000,ACK:1001

此時SEQ為2000,ACK為1001。代表現在傳遞數據包的序號為2000,如果接收無誤,請通知我向你傳遞2001號數據包。而ACK1001的含義:剛才傳輸的SEQ為1000的數據包接收無誤,現在請傳遞2001號數據包。對主機A首次傳輸的數據包的確認信息(ACK:1001)和為主機B傳輸數據做准備的同步消息(SEQ 2000)捆綁發送,因此,這種類型的消息又稱SYN+ACK.

收發數據前向數據包分配序號,並向對方通報次序號,這都是為防止數據丟失所做的准備。通過向數據包分配序號並確認,可以在數據丟失時馬上查看並重傳丟失的數據包。因此,TCP可以保證可靠的數據傳輸。最后觀察主機A向主機B傳輸的消息:

[ACK] SEQ:1001,ACK:2001

已正確收到傳輸的SEQ為2000的數據包,現在可以傳輸SEQ為2001的數據包。這樣就傳輸了添加ACK 2001的ACK信息。至此,主機A和主機B確認了彼此均就緒。

與對方主機的數據交換

通過第一步的三次握手過程完成了數據交換的准備,下面是正式開始收發數據,比如主機A分兩次(2個數據包)向主機B傳遞200字節的過程。首先,主機A通過1個數據包發送100個字節的數據,數據包的SEQ為1200.主機B為了確認這一點,向主機A發送ACK1301消息。此時的ACK號為1301,原因在於ACK號的增量為傳輸的數據字節數。ACK消息:

ACK號-》SEQ號+傳遞的字節數+1,與三次握手協議相同,最后加1是為了告知對方下次要傳遞的SEQ號。如果在傳輸的過程中出現錯誤,則重傳數據,tcp套接字啟動計時器以等待ACK應答。若相應計時器超時,則重傳。

斷開與套接字的連接

先由套接字A向套接字B傳遞斷開連接的消息,套接字B發出確認收到的消息,然后向套接字A傳遞可以斷開連接的消息,套接字A同樣發出確認消息。數據包內的FIN表示斷開連接,也就是說,雙發各發送1次FIN消息后斷開連接。此過程經歷四個階段,因此又稱四次握手。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM