1. 程式人生 > >基於TCP的服務端/客戶端

基於TCP的服務端/客戶端

基於TCP的服務端/客戶端

根據資料傳輸方式不同,基於網路協議的套接字一般分為TCP套接字(也叫基於流stream的套接字)UDP套接字。

 TCP/IP協議棧(Stack,層)eg:

應用層

            /   \

          TCP   UDP

           \     /

            IP

             |

鏈路層

各層可能通過作業系統等軟體實現,也可能通過類似NIC的硬體裝置實現

0>開放系統

以多個標準為依據設計的系統稱為開放系統,TCPIP協議棧也屬於其中之一

優點:路由器來完成IP層互動任務,網絡卡:網絡卡製造商都會遵守鏈路層的協議標準

 1>鏈路層:是物理連結領或標準的結果,也是最基本的領域,專門定義LANWANMAN等網路標準

 2>IP層:準備好物理連線後就要傳輸資料。為了在複雜的網路中傳輸資料,首先需要考慮路徑的選擇,向目標傳輸

資料需要經過哪條路徑?解決此問題就是IP層,該層使用的協議就是IPIP本身是面向訊息的,不可靠的協議。每次

傳輸資料時會幫我們選擇路徑,但並不一致。如果傳輸中發生路徑錯誤,則選擇其他路徑;但如果發生資料丟失或

錯誤,則無法解決--IP協議無法應對資料錯誤

 3>TCP/UDP層:IP層解決資料傳輸中的路徑選擇問題,只需照此路徑傳資料即可。TCPUDP層以

IP層提供的路徑資訊為

基礎完成實際資料傳輸--傳輸層。UDPTCP簡單

 TCPIP層二者關係:

 IP層只關注1個數據包(資料傳輸的基本單位)的傳輸過程。因此,即使傳輸多個數據包,每個資料包也是由IP層實際傳輸的

,也就是說傳輸本身是不可靠的。若只利用IP層傳輸資料,則有可能導致後傳輸的資料包B比先傳輸的資料包A提早到達。另外

,傳輸的資料包A/B/C中有可能收到AC,甚至收到的C可能已損毀

 TCP如果資料交換過程中可以確認對方已收到資料,並重傳丟失的資料,那麼即便IP層不保證資料傳輸,這類通訊也是可靠的

 4>應用層:

上述內容是套接字通訊過程中自動處理的。選擇資料傳輸路徑,資料確認過程都被隱藏到套接字內部。而與其說是

隱藏,倒不

使程式設計師從這些細節中解放出來的表達更為準確。程式設計師程式設計時無需考慮這些過程,但這並不意味著不用掌握這些短識。只有

掌握了這些理論,才能編寫出符合需求的網路程式

總之,向各位提供的工具就是套接字,只需利用套接字編出程式即可。編寫軟體的過程中,需要根據程式特點決定伺服器和客戶端之

間的資料傳輸規則---應用層協議。

網路程式設計的大部份內容就是設計並實現應用層協議

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

 TCP的伺服器端預設的函式呼叫順序:

 socket() 建立套接字

    |

    V

 bind()分配套接字地址

    |

    V

 listen()等待連線請求狀態

    |

    V

 accept() 充許連線

    |

    V

 read()/write() 資料交換

    |

    V

 close() 斷開連線

 socket,bind前面已說明,bind()(給套接字分配了地址,接下來就要通過呼叫listen()進入等待連線請求狀態

只有呼叫了listen(),客戶端才能進入可發出連線請求的狀態----這時客戶端才能呼叫connect()(若提前呼叫將發生錯誤)

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

 sock:希望進入等待連線狀態的套接字檔案描述符,傳遞的描述符套接字引數為服務端套接字(監聽套接字)

 backlog:連線請求等待佇列(Queue)的長度,若為5,則佇列長度為5,表示最多使5個連線請求進入佇列。(與服務端的特性有關,

像頻繁接收請求的Web服務端至少應為15.另外,連線請求佇列的大小始終根據實驗結果而定。)

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

 sock:伺服器套接字的檔案描述符

 addr:儲存發起連線請求的客戶端地址資訊的變數地址值,呼叫函式後向傳遞來的地址變數引數填充客房端地址資訊

 addrlen:第二個引數addr結構體的長度,但時存有長度的變數地址。函式呼叫完成後,該變數即被填入客戶端地址長度

 accept()受理連線請求等待佇列中待處理的客戶端連線請求。函式呼叫成功時,accetp()內部將產生用於資料I/O的套接字,並返回其

檔案描述符。需要強調的是,套接字是自動建立的,並自動與發起連線請求的客戶端建立連線。

呼叫accept()從隊頭取1個連線請求與客戶端建立連線,並返回建立的套接字檔案描述符。另外,呼叫accetp()時若等待佇列為空,則

 accept()不會返回,直到佇列中出現新的客戶端連線

 8.2 TCP客戶端的預設函式呼叫順序

 socket() 建立套接字

    |

    V

 connect()請求連線

    |

    V

 read()/write() 資料交換

    |

    V

 close() 斷開連線

與服務端相比,區別就在於"請求連線,它是建立客戶端套接字後向伺服器端發起的連線請求。伺服器端呼叫

 listen()建立連線請求等待佇列,之後客戶端即可請求連線。

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

 sock:客戶端套接字檔案描述符

 servaddr:儲存目標伺服器端地址資訊的變數地址值

 addrlen:以位元組為單位傳遞已傳遞給第二個結構體引數servaddr的地址變數找度

客戶端呼叫connect(),發生以下情況之一才會返回(完成函式呼叫)

 1>伺服器端接收連線請求

 2>發生斷網等異常情況而中斷連線請求

需要注意,所謂的接收連線並不意味著伺服器端呼叫accept(),其實是服務端把連線請求資訊記錄到等待佇列。因此connect()

返回後並不立即進行資料交換

 3>實現服務端必經過過程之一就是給套接字分配IP和埠號。但客戶實現過程中並未出現套接字地址分配,而是建立套接字後立即呼叫

 connect(). 客戶端呼叫connect()時,作業系統,更準確地說是在核心中,IP用計算機(主機)的IP,埠隨機分配地址---客戶端

IP和埠呼叫connect()時自動分配,無需呼叫標記的bind()進行分配

 8.3 基於TCP的服務端/客戶端函式呼叫關係

前面講解TCP伺服器/客戶端的實現順序,實際上二者並非相互獨立

伺服器

 socket() 建立套接字

    |

    V

 bind()分配套接字地址客戶端

    |                                   socket() 建立套接字

    V                                         |

 listen()等待連線請求狀態                        V

    |   <---------------------------—--connect()請求連線

    V                      or       /         |

 accept() 充許連線                   /

    |   <------------------------  /          |

    V                                         V

 read()/write() 資料交換  <------->     read()/write()資料交換

    |                                         |

    V                                         V

 close() 斷開連線   <------------->      close()斷開連線

總體流程整理:伺服器端建立套接字後連續呼叫bindlisten函式進入等待狀態,客戶端通過呼叫connect()

發起連線請求.需要注意的是,客戶端只能等到服務端呼叫listen()後才能調connect().同時要清楚,客戶端呼叫

 connect()前,伺服器端有可能率先呼叫accept().當然,此時服務端在呼叫accept()時進入阻塞(blocking)

態,直到客戶端呼叫connect()為止

 8.4實現迭代伺服器端/客戶端

 1>實現迭代伺服器端

編寫回聲(echo)服務端/客戶端----服務端將客戶端傳輸的字串資料原封不動地傳回客戶端,就像回聲一樣。

之前的hello world服務端處理完髮1個客戶端連線請求即退出,連線請求等待佇列實際沒有太大意義。

設定好等待佇列的大小後,應向所有客戶疫提供服務。如果想繼續受理後續的客戶疫連線請求,最簡單的辦法就是

插入迴圈語句反覆呼叫accetp()

 socket() 建立套接字

    |

    V

 bind()分配套接字地址

    |

    V

 listen()等待連線請求狀態

    | <--------------------

    V                     |

 accept() 充許連線          |

    |                     |

    V                     |

 read()/write() 資料交換    |

    |---------------------

    V

 close() 斷開連線

呼叫accept()後,緊接著呼叫I/O相關的read(),write(),然後呼叫close(),這並非針對服務端套接字,而

是針對accept()呼叫時建立的套接字。呼叫close()就意味著結束了針對某一客戶端的服務,此時如果還想服務於

其他客戶端,就要重新呼叫accept()

2>實現迭代伺服器端/客戶端

前面講的就是迭代伺服器端。媽即使服務端以迭代方式運轉,客戶端程式碼亦無太大區別。

程式的基本執行方式:

 .服務端在同一時刻只與一個客戶端相連,並提供回聲服務

 .服務端依次向5個客戶端提供服務並退出

 .客戶端接收使用者輸入的字串併發送到服務端

 .服務端將接收到的字串資料傳回客戶端,即回聲

 .服務端與客戶端之間的字串回聲一直執行到客戶端輸入Q為止

 // 服務端

 #define BUF_SIZE 1024

 int echo_serverc(int argc,char *argv[]) {

 int serv_sock,clnt_sock;

 char message[BUF_SIZE];

 int str_len,i;

 struct sockaddr_in serv_adr,clnt_adr;

 socklen_t clnt_adr_sz;

 if (argc!=2) {

 printf("usege:%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 ++) {

 clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &clnt_adr_sz);

 if (clnt_sock == -1) {

 error_handling("accept() error");

 }else {

 printf("Connected client %d \n",i + 1);

 }

 while ((str_len = (int)read(clnt_sock, message, BUF_SIZE))!=0) {

 write(clnt_sock, message, str_len);

 }

 close(serv_sock);

 }

 return 0;

 }

 // 客戶端

 #define BUF_SIZE 1024

 int echo_clientc(int argc,char *argv[]) {

 int sock;

 char message[BUF_SIZE];

 long str_len;

 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("127.0.0.1");

 serv_adr.sin_port = htons(atoi("9190"));

 if (connect(sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr))== -1) {

 error_handling("connect error");

 }else {

 puts("Conneted...");

 }

 while (1) {

 fputs("input messae(Q to quit):", stdout);

 fgets(message,BUF_SIZE,stdin);

 if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) {

 break;

 }

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

 str_len = read(sock, message, BUF_SIZE-1);

 message[str_len]= 0;

 printf("message from server:%s",message);

 }

 close(sock);

 return 0;

 }

...