【TCP/IP網絡編程】:04基於TCP的服務器端/客戶端

摘要:結合前面所講述的知識,本篇文章主要介紹了簡單服務器端和客戶端實現的框架流程及相關函數接口。程序員

理解TCP和UDP算法

根據數據傳輸方式的不一樣,基於網絡協議的套接字通常分爲TCP套接字和UDP套接字(本系列文章主要圍繞TCP的內容講解)。編程

TCP(Transmission Control Protocol)即傳輸控制協議,意爲「對數據傳輸過程的控制」。所以,關注控制方法及範圍有助於正確理解TCP套接字。服務器

TCP/IP協議棧網絡

TCP/IP協議棧共分爲4層,能夠理解爲將數據收發分爲了4個層次化的過程,以下圖所示。各層能夠經過操做系統等軟件實現,也可經過相似NIC的硬件設備實現。相較於數據通訊過程的7層協議棧(OSI 7層模型),對於普通程序員來講掌握這四層就能夠了。多線程

TCP/IP協議棧併發

TCP/IP協議的誕生背景框架

「經過因特網完成有效的數據傳輸」這一課題是涉及到了硬件、系統、路由算法等各個領域的一個大系統。所以,當時相關領域的專家就聚在一塊兒討論,肯定將這一大課題按不一樣領域分紅若干小模塊,這就出現了多種協議,它們經過層級結構創建了緊密聯繫。socket

將協議分爲多個層次有不少優勢,最重要的緣由是爲了經過標準化操做設計開放式系統。標準自己就在於對外公開,引導更多人遵照規範。其中,以多個標準爲依據設計的系統稱爲開放式系統,TCP/IP協議棧即是其中之一。ide

鏈路層

鏈路層是物理連接領域標準化的結果,也是最基本的領域,專門定義LAN、WAN、MAN等網絡標準。若兩臺主機經過網絡進行數據交換,則須要經過下圖所示的物理鏈接,鏈路層就負責這些標準。

網絡鏈接結構

IP層

準備好物理鏈接後就須要傳輸數據,而在複雜的網絡中傳輸數據,首先就是要考慮經過哪條路徑將數據傳輸至目標主機?這就是IP層協議解決的問題。

IP自己是面向消息、不可靠的協議,所以,IP協議沒法應對各類可能的數據錯誤。

TCP/UDP層

TCP和UDP層以IP層提供的路徑信息爲基礎完成實際的數據傳輸,故該層又稱爲傳輸層。IP層只關注1個數據包(數據傳輸的基本單位)的傳輸過程,對於多個數據包的傳輸也是由IP層完成對每一個數據包的實際傳輸。所以,正如前面所述,IP層對數據傳輸的過程並不可靠。而TCP協議的性質則向不可靠的IP協議賦予了可靠性,下圖是TCP對網絡丟包的處理。

TCP協議

應用層

以上協議的處理過程都是套接字通訊自動處理的,如選擇數據傳輸路徑、數據確認過程,這些都被隱藏到了套接字內部。編寫軟件的過程當中,須要根據程序特色決定服務器端和客戶端之間的數據傳輸規則,這即是應用層協議。而網絡編程的大部份內容就是設計並實現應用層協議。

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

TCP服務器端的默認函數調用

大部分服務器端默認函數調用都是按照下圖所示的順序來執行的,其中socket及bind函數前文已有介紹,下面介紹以後的實現過程。

 TCP服務器端函數調用順序

進入等待鏈接請求狀態 - listen

調用listen函數使服務器端進入等待鏈接請求的狀態,此時客戶端才能調用connect函數進入發出鏈接請求的狀態,若提早調用connect則會報錯(Connection refused)。

#include <sys/socket.h>

int listen(int sock, int backlog);
    -> 成功時返回0,失敗時返回-1

其中,backlog爲鏈接請求等待隊列的長度,若爲N則表示最多使N個鏈接請求進入隊列(鏈接請求等待隊列又分爲已鏈接和未鏈接等待隊列,這裏backlog表示已鏈接等待隊列長度,其實目前並未有對backlog參數的確切定義,須要根據實際環境肯定)。「服務器端處於等待鏈接請求狀態」是指,客戶端鏈接請求時,受理鏈接前一直使鏈接請求處於等待狀態,該過程以下圖所示。

 等待鏈接請求狀態

listen函數的第一個參數是服務器端套接字,如同一個門衛監聽到來的鏈接請求,並將這些請求送往鏈接請求等候室;第二個參數與服務器的特性有關,根據服務器的工做性質來決定適當的隊列大小值。

受理客戶端鏈接請求 - accept

調用listen函數後,如有鏈接請求則按序受理,受理請求則意味着進入可收發數據的狀態。監聽套接字已有本身的工做職責,此時須要建立一個新的會話套接字來服務發起鏈接的客戶端套接字。

#include <sys/socket.h>

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

第一個參數是服務器套接字文件描述符;第二個參數用於保存客戶端地址信息;第三個參數用於保存客戶端地址信息的長度,但首先須要傳入地址信息結構長度信息。

accept函數受理鏈接請求等待隊列中待處理的客戶端鏈接請求,成功時返回新生成的用於數據I/O套接字的文件描述符。該I/O套接字是自動建立的,且已自動創建了與發起鏈接請求的客戶端之間的鏈接。accept函數的調用過程以下圖所示。

 受理鏈接請求狀態

調用accept函數會從等待鏈接請求隊列頭處取1個鏈接請求與客戶端創建鏈接,並返回建立的套接字文件描述符。若是此時等待隊列爲空,則accept函數會發生阻塞,直到隊列中出現新的客戶端鏈接

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

客戶端的函數調用相較於服務器端要簡單許多,由於套接字建立和鏈接請求即是一個簡單客戶端的所有內容,其函數調用過程以下。

 TCP客戶端函數調用順序

服務器端調用listen函數建立鏈接請求隊列,以後客戶端便可發起請求鏈接。

#include <sys/socket.h>

int connect(int sock, struct sockaddr *servaddr, socklen_t addrlen);
    -> 成功時返回0,失敗時返回-1

客戶端調用connect函數後,發生如下狀況之一時纔會返回:

  a. 服務器端接收鏈接請求

  b. 發生斷網等異常狀況而中斷鏈接請求

所謂「接收鏈接」並不意味着服務器端須要調用accept函數,實際上是服務器端把鏈接請求信息記錄到等待隊列的過程。所以,connect函數成功返回並不意味着能夠當即進行數據交換。

以前的文章中有提到過這樣一個疑問,服務器端須要調用bind函數綁定地址信息到服務器端套接字,那爲什麼客戶端沒有這一過程呢?其實客戶端套接字也是須要分配IP和端口號等地址信息的,只不過這一步驟被操做系統隱藏了。那客戶端又是什麼時候、何地、如何分配地址呢?

  什麼時候?  調用connect函數時

  何地?  操做系統,準確說時在內核中

  如何?  IP使用計算機的IP,端口號隨機

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

服務器端與客戶端的函數調用關係並不是相互獨立的,其交互關係大體以下。其中,須要重點理解客戶端connect函數的調用時機及服務器端對connect函數發起鏈接請求的反饋動做(大名鼎鼎的三次握手就這這個過程當中完成)。

 函數調用關係

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

以上介紹了TCP的相關知識,下面給出回聲服務器端/客戶端相關源碼以供瀏覽學習。

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <string.h>
 4 #include <unistd.h>
 5 #include <arpa/inet.h>
 6 #include <sys/socket.h>
 7 
 8 #define BUF_SIZE 1024
 9 void error_handling(char *message);
10 
11 int main(int argc, char *argv[])
12 {
13     int serv_sock, clnt_sock;
14     char message[BUF_SIZE];
15     int str_len, i;
16     
17     struct sockaddr_in serv_adr;
18     struct sockaddr_in clnt_adr;
19     socklen_t clnt_adr_sz;
20     
21     if(argc!=2) {
22         printf("Usage : %s <port>\n", argv[0]);
23         exit(1);
24     }
25     
26     serv_sock=socket(PF_INET, SOCK_STREAM, 0);   
27     if(serv_sock==-1)
28         error_handling("socket() error");
29     
30     memset(&serv_adr, 0, sizeof(serv_adr));
31     serv_adr.sin_family=AF_INET;
32     serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
33     serv_adr.sin_port=htons(atoi(argv[1]));
34 
35     if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
36         error_handling("bind() error");
37     
38     if(listen(serv_sock, 5)==-1)
39         error_handling("listen() error");
40     
41     clnt_adr_sz=sizeof(clnt_adr);
42 
43     for(i=0; i<5; i++)
44     {
45         clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
46         if(clnt_sock==-1)
47             error_handling("accept() error");
48         else
49             printf("Connected client %d \n", i+1);
50     
51         while((str_len=read(clnt_sock, message, BUF_SIZE))!=0)
52             write(clnt_sock, message, str_len);
53 
54         close(clnt_sock);
55     }
56 
57     close(serv_sock);
58     return 0;
59 }
60 
61 void error_handling(char *message)
62 {
63     fputs(message, stderr);
64     fputc('\n', stderr);
65     exit(1);
66 }
echo_server
 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <string.h>
 4 #include <unistd.h>
 5 #include <arpa/inet.h>
 6 #include <sys/socket.h>
 7 
 8 #define BUF_SIZE 1024
 9 void error_handling(char *message);
10 
11 int main(int argc, char *argv[])
12 {
13     int sock;
14     char message[BUF_SIZE];
15     int str_len;
16     struct sockaddr_in serv_adr;
17 
18     if(argc!=3) {
19         printf("Usage : %s <IP> <port>\n", argv[0]);
20         exit(1);
21     }
22     
23     sock=socket(PF_INET, SOCK_STREAM, 0);   
24     if(sock==-1)
25         error_handling("socket() error");
26     
27     memset(&serv_adr, 0, sizeof(serv_adr));
28     serv_adr.sin_family=AF_INET;
29     serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
30     serv_adr.sin_port=htons(atoi(argv[2]));
31     
32     if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
33         error_handling("connect() error!");
34     else
35         puts("Connected...........");
36     
37     while(1) 
38     {
39         fputs("Input message(Q to quit): ", stdout);
40         fgets(message, BUF_SIZE, stdin);
41         
42         if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))
43             break;
44 
45         write(sock, message, strlen(message));
46         str_len=read(sock, message, BUF_SIZE-1);
47         message[str_len]=0;
48         printf("Message from server: %s", message);
49     }
50     
51     close(sock);
52     return 0;
53 }
54 
55 void error_handling(char *message)
56 {
57     fputs(message, stderr);
58     fputc('\n', stderr);
59     exit(1);
60 }
echo_client

服務器端經過以下實現方式可循環服務發起鏈接的客戶端,但每次僅能服務一個客戶端(後續使用多線程或多進程的框架實現即可處理併發的狀況)。客戶端經過調用close函數主動發起斷連請求,服務器端收到該消息(EOF)便從阻塞的read函數中返回,此時read返回值爲0。

 迭代服務器端代碼實現流程

回聲客戶端存在的問題

回聲客戶端傳輸接收數據的流程以下。回顧以前關於TCP性質的介紹,咱們知道TCP是沒有數據邊界的,即write函數傳輸的數據可能在屢次調用以後一次發送;一樣read函數的調用也可能在還沒有收到所有數據包時返回。那麼這個問題該如何解決?

write(sock, message, strlen(message));
str_len=read(sock, message, BUF_SIZE-1);
message[str_len]=0;
printf("Message from server: %s", message);

結合服務器端的代碼來看,很容易能夠知道客戶端須要接收數據的大小,所以加一個循環判斷read結束條件便可。

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

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);

上面的實現確實解決了當前所面臨的問題,但更多的時候接收數據端並不能肯定待接收數據的大小等相關信息。所以,問題的根因並不在於客戶端,而是咱們應該定義符合需求的應用層協議。好比上面的問題,若是數據收發雙方預先協定好數據的邊界規則,或將數據包大小等相關信息寫入特定字段來表示問題即可獲得解決。

相關文章
相關標籤/搜索