TCP三次握手、四次挥手+多线程并发处理
目录
一、三次握手建立连接
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粘包的解决方法?
- 消息定长:在发送方和接收方约定固定的消息长度,每次发送和接收的数据长度相同,这样接收方可以根据固定长度来截取数据包。
- 使用特殊符号分隔:在数据包之间加入特殊符号作为分隔符,接收方根据分隔符来区分不同数据包。
- 增加消息头:在数据包头部添加额外的消息头信息,包括消息长度等,接收方通过消息头信息来解析数据包。