理解TCP和UDPhtml
根據數據傳輸方式的不一樣,基於網絡協議的套接字通常分爲TCP套接字和UDP套接字。由於TCP套接字是面向鏈接的,所以又稱爲基於流(stream)的套接字。TCP是Transmission Control Protocol(傳輸控制協議)的簡寫,意爲「對數據傳輸過程的控制」。所以,學習控制方法及範圍有助於正確理解TCP套接字算法
TCP/IP協議棧編程
講解TCP前先介紹TCP所屬的TCP/IP協議棧(Stack,層),如圖1-1所示:服務器
圖1-1 TCP/IP協議棧網絡
從圖1-1能夠看出,TCP/IP協議棧共分爲四層,能夠理解爲數據收發分紅了四個層次化過程。也就是說,面對「基於互聯網的有效數據傳輸」的命題,並不是經過一個龐大的協議解決問題,而是經過層次化方案——TCP/IP協議棧解決,經過TCP套接字收發數據須要藉助四層,如圖1-2所示:併發
圖1-2 TCP協議棧socket
反之,經過UDP套接字收發數據時,利用圖1-2的四層協議棧來完成:函數
圖1-3 UDP協議棧post
各層可能經過操做系統等軟件實現,也可能經過相似NIC的硬件設備實現學習
TCP/IP協議的誕生背景
「經過因特網完成有效數據傳輸」這個課題讓許多專家彙集到一塊兒,不一樣人負責不一樣模塊,如:硬件、系統、路由。爲何要這樣作呢?由於編寫軟件前須要構建硬件系統,在此基礎上須要經過軟件實現各類算法,因此才須要衆多領域的專家進行討論,以造成各類規定。把「經過因特網完成有效數據傳輸」問題按照不一樣領域劃分紅小問題後,出現了多種協議,它們經過層級結構創建緊密聯繫
把協議分紅多個層次具備哪些優勢?協議設計更容易?這是優勢之一,但更重要的緣由是:爲了經過標準化操做設計開放式系統。標準自己就在於對外公開,引導更多人遵循。以多個標準爲依據所設計的系統稱爲開放式系統,咱們如今學習的TCP/IP協議棧也屬於其中之一。那麼開放式系統具備哪些優勢呢?比方:路由器用來完成IP層交互任務,某公司原先使用A路由器,可將其替換成B路由器,即使A、B這兩種路由器並不是同一產商也能夠順利替換,由於全部的路由器生產產商都會按照IP層標準制造
再舉個例子,你們的計算機通常都裝有網卡(網絡接口卡),即使沒安裝也不要緊,網卡很容易買到,由於全部的網卡製造商都會遵照鏈路層的協議標準,這就是開放式系統的優勢
鏈路層
接下來逐層瞭解TCP/IP協議棧,先講鏈路層。鏈路層是物理連接領域標準化的結果,也是最基本的領域,專門定義LAN、WAN、MAN等網絡標準。若兩臺主機經過網絡進行數據進行交換,則須要圖1-4所示的物理鏈接,鏈路層就負責這些標準:
圖1-4 網絡鏈接結構
IP層
準備好物理鏈接後就要傳輸數據,爲了在複雜的網絡中傳輸數據,首先須要考慮路徑的選擇。向目標傳輸數據須要通過哪條路徑?解決此問題就是IP層,該層使用的協議就是IP。IP自己是面向消息的、不可靠的協議。每次傳輸數據時會幫咱們選擇路徑,但每次傳輸時的路徑並不一致。若是傳輸中發生路徑錯誤,則選擇其餘路徑;但若是發生數據丟失或損壞,則沒法解決。換言之,IP協議沒法應對數據錯誤
TCP/UDP層
IP層解決數據傳輸中的路徑選擇問題,只需照此路徑傳輸數據便可。TCP和UDP層以IP層提供的路徑信息爲基礎完成實際的數據傳輸,故該層又稱傳輸層。UDP比TCP簡單,咱們後面還會在討論,如今只解釋TCP。TCP能夠保證可靠的數據傳輸,但它發送數據時以IP層爲基礎,IP層是面向消息的,是不可靠的,那TCP又是如何保證消息的可靠傳輸呢?
IP層只關注一個數據包(數據傳輸的基本單位)的傳輸過程。所以,即便傳輸多個數據包,每一個數據包也是由IP層實際傳輸的,也就是說傳輸順序及傳輸自己都是不可靠的。若只利用IP層傳輸數據,則有可能後發送的數據包比早發生的數據包先到達目標主機。另外,傳輸的數據包A、B、C中可能只收到A和C,B可能丟失或接收到時已損壞。但若添加TCP協議則會按照如圖1-5的方式進行數據傳輸:
圖1-5 傳輸控制協議
咱們能夠看到,當主機A發送1號數據包給主機B時,必須等到主機B確認1號數據包接收成功,纔會接着發送2號數據包,若是主機A發送1號數據包卻遲遲收不到主機B回覆的接收成功,則會認爲是超時,並從新發送一個1號數據包
實現基於TCP的服務端/客戶端
圖1-6給出了TCP服務器端默認的函數調用順序,大部分TCP服務器端都按照該順序調用
圖1-6 TCP服務端函數調用順序
調用socket函數建立套接字,聲明並初始化地址信息結構體變量,調用bind函數向套接字分配地址。這兩個階段以前都討論過了,下面講解以後的幾個過程
進入等待鏈接請求狀態
咱們已調用bind函數給套接字分配了地址,接下來就要經過調用listen函數進入等待鏈接請求狀態。只有調用了listen函數,服務端套接字才能進入可接收鏈接的狀態,換言之,這時,客戶端才能調用connect函數(若提早調用則會發生錯誤)
#include <sys/socket.h> int listen(int sockfd, int backlog);//成功時返回0,失敗時返回-1
先解釋一下等待鏈接請求狀態的含義和鏈接請求等待隊列。「服務器端處於等待鏈接請求狀態」是指,客戶端請求鏈接時,服務器端受理鏈接前一直處於等待狀態,當有多個客戶端一塊兒發送鏈接請求時,服務器端套接字只能處理一個鏈接請求,而其餘的鏈接請求,只能暫時放在請求隊列,即listen函數的第二個參數
受理客戶端鏈接請求
調用listen函數後,如有新的鏈接請求,則應按序受理。受理請求意味着進入可接收數據的狀態,這裏進入這種狀態的所需部件固然仍是套接字,可能有人會想使用服務器端套接字,但服務器端套接字已經用於監聽,若是將其用於與客戶端交換數據,那麼誰來監聽客戶端的鏈接請求呢?所以須要另一個套接字,但不必親自建立,accept函數將自動建立套接字,並鏈接到發起請求的客戶端
#include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);//成功時返回建立的套接字文件描述符,失敗時返回-1
accept函數受理鏈接請求等待隊列中待處理的客戶端鏈接請求,函數調用成功時,accept函數內部將產生用於數據I/O的套接字,並返回其文件描述符。須要強調的是,套接字是自動建立的,並自動與發起鏈接請求的客戶端創建鏈接
這裏,咱們從新回顧TCP/IP網絡編程之網絡編程和套接字這一章中的hello_server.c
hello_server.c
#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 serv_sock; int clnt_sock; struct sockaddr_in serv_addr; struct sockaddr_in clnt_addr; socklen_t clnt_addr_size; char message[] = "Hello world!"; if (argc != 2) { printf("Usage: %s <port>\n", argv[0]); exit(1); } serv_sock = socket(AF_INET, SOCK_STREAM, 0); if (serv_sock == -1) error_handling("sock() error"); memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port = htons(atoi(argv[1])); if (bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) error_handling("bind() error"); if (listen(serv_sock, 5) == -1) error_handling("listen() error"); clnt_addr_size = sizeof(clnt_addr); clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size); if (clnt_sock == -1) error_handling("accept() error"); write(clnt_sock, message, sizeof(message)); close(clnt_sock); close(serv_sock); return 0; } void error_handling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); }
TCP客戶端的默認函數調用順序
接下來說解客戶端的實現順序,咱們前面說過,客戶端的套接字實現比服務器端要簡單的多,由於建立套接字和請求鏈接就是客戶端的所有內容,如圖1-7:
圖1-7 TCP客戶端函數調用順序
與服務器端相比,區別就在於「請求鏈接」,它是建立客戶端套接字後向服務器端發起的鏈接請求。服務器端調用listen函數後建立鏈接請求等待隊列,以後客戶端便可請求鏈接。那如何發起鏈接請求呢?經過connect函數完成:
#include <sys/socket.h> int connect(int sock_fd, struct sockaddr *serv_addr, socklen_t addrlen);//成功時返回0,失敗時返回-1
客戶端調用connect函數後,發生如下狀況之一纔會返回:
須要注意,所謂的「接收鏈接」並不意味着服務器端調用accept函數,實際上是服務器端把鏈接請求信息記錄到等待隊列,所以connect函數返回後並不當即進行數據交換
這裏,咱們再回顧以前的hello_client.c
#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 sock; struct sockaddr_in serv_addr; char message[30]; int str_len; if (argc != 3) { printf("Usage: %s <IP> <port>\n", argv[0]); exit(1); } sock = socket(AF_INET, SOCK_STREAM, 0); if (sock == -1) error_handling("sock() error"); memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = inet_addr(argv[1]); serv_addr.sin_port = htons(atoi(argv[2])); if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -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服務器端/客戶端的實現順序,實際上兩者並不是相互獨立,讓咱們畫一下它們之間的交互過程,如圖1-8所示
圖1-8 函數調用關係
圖1-8的整體流程以下:服務器端建立套接字後聯繫調用bind、listen函數進入等待狀態,客戶端經過調用connect函數發起鏈接請求,須要注意的是,客戶端只能等到服務器端調用listen函數後才能調用connect函數。同時要清楚,客戶端調用connect前,服務器端可能先調用了accept函數。固然,此時服務器端在調用accept函數時進入了阻塞狀態,直到客戶端調用connect函數爲止
實現迭代服務器端/客戶端
如今,讓咱們來編寫一個回聲服務器端/客戶端,所謂回聲,就是服務器端將客戶端傳輸的字符串數據原封不動地回傳給客戶端,不過在此以前,須要解釋一下何爲迭代服務器端。以前咱們所看到的Hello world服務器端處理完一個客戶端鏈接請求則退出程序,鏈接請求等待隊列是實際上沒太大意義,這並不是咱們所需的服務器端,設置好等待隊列後,應向全部客戶端提供服務,若是在受理完一個客戶端請求鏈接後,還須要再受理其餘的請求鏈接,改怎麼擴展代碼?最簡單的辦法就是經過循環語句返回調動accept函數,如圖1-9
圖1-9 迭代服務器端的函數調用順序
圖1-9能夠看出,調用accept函數後,緊接着調用I/O相關的read、write函數,而後調用close函數。這並不是針對服務器端套接字,而是針對accept函數調用時所建立的套接字。調用close函數就意味着結束了針對某一客戶端的服務,此時若是還想服務於其餘客戶端,就要從新調用accept函數。目前,咱們的服務器端套接字同一時刻只能服務於一個客戶端鏈接,未來學完進程和線程後,就能夠編寫同時服務多個客戶端的服務器端了
迭代回聲服務器端/客戶端
即時服務器端以迭代方式運轉,客戶端代碼亦無太大區別,接下來建立迭代回聲服務器端及與之配套的回聲客戶端,首先整理一下程序的基本運行方式:
echo_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 void error_handling(char *message); int main(int argc, char *argv[]) { int serv_sock, clnt_sock; char messag[1024]; int str_len, i; 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++) { 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 = read(clnt_sock, messag, 1024)) != 0) write(clnt_sock, messag, str_len); close(clnt_sock); } close(serv_sock); return 0; } void error_handling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); }
echo_client.c
#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 sock; char message[1024]; int 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(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, 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); } close(sock); return 0; } void error_handling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); }
編譯echo_server.c並運行,服務器端套接字將等待客戶端鏈接請求
# gcc echo_server.c -o echo_server # ./echo_server 8500
編譯echo_client.c並分三次運行
# gcc echo_client.c -o echo_client # ./echo_client 127.0.0.1 8500 Connected.......... Input message(Q to quit):Hello Message from server:Hello Input message(Q to quit):world Message from server:world Input message(Q to quit):Q # ./echo_client 127.0.0.1 8500 Connected.......... Input message(Q to quit):Java Message from server:Java Input message(Q to quit):Python Message from server:Python Input message(Q to quit):Golang Message from server:Golang Input message(Q to quit):Q # ./echo_client 127.0.0.1 8500 Connected.......... Input message(Q to quit):Spring Message from server:Spring Input message(Q to quit):Flask Message from server:Flask Input message(Q to quit):Gin Message from server:Gin Input message(Q to quit):Q
最後可看到服務器端套接字程序打印以下:
# ./echo_server 8500 Connected client 1 Connected client 2 Connected client 3
能夠看到,服務器端套接字共處理了3次客戶端鏈接請求
回聲客戶端存在的問題
下面是echo_client.c的代碼
write(sock, message, strlen(message)); str_len = read(sock, message, 1024 - 1); message[str_len] = 0; printf("Message from server:%s", message);
以上的代碼有個錯誤假設:每次調用read、write函數時都會以字符串爲單位執行實際的I/O操做。可是別忘了,TCP不存在數據邊界。所以,屢次調用write函數傳遞字符串有可能一次性傳遞到服務端,此時,客戶端有可能從服務端收到多個字符串,這不是咱們但願看到的結果
還要考慮另一種狀況:字符串太長,須要分兩次數據包發送,客戶端有可能在還沒有收到所有數據包時就調用read函數。這些都是TCP特性的問題,咱們將在下一章給出解決的辦法