UNIX再学习 -- TCP/UDP 客户机/服务器
生活随笔
收集整理的這篇文章主要介紹了
UNIX再学习 -- TCP/UDP 客户机/服务器
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
一、TCP 客戶機/服務器
1、TCP 協議的基本特征
TCP 提供客戶機與服務器的連接
一個完整 TCP 通信過程需要依次經歷三個階段
首先,客戶機必須建立與服務器的連接,所謂虛電路。 然后,憑借已建立好的連接,通信雙方相互交換數據。 最后,客戶機與服務器雙雙終止連接,結束通信過程。TCP 保證數據傳輸的可靠性
TCP 的協議棧底層在向另一端發送數據時,會要求對方在一個給定的時間窗口內返回確認。如果超過了這個時間窗口仍沒有收到確認,則 TCP 會重傳數據并等待更長的時間。只有在數次重傳均告失敗以后,TCP 才會最終放棄。TCP 含有用于動態估算數據往返時間(Round-Trip Time, RTT)的算法,因此它知道等待一個確認需要多長時間。TCP 保證數據傳輸的有序性
TCP 的協議棧底層在向另一端發送數據時,會為所發送數據的每個字節指定一個序列號。即使這些數據字節沒有能夠按照發送時的順序到達接收方,接收方的 TCP 也可以根據它們的序列號重新排序,再把最后的結果交給應用程序。 如果 TCP 收到重復的數據(比如發送方認為數據已丟失并重傳,但它可能并沒有真的丟失,而只是由于網絡擁塞而被延誤),它也可以根據序列號做出判斷,丟棄重復的數據。TCP 提供流量控制
TCP 的協議棧底層在從另一端接收數據時,會不斷告知對方它能夠接收多少字節的數據,即所謂通告窗口。任何時候,這個窗口都反映了接收緩沖區可用空間的大小,從而確保不會因為發送方發送數據過快而導致接收緩沖區溢出。TCP 是流式傳輸協議
TCP 是一個字節流協議,無記錄邊界應用程序如果需要確定記錄邊界,必須自己實現
TCP 是全雙工的
在給定的連接上,應用程序在任何時候都既可以發送數據也可以接受數據。因此,TCP 必須跟蹤每個方向上數據流的狀態信息,如序列號和通告窗口的大小。2、TCP 連接的生命周期
(1)建立連接
被動打開
服務器必須首先做好準備隨時接受來自客戶機的連接請求。三路握手
客戶機的 TCP 協議棧服務器發送一個 SYN 分節,告知對方自己將在連接中發送數據的初始序列號,謂之主動打開。 服務器的 TCP 協議棧向客戶機發送一個單個分節,其中不僅包括對客戶機 SYN 分節的 ACK 應答,還包含服務器自己的 SYN 分節,以告知對方自己再同一連接中發送數據的初始序列號。 客戶機的 TCP 協議棧向服務器返回 ACK 應答,以表示對服務器所發 SYN 的確認。(2)交換數據
一旦連接建立,客戶機即可構造請求并發往服務器。 服務器接收并處理來自客戶機的請求包,構造響應包。 服務器向客戶機發送響應包,同時捎帶對客戶機請求包的 ACK 應答。到哪如果服務器處理請求和構造響應的時間長于 200 毫秒,則應答也可能先于響應發出。 客戶機接收來自服務器的響應包,同時向對方發送 ACK 應答。(3)終止連接
客戶機或者服務器主動關閉連接,TCP 協議棧向對方發送 FIN 分節,表示數據通信結束。如果此時尚有數據滯留于發送緩沖區中,則 FIN 分節跟在所有未發送數據之后。 接收到 FIN 分節的另一端執行被動關閉,一方面通過 TCP 協議棧向對方發送 ACK 應答,另一方面向應用程序傳遞文件結束符。如果此時接收緩沖區不空,則將所接收到的 FIN 分節追加到接收緩沖區的末尾。 一段時間以后,方才接收到 FIN 分節的進程關閉自己的連接,同時通過 TCP 協議棧向對方發送 FIN 分節。 對方在收到 FIN 分節后發送 ACK 應答。3、常用函數
(1)函數 listen:啟動偵聽
在指定套接字上啟動對連接請求的偵聽 #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int listen(int sockfd, int backlog); 返回值:成功返回 0,失敗返回 -1《1》參數解析
sockfd:套接字描述符 backlog:未決鏈接請求的最大值《2》函數解析
socket 函數所創建的套接字一律被初始化為主動套接字,即可以通過后續 connect 函數調用向服務器發起連接請求的客戶機套接字。listen 函數可以將一個這樣的主動套接字轉換為被動套接字,既可以等待并接受來自客戶機的連接請求的服務器套接字。 被 listen 函數啟動偵聽的套接字將由 CLOSED 狀態轉入 LISETN 狀態。 客戶機調用 connect 函數即開啟了 TCP 連接建立的第一路握手:通過協議棧向服務器發送 SYN 分節。服務器的 LISTEN 套接字一旦收到該分節,即創建一個新的處于 SYN_RCVD 裝填的套接字,并將其排入未完成連接隊列。 服務器的 TCP 協議棧不斷監視未完成連接隊列的狀態,并在適當的時機依次處理其中等待連接的套接字。一旦某個 套接字上的第二、三路握手完成,由 SYN_RCVD 狀態轉入 ESTABLISTEN 狀態,即被移送到已完成連接隊列。 兩個隊列中的套接字個數之和不能超過 backlog 參數值。若未完成連接隊列和已完成連接隊列中的套接字個數之和已經達到 backlog,此時又有客戶機通過 connect 函數發起連接請求,則該請求所產生的 SYN 分節將被服務器的 TCP 協議棧直接忽略。客戶機的 TCP 協議棧會因第一路握手應答超時而重發 SYN 分節,期望不久能在未決隊列中找到空閑位置。若多次重發均告失敗,則客戶機放棄,connect 函數返回失敗。 客戶機對 connect 函數的調用在第二路握手完成時即返回,而此時服務器連接套接字可能還子啊未完成連接隊列(第三路握手尚未完成)或已完成連接隊列(套接字尚未返回給用戶進程)中。這種情況下客戶機發送的數據,會被服務器的 TCP 協議棧排隊緩存,直到接收緩沖區滿為止。
(2)函數 accept:等待連接
在指定套接字上等待并接受連接請求 #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 返回值:成功返回連接套接字描述符,失敗返回 -1《1》參數解析
sockfd:偵聽套接字描述符 addr:輸出連接請求發起者地址結構 addrlen:輸入/輸出,連接請求發起者地址結構長度(以字節為單位)《2》函數解析
accept 函數由 TCP 服務器調用,返回排在已完成連接隊列首部的連接套接字對象的描述符,若隊列為空則阻塞。 若 accept 函數執行成功,則通過 addr 和 addrlen 向調用者輸出發起連接請求的客戶機的協議地址及其字節長度。 注意 addrlen 既是輸入參數也是輸出參數。調用 accept 函數時,指針 addrlen 所指向的變量被初始化 addr 結構體的字節大小;等到該函數返回時,該指針的目標則被更新為系統內核保存在 addr 結構體內的實際字節數。 accept 函數成功返回的是一個有別于其參數套接字,由系統內核自動生成的全新套接字描述符。它代表與客戶機的 TCP 連接,因此被稱為連接套接字,而該函數的第一個參數則被稱為偵聽套接字。通常一個服務器只有一個偵聽套接字,且一直存在直到服務器關閉,而連接套接字則是一個客戶機一個,專門負責與該客戶機的通信。(3)函數 recv:接收數據
通過指定套接字接收數據 #include <sys/types.h> #include <sys/socket.h> ssize_t recv(int sockfd, void *buf, size_t len, int flags); 返回值:成功返回實際接收到的字節數,失敗返回 -1《1》參數解析
sockfd:套接字描述符 buf:應用程序接收緩沖區 len:期望接收的字節數 flags:接收標志,一般取 0,還可取以下值:? ? MSG_DONTWAIT ? ?以非阻塞方式接受數據 ? ? MSG_OOB ? ? ? ? ? ? ? 接收帶外數據 ? ? MSG_PEEK ? ? ? ? ? ? ? 只查看可接收的數據,函數返回后數據依然留在接收緩沖區中
如前所述,客戶機或者服務器主動關閉連接,TCP 協議棧向對方發送 FIN 分節,表示數據通信結束,接收到 FIN 分節的另一端執行被動關閉,一方面通過 TCP 協議棧向對方發送 ACK 應答,另一方面向應用程序傳遞文件結束符,此時 recv 函數返回 0.
《2》阻塞于非阻塞
套接字 I/O 的缺省方式都是阻塞的。對于 TCP 而言,如果接收緩沖區中沒有數據,recv 函數將會阻塞,直到有數據到來并被復制到 buf 緩沖區時才會返回。此時所接收都的數據可能比 len 參數期望接收的字節數少。除非調用 recv 函數時使用 MSG_WAITALL 標志,不接收到 len 字節的數據,函數就不返回。但即便使用了 MSG_WAITALL 標志,實際接收到的字節數在以下三種情況下仍然可能比期望的少。 ? ? 函數被信號中斷 ? ? 連接被對方終止 ? ? 發生套接字錯誤 MAG_DONTWAIT 標志令接收過程以非阻塞方式進行,即便收不到數據,recv 函數也會立即返回,返回值 -1,errno 為 EAGAIN 或 EWOULDBLOCK(4)函數 send:發送數據
通過指定套接字發送數據 #include <sys/types.h> #include <sys/socket.h> ssize_t send(int sockfd, const void *buf, size_t len, int flags); 返回值:成功返回實際被發送的字節數,失敗返回 -1《1》參數解析
sockfd:套接字描述符 buf:應用程序發送緩沖區 len:期望發送的字節數 flags:發送標志,一般取 0,還可以取以下值: ? ? MSG_DONTWAIT ? ?以非阻塞方式發送數據 ? ? MSG_OOB ? ? ? ? ? ? ? 發送帶外數據 ? ? MSG_DONTROUTE ?不查路由器,直接在本地網絡中尋找目的主機《2》阻塞于非阻塞
套接字 I/O的缺省方式都是阻塞的。對于 TCP 而言,如果發送緩沖區中沒有足夠的空閑空間,send 函數將會阻塞,直到其空閑空間足以容納 len 字節的待發送數據,并在將全部待發送數據復制到發送緩沖區后才會返回。 MSG_DONTWAIT 標志令發送過程以非阻塞方式進行,即便發送緩沖區中一個字節的空閑空間都沒有,send 函數也會立即返回,返回值為 -1,errno 為 EAGAIN 或 EWOULDBLOCK。 在非阻塞方式下,如果發送緩沖區中尚有少量空閑空間,則會將部分待發送數據復制到發送緩沖區,同時返回復制到發送緩沖區中的字節數。4、編程模型
基于 TCP 協議實現網絡通信的編程模型5、服務模型
迭代服務
服務器在單線程中以循環迭代的方式依次處理每個客戶機的業務需求。迭代模型的前提是針對每個客戶機的處理時間必須足夠短暫,否則會延誤對其客戶機的響應。并發服務
主進程阻塞在 accept 函數上。每當一個客戶機與服務器建立連接,accept 函數返回,即通過 fork 函數創建子進程,主進程繼續等待新的連接,子進程處理客戶機業務。首先服務器主進程阻塞于針對偵聽套接字的 accept 調用,客戶機進程通過 connect 函數向服務器發起連接請求
客戶機的連接請求被系統內核接受,服務器主進程從 accept 函數中返回,同時得到可用于通信的連接套接字
服務器主進程調用 fork 函數創建子進程,子進程復制父進程的文件描述符,因此子進程也有偵聽和連接兩個套接字描述符
服務器主進程關閉連接套接字;服務器子進程關閉偵聽套接字。主進程通過循環繼續阻塞于針對偵聽套接字的 accept 調用,而子進程則通過連接套接字與客戶機通信
套接字描述符與普通的我那件描述符一樣,是帶有引用計數的。在一個套接字描述符上調用 close 函數,并不一定真的關閉該套接字,而只是將其引用計數減一。只有當套接字描述符的引用計數被減到零時,才真的會釋放該套接字對象所占用的資源,并向對方發送 FIN 分節。因此服務器主進程關閉連接套接字,并不會影響子進程通過該套接字與客戶機通信。同理,服務器子進程關閉偵聽套接字也不會影響主進程通過套接字繼續等待連接。
如果服務器主進程在創建子進程后不關閉連接套接字,一方面將耗盡其可用文件描述符;令一方面在子進程結束通信關閉鏈接套接字時,其描述符上的引用計數只會由 2 變成 1,而不會變成 0,TCP 協議棧將永遠保持此連接。
6、示例說明
基于 TCP 協議的客戶機與服務器
//服務器 tcpA.c #include <stdio.h> #include <sys/socket.h> #include <netinet/in.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> int main() {int listenfd = socket (AF_INET, SOCK_STREAM, 0);if (listenfd == -1){perror ("socket");exit (EXIT_FAILURE);}struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons (8888);addr.sin_addr.s_addr = INADDR_ANY;if (bind (listenfd, (struct sockaddr*)&addr, sizeof (addr)) == -1){perror ("bind");exit (EXIT_FAILURE);}if (listen (listenfd, 1024) == -1){perror ("listen");exit (EXIT_FAILURE);}struct sockaddr_in addrcli = {};socklen_t addrlen = sizeof (addrcli);int connfd = accept (listenfd, (struct sockaddr*)&addrcli, &addrlen);if (connfd == -1){perror ("accept");exit (EXIT_FAILURE);}printf ("服務器已接受來自%s:%hu客戶機的連接請求\n", inet_ntoa (addrcli.sin_addr),ntohs (addrcli.sin_port));char buf[1024];ssize_t rcvd = recv (connfd, buf, sizeof (buf), 0);if (rcvd == -1){perror ("recv");exit (EXIT_FAILURE);}if (rcvd == 0){printf ("客戶機已關閉連接\n");exit (EXIT_FAILURE);}buf[rcvd] = '\0';printf ("客戶端說:%s\n", buf);printf ("服務器說:");gets (buf);ssize_t sent = send (connfd, buf, strlen (buf) * sizeof (buf[0]), 0);if (sent == -1){perror ("send");exit (EXIT_FAILURE);}if (close (listenfd) == -1){perror ("close");exit (EXIT_FAILURE);}if (close (connfd) == -1){perror ("close");exit (EXIT_FAILURE);}return 0; } //客戶端 tcpB.c #include <stdio.h> #include <sys/socket.h> #include <netinet/in.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> int main() {int listenfd = socket (AF_INET, SOCK_STREAM, 0);if (listenfd == -1){perror ("socket");exit (EXIT_FAILURE);}struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons (8888);addr.sin_addr.s_addr = inet_addr("127.0.0.1");if (connect (listenfd, (struct sockaddr*)&addr, sizeof (addr)) == -1){perror ("connect");exit (EXIT_FAILURE);}char buf[1024] = "你好,服務器";printf ("客戶端說:%s\n", buf);ssize_t sent = send (listenfd, buf, strlen (buf) * sizeof (buf[0]), 0);if (sent == -1){perror ("send");exit (EXIT_FAILURE);}ssize_t rcvd = recv (listenfd, buf, sizeof (buf), 0);if (rcvd == -1){perror ("recv");exit (EXIT_FAILURE);}buf[rcvd] = '\0';printf ("服務器說:%s\n", buf);if (close (listenfd) == -1){perror ("close");exit (EXIT_FAILURE);}return 0; } 輸出結果: 在一個終端執行: # ./tcpA 服務器已接受來自127.0.0.1:41428客戶機的連接請求 客戶端說:你好,服務器 服務器說:hello另一個終端執行: # ./tcpB 客戶端說:你好,服務器 服務器說:hello
二、UDP 客戶機/服務器
1、UDP 協議的基本特點
UDP 不提供客戶機與服務器的連接
UDP 的客戶機與服務器不必存在長期關系。一個 UDP 的客戶機在通過一個套接字向一個 UDP 服務器發送了一個數據報之后,馬上可以通過同一個套接字向另一個 UDP 服務器發送另一個數據報。同樣,一個 UDP 服務器也可以通過同一個套接字接收來自不同客戶機的數據報。UDP 不保證數據傳輸的可靠性和有序性
UDP 的協議棧底層不提供諸如確認、超時重傳、RTT估算以及序列號等機制。因此 UDP 數據報在網絡傳輸的過程中,可能丟失,也可能重復,甚至重新排序。應用程序必須自己處理這些情況。UDP 不提供流量控制
UDP 協議棧底層只是一味地按照發送方的速率發送數據,全然不顧接收方的緩沖區是否裝得下。UDP 是記錄式傳輸協議
每個 UDP 數據報都有一定長度,一個數據報就是一條記錄。如果數據報正確地到達了目的地,那么數據報的長度將被傳遞接收方的應用進程。UDP 是全雙工的
在一個 UDP 套接字上,應用程序在任何時候都既可以發送數據也可以接受數據。2、常用函數
(1)函數 recvfrom:接收數據
從指定的地址結構接收數據 #include <sys/types.h> #include <sys/socket.h> ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen); 返回值:成功返回實際接收到的字節數,失敗返回 -1《1》參數解析
sockfd:套接字描述符 buf:應用程序接收緩沖區 len:期望接收的字節數 flag:接收標志,一般取 0,還可取以下值: ? ? MSG_DONTWAIT ? ?以非阻塞方式接收數據 ? ? MSG_OOB ? ? ? ? ? ? ? 接收帶外數據 ? ? MSG_PEEK ? ? ? ? ? ? ? 只查看可接收的數據,函數返回后數據依然留在接收緩沖區中 ? ? MSG_WAITALL ? ? ? ?等待所有數據,即不接收到 len 字節的數據,函數就不返回 src_addr:輸出數據報發送者的地址結構,可置為 NULL addrlen:輸出 src_addr 參數所指向內存塊的字節數,輸出數據發送者地址結構的字節數,可置為 NULL《2》函數解析
recvfrom 函數返回 0,表示接收到一個空數據報(只有 IP 和 UDP 包頭而無數據內容),與對方是否關閉套接字無關。(2)函數 sendto:發送數據
向指定的地址結構發送數據 #include <sys/types.h> #include <sys/socket.h> ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen); 返回值:成功返回實際被發送的字節數,失敗返回 -1《1》參數解析
sockfd:套接字描述符 buf:應用程序發送緩沖區 len:期望發送的字節數 flags:發送標志,一般取 0,還可取以下值: ? ? MSG_DONTWAIT ? ?以非阻塞方式發送數據 ? ? MSG_OOB ? ? ? ? ? ? ? 發送帶外數據 ? ? MSG_DONTROUTE 不查路由表,直接在本地網絡中尋找目的主機 dest_addr:數據報接收者的地址結構 addrlen:數據報接收者地址結構的字節數3、編程模型
基于 UDP 協議的無連接編程模型
UDP 服務器的阻塞焦點不在 accept 函數上,而在 recvfrom 函數上。任何一個 UDP 客戶機通過 sendto 函數發送的請求數據都可以被 recvfrom 函數返回給 UDP 服務器,其輸出的客戶機地址結構 src_addr 可直接被用于向客戶機返回響應時調用 sendto 函數的輸入 dest_addr
基于 UDP 協議的有連接編程模型
UDP 的 connect 函數與 TCP 的 connect 函數完全不同,既無三路握手,亦無虛擬電路,而僅僅是將傳遞給該函數的對方地址結構緩存在套接字對象中。此后收發數據時,可不使用 recvfrom/sendto 函數,而是使用 recv/send 或者 read/write 函數,直接和所連接的對方主機通信。
3、服務模型
迭代服務
基于 UDP 協議建立通信的客戶機和服務器,不需要維持長期的連接。因此 UDP 服務器在一個單線程中,以循環迭代的方式即可處理來自不同客戶機的業務需求。4、示例說明
基于 UDP 協議的客戶機和服務器
//服務器 udpA.c #include <stdio.h> #include <sys/socket.h> #include <netinet/in.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> int main() {int listenfd = socket (AF_INET, SOCK_DGRAM, 0);if (listenfd == -1){perror ("socket");exit (EXIT_FAILURE);}struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons (8888);addr.sin_addr.s_addr = INADDR_ANY;if (bind (listenfd, (struct sockaddr*)&addr, sizeof (addr)) == -1){perror ("bind");exit (EXIT_FAILURE);}char buf[1024];struct sockaddr_in addrcli = {};socklen_t addrlen = sizeof (addrcli);ssize_t rcvd = recvfrom (listenfd, buf, sizeof (buf), 0, (struct sockaddr*)&addrcli, &addrlen);if (rcvd == -1){perror ("recvfrom");exit (EXIT_FAILURE);}buf[rcvd] = '\0';printf ("客戶端說:%s\n", buf);printf ("服務器說:");gets (buf);ssize_t sent = sendto (listenfd, buf, strlen (buf) * sizeof (buf[0]), 0, (struct sockaddr*)&addrcli, sizeof (addrcli));if (sent == -1){perror ("send");exit (EXIT_FAILURE);}if (close (listenfd) == -1){perror ("close");exit (EXIT_FAILURE);}return 0; } //客戶端 udpB.c #include <stdio.h> #include <sys/socket.h> #include <netinet/in.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> int main() {int listenfd = socket (AF_INET, SOCK_DGRAM, 0);if (listenfd == -1){perror ("socket");exit (EXIT_FAILURE);}struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons (8888);addr.sin_addr.s_addr = inet_addr("127.0.0.1");char buf[1024] = "你好,服務器";printf ("客戶端說:%s\n", buf);ssize_t sent = sendto (listenfd, buf, strlen (buf) * sizeof (buf[0]), 0, (struct sockaddr*)&addr, sizeof (addr));if (sent == -1){perror ("send");exit (EXIT_FAILURE);}struct sockaddr_in addrser = {};socklen_t addrlen = sizeof (addrser);ssize_t rcvd = recvfrom (listenfd, buf, sizeof (buf), 0, (struct sockaddr*)&addrser, &addrlen);if (rcvd == -1){perror ("recvfrom");exit (EXIT_FAILURE);}buf[rcvd] = '\0';printf ("服務器說:%s\n", buf);if (close (listenfd) == -1){perror ("close");exit (EXIT_FAILURE);}return 0; } 輸出結果: 在一個終端執行: # ./udpA 客戶端說:你好,服務器 服務器說:hello另一個終端執行: # ./udpB 客戶端說:你好,服務器 服務器說:hello
總結
以上是生活随笔為你收集整理的UNIX再学习 -- TCP/UDP 客户机/服务器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 给即将35岁的产品经理提个醒
- 下一篇: 机器学习笔记(十七)——EM算法的推导