目錄
一、三次握手建立連接
1.1 標記位
1.2 三次握手的過程
二、四次揮手斷開連接
?三、模擬服務器和客戶端收發數據
四、多線程并發處理
五、TCP粘包問題
5.1 什么是TCP粘包?
5.2 TCP粘包會有什么問題?
5.3 TCP粘包的解決方法?
一、三次握手建立連接
1.1 標記位
????????SYN:用于建立連接的初始握手。發送方發送一個SYN報文段給接收方,請求建立連接。
????????ACK:用于確認數據的傳輸。當成功接收到數據后,接收方發送一個帶有ACK標記的報文段回復發送方,確認已經收到了數據。
????????FIN:用于關閉連接。當發送方發送完所有數據后,會發送一個帶有FIN標記的報文段,請求關閉連接。接收方在收到FIN報文段后,發送一個帶有ACK標記的報文段進行確認,并使用一個定時器在一段時間后關閉連接。
1.2 三次握手的過程
三次握手是TCP協議中用于建立連接的過程,具體步驟如下:
- 客戶端向服務器發送一個SYN(同步)的數據包,表示客戶端請求建立連接。該數據包中還包含客戶端隨機生成的初始序列號(Sequence Number,seq),比如 seq = x。
- 服務器收到客戶端發送的SYN數據包后,向客戶端發送一個ACK(確認)和SYN的組合數據包,服務器將自己的初始序列號(seq = y)發送給客戶端,同時將客戶端的序列號加 1 作為確認號(Acknowledgment Number,ack = x + 1),表示服務器同意建立連接,并向客戶端發送確認信息。
- 客戶端收到服務器發送的ACK和SYN的數據包后,向服務器發送一個ACK確認數據包,該包的確認號為服務器的序列號加 1(ack = y + 1),而序列號為客戶端在第一次握手中發送的序列號加 1(seq = x + 1),表示客戶端也同意建立連接。
經過以上三個步驟,客戶端和服務器就成功建立了連接,可以開始進行數據傳輸。這個過程就是TCP協議中三次握手的過程。
圖解如下:
二、四次揮手斷開連接
四次揮手是TCP協議中用于關閉連接的過程,具體步驟如下:
- 第一次揮手:當客戶端確定自己已經沒有數據要發送時,向服務器發送一個FIN(結束)數據包,其中包含自己的序列號(seq = u),表示客戶端關閉數據傳輸。
- 第二次揮手:服務器接收到客戶端發送的FIN后,向客戶端發送一個ACK確認數據包,確認號為客戶端的序列號加 1(ack = u + 1),表示服務器收到了關閉請求。此時,服務器可能還有數據需要繼續發送,所以連接不會立即關閉,而是進入半關閉狀態。
- 第三次揮手:當服務器確定自己沒有數據要發送時,向客戶端發送一個FIN數據包,其中包含自己的序列號(seq = v),表示服務器也準備關閉連接。
- 第四次揮手:客戶端接收到服務器發送的FIN后,向服務器發送一個ACK確認數據包,確認號為服務器的序列號加 1(ack = v + 1),序列號為之前發送 FIN 包時的序列號加 1(seq = u + 1),表示客戶端收到了關閉請求,連接正式關閉。
通過以上四個步驟,客戶端和服務器完成了關閉連接的過程。值得注意的是,四次揮手中的每一次揮手都需要對方發送確認,確保雙方都能安全地關閉連接。
圖解如下:
那么,揮手能不能是3次呢?答案是可以的,第二次揮手和第三次揮手是可以合并在一起的。
?三、模擬服務器和客戶端收發數據
服務器ser
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>int socket_init();int main()
{int sockfd=socket_init();if(sockfd==-1){exit(1);}while(1){struct sockaddr_in caddr;//記錄客戶端地址ip,portint len=sizeof(caddr);int c=accept(sockfd,(struct sockaddr*)&caddr,&len);//caddr存放客戶端ip,port,可能會阻塞if(c<0){continue;}printf("accept c=%d\n",c);while(1){char buff[128]={0};int n=recv(c,buff,127,0);//有可能阻塞,如果返回-1則失敗,是0則對方關閉了if(n<=0){break;}printf("recv:%s\n",buff);send(c,"ok",2,0);//write(c,"ok",2);}close(c);printf("cilent close\n");}
}
int socket_init()
{int sockfd=socket(AF_INET,SOCK_STREAM,0);//TCP,創建套接字if(sockfd==-1){return -1;}struct sockaddr_in saddr;//ipv4地址族,對應套接字的地址memset(&saddr,0,sizeof(saddr));saddr.sin_family=AF_INET;saddr.sin_port=htons(6000);saddr.sin_addr.s_addr=inet_addr("127.0.0.1");int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//綁定ip,port,指定if(res==-1){printf("bind err\n");return -1;}res=listen(sockfd,5);//創建監聽隊列if(res==-1){return -1;}return sockfd;
}
- 先調用socket_init()函數初始化服務器端的套接字,綁定IP和端口,并開始監聽連接請求。
- 不斷循環accept()函數接受客戶端的連接請求,一旦有客戶端連接則創建一個新的套接字來處理與客戶端的通信。
- 在客戶端連接成功后,進入一個無限循環,不斷接收客戶端發送的消息,并打印消息內容。
- 如果接收到的消息長度小于等于0,則說明客戶端關閉了連接,跳出循環,關閉與客戶端的連接。
- 如果收到消息,則回復客戶端消息為"ok", 繼續接受下一條消息。
客戶端cil
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>int main()
{int sockfd=socket(AF_INET,SOCK_STREAM,0);if(sockfd==-1){exit(1);}struct sockaddr_in saddr;//服務器的地址memset(&saddr,0,sizeof(saddr));saddr.sin_family=AF_INET;saddr.sin_port=htons(6000);saddr.sin_addr.s_addr=inet_addr("127.0.0.1");int res=connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//發起連接,三次握手if(res==-1){printf("connet err\n");exit(1);}while(1){printf("input:\n");char buff[128]={0};fgets(buff,128,stdin);if(strncmp(buff,"end",3)==0){break;}send(sockfd,buff,strlen(buff)-1,0);memset(buff,0,128);recv(sockfd,buff,127,0);printf("buff=%s\n",buff);}close(sockfd);exit(0);
}
- 創建了一個套接字socket,并指定為AF_INET和SOCK_STREAM,表示使用IPv4和TCP協議。
- 初始化服務器的地址saddr,包括IP地址為"127.0.0.1"、端口號為6000等信息。
- 調用connect()函數連接到服務器端,進行三次握手建立連接。
- 進入一個循環,不斷接收用戶輸入的消息,將消息發送給服務器,并接收服務器的回復。
- 如果用戶輸入為"end",則跳出循環,關閉套接字,結束程序。
運行結果:
那么send后會直接將數據發送出去嗎?答案不然,會將數據先發送到發送緩沖區,然后將數據發給接收緩沖區,最后才會接收到數據。
我們來驗證一下:
?把ser.c中的int n = recv(c,buff,127,0)改為int n = recv(c,buff,1,0),執行后結果:
每輸一個應該輸出五個ok,但此時只輸出一個ok,剩下四個ok在recv的緩沖區中,4個ok八個字符所以緩沖區有8個字符。
通過netstat -natp 命令可以顯示
再輸入一個a,輸出四個ok
此時緩沖區中有一個ok,2個字符
TCP字節流服務
四、多線程并發處理
服務器ser
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<pthread.h>int socket_init();
void* fun(void* arg)
{int* p=(int*)arg;int c=*p;free(p);while(1){char buff[128]={0};int n=recv(c,buff,127,0);//有可能阻塞,如果返回-1則失敗,是0則對方關閉了if(n<=0){break;}printf("recv:%s\n",buff);send(c,"ok",2,0);//write(c,"ok",2);}close(c);printf("cilent close\n");
}
int main()
{int sockfd=socket_init();if(sockfd==-1){exit(1);}while(1){struct sockaddr_in caddr;//記錄客戶端地址ip,portint len=sizeof(caddr);int c=accept(sockfd,(struct sockaddr*)&caddr,&len);//caddr存放客戶端ip,port,可能會阻塞if(c<0){continue;}printf("accept c=%d\n",c);int* p=(int*)malloc(sizeof(int));if(p==NULL){close(c);continue;}*p=c;pthread_t id;pthread_create(&id,NULL,fun,(void*)p);}
}
int socket_init()
{int sockfd=socket(AF_INET,SOCK_STREAM,0);//TCP,創建套接字if(sockfd==-1){return -1;}struct sockaddr_in saddr;//ipv4地址族,對應套接字的地址memset(&saddr,0,sizeof(saddr));saddr.sin_family=AF_INET;saddr.sin_port=htons(6000);saddr.sin_addr.s_addr=inet_addr("127.0.0.1");int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//綁定ip,port,指定if(res==-1){printf("bind err\n");return -1;}res=listen(sockfd,5);//創建監聽隊列if(res==-1){return -1;}return sockfd;
}
客戶端cil
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>int main()
{int sockfd=socket(AF_INET,SOCK_STREAM,0);if(sockfd==-1){exit(1);}struct sockaddr_in saddr;//服務器的地址memset(&saddr,0,sizeof(saddr));saddr.sin_family=AF_INET;saddr.sin_port=htons(6000);saddr.sin_addr.s_addr=inet_addr("127.0.0.1");int res=connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//發起連接,三次握手if(res==-1){printf("connet err\n");exit(1);}while(1){printf("input:\n");char buff[128]={0};fgets(buff,128,stdin);if(strncmp(buff,"end",3)==0){break;}send(sockfd,buff,strlen(buff)-1,0);memset(buff,0,128);recv(sockfd,buff,127,0);printf("buff=%s\n",buff);}close(sockfd);exit(0);
}
?運行結果:
五、TCP粘包問題
5.1 什么是TCP粘包?
????????TCP粘包是指發送方連續發送的多個數據包,在傳輸過程中可能會被TCP協議合并成一個大的數據包,在接收方收到時無法正確區分這些數據包的邊界,導致粘在一起,從而引起粘包現象。這樣就會影響接收方對數據的解析和處理。簡而言之,就是多次send發送數據,被對方一次recv收到了。
5.2 TCP粘包會有什么問題?
- 數據解析錯誤:如果接收方無法正確區分數據包的邊界,可能導致數據解析錯誤,無法按照預期的方式處理數據。
- 數據錯誤:如果多個數據包粘在一起,可能導致數據包數據內容混雜,造成數據錯誤或丟失。
- 性能影響:數據粘包會增加解析數據的復雜性,影響系統性能。
5.3 TCP粘包的解決方法?
- 消息定長:在發送方和接收方約定固定的消息長度,每次發送和接收的數據長度相同,這樣接收方可以根據固定長度來截取數據包。
- 使用特殊符號分隔:在數據包之間加入特殊符號作為分隔符,接收方根據分隔符來區分不同數據包。
- 增加消息頭:在數據包頭部添加額外的消息頭信息,包括消息長度等,接收方通過消息頭信息來解析數據包。