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消息后斷開連接。此過程經歷四個階段,因此又稱四次握手。

