tcp协议和udp协议是传输层的两大协议,今天回顾一下tcp协议。
tcp全名是 传输控制协议,用于可靠传输场景,tcp协议通过复杂的手段保证传输的可靠性和速度,主要包括:
--可靠性方面:校验和、序列号(保证有序到达)、确认应答、超时重传、连接管理、流量控制、拥塞控制
--速度方面:滑动窗口、快速重传、延时应答、捎带应答
我们从tcp协议的报头开始研究tcp是如何通过上述手段实现数据的可靠传输的。
tcp协议报头:
--源端口号&&1目的端口号:和源ip和目的ip唯一确定1个tcp连接
--序号seq:占4个字节32位,标识本次发送数据组的第一个序号;tcp协议中,发送数据的每个字节都会有一个序号,例如,一报文段的序号为300,而且数据共100字节,则下个报文段的序号就是400
--确认序号ack:是期望对方下次发送数据的第一个字节的序号,只有当ACK标志位为1的时候,确认序号才有用;
--4位首部长度:又叫数据偏移量,实际上就是TCP段首部的长度
--6个标志位:
URG:当URG=1时,注解此报文应尽快传送,和16位紧急指针一起使用。
ACK:只有ACK=1时确认序号才有用;PSH:PSH=1时接收方应该尽快将本报文段立即传送给其应用层。
RST:复位报文段,当RST=1时,表示出现连接错误,必须释放连接,然后再重建传输连接。
SYN:同步报文段,当SYN=1,ACK=0时表示请求建立一个连接,带有SYN标志位的报文段为同步报文;
FIN:通知对端, 本端即将关闭. 我们把含有FIN标识的报文称为结束报文段
--窗口大小:tcp通过滑动窗口来进行流量控制,可以理解为接收端所能够提供的缓冲区的大小;
--校验和:校验和覆盖整个tcp报文段,包括首部和数据,由发送方计算,接收方校验;
--紧急指针:只有URG为1的时候,紧急指针才有效;紧急指针是1个正的偏移量,序号和紧急指针的值相加,就是紧急数据的最后1个字节的序号;
三次握手
第一次握手:client向server发送连接请求报文,SYN=1, 随机生成初始序列号seq=x;然后client进入SYN-SENT状态;tcp规定SYN=1的同步报文不能携带数据,但是必须消耗1个序列号;
第二次握手:server回复client, 如果同意连接,就发送确认报文:ACK=1,SYN=1,确认号ack=x+1,同时为自己生成1个初始化序列号seq=y;此时server进入SYN-RCVD;
第三次握手:client收到后向server发出确认, ACK=1,序号ack=y+1, 此时连接建立,client进入ESTABLISHED状态;TCP规定,确认报文可以携带数据,如果不懈怠数据则不消耗序列号;
如果没有三次握手,而是两次:那么有可能出现无效的tcp连接;网络中时长会出现延迟,如果client发送了第1个请求由于网络原因一直没有到达server,client等待一会后没有收到回应再次发送了另一次连接请求,并且顺利完成了连接建立,数据传输,断开连接;而随后旧的连接终于到达了server,而server给了回应则tcp连接建立,而这个连接已经没有作用了;而如果是三次握手,client则可以不确认这个连接;
四次挥手
第一次:client发送FIN=1, seq=u(上次收到的报文的序号+1),然后进入FIN-wait-1;tcp规定FIN报文段不携带数据,但是同样消耗1个序号;
第二次:server收到后返回确认,ACK=1, 确认号ack=u+1,并且带上自己的序号seq=v, 随后server进入CLOSE-WAIT状态,client收到后进入FIN-WAIT-2状态;
此时已经进入半断开状态,client已经没有数据发送,但是如果服务端发送数据,client仍然要接受;
第三次:server数据发送完成后,r发送请求断开报文,FIN=1, ACK=1,seq=w, ack=u+1,随后进入LAST-ACK状态,等待client的最后确认
第四次:client向server确认,ACK=1, seq=w+1, seq=u+1,随后client进入2*MSL的TIME-WAIT状态才会进入CLOSED,而服务器只要收到了客户端发出的确认,立即进入CLOSED状态;
为什么最后客户端还要等待 2*MSL的时间呢?
MSL(Maximum Segment Lifetime),TCP允许不同的实现可以设置不同的MSL值。
第一,保证客户端发送的最后一个ACK报文能够到达服务器,因为这个ACK报文可能丢失,站在服务器的角度看来,我已经发送了FIN+ACK报文请求断开了,客户端还没有给我回应,应该是我发送的请求断开报文它没有收到,于是服务器又会重新发送一次,而客户端就能在这个2MSL时间段内收到这个重传的报文,接着给出回应报文,并且会重启2MSL计时器。
第二,防止类似与“三次握手”中提到了的“已经失效的连接请求报文段”出现在本连接中。客户端发送完最后一个确认报文后,在这个2MSL时间中,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样新的连接中不会出现旧连接的请求报文。
TIME-WAIT可能会引起BIND失败问题:
编辑内核文件/etc/sysctl.conf,加入以下内容:
net.ipv4.tcp_syncookies =1 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭;
net.ipv4.tcp_tw_reuse =1表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;
net.ipv4.tcp_tw_recycle =1表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。
net.ipv4.tcp_fin_timeout 修改系默认的 TIMEOUT 时间
确认应答机制
tcp对每个字节都进行了编号,每次发送端发送完数据,接收端返回确认ACK的时候,都需要告诉发送端自己收到哪里了,你下次从哪里发:
超时重传:
发送端发送完数据如果一直没有收到ACK,就会重发,但是也有可能出现接受端收到了数据,但是ACK请求丢失了的情况,不过没关系,有序号存在,接受端会再次发送自己收到的数据编号;
超时重发的时间间隔会以500ms为单位,后续都是整数倍后重发。
滑动窗口
如果每次发送数据后,都要等待ACK确认后才能继续发送数据,这样显然传输速度不够快,尤其是双方请求往返时间较长的时候;并且,发送方的发送速度&&接收方的接受速度是不一定匹配的,需要一个同步手段防止数据溢出,滑动窗口即是解决上述问题的;
发送方维持一个窗口,只有落在窗口范围的的数据才允许发送,接受方同样维护一个窗口,只有落在窗口内的数据才被接受;
发送方数据分为:
--已发送并收到ACK的数据:这个会从窗口删掉
--已发送未确认的数据:这个是窗口内数据
--缓冲区内待发送数据:这些数据在窗口内,接收端允许发送,需要尽快发送,窗口的大小完全由接收方告知
--待发送数据:这些数据不在窗口内,接受端也不允许发送,因为接收端的缓冲区也有限;
如果出现了丢包,也不用担心,分两种情况:
1--数据收到了,但是ACK丢了:
这种问题不大,因为还会有后续的其它ACK来确认对方收到了哪些包
2--数据丢了
如果数据丢了,接收方发送ACK的时候不会跳过确认序号,如果1~1000收到了,1001~2000丢了,2001~3000收到了,接收方返回的ACK的序号会一直是1001,并且每次收到其它序号的请求都会发送1001,发送端连续收到3次1001后就会重传;随后接收端再次发送的ACk序号就会是当前收到的连续的最大序号了,例如:1~7001,这种机制叫快重传
流量控制
tcp协议会根据接收方的处理速度来调整发送端的发送速度,这个机制叫流量控制。
接受方会将自己可以接受的缓冲区大小放入头部的窗口大小字段,发送ACK确认的时候告知发送端;
窗口越大说明网络吞吐量越高,如果发送端发现自己缓冲区快满了,就会缩小窗口大小;如果接收方处理不过来,缓冲区满了,就会把窗口大小设置为0;随后发送端会暂停发送,但是会定期发送探测请求,等接收端告知窗口大小;
实际的窗口大小计算方法:在选项中会有1个窗口扩大因子选项M,实际的窗口大小是报文窗口大小左移M位,左移1位相当于*2倍;
拥塞控制
虽然有了滑动窗口这个大杀器,但是
拥塞控制是为了解决一开始发送数据速度的问题:
因为一开始并不清楚接收方的网络状态,如果一开始就发送大量的数据有可能会存在问题。所以TCP引入慢启动机制。先发少量的数据, 探探路, 摸清当前的网络状态以后, 再决定按照多大的速度传输数据.
在此引入拥塞窗口的概念:
--发送开始的时候, 定义拥塞窗口大小为1;
--每次收到一个ACK应答, 拥塞窗口加1;
--每次发送数据包的时候, 将拥塞窗口的大小和收到对方窗口的大小比较,取较小的作为窗口大小;
这样拥塞窗口的增长速度是指数级别的,慢启动只是刚开始慢,但是增长非常快。为了防止窗口大小增长过快,会有一个限制,即慢启动的阈值,一旦超过这个阈值,就会变成线性增长, 当TCP开始启动的时候, 慢启动阈值等于窗口最大值,在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回1。
少量的丢包, 我们仅仅是触发超时重传;
大量的丢包, 我们就认为是网络拥塞;
当TCP通信开始后, 网络吞吐量会逐渐上升;
随着网络发生拥堵, 吞吐量会立刻下降.
延迟应答
如果在收到数据后立刻返回ACK,这个时候可能缓冲区较小,而接收方有可能处理数据很快,这时候如果延时一小会再发送应答,那么窗口大小(缓冲区大小)就会很大,窗口越大, 网络吞吐量就越大, 传输效率就越高,所以TCP引入了延迟应答机制。
但是也不是所有的数据包都延迟应答,有两个限制:
1:数量限制:每N个包就应答一次
2:时间限制:每超过最大延迟时间就应答一次
一般N取2, 最大延迟时间取200ms
捎带应答
在延迟应答的基础上, 我们发现, 很多情况下
客户端和服务器在应用层也是 “一发一收” 的
意味着客户端给服务器说了 “How are you”
服务器也会给客户端回一个 “Fine, thank you”
那么这个时候ACK就可以搭顺风车, 和服务器回应的 “Fine, thank you” 一起发送给客户端
面向字节流
创建一个TCP的socket, 同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区;
调用write时, 数据会先写入发送缓冲区中;
如果发送的字节数太大, 会被拆分成多个TCP的数据包发出;
如果发送的字节数太小, 就会先在缓冲区里等待, 等到缓冲区大小差不多了, 或者到了其他合适的时机再发送出去;
接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区;
然后应用程序可以调用read从接收缓冲区拿数据;
另一方面, TCP的一个连接, 既有发送缓冲区, 也有接收缓冲区,
那么对于这一个连接, 既可以读数据, 也可以写数据, 这个概念叫做 全双工
由于缓冲区的存在, 所以TCP程序的读和写不需要一一匹配
例如:
写100个字节的数据, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节;
读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次read一个字节, 重复100次;
粘包问题
首先需要明确的是,粘包说的是应用层的数据包;TCP没有UDP一样的报文长度字段,但是有序号,在TCP角度,数据都按照序号在缓冲区中排好了顺序,但是在应用层看来,这些数据都是连续的字节数据,那么程序就不知道从哪到哪是一个完整的应用层数据包。
解决办法:其实各种不同办法目的都是明确边界
1--约定固定大小
2--长度不固定的话,约定数据包长度字段
3--约定明确的分隔符
UDP没有粘包问题,因为UDP只要没有向上层交付数据,就保存有数据包长度;UDP是一个一个将数据交付给应用层的,有明确的界限;
TCP异常情况
--进程终止:进程终止会释放文件描述符, 仍然可以发送FIN. 和正常关闭没有什么区别.
--机器重启:和进程终止一样
--机器断网:一旦对方有写入操作,就会发现机器不在了,就会RESET;即使一直不写入,TCP的保活计时器也会定期探测对方,如果对方不在就会释放这个连接;
--一些应用层协议也有类似的探活手段
总结:
--保障可靠性手段:
1、校验和
2、序列号(保障有序性)
3、确认应答机制
4、超时重传
5、流量控制
6、拥塞控制
7、连接管理
--保障速度的手段:
1、滑动窗口
2、快重传
3、延迟应答
4、捎带应答
--定时器
1、保活定时器
2、超时重传定时器
3、TIME-WAIT定时器