1? TCP協議
? ? ? ? TCP提供客戶機與服務器的鏈接。一個完整TCP通信過程需要經歷三個階段
? ? ? ? 1)首先,客戶機必須建立與服務器的連接,所謂虛電路
? ? ? ? 2)然后,憑借已建立好的連接,通信雙方相互交換數據
? ? ? ? 3)最后,客戶機與服務器雙雙終止連接,結束通信過程
? ? ? ? TCP保證數據傳輸的可靠性(超時重傳、反向確認):
? ? ? ? TCP的協議棧底層在向另一端發送數據時,會要求對方在一個給定的時間窗口內返回確認。如果超過了這個時間窗口仍沒有收到確認,則TCP會重傳數據并等待更長的時間。只有在數次重傳均告失敗以后,TCP才會最終放棄。TCP含有用于動態估算數據往返時間(Round-Trip Time, RTT)的算法,因此它知道等待一個確認需要多長時間。
? ? ? ? TCP保證數據傳輸的有序性:
? ? ? ? TCP的協議棧底層在向另一端發送數據時,會為所發送數據的每個字節指定一個序列號。即使這些數據字節沒有能夠按照發送時的順序到達接收方,接收方的TCP也可以根據它們的序列號重新排序,再把最后的結果交給應用程序。
? ? ? ? TCP是全雙工的:
? ? ? ? 在給定的連接上,應用程序在任何時候都既可以發送數據也可以接收數據。因此,TCP必須跟蹤每個方向上數據流的狀態信息,如序列號和通告窗口的大小。
1.1??三次握手(建立連接)
?????????????????????????
? ? ? ? ? ? ? ? ? ? ????????????????? ? ?三次握手?????????????????????????????????????????????????? ? ? ? ?交換數據
? ? ? ? 1)客戶機的TCP協議棧向服務器發送一個SYN分節(SYN比特位是1),告知對方自己將在連接中發送數據的初始序列號,稱為主動打開。
? ? ? ? 2)服務器的TCP協議棧向客戶機發送一個單個分節,其中不僅包括對客戶機SYN分節的ACK應答,還包含服務器自己的SYN分節(ACK和SYN比特位均是1),以告知對方自己在同一連接中發送數據的初始序列號。
? ? ? ? 3)客戶機的TCP協議棧向服務器返回ACK應答,以表示對服務器所發SYN的確認。
?
? ? ? ? 交換數據:
? ? ? ? - 一旦連接建立,客戶機即可構造請求包,并發往服務器。服務器接收并處理來自客戶機的請求包,構造響應包。
? ? ? ? - 服務器向客戶機發送響應包,同時捎帶對客戶機請求包的ACK應答(反向確認)。
? ? ? ? - 客戶機接收來自服務器的響應包,同時向對方發送ACK應答(反向確認)。
1.2? 四次揮手(關閉連接)
? ? ? ???????????????????????????????????? ? ? ? ? ? ? ?
????????1)客戶機或者服務器主動關閉連接,TCP協議向對方發送FIN分節,表示數據通信結束。如果此時尚有數據滯留于發送緩沖區中,則FIN分節跟在所有未發送數據之后。
????????2)接收到FIN分節的另一端執行被動關閉,一方面通過TCP協議棧向對方發送ACK應答,另一方面向應用程序傳遞文件結束符。
????????3)一段時間后,方才接收到FIN分節的進程關閉自己的鏈接,同時通過TCP協議棧向對方發送FIN分節。
????????4)對方在收到FIN分節后發送ACK應答。
2? TCP函數
2.1? listen()
? ? ? ? #include <sys/socket.h>
? ? ? ? int? listen( int socket,? ?int backlog );
? ? ? ? ? ? ? ? 功能:啟動偵聽,在指定套接字上啟動對連接請求的偵聽功能
? ? ? ? ? ? ? ? sockfd:套接字描述符,在調用此函數之前是一個主動套接字,是不能感知連接請求的
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?在調用此函數并成功返回后,是一個被動套接字,具有感知連接請求的能力。
? ? ? ? ? ? ? ? backlog:未決連接請求隊列的最大長度,一般取不小于1024的值
? ? ? ? ? ? ? ? 返回值:成0敗-1?
2.2? accept()
????????三次握手始于connect(),終于accept()結束
????????#include <sys/socket.h>
? ? ? ? int accept( int sockfd,? ?struct sockaddr* addr,? ?socklen_t* addrlen );
? ? ? ? ? ? ? ? 功能:等待并接受連接請求,在指定套接字上阻塞,直到連接建立完成
? ? ? ? ? ? ? ? sockfd:偵聽套接字描述符
? ? ? ? ? ? ? ? addr:輸出連接請求發起方的地址信息
? ? ? ? ? ? ? ? addrlen:輸出連接請求發起方的地址信息字節數
? ? ? ? ? ? ? ? 返回值:成功返回可用于后續通信的連接套接字描述符,失敗返回-1?
2.3? recv()
? ? ? ? #include <sys/socket.h>
? ? ? ? ssize_t recv( int sockfd,? ?void* buf,? ?size_t count,? ?int flags );
? ? ? ? ? ? ? ? 功能:接收數據
? ? ? ? ? ? ? ? flags:取0則與read()函數等價。另外也可取以下值
? ? ? ? ? ? ? ? ? ? ? ? ? ?MSG_DONTWAIT? 以非阻塞方式接收數據
? ? ? ? ? ? ? ? ? ? ? ? ? ?MSG_OOB? ? ? ? ? ? ?接收帶外數據
? ? ? ? ? ? ? ? ? ? ? ? ? ?MSG_WAITALL? ? ? 等待所有數據,即不接收到count個字節就不返回
? ? ? ? ? ? ? ? 返回值:成功返回實際接收到的字節數,失敗返回-1??
2.4? send()
? ? ? ? #include <sys/socket.h>
? ? ? ? ssize_t send( int sockfd,? ?void const* buf,? ?size_t count,? ?int flags );
? ? ? ? ? ? ? ? 功能:發送數據
? ? ? ? ? ? ? ? flags:取0則與write()函數等價。另外也可取以下值
? ? ? ? ? ? ? ? ? ? ? ? ? ?MSG_DONTWAIT? 以非阻塞方方式發送數據
? ? ? ? ? ? ? ? ? ? ? ? ? ?MSG_OOB? ? ? ? ? ? ?接收帶外數據
? ? ? ? ? ? ? ? ? ? ? ? ? ?MSG_DONTROUTE?不查路由表,直接在本地網絡中尋找目的主機
? ? ? ? ? ? ? ? 返回值:成功返回實際發送的字節數,失敗返回-1?
3? TCP編程模型
????????
4? 通信終止
4.1? 客戶機主動終止
? ? ? ? 在某個時刻,客戶機認為已經不再需要服務器繼續為其提供服務器了,于是它在接收完最后一個響應包以后,通過close()函數關閉與服務器通信的套接字。
? ? ? ? 客戶機的TCP協議棧向服務器發送FIN分節并得到對方的ACK應答。
? ? ? ? 服務器專門負責與客戶機通信的子進程,此刻正視圖通過recv()接收下一個請求包,結果卻因為收到來自客戶機的FIN分節而返回0。
????????于是該子進程退出收發循環,同時通過close()關閉連接套接字,導致服務器的TCP協議棧向客戶機發送FIN分節,使對方進入TIME_WAIT狀態,并在收到對方ACK應答以后,自己進入CLOSED狀態。
? ? ? ? 隨著收發循環的退出,服務器子進程終止,并在服務器主進程的SIGCHLD(17)信號處理函數中被回收。
????????通信過程宣告結束。
4.2? 服務器主動終止
? ? ? ? 服務器專門負責和某個特定客戶機通信的子進程,在運行過程中出現錯誤,不得不調用close()函數關閉連接套接字,或者直接退出,甚至被信號殺死。于是服務器的TCP協議棧向客戶機發送FIN分節并得到對方的ACK應答。
? ? ? ? A、如果客戶機這時正視圖通過recv()接收響應包,那么該函數會返回0。客戶機可據此判斷服務器已宕機,直接通過close()關閉與服務器通信的套接字,終止通信進程。
? ? ? ? B、如果客戶機這時正視圖通過send()發送請求包,那么該函數并不會失敗,但會導致對方以RST分節做出響應,該響應分節甚至會先于FIN分節被緊隨其后的recv()收到并返回-1,同時置errno為ECONNRESET。這也是終止通信的條件之一。
4.3? 服務器主機不可達(主機崩潰、網絡中斷、路由失效...)
? ? ? ? 在服務器主機不可達的情況下,無論是客戶機還是服務器,它們的TCP協議棧都不可能再有任何數據分節的交換。因此,客戶機通過send()函數發送完請求包以后,會阻塞在recv()上等待來自服務器的響應包。
? ? ? ? 這是客戶機的TCP協議棧會持續地重傳數據分節,視圖得到對方的ACK應答。源自伯克利的實現最多重傳12次,最長等待9分鐘。
? ? ? ? 當TCP最終決定放棄時,會通過recv()向用戶進程返回失敗,并置errno為ETIMEOUT或EHOSTUNREACH或ENETUNREACH。
? ? ? ? 此后,即使服務器主機被重啟,或者通信線路被恢復,由于TCP協議棧已丟失了與先前連接有關的信息,通信依然無法繼續,對所接收到的一切數據一律響應RST分節,只有在重新建立TCP連接后,才能繼續通信。
5? 域名解析
? ? ? ? IP地址是網絡上標識站點的數字地址,為了方便記憶,采用域名來代替IP地址標識站點地址。
? ? ? ? 域名解析就是域名到IP地址的轉換過程,由DNS服務器完成。
? ? ? ? 當應用過程需要將一個主機域名映射為IP地址時,就調用域名解析函數,解析函數將待轉換的域名放在DNS請求中,以UDP報文方式發給本地域名服務器。本地的域名服務器查到域名后,將對應的IP地址放在應答報文中返回。
5.1? gethostbyname()
? ? ? ? #include <netdb.h>
? ? ? ? struct hostnet* gethostbyname( char const* host_name );
? ? ? ? ? ? ? ? 功能:通過參數所傳的主機域名,獲取主機信息
? ? ? ? ? ? ? ? host_name:主機域名
? ? ? ? ? ? ? ? 返回值:成功返回表示主機信息的結構體指針,失敗返回NULL?
? ? ? ? 注意,該函數需要在聯網情況下使用。
????????
? ? ? ? struct hostent {
? ? ? ? ? ? ? ? char? ?*h_name;? ? ? ? // 主機官方名
? ? ? ? ? ? ? ? char? ?**h_aliases;? ? // 主機別名表
? ? ? ? ? ? ? ? int? ?h_addrtype;? ? ? ?// 地址類型
? ? ? ? ? ? ? ? int? ?h_length;? ? ? ? ? ?// 地址長度
? ? ? ? ? ? ? ? int? ?**h_addr_list;? ? // IP地址表
? ? ? ? };
? ? ? ? 對于WEB服務器而言,主機官方名有一個,而主機別名可能有多個,這些別名都是為了便于用戶記憶。同時IP地址也可能有多個。
? ? ? ? h_aliases? ?->? ?*? ?->? ?"xxx\n"
? ? ? ? ? ? ? ? ? ? ? ? ? ->? ?*? ?->? ?"xxx\n"
? ? ? ? ? ? ? ? ? ? ? ? ? NULL;
? ? ? ? h_addr_list ->? ?*? ?->? ?in_addr
? ? ? ? ? ? ? ? ? ? ? ? ? ->? ?*? ?->? ?in_addr
? ? ? ? ? ? ? ? ? ? ? ? ? NULL
//tcpser.c 基于tcp的服務器
#include<stdio.h>
#include<string.h>
#include<ctype.h> // toupper()
#include<sys/socket.h>//網絡
#include<sys/types.h>//網絡
#include<arpa/inet.h>//網絡
#include<signal.h>
#include<sys/wait.h>
#include<errno.h>
#include<unistd.h>
//信號處理函數,收尸int main(void){//捕獲17號信號printf("服務器:創建套接字\n");int sockfd = socket(AF_INET,SOCK_STREAM,0);if(sockfd == -1){perror("socket");}//組織地址結構printf("服務器:組織地址結構\n");struct sockaddr_in ser;ser.sin_family = AF_INET;ser.sin_port = htons(8980); //8980由小端轉大端//ser.sin_addr.s_addr = inet_addr("192.168.222.136");ser.sin_addr.s_addr = INADDR_ANY;//綁定地址結構和套接字printf("服務器:綁定套接字和地址結構\n");if(bind(sockfd,(struct sockaddr*)&ser,sizeof(ser)) == -1){perror("bind");return -1;}//開啟偵聽 套接字 主動-->被動-->感知連接請求printf("服務器:啟動偵聽\n");if(listen(sockfd,1024) == -1){perror("listen");return -1;}for(;;){//等待并建立通信連接printf("服務器:等待并建立通信連接\n");struct sockaddr_in cli;//用來輸出客戶端的地址結構socklen_t len = sizeof(cli);//用來輸出地址結構大小int conn = accept(sockfd,(struct sockaddr*)&cli,&len);if(conn == -1){perror("accept");return -1;}printf("服務器:接收到%s:%hu的客戶端的連接\n",inet_ntoa(cli.sin_addr),ntohs(cli.sin_port));//創建子進程 fork//子進程服務業務處理//業務處理printf("服務器:業務處理\n");for(;;){//接受客戶端發來的小寫的串char buf[64] = {};ssize_t size = read(conn,buf,sizeof(buf)-1);if(size == -1){perror("read");return -1;}if(size == 0){printf("服務器:客戶端斷開連接\n");break;}//轉成大寫for(int i = 0;i < strlen(buf);i++){buf[i] = toupper(buf[i]);}//將轉成大寫的串回傳客戶端if(write(conn,buf,strlen(buf)) == -1){perror("write");return -1;}}printf("服務器:關閉套接字\n");close(conn);}close(sockfd); return 0;
}#include<stdio.h>
#include<string.h>
#include<ctype.h> // toupper()
#include<sys/socket.h>//網絡
#include<sys/types.h>//網絡
#include<arpa/inet.h>//網絡
#include<signal.h>
#include<sys/wait.h>
#include<errno.h>
#include<unistd.h>
//信號處理函數,收尸
void sigchild(int signum){printf("服務器:捕獲到%d號信號\n",signum);for(;;){pid_t pid = waitpid(-1,NULL,WNOHANG);if(pid == -1){if(errno == ECHILD){printf("服務器:沒有子進程\n");break;}else{perror("waitpid");return ;}}else if(pid == 0){printf("服務器:子進程在運行\n");break;}else{printf("服務器:回收了%d進程的僵尸\n",pid);}}
}int main(void){//捕獲17號信號if(signal(SIGCHLD,sigchild) == SIG_ERR){perror("signal");return -1;}printf("服務器:創建套接字\n");int sockfd = socket(AF_INET,SOCK_STREAM,0);if(sockfd == -1){perror("socket");}//組織地址結構printf("服務器:組織地址結構\n");struct sockaddr_in ser;ser.sin_family = AF_INET;ser.sin_port = htons(8980);//ser.sin_addr.s_addr = inet_addr("192.168.222.136");ser.sin_addr.s_addr = INADDR_ANY;//接受任意IP地址的數據(服務器可能有多個地址)//綁定地址結構和套接字printf("服務器:綁定套接字和地址結構\n");if(bind(sockfd,(struct sockaddr*)&ser,sizeof(ser)) == -1){perror("bind");return -1;}//開啟偵聽 套接字 主動-->被動-->感知連接請求printf("服務器:啟動偵聽\n");if(listen(sockfd,1024) == -1){perror("listen");return -1;}for(;;){//等待并建立通信連接printf("服務器:等待并建立通信連接\n");struct sockaddr_in cli;//用來輸出客戶端的地址結構socklen_t len = sizeof(cli);//用來輸出地址結構大小int conn = accept(sockfd,(struct sockaddr*)&cli,&len);if(conn == -1){perror("accept");return -1;}printf("服務器:接收到%s:%hu的客戶端的連接\n",inet_ntoa(cli.sin_addr),ntohs(cli.sin_port));//創建子進程 forkpid_t pid = fork();if(pid == -1){perror("fork");return -1;}//子進程服務業務處理//業務處理if(pid == 0){close(sockfd);printf("服務器:業務處理\n");for(;;){//接受客戶端發來的小寫的串char buf[64] = {};ssize_t size = read(conn,buf,sizeof(buf)-1);if(size == -1){perror("read");return -1;}if(size == 0){printf("服務器:客戶端斷開連接\n");break;}//轉成大寫for(int i = 0;i < strlen(buf);i++){buf[i] = toupper(buf[i]);}//將轉成大寫的串回傳客戶端if(write(conn,buf,strlen(buf)) == -1){perror("write");return -1;}}printf("服務器:關閉套接字\n");close(conn);return 0;//!!!!!!}//父進程代碼,關閉通信套接字close(conn);//通信套接字(1個通信套接字對應1個客戶端)}close(sockfd);//偵聽套接字(整個服務器只有1個)return 0;
}//結合tcpcli,1臺虛擬機做服務器執行本代碼,另一臺做客戶端執行tcpcli
//tcpcli.c 基于的客戶端
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<arpa/inet.h>int main(void){//創建套接字printf("客戶端:創建套接字\n");int sockfd = socket(AF_INET,SOCK_STREAM,0);if(sockfd == -1){perror("socket");return -1;}//準備服務器的地址結構printf("客戶端:準備服務器的地址結構\n");struct sockaddr_in ser;ser.sin_family = AF_INET;ser.sin_port = htons(8980);ser.sin_addr.s_addr = inet_addr("192.168.222.136");//發起連接printf("客戶端:發起連接\n");if(connect(sockfd,(struct sockaddr*)&ser,sizeof(ser)) == -1){perror("connect");return -1;}//業務處理for(;;){//通過鍵盤獲取小寫的串char buf[64] = {};fgets(buf,sizeof(buf),stdin);// ! 退出if(strcmp(buf,"!\n") == 0){break; }//將小寫的串發送給服務器if(send(sockfd,buf,strlen(buf),0) == -1){perror("send");return -1;}//接受服務器回傳大寫的串if(recv(sockfd,buf,sizeof(buf)-1,0) == -1){perror("recv");return -1;}//顯示printf("%s",buf);}printf("客戶端:關閉套接字\n");close(sockfd);return 0;
}
1