TCP/IP網絡編程之基於TCP的服務端/客戶端(二)


回聲客戶端問題

上一章TCP/IP網絡編程之基於TCP的服務端/客戶端(一)中,我們解釋了回聲客戶端所存在的問題,那么單單是客戶端的問題,服務端沒有任何問題?是的,服務端沒有問題,現在先讓我們回顧下服務端的I/O代碼

echo_server.c

……
while ((str_len = read(clnt_sock, messag, 1024)) != 0)
	write(clnt_sock, messag, str_len);
……

    

接着,我們回顧客戶端的代碼

echo_client.c

……
write(sock, message, strlen(message));
str_len = read(sock, message, 1024 - 1);
……

  

二者都在循環調用read或write函數,實際上之前的回聲客戶端將100%接收自己傳輸的數據,只不過接收數據時的單位有些問題

觀察下面的代碼,我們可以知道回聲客戶端傳輸的是字符串,而且是通過調用write函數一次性發送,之后還調用了一次read函數,期待着接收自己傳輸的字符串,這就是問題所在

echo_client.c

……
while (1)
{
	fputs("Input message(Q to quit):", stdout);
	fgets(message, 1024, stdin);
	if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
		break;
	write(sock, message, strlen(message));
	str_len = read(sock, message, 1024 - 1);
	message[str_len] = 0;
	printf("Message from server:%s", message);
}
……

  

既然回聲客戶端會收到所有字符串數據,是否只需多等一會?過一段時間再調用read函數是否可以一次性讀取所有的字符串內容?的確,過一段時間后即可接收,但需要多久?1秒還是1分鍾?沒人知道。而且這也不符合常理,正常應該客戶端在收到字符串數據時立即讀取並輸出

其實問題很容易解決,因為可以提前確定接收數據的長度,若之前傳輸了20個字節長的字符串,則在接收時循環調用read函數讀取20個字節即可,下面,我們解決方案的代碼

echo_client2.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
void error_handling(char *message);

int main(int argc, char *argv[])
{
    int sock;
    char message[BUF_SIZE];
    int str_len, recv_len, recv_cnt;
    struct sockaddr_in serv_adr;

    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_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_adr.sin_port = htons(atoi(argv[2]));

    if (connect(sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
        error_handling("connect()error");
    else
        puts("Connected..........");

    while (1)
    {
        fputs("Input message(Q to quit):", stdout);
        fgets(message, BUF_SIZE, stdin);
        
        if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
            break;

        str_len = write(sock, message, strlen(message));

        recv_len = 0;
        while (recv_len < str_len)
        {
            recv_cnt = read(sock, &message[recv_len], BUF_SIZE - 1);
            if (recv_cnt == -1)
                error_handling("read()error!");
            recv_len += recv_cnt;
        }

        message[recv_len] = 0;
        printf("Message from server:%s", message);
    }
    close(sock);
    return 0;
}

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

  

在45~56行是變更及添加部分,echo_client.c僅調用了一次read函數,上述示例為了接收所有傳輸數據而循環調用read函數。另外代碼48行還可以寫成如下形式:

while (recv_len != str_len)
{
	……
}

  

接收的數據大小應和傳輸的相同,因此recv_len中保存的值等於str_len保存的值時,即可跳出while循環

回聲客戶端可以提前知道接收數據的長度,但這只是特例,多數情況下,我們是不知道接收數據的長度,那么我們應該如何收發數據?此時需要的就是應用層協議的定義,之前回聲服務端/客戶端中曾經定義:收到Q就立即終止連接

同樣,收發數據過程中也需要定好規則(協議)以表示數據的邊界,或提前告知收發數據的大小。服務端/客戶端實現過程中逐步定義的這些規則集合就是應用層協議,可以看出應用層協議並不是什么高深的技術,僅僅是為了特定程序而制定的規則

下面我們就來編寫一個應用層協議的程序,該程序中服務端從客戶端獲得多個數字和運算符信息。服務器端收到這些信息后根據運算符對數字做處理再返回給客戶端,例如:客戶端向服務端傳遞3、5、9的同時請求加法運算,那么服務端做完3+5+9=17的結果后會返回給客戶端

op_client.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
#define RLT_SIZE 4
#define OPSZ 4
void error_handling(char *message);
int main(int argc, char *argv[])
{
    int sock;
    char opmsg[BUF_SIZE];
    int result, opnd_cnt, i;
    struct sockaddr_in serv_adr;
    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_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_adr.sin_port = htons(atoi(argv[2]));

    if (connect(sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
        error_handling("connect() error!");
    else
        puts("Connected......");
    fputs("Operand count:", stdout);
    scanf("%d", &opnd_cnt);
    opmsg[0] = (char)opnd_cnt;

    for (i = 0; i < opnd_cnt; i++)
    {
        printf("Operand %d:", i + 1);
        scanf("%d", (int *)&opmsg[i * OPSZ + 1]);
    }
    fgetc(stdin);
    fputs("Operator:", stdout);
    scanf("%c", &opmsg[opnd_cnt * OPSZ + 1]);
    write(sock, opmsg, opnd_cnt * OPSZ + 2);
    read(sock, &result, RLT_SIZE);
    printf("Operation result:%d\n", result);
    close(sock);
    return 0;
}
void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

  

  • 第8、9行:將待計算的數字的字節數和運算結果的字節數設為常數
  • 第14行:為收發數據准備的內存空間,需要數據積累到一定程度后再收發,因此通過數組創建
  • 第37、38行:從用戶的輸入中得到待算數個數后,保存至數組opmsg。強制轉換成char類型,因為協議規定待算數個數應通過一個字節整數型傳遞,因此不能超過一個字節整數型能夠表示的范圍。示例中用的是有符號整數型,但待算數個數不能是負數,因此使用無符號整數型更合理
  • 第40~44行:從用戶的輸入中得到待算整數,保存到數組opmsg。4字節int型數據要保存到保存到char數組,因而在轉換成int指針類型
  • 第45行:第47行中輸入字符,在此之前調用fgetc函數刪掉緩沖中的字符'\n'
  • 第47行:最后輸入運算符信息,保存到opmsg數組
  • 第48行:調用write函數一次性傳輸opmsg數組中的運算相關信息,可以調用一次write函數進行傳輸,也可以分多次調用
  • 第49行:保存服務端傳輸的運算結果,待接收的數據長度為4字節,因此調用一次read函數即可接收

 

 

圖1-1   客戶端op_client.c的數據傳送格式

從圖1-1可以看出,若想在同一數組中保存並傳輸多種數據類型,應把數組聲明為char類型。而且需要額外做一些指針及數組運算。接下來給出服務端代碼:

op_server.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
#define OPSZ 4
void error_handling(char *message);
int calculate(int opnum, int opnds[], char operator);

int main(int argc, char *argv[])
{
    int serv_sock, clnt_sock;
    char opinfo[BUF_SIZE];
    int result, opnd_cnt, i;
    int recv_cnt, recv_len;
    struct sockaddr_in serv_adr, clnt_adr;
    socklen_t clnt_adr_sz;
    if (argc != 2)
    {
        printf("Usage:%s<port>\n", argv[0]);
        exit(1);
    }
    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    if (serv_sock == -1)
        error_handling("socket() error");
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_adr.sin_port = htons(atoi(argv[1]));
    if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
        error_handling("bind() error");
    if (listen(serv_sock, 5) == -1)
        error_handling("listen() error");
    clnt_adr_sz = sizeof(clnt_adr);

    for (i = 0; i < 5; i++)
    {
        opnd_cnt = 0;
        clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &clnt_adr_sz);
        read(clnt_sock, &opnd_cnt, 1);

        recv_len = 0;
        while ((opnd_cnt * OPSZ + 1) > recv_len)
        {
            recv_cnt = read(clnt_sock, &opinfo[recv_len], BUF_SIZE - 1);
            recv_len += recv_cnt;
        }
        result = calculate(opnd_cnt, (int *)opinfo, opinfo[recv_len - 1]);
        write(clnt_sock, (char *)&result, sizeof(result));
        close(clnt_sock);
    }
    close(serv_sock);
    return 0;
}
int calculate(int opnum, int opnds[], char op)
{
    int result = opnds[0], i;
    switch (op)
    {
    case '+':
        for (i = 1; i < opnum; i++)
            result += opnds[i];
        break;
    case '-':
        for (i = 1; i < opnum; i++)
            result -= opnds[i];
        break;
    case '*':
        for (i = 1; i < opnum; i++)
            result *= opnds[i];
        break;
    }
    return result;
}
void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

  

  • 第38行:為了接收5個客戶端的連接請求而編寫的for語句
  • 第42行:首先接收待算數個數
  • 第45~49行:根據第42行中的待算數個數接收待算數
  • 第50行:調用calculate函數的同時傳遞待算數和運算符信息參數
  • 第51行:向客戶端傳輸calculate函數返回的運算結果

編譯op_server.c並運行

# gcc op_server.c -o op_server
# ./op_server 8500

  

編譯op_client.c並運行

# gcc op_client.c -o op_client
# ./op_client 127.0.0.1 8500
Connected......
Operand count:3
Operand 1:1
Operand 2:2
Operand 3:3
Operator:+
Operation result:6
# ./op_client 127.0.0.1 8500
Connected......
Operand count:2
Operand 1:2
Operand 2:15
Operator:*
Operation result:30

  

TCP原理

之前我們說過,TCP套接字的數據收發無邊界,服務端即使調用一次write函數傳輸60個字節的數據,客戶端也有可能分3次調用read函數,每次讀取20個字節的數據。但此處也有些疑問,服務端一次性傳輸60個字節的數據,而客戶端分批讀取。客戶端在讀取20個字節后,剩下的40個字節在何處等候呢?實際上,write函數調用后並非立即傳輸數據,read函數調用后也並非馬上接收數據。更准確地說,圖1-2所示,write函數調用瞬間,數據將移至輸出緩沖;read函數調用瞬間,從輸入緩沖讀取數據

圖1-2   TCP套接字的I/O緩沖

如圖1-2所示,調用write函數時,數據將移到輸出緩沖,在適當的時候(不管是分別傳送還是一次性傳送)傳向對方的輸入緩沖。這時對方將調用read函數從輸入緩沖讀取數據。這些I/O緩沖特性如下:

  • I/O緩沖在每個TCP套接字中單獨存在
  • I/O緩沖在創建套接字時自動生成
  • 即使關閉套接字也會繼續傳遞輸出緩沖中遺留的數據
  • 關閉套接字將丟失輸入緩沖中的數據

如果客戶端輸入緩沖有50個字節,但服務端卻有100個字節需要傳輸,是否會造成數據丟失?之前說過TCP是可靠的,不會發生超過輸入緩沖大小的數據傳輸,因為TCP會控制數據流,其中有滑動窗口協議,接收數據的套接字每次會告訴發送數據的套接字可傳遞的最大字節數,於是發送數據的套接字收到這個數字后傳遞等長度大小的數據,待接收數據的套接字發現緩沖中騰出更多位置,會告訴發送數據的套接字,接收更多的數據。因此,TCP不會因為緩沖滿了而丟失數據

TCP內部工作原理1:與對方套接字的連接

TCP套接字從創建到消失所經過程分為如下三步:

  1. 與對方套接字建立連接
  2. 與對方套接字進行數據交換
  3. 斷開與對方套接字的連接

TCP在實際通信過程中也會經歷三次對話過程,因此,該過程又稱為Three-way handshake(三次握手),如圖1-3所示:

圖1-3   TCP套接字的連接設置過程

套接字是以雙全工方式工作的,也就是說它可以雙向傳遞數據。因此,收發數據前需要做一些准備,首先,請求連接的主機A向主機B傳遞如下消息:

[SYN] SEQ:1000, ACK:-

該消息中SEQ為1000,ACK為空,而SEQ為1000的含義是:主機A現傳遞的數據包序號為1000給主機B,如果接收無誤,請通知主機A向主機B傳遞1001號數據包。這是首次請求連接時使用的消息,又稱SYN。SYN是Synchronize的簡寫,表示收發數據前傳輸的同步消息。接下來,主機B向主機A傳遞消息:[SYN+ACK] SEQ:2000, ACK:1001。此時SEQ為2000,ACK為1001,而SEQ具體含義和之前一樣:主機B現傳遞的數據包序號為2000給主機A,如果接收無誤,請通知主機B向主機A傳遞2001號數據包

而ACK 1001的含義為:剛才傳輸的SEQ為1000的數據包接收無誤,請傳遞SEQ為1001的數據包。對於主機A首次傳輸的數據包的確認消息(ACK 1001)和主機B傳輸數據做准備的同步消息(SEQ 2000)捆綁發送,因此,此種類型的消息又稱SYN+ACK

收發數據前向數據包分配序號,並向對方通報此序號,這都是為防止數據丟失所做的准備。通過向數據包分配序號並確認,可以在數據丟失時馬上查看並重傳丟失的數據包。因此,TCP可以保證可靠的數據傳輸。最后觀察主機A向主機B傳輸消息:[ACK] SEQ: 1001, ACK: 2001。我們都明白SEQ和ACK的含義了,主機A現在向主機B傳遞1001號數據包,並表示2001號數據包接收無誤。至此,主機A和主機B確認了彼此均就緒

TCP內部工作原理2:與對方主機的數據交換

通過第一步三次握手過程完成了數據交換准備,下面就開始正式收發收據。其默認方式如圖1-4所示:

圖1-4   TCP套接字的數據交換過程

圖1-4給出了主機A分兩次向主機B傳輸各100字節的過程。首先主機A通過一個數據包發送100個字節的數據,數據包的SEQ為1200。主機B為了確認這一點,向主機A發送ACK1301消息。此時的ACK號為1301而非1201,原因在於ACK號的增量為傳輸的數據字節數。假設每次ACK號不加傳輸的字節數,這樣雖然可以確認數據包到達目標主機,但無法明確100字節全部正確到達還是丟失了一部分。因此ACK消息公式為:ACK號 = SEQ號 + 傳遞字節數 + 1

與三次握手協議相同,最后加一是為了告知對方下次要傳遞的SEQ號。下面分析傳輸過程中數據包消失的情況,如圖1-5:

如圖1-5   TCP套接字數據傳輸過程中發生錯誤

圖1-5表示通過SEQ 1301數據包向主機B傳遞100字節數據。但中間發生了錯誤,主機B未收到,經過一段時間,主機A仍為收到對於SEQ 1301的ACK確認,因此試着重傳該數據包。為了完成數據包的重傳,TCP套接字啟動計時器以等待ACK應答。若計時器發生超時則重傳

TCP的內部工作原理3:斷開與套接字的連接

TCP套接字的結束過程也與之前相似,如果對方還有數據需要傳輸時直接斷掉連接會出現問題,所以斷開連接時需要雙方協商,先由套接字A向套接字B傳遞斷開連接的消息,套接字B發出確認收到的消息,然后向套接字A傳遞可以斷開連接的消息,套接字A同樣發出確認的消息,如圖1-6所示:

圖1-6   TCP套接字斷開連接過程

圖1-6數據包內的FIN表示斷開連接。也就是說,雙方各發送一次FIN消息后斷開連接,此過程經歷四個階段,因此稱為四次握手(Four-way handshaking)。圖1-6向主機A傳遞兩次ACK 5001,也許這會讓大家感到困惑,這里做一下解答:主機A向主機B發送FIN消息,告訴主機B自己沒有數據可以發送,但如果主機B還有數據沒發送完,可以不必急着關閉套接字,可以繼續發送數據。於是,主機B發送ACK。當主機B確認數據發送完畢,則向主機A發送FIN消息,告訴主機A數據全部發送關閉,准備關閉連接。主機A收到FIN消息,還是不相信網絡,怕主機B不知道要關閉,所以發送ACK,如果主機B沒有收到可以重傳。主機B收到ACK后,就知道可以斷開連接了,於是主機A等待在超時時間內沒有收到回復, 則證明主機B已關閉連接,於是主機A也跟着關閉連接


免責聲明!

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



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