文章目录
TCP协议
特点
面向链接、字节流、可靠传输
tcp的连接是全双工的
面向连接
使用TCP协议通信的双方必须先建立连接,然后才能开始的数据的读写。双方都必须为连接分配必要的内核资源,一管理连接的状态和连接上数据的传输。
字节流
字节流:发送端执行的写操作次数和接收端执行的读操作此时之间没有任何数量关系。
具体表现在:
- 当发送端应用程序连续执行多次写操作时 ,TCP模块先将这些数据放入TCP缓冲区中 。当TCP模块 真正开始发送数据时,发送缓冲区中这些等待发送的数据可能被封装成一个或多个TCP报文段发出。因此,TCP模块发送出的TCP报文端的个数和应用程序执行的写操作次数之间没有固定的数量关系。
- 当接收端收到一个或多个TCP报文段后,TCP模块将他们携带的应用程序数据按照TCP报文段的序号依次放入TCP接收端缓冲区中,并通知应用程序读取数据。接收端应用程序可以一次性将TCP接收缓冲区的数据全部读出,也可以分多次读取数据,这取决于用户指定的应用程序读缓冲区的大小。因此,应用程序执行的读操作次数和TCP模块接收的TCP报文段个数之间也没有固定的数量关系。
可靠传输
- TCP协议采用发送应答机制,即发送端发送的每个TCP报文段都必须得到接收方的应答,才认为这个TCP报文段传输成功。
- TCP采用超时重传机制,发送端在发出一个TCP报文之后启动定时器,如果在定时时间内未收到应答,它将重发该报文段
- 因为TCP报文段最终是以IP数据报发送的,而IP数据报到达接收端可能乱序、重复,所以TCP协议还会对接收到的TCP报文段重排、整理,再交付给应用层。
TCP头部结构
一个客户端和服务器进行持续通信
客户端:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<assert.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<arpa/inet.h>
int main()
{
int sockfd=socket(AF_INET,SOCK_STREAM,0);
assert(sockfd!=-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));
assert(res!=-1);
while(1)
{
char buff[128]={0};
printf("input:\n");
fgets(buff,128,stdin);
if(strncmp(buff,"end",3)==0)
{
break;
}
send(sockfd,buff,strlen(buff),0);
//write(sockfd,buff,strlen(buff));
memset(buff,0,128);
recv(sockfd,buff,127,0);
printf("buff=%s\n",buff);
}
close(sockfd);
}
三次握手
客户端可能会发生的阻塞原因分析
connect()失败
其余可能发生阻塞的地方参照:服务器端可能会发生的阻塞原因分析
服务器端:
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<stdio.h>
#include<assert.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
int main()
{
int sockfd=socket(AF_INET,SOCK_STREAM,0);
assert(sockfd!=-1);
struct sockaddr_in saddr,caddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family=AF_INET;
saddr.sin_port=htons(6000);//1024 root,4096 reserve,5000+ tempary
saddr.sin_addr.s_addr=inet_addr("127.0.0.1");
int res=bind(sockfd,(struct sockaddr *)&saddr,sizeof(saddr));
assert(res!=-1);
listen(sockfd,5);//create listen queue
while(1)
{
int len=sizeof(caddr);
int c=accept(sockfd,(struct sockaddr *)&caddr,&len);
if(c<0)
{
continue;
}
printf("accept c=%d,ip=%s,port=%d\n",c,inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port));
while(1)
{
char buff[128]={0};
int n=recv(c,buff,1,0);
if(n<=0)
{
break;
}
printf("buff=%s\n",buff);
send(c,"ok",2,0);
}
printf("one client over\n");
close(c);
}
}
c和sockfd的区别
服务器端可能会发生的阻塞原因分析
bind()失败
- 试图绑定一个已经在使用的端口
- 被绑定的地址是受保护的地址,仅超级用户能够访问。比如普通用户将sock绑定到知名服务端口(端口号为0~1023)
listen()永远不会发生阻塞
Linux:linsten 5:已完成三次握手队列的长度
Unix: listen x:未完成和已完成三次握手的两个队列的长度
accept()失败
- 当已完成三次握手的队列为空时,accept阻塞。
recv()阻塞
recv:读取数据的多少取决于recv读取数据的多少和接收缓冲区里有多少数据。
send()阻塞
- 发送消息的缓冲区已经满了,再send()就会发生阻塞
粘包
两次send被一次recv到了
解决粘包
- 加标记
- 每次只发送一个数据