1. 程式人生 > >Linux C基於TCP的網路程式設計(三次握手)

Linux C基於TCP的網路程式設計(三次握手)

網路通訊(TCP)

TCP(面向連線的通訊協議):在通訊中時刻保持連線,這種通訊方式類似於打電話,能保證安全可靠資料不丟失,但是與UDP相比傳輸速度較低。
TCP和UDP通訊收發函式區別:記住socket中是否存放有IP 和埠資訊,TCP有,UDP無。同時TCP伺服器需要多個 socket 物件,對應多個連線。

TCP程式設計模型:

程序A:建立socket->準備地址->繫結->監聽(設定佇列長度)->等待連線->通訊->關閉socket
程序B:建立socket->準備地址->連線->通訊->關閉socket

listen函式:設定socket最大的排隊數量(監聽)
 int listen(int sockfd, int backlog);
accept函式:等待其他主機與當前socket建立連線關係
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • accept()的返回值:成功返回建立連線成功的描述符,此後的通訊都用此描述符,失敗返回-1
recv函式:TCP網路通訊專用的資料接收
 ssize_t recv(int sockfd, void *buf, size_t len, int flags);
  • sockfd:此時為accept的返回值
  • buf:資料緩衝區
  • len:緩衝區大小
  • flags:一般為0
send函式:TCP網路通訊專用的資料傳送
 ssize_t send(int sockfd, const void *buf, size_t len, int flags);


實現程式碼 編寫一個服務端程式(子程序來處理客戶端發來的訊息)。

什麼是TCP的三次握手?

這裡寫圖片描述
SYN:同步序列編號(Synchronize Sequence Numbers)。是TCP/IP建立連線時使用的握手訊號。
ACK:ACK (Acknowledgement)即是確認字元,在資料通訊中,接收站發給傳送站的一種傳輸類控制字元。表示發來的資料已確認接收無誤。在TCP/IP協議中,如果接收方成功的接收到資料,那麼會回覆一個ACK資料。通常ACK訊號有自己固定的格式,長度大小,由接收方回覆給傳送方。
FIN

:FIN(finish)為TCP報頭的碼位欄位,該位置為1的含義為傳送方位元組流結束,用於關閉連線。當兩端交換帶有FIN標誌的TCP報文段並且每一端都確認另一端傳送的FIN包時,TCP連線將會關閉。FIN位字面上的意思是連線一方再也沒有更多新的資料傳送。然而,那些重傳的資料會被傳送,直到接收端確認所有的資訊。

  • 第一次握手:客戶端傳送syn包(syn=j)到伺服器,並進入SYN_SEND狀態,等待伺服器確認;
  • 第二次握手:伺服器收到syn包,必須確認客戶的syn(ack=j+1),同時自己也傳送一個SYN包(syn=k),即SYN+ACK包,此時伺服器進入SYN_RECV狀態;
  • 第三次握手:客戶端收到伺服器的SYN+ACK包,向伺服器傳送確認包ACK(ack=k+1),此包傳送完畢,客戶端和伺服器進入ESTABLISHED狀態,完成三次握手。連線建立後,客戶端和伺服器就可以開始進行資料傳輸了。

為什麼需要三次握手?

  • 考慮一次的問題,首先TCP是面向連線,一次握手肯定建立不了連線,正如將石頭扔入井中,只有聽到響聲才會知道落水了,因此客戶端給伺服器發出請求資訊卻沒有得到迴應,客戶端是沒法判斷是否傳送成功然後建立連線的。
  • 如果使用的是兩次握手建立連線,假設有這樣一種場景,客戶端傳送了第一個請求連線並且沒有丟失,只是因為在網路結點中滯留的時間太長了,由於TCP的客戶端遲遲沒有收到確認報文,以為伺服器沒有收到,此時重新向伺服器傳送這條報文,此後客戶端和伺服器經過兩次握手完成連線,傳輸資料,然後關閉連線。此時此前滯留的那一次請求連線,網路通暢了到達了伺服器,這個報文字該是失效的,但是,兩次握手的機制將會讓客戶端和伺服器再次建立連線,這將導致不必要的錯誤和資源的浪費。
  • 如果採用的是三次握手,就算是那一次失效的報文傳送過來了,服務端接受到了那條失效報文並且回覆了確認報文,但是客戶端不會再次發出確認。由於伺服器收不到確認,就知道客戶端並沒有請求連線。三次握手就是為了防止已經失效的連線請求報文突然又傳送到了伺服器,從而產生錯誤。

什麼是四次揮手?

這裡寫圖片描述
由於TCP連線是全雙工的,因此每個方向都必須單獨進行關閉。這原則是當一方完成它的資料傳送任務後就能傳送一個FIN來終止這個方向的連線。收到一個 FIN只意味著這一方向上沒有資料流動,一個TCP連線在收到一個FIN後仍能傳送資料。首先進行關閉的一方將執行主動關閉,而另一方執行被動關閉。

  • 1、 TCP客戶端傳送一個FIN,用來關閉客戶到伺服器的資料傳送。
  • 2、 伺服器收到這個FIN,它會先發回一個ACK,因為如果此時服務端到客戶端還有資料未傳輸完,則還需要傳輸資料,此時收到ACK確認的客戶端會進入FIN_WAIT狀態。
  • 3、 當伺服器資料傳輸完成,覺得可以關閉與客戶端的連線時,會發送一個FIN給客戶端。
  • 4、 客戶端收到服務端的FIN後,知道可以真正關閉連線了,則會發回ACK報文確認,但怕由於網路原因服務端接收不到ACK,客戶端會在傳送後進入TIME_WAIT,以防止服務端沒有收到ACK可以進行重傳。當客戶端等待了2MSL後沒有收到迴應,則證明服務端收到ACK並關閉了,那麼客戶端也就自己關閉了連線。

提問1:TCP是面向連線的,三次握手後,源IP一定是真實的?
答案:錯誤。
TCP SYN 泛洪:
對於TCP協議,當客戶端向伺服器發起連線請求並初始化時,伺服器一端的協議棧會留一塊緩衝區來處理“握手”過程中的資訊交換。請求建立連線時傳送的資料包的包頭SYN位就表明了資料包的順序,攻擊者可以利用在短時間內快速發起大量連線請求,以致伺服器來不及響應。同時攻擊者還可以偽造源IP地址。也就是說攻擊者發起大量連線請求,然後掛起在半連線狀態,以此來佔用大量伺服器資源直到拒絕服務。雖然緩衝區中的資料在一段時間內(通常是三分鐘)都沒有迴應的話,就會被丟棄,但在這段時間內,大量半連線足以耗盡伺服器資源。

TCP LAND:
LAND攻擊利用了TCP連線建立的三次握手過程,通過向一個目標主機發送一個用於建立請求連線的TCP SYN報文而實現對目標主機的攻擊。與正常的TCP SYN報文不同的是:LAND攻擊報文的源IP地址和目的IP地址是相同的,都是目標主機的IP地址。這樣目標主機接在收到這個SYN 報文後,就會向該報文的源地址傳送一個ACK報文,並建立一個TCP連線控制結構,而該報文的源地址就是自己。由於目的IP地址和源IP地址是相同的,都是目標主機的IP地址,因此這個ACK 報文就發給了目標主機本身。

提問2:為什麼TIME_WAIT狀態需要經過2MSL(最大報文段生存時間)才能返回到CLOSE狀態?
答:首先說明一下什麼是2MSL?MSL即Maximum Segment Lifetime,也就是報文最大生存時間,引用《TCP/IP詳解》中的話:“它(MSL)是任何報文段被丟棄前在網路內的最長時間。”那麼,2MSL也就是這個時間的2倍。
第一,保證客戶端傳送的最後一個ACK報文能夠到達伺服器,因為這個ACK報文可能丟失,站在伺服器的角度看來,我已經發送了FIN+ACK報文請求斷開了,客戶端還沒有給我回應,應該是我傳送的請求斷開報文它沒有收到,於是伺服器又會重新發送一次,而客戶端就能在這個2MSL時間段內收到這個重傳的報文,接著給出迴應報文,並且會重啟2MSL計時器。
第二,防止類似與“三次握手”中提到了的“已經失效的連線請求報文段”出現在本連線中。客戶端傳送完最後一個確認報文後,在這個2MSL時間中,就可以使本連線持續的時間內所產生的所有報文段都從網路中消失。這樣新的連線中不會出現舊連線的請求報文。

什麼是可靠的連線(A要知道:A->B&&B->A ,B要知道:B->A&&A->B);
服務端server:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

typedef struct sockaddr* saddrp;

int main(int argc, char const *argv[])
{
    int sockfd = socket(AF_INET,SOCK_STREAM,0);
    if (0 > sockfd)
    {
        perror("socket");
        return -1;
    }

    struct sockaddr_in addr = {};
    addr.sin_family = AF_INET;
    addr.sin_port = htons(atoi(argv[1]));//通過命令列設定埠與ip
    addr.sin_addr.s_addr = inet_addr(argv[2]);

    int ret = bind(sockfd,(saddrp)&addr,sizeof(addr));
    if (0 > ret)
    {
        perror("bind");
        return -1;
    }

    listen(sockfd,1024);//設定監聽數量

    struct sockaddr_in client_addr = {};
    socklen_t addr_len = sizeof(client_addr);
    bool quit_flag = false;//程序結束標誌

    while(1)
    {
        int client_fd = accept(sockfd,(saddrp)&client_addr,&addr_len);
        if(0 == fork())
        {
            while(1)
            {
                char buf[255] = {};
                int val = recv(client_fd,buf,sizeof(buf),0);
                if(0 == strcmp(buf,"q"))
                {
                    quit_flag = true;
                    break;
                }
                sprintf(buf,"Server ID:%d:%s",getpid(),"I will reply u soon");
                send(client_fd,buf,strlen(buf)+1,0);
            }
            close(client_fd);
            if(quit_flag == true) exit(0);
        }
    } 
    close(sockfd);
    return 0;
}
客戶端client:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

typedef struct sockaddr* saddrp;

int main(int argc, char const *argv[])
{
    int sockfd = socket(AF_INET,SOCK_STREAM,0);
    if (0 > sockfd)
    {
        perror("socket");
        return -1;
    }

    struct sockaddr_in addr = {};
    addr.sin_family = AF_INET;
    addr.sin_port = htons(atoi(argv[1]));
    addr.sin_addr.s_addr = inet_addr(argv[2]);

    int ret =connect(sockfd,(saddrp)&addr,sizeof(addr));
    if (0 > ret)
    {
        perror("connect");
        return -1;
    }

    while(1)
    {
        char buf[255] = {};
        printf(">");
        gets(buf);
        send(sockfd,buf,strlen(buf)+1,0);
        if(0 == strcmp(buf,"q")) break;
        ret = recv(sockfd,buf,sizeof(buf),0);
        if (0 > ret)
        {
            perror("read");
            return -1;
        }
        printf("Recv:%d Bytes.\nShow:%s\n",ret,buf);
        if(0 == strcmp(buf,"q")) break;
    }
    close(sockfd);
    return 0;
}

執行結果:
執行結果
返回