目录
如何理解TCP/UDP协议
什么是TCP?
TCP报文结构
TCP如何保证可靠性
如何编写TCP服务器
关于TCP中的accept(),listen(),bind()函数的理解?
TCP粘包问题
TCP(Transmission ControlProtocol,传输控制协议)
1.它是传输层的一个协议
2.它是一个面向连接的协议,连接前必须经历“三次握手”(我们可以理解为打电话,只有电话通了,两个人才可以说
话)
3.它是一种可靠传输
4.面向字节流(可以理解为像水一样传输数据)
5.TCP发送的数据是有序的
6.TCP有粘包问题
7.TCP传输效率低
8.若通信数据完整性需让位与通信实时性,则应该选用 TCP 协议(如文件传输、重要状态的更新等);则使用 UDP 协议(如视频传输、实时通信等)。
UDP(User Datagram Prototocol 用户数据报协议)
1.它是传输层的一个协议
2.它是一个无连接的协议(我们可以理解为发短信,不需要建立连接就可以发送数据)
3.它是一种不可靠传输(因为它面向无连接,所以必然是一种不可靠的传输)
4.面向数据报(可以理解为像冰块一样一块一块发送数据)
源端口、目标端口:计算机上的进程要和其他进程通信是要通过计算机端口的,而一个计算机端口某个时刻只能被一个进程占用,所以通过指定源端口和目标端口,就可以知道是哪两个进程需要通信。源端口、目标端口是用16位表示的,可推算计算机的端口个数为2^16个。
序列号:表示本报文段所发送数据的第一个字节的编号。在TCP连接中所传送的字节流的每一个字节都会按顺序编号。由于序列号由32位表示,所以每2^32个字节,就会出现序列号回绕,再次从 0 开始。那如何区分两个相同序列号的不同TCP报文段就是一个问题了,后面会有答案,暂时可以不管。
确认号:表示接收方期望收到发送方下一个报文段的第一个字节数据的编号。也就是告诉发送发:我希望你(指发送方)下次发送的数据的第一个字节数据的编号是这个确认号。也就是告诉发送方:我希望你(指发送方)下次发送给我的TCP报文段的序列号字段的值是这个确认号。
TCP首部长度:由于TCP首部包含一个长度可变的选项部分,所以需要这么一个值来指定这个TCP报文段到底有多长。或者可以这么理解:就是表示TCP报文段中数据部分在整个TCP报文段中的位置。该字段的单位是32位字,即:4个字节。
URG:表示本报文段中发送的数据是否包含紧急数据。URG=1,表示有紧急数据。后面的紧急指针字段只有当URG=1时才有效。
ACK:表示是否前面的确认号字段是否有效。ACK=1,表示有效。只有当ACK=1时,前面的确认号字段才有效。TCP规定,连接建立后,ACK必须为1。
PSH:告诉对方收到该报文段后是否应该立即把数据推送给上层。如果为1,则表示对方应当立即把数据提交给上层,而不是缓存起来。
RST:只有当RST=1时才有用。如果你收到一个RST=1的报文,说明你与主机的连接出现了严重错误(如主机崩溃),必须释放连接,然后再重新建立连接。或者说明你上次发送给主机的数据有问题,主机拒绝响应。
SYN:在建立连接时使用,用来同步序号。当SYN=1,ACK=0时,表示这是一个请求建立连接的报文段;当SYN=1,ACK=1时,表示对方同意建立连接。SYN=1,说明这是一个请求建立连接或同意建立连接的报文。只有在前两次握手中SYN才置为1。
FIN:标记数据是否发送完毕。如果FIN=1,就相当于告诉对方:“我的数据已经发送完毕,你可以释放连接了”
窗口大小:表示现在运行对方发送的数据量。也就是告诉对方,从本报文段的确认号开始允许对方发送的数据量。
校验和:提供额外的可靠性。具体如何校验,参考其他资料。
紧急指针:标记紧急数据在数据字段中的位置。
选项部分:其最大长度可根据TCP首部长度进行推算。TCP首部长度用4位表示,那么选项部分最长为:(2^4-1)*4-20=40字节。
选项部分的应用:
校验和
在数据传输的过程中,将发送的数据段都当做一个16位的整数。将这些整数加起来。并且前面的进位不能丢弃,补在后
面继续相加,最后取反,得到校验和。
发送方:在发送数据之前计算检验和,并进行校验和的填充。
接收方:收到数据后,对数据以同样的方式进行计算,求出校验和,与发送方的进行比对。
注意:如果接收方比对校验和与发送方不一致,那么数据一定传输有误。但是如果接收方比对校验和与发送方一致,数
据不一定传输成功
序列号
序列号:TCP传输时将每个字节的数据都进行了编号,这就是序列号。
确认应答
TCP传输的过程中,每次接收方收到数据后,都会对传输方进行ACK确认应答。也就是发送ACK报文。
这个ACK报文当中带有对应的确认序列号,告诉发送方,接收到了多少数据,下一次的数据从哪个序列号开始发。
超时重传
发送方在发送完数据后等待一个时间,时间到达没有接收到ACK报文,那么对刚才发送的数据进行重新发送.这个等待时间是动态计算的,一般500ms为一个单位进行控制重发一次后,仍未响应,那么等待2*500ms的时间后,再次重传。等待4*500ms的时间继续重传。以一个指数的形式增长。累计到一定的重传次数,TCP就认为网络或者对端出现异常,强制关闭连接
连接管理
连接管理就是三次握手与四次挥手的过程
TCP三次握手和四次挥手
流量控制
如果发送端的发送速度太快,导致接收端的结束缓冲区很快的填充满了。此时如果发送端仍旧发送数据,那么接下来发
送的数据都会丢包,继而导致丢包的一系列连锁反应TCP根据接收端对数据的处理能力,决定发送端的发送速度,这个机制就是流量控制。
滑动窗口
实际上是接收端接收数据的缓冲区的还剩余多少文里的窗口大小的值的改变进而改变自己的发送速度。如果接收到窗口大小为0,那 么发送方将停止发送数据。并定期的向接收端发送窗口探测数据段,让接收端把窗口大小反馈发送端
拥塞控制
为了解决数据拥堵的问题,
TCP引入了慢启动的机制,在开始发送数据时,先发送少量的数据探路。探清当前的网络状态如何,再决定多大的速度
进行传输。这时候就引入一个叫做拥塞窗口的概念
1.TCP连接初始化,将拥塞窗口cwind设置为1个报文段,即cwind=1;
2. 执行慢开始算法,cwind按指数规律增长(最大报文段MSS值的倍数),直到cwind == ssthresh时,开始执行拥塞
避免算法,cwind按线性规律增长;
3. 当网络发生拥塞,把ssthresh值更新为拥塞前ssthresh值的一半,cwind重新设置为1,再按照 [2] 执行
延迟应答
如果接收数据的主机⽴刻返回ACK应答, 这时候返回的窗⼝可能⽐较⼩可以在稍微等一会儿再进行应答(规定等待时间之内),因为处理端口对缓存区的处理速度可能很快, 10ms之内就把刚收到的数据从缓冲区消费掉了;这样的延迟应答返回的窗口可能会更大
TIME_WAIT状态如何避免
首先服务器可以设置SO_REUSEADDR套接字选项来通知内核,如果端口忙,但TCP连接位于TIME_WAIT状态时可以重用
端口
服务器server.c
int main()
{
//创建socket IPV4 TCP 指定协议 不关心置0
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
perror("socket");
}
//定义两个addr_in 结构体
struct sockaddr_in server_socket;
struct sockaddr_in client_socket;
//把server_socket置为零且包括‘\0’
bzero(&server_socket,sizeof(server_socket));
//初始化server_socket
server_socket.sin_family = AF_INET;
server_socket.sin_port = htons(_PORT_);//宏_PORT_被define成某个端口号
//网络地址为INADDR_ANY,这个宏可以表示本地的任意IP地址
//因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这个设置可以在所有的IP地址上监听, 直到与某个客户端建立了连接时才确定下来到底用哪个IP地址;
server_socket.sin_addr.s_addr = htonl(INADDR_ANY);//htohl表示主机字节序转网络字节序
//将服务器的ip和端口号进行绑定
if(bind(sock,(struct sockaddr *)&server_socket,sizeof(struct sockaddr_in)) < 0)
{
printf("bind error,error code is %d,error string is:%s\n",errno,strerror(errno));
close(sock);
return 1;
}
//绑定成功开始进入监听模式,最大监听_BACKLOG_ 定义为10
if(listen(sock,_BACKLOG_) < 0)
{
printf("listen error,error code is %d,error string is %s\n",errno,strerror(errno));
close(sock);
return 2;
}
printf("bind and listen success,wait accept...\n");
while(1)
{
socklen_t len = 0;
//accept为阻塞式等待,若想改为非阻塞,可考虑添加IO多路复用机制,select,poll,epoll
//三次握⼿完成后, 服务器调⽤accept()接受连接
//若成功返回一个新的sock,专门用于与已连接的客户端通信
int client_sock = accept(sock,(struct sockaddr *)&client_socket,&len);
if(client_sock < 0)
{
printf("accept error, error is %d,errstring is %s\n",errno,strerror(errno));
close(sock);
return 3;
}
//开辟出一块新的缓存区用于存放IP地址
char buf_ip[INET_ADDRSTRLEN];
memset(buf_ip,'\0',sizeof(buf_ip));
// inet_ntop函数将用于网络传输的数值格式 转化为 点分十进制的ip地址 适用于IPV4,IPV6
//inet_ntop(AF_INET,&client_socket.sin_addr,buf_ip,sizeof(buf_ip));
while(1)
{
//建立连接之后两端开始进行数据的交互式传输
//创建缓存区并清空
char buf[1024];
memset(buf,'\0',sizeof(buf));
//从sock中读取来自客户端的数据进buf
read(client_sock,buf,sizeof(buf));
//清空buf缓存区,往buf里面写服务器端响应
memset(buf,'\0',sizeof(buf));
fgets(buf,sizeof(buf),stdin);
buf[strlen(buf)-1] = '\0';
//再把buf的数据往sock发送
write(client_sock,buf,strlen(buf)+1);//在这里strlen(buf)+1和sizeof(buf)的区别
printf("please wait...\n");
}
}
close(sock);
return 0;
}
客户端client.c
int main(int argc,char *argv[])
{
if(argc != 2)
{
printf("usage :client IP\n");
return 1;
}
//存放命令行参数(ip地址)
char *str = argv[1];
char buf[1024];
memset(buf,'\0',sizeof(buf));
// 定义一个addr_in结构体作为服务器端的标识
struct sockaddr_in server_sock;
//创建一个sock
int sock = socket(AF_INET,SOCK_STREAM,0);
//初始化server_sock为0
bzero(&server_sock,sizeof(server_sock));
server_sock.sin_family = AF_INET;
//把点分十进制整型转为网络字节序
inet_pton(AF_INET,SERVER_IP,&server_sock.sin_addr);
//主机字节序转为网络字节序
server_sock.sin_port = htons(SERVER_PORT);
//向服务器端发起连接请求
int ret = connect(sock,(struct sockaddr*)&server_sock,sizeof(server_sock));
if(ret < 0)
{
printf("connect failed...,errno is %d,errstring is %s\n",errno,strerror(errno));
}
printf("connect is success...\n");
//进入事件循环
while(1)
{
//从标准输入写数据进buf
fgets(buf,sizeof(buf),stdin);
buf[strlen(buf)-1] = '\0';
write(sock,buf,sizeof(buf));
//strncasecmp为忽略大小写的比较, 如果发现
if(strncasecmp(buf,"quit",4) == 0)
{
//如果发现用户输入quit,则退出
printf("quit\n");
break;
}
read(sock,buf,sizeof(buf));
}
close(sock);
return 0;
}
如何理解accept返回值
返回的是一个不同的sock_fd,这个socket保存的是你客户端的socket信息,socket编程服务端客户端都需要知道 协议、
服务器ip、服务器端口、客户端ip、客户端端口这些信息
成功返回之后,tcp服务器与客户端将使用这个新的sock_fd进行通信,失败返回一个非法值(宏)
如何理解listen的第二个参数
三次握手的 过程中会维护着两个队列SYN_RCVD(处于三次握手中的),ESTABLISHED(已经完成三次握手,但还没被服务
器accept的)
1:未完成队列:每个这样的SYN分节对应其中一项,已有某个客户端发出并到达服务器,而服务器正在等待完成相应的
TCP三路握手过程。这些套接口处于SYN_RCVD
2:已完成队列:每个已经完成TCP三次握手过程的客户对应其中一项。 这些套接口处于ESTABLISHED
第二个参数就是未完成队列的大小
指的是在完成TCP三次握手后的队列。即在系统accept之前的队列。
如果系统没有调用accpet把这个队列的数据拿出来。一旦这个队列满了。未连接队列的请求过不来。导致未连接队列里的
请求会超时或者拒绝。如果系统调用了accpet队列接受请求数据。那么就会把接受到请求移除已完成队列。 这时候已完成
队列又可以使用了。
最后 说了如果开启了syncookies 忽略listen的第二个参数
bind失败可能的原因
试图绑定一个已经在使用的端口。
试图绑定一个正在处于TIME_WAIT状态的端口。
绑定一个为小于1024的端口号,因为1到1024是保留端口号
bind 最常见的用法是关联端口号和服务器,并使用通配符地址(INADDR_ANY),它允许任何接口为到来的连接所使
用。
粘包有两种:
1.粘在一起的包都是完整的数据包
2.粘在一起的包有不完整的包
(面向连接) TCP粘包是在一次接收数据不能完全地体现一个完整的消息数据。
(非面向连接)UDP不会出现粘包,因为UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲
区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口
等信息),这样,对于接收端来说,就容易进行区分处理了。
造成粘包问题:
1.发送端需要等缓冲区满才发送出去,造成粘包
2.接收方不及时接收缓冲区的包,造成多个包接收
如何解决粘包问题:
(1)发送固定长度的消息
(2)把消息的尺寸与消息一块发送
(3)使用特殊标记来区分消息间隔