1、TCP头部格式:
头部一般20个字节,但是有可变长度字段,因此是不定长的头部;头部包括了源端口号以及目的端口号、序列号字段(Seq)、确认应答字段(ACK)以及控制位、窗口大小、校验和等。
其中控制位主要比如有ACK、RST、FIN、SYN四个;
SYN是在请求建立连接的时候置一
RST在出现异常希望强制断开的时候置一
ACK的值表示收到了这个ACK_NUM-1及以前的报文段
FIN表示希望断开连接,并且之后不再发送应用数据了。
2、什么是TCP连接?
本质来说,TCP建立连接的过程就是去协商信息的过程,具体的信息包括:Socket、序列号、窗口大小;双方信息协商好,可以认为TCP连接就建立了,所以说一个TCP连接,就是为了保证数据在两端之间可靠交付而去维护的一些信息状态的集合,
3、TCP与UDP的区别?
a. 从连接层面来说,TCP是面向连接的,UDP是无连接的;
b. 从服务对象来说,TCP只能是一对一的,UDP可以一对一、一对多、甚至多对多;
c. 可靠性:TCP能够向上层提供可靠的交付,包括了数据无差错、不丢失、不乱序、不重复等等。但是UDP是不保证可靠交付的,基于UDP又想要可靠只能在应用层去完成。
d. 拥塞控制和流量控制:TCP有,UDP无
e. 头部开销:TCP是变长的头部,一般是20个字节起;UDP是定长头部,都是8个字节;
f. 传输方式:TCP是面向字节流的,发送的消息是没有边界的,但是UDP发送的消息有边界,是一个包一个包的发送的。
g. 分片方式:基于TCP的话,超过了MSS,则会在TCP层进行分段,然后才向下交付给IP层,保证IP层的数据包不会超过MTU(1500Byte),如果丢失了段,那就只重发丢失的段即可;UDP中不会再传输层分片,如果大于了MTU,会在IP层进行分片;
h. 应用场景:HTTP/HTTPS、FTP一般基于TCP;DNS、音频视频一般基于UDP协议。
4. TCP连接建立的过程
TCP连接的建立会经历经典的三次握手
a、第一次握手
SYN
,首先由客户端向服务端发送一个控制位置1的SYN报文,请求建立连接,同时初始化一个
随机值作为序号字段
(client_isn);报文发送后,客户端进入
SYN_SENT
状态。
b、第二次握手
ACK+SYN
,服务端收到了SYN后,服务端也会初始化
随机值作为序列号字段
(server_isn),控制位中的ACK与SYN均置为一,返回一个ACK+SYN报文,在响应客户端的同时向客户端申请连接(为什么不四次握手的原因也在此,即第二次和第三次握手可合并);报文发送后,服务端进入
SYN_RCVD
状态。
c、第三次握手
ACK
,客户端收到SYN+ACK后,返回一个ACK报文,表示同意与服务端建立连接,这时返回报文的
序列号字段为client_isn+1
,控制位的ACK置一;报文发送后,客户端进入
ESTABLISHED
状态,对方服务端收到ACK后也会进入ESTABLISHED状态,至此三次握手完成。
5. TCP连接为什么三次握手,不两次或四次?
首先回答为什么不两次握手,
TCP不采用两次握手
最主要的原因是防止历史冗余连接的初始化,考虑一个场景,如何发送了一个SYN在网络中阻塞了,客户端又发送了一个新的SYN,这时候如果旧的SYN又比新的SYN先到达服务端,那么服务端就会响应一个ACK=旧的Seq_num + 1,但是客户端看到ACK的序号明显不是想要的 = 新的Seq_num + 1,就会向服务端发送一个RST,强制中断这个一个TCP连接,直到新的SYN到了服务端,服务端响应ACK=新的Seq_num + 1,这时候客户端才会与其正常建立连接。如果只有两次握手的话,就没有办法阻止历史连接的初始化,需要多i一次来给客户端判断是否当前的连接是最新的连接。
另外一个原因是TCP建立连接的过程需要客户端和服务端双方同步序列号,按理说应该客户端发送给服务端序列号,服务端响应,之后服务端向客户端发送序列号,客户端响应,这样一来一去才能保证双方的序列号都被可靠的同步了,如果只有两次握手,那么只能保证客户端的序列号被服务端同步了,但是服务端的序列号不一定被客户端同步,这样就只能保证可靠的是:客户端能向服务端发送,服务端只能接收。
另外三次握手还可以避免资源的浪费,比如,如果两次握手,客户端发起SYN的时候因为网络阻塞重发了SYN,因为没有第三次握手,所以在第二次握手的时候服务端就已经建立起了连接,也就是每一个SYN服务端就会建立一个连接,因此会有很多冗余的连接,造成不必要的资源浪费。
不采用四次握手很简单
,虽然四次握手可以保证双方的序列号被可靠同步,但是第二次和第三次其实是可以合并为一次的,所以就没有必要多一次通信。
6.TCP断开连接:四次挥手
TCP在断开连接的时候要经历四次挥手的过程,注意客户端和服务端都可以主动断开连接,下面以客户端主动断开为例。
- 首先客户端主动断开,会发送一个FIN=1的报文,表示之后客户端将不再发送应用数据,希望断开连接,这是客户端进入FIN_WAIT_1状态。
- 服务端收到FIN报文后,知道了客户端想要断开连接,返回一个ACK,同时客户端这时候进入CLOSED_WAIT状态
- 客户端收到ACK后,进入FIN_WAIT_2状态;这时候服务端会处理或者发送一些还没有发送的数据,处理完后,服务端会向客户端发送一个FIN报文,表示服务端也不会向客户端再发送数据了,这时候服务端进入LAST_ACK状态。
- 客户端收到服务端的FIN后,返回一个ACK,同时进入一个时长为2MSL的TIME_WAIT状态(防止最后一个ACK丢失),2MSL之后客户端自动进入CLOSED状态。服务端收到ACK后,也进入CLOSED状态。
谁主动发起的断开,谁才会有TIME-WAIT状态,
7.为什么需要四次挥手?
建立连接时,四次握手中的第二次和第三次可以简化为一步,从而是三次握手,但是在断开连接时,第二步ACK和第三步FIN不能同时发送,因为在服务端FIN前,需要将其未发送的数据或者未处理的数据处理完以后才能发送FIN,所以ACK和FIN不能合并一步,因此需要四次挥手。
8.为什么需要TIME_WAIT状态?以及等待时间为什么是 2MSL?
首先需要TIME_WAIT的原因是:
1、防止具有相同四元组的数据包被接收,导致数据错乱:如果不要TIME-WAIT,直接关闭,那么如果刚好相同四元组的TCP连接又建立了,而且新的客户端的ACK刚好是之前旧TCP连接中正在发送的序列号,那么这时候旧网络的TCP报文就可能被新连接中的客户端接收,造成严重的数据错乱,因此添加一个TIME-WAIT来保证这些历史的TCP报文段能在旧连接中正常消亡以后才断开。
2、如果TIME-WAIT没有或者过短,那就可能不能保证被动关闭方是否已经接收到了最后的ACK,这时候被动关闭方如果真的没有接收到ACK,那就会长期处于LAST_ACK的状态,占用内存和端口。
为什么2MSL?
一个MSL是网络段在网络中能存在的最大生命时间,两个也就是往返的最大时间,那么在2*MSL的时间内如果没有重发的FIN,则说明ACK没有丢失,那就可以放心关闭了,如果收到了FIN,就说明ACK丢失了,那就重发一次ACK,并且TIME_WAIT重新计时;总的来说,2MSL就是一个比较充裕的时间来等待看最后一次ACK是否丢失。
9.客户端突然崩溃了,服务端怎么办?
TCP内有个保活机制,也就是说在一定时间内如果双方没有通信那就会激活保活机制,
所谓保活机制就是在一定时间内向对方发送多个探测报文,如果对方没有响应则认为这个TCP连接已经死亡。
发送探测报文会有三种情况:
a、对方正常响应,那就说明TCP连接正常;
b、如果对方是崩溃后重启了程序,那么这时候对方会无法识别这个报文,就会返回一个RST,表示强制中断TCP连接;
c、如果对方真的崩溃了,那么多次探测报文都不会有响应,就可以认为TCP连接已经死亡,探测方就可以释放响应的资源了。
10.基于TCP的Socket编程流程
Socket,中文翻译套接字,本质上是一个int类型的数值,通过四元组(源IP、源端口、目的IP、目的端口)加上网络协议栈的形式来标记一个会话连接, 客户端与服务端各自维护本地的Socket,拿到了连接句柄,往这个连接中写入数据就相当于向对端主机发送数据,读出数据就相当于从对端主机接收数据;
Socket编程可以基于TCP也可以基于UDP,不过基于TCP更加多见,一般来说,基于TCP的Socket编程流程如下;
对于服务端来说,先创建初始化Socket,Bind绑定IP和端口,Listen开始阻塞监听;客户端方同样也是先创建Socket,然后调用Connect方法进行,从Connect开始就相当于在进行TCP的三次握手,调用Connect方法阻塞等待,如三次握手熟悉的流程,客户端发送SYN报文后,进入SYN_SENT状态,服务端收到后进入SYN_RCVD状态同时服务端返回SYN+ACK报文,同时服务端调用Accept方法并阻塞等待,并将此加入到SYN队列(半连接队列)中,客户端收到SYN+ACK后,相当于Connect方法的调用得以返回,此时客户端方进入ESTABLISHED状态,同时回复一个ACK确认应答报文,服务端收到ACK后,即Accept方法的调用得以返回,这个过程还包含了删除半连接队列中的对象,创建一个新的全连接加入到Accept队列中,然后在Accept方法调用里将这个连接返回给用户操作,至此服务端也进入ESTABLISHED状态,双方完成三次握手,可以开始Socket通信了。通信过程中客户端write方法写入数据,服务端read方法读出客户端的数据。
最后客户端需要断开时,调用close()方法,开始TCP的四次挥手,客户端向服务端发送FIN报文,进入FIN_WAIT_1状态,表示之后不再发送应用数据了,即在Socket角度就是客户端会向文件缓冲区中写入EOF文件结束符,服务端读取到以后就知道客户端之后不会再发送数据了,返回一个ACK给客户端,服务端进入了CLOSE_WAIT状态,客户端进入FIN_WAIT_2状态,服务端可能会继续处理一段时间的数据,处理完毕后服务端也会调用close方法,客户端收到后返回一个ACK,然后进入TIME_WAIT状态,等待2MSL如果没有重发的FIN报文就关闭,服务端收到客户端的ACK后也会进入CLOSED状态,完成四次挥手。
11.TCP的重传机制
TCP的重传机制是保证TCP段不丢失的一个保障,网络环境很复杂不一定发送的所有数据包对端都能在一次接收,重传机制就是来解决这个问题。
常见的重传机制:
超时重传、快速重传、SACK、D-SACK
,下面分别介绍四种重传机制
超时重传
:顾名思义就是预设定的超时时间到了后对方还没有给我们ACK的话,发送方就主动重传数据包,
在发送包丢失或者ACK包丢失都会触发超时重传
,但是这个超时时间RTO的设置比较麻烦,因为如果设置得比较短,那么就会有大量不必要的重传反而增加网络负担,如果设置得比较长,网络的效率也会比较低,所以说精确去度量超时时间RTO是很重要的,决定了超时重传机制是否真的高效,
一般来说RTO的时间应该略大于往返时延RTT,叫做RTTs(加权平均往返时间)
。在Linux系统中,有专门动态计算RTO的。
快速重传
:快速重传是在收到对端三次相同ACK号的报文后触发的,就不会等超时时间到来直接重传,不过重传的话会有一个问题是只重传之前的一个还是重传所有的报文,SACK可以解决。
SACK
:即Selective Acknowledgement,是带有确认的重传机制,他的触发机制和快速重传也是一样,不过他在丢失包时会用SACK参数字段来记录目前已经收到的包,比如丢失了包20-30,但是后序发的30-40、40-50成功接受了,这时候服务端返回的报文就会标识:ACK=20,SACK=30-50,这样我客户端在重发的时候就知道了,重发20-30即可,就不需要把所有的包都重发了,效率更高,本质上来说是把缓存地图返回给了发送方。
D-SACK
:即Duplicate-Selective Acknowledgement,D-SACK可以让发送方更精确地判断是发送的数据包丢失了还是ACK丢失了还是说都没有发生只是网络阻塞了;如果SACK的第一个段被ACK覆盖,那么就是D-SACK,也就是如果返回的包中的参数是ACK=3000 SACK=1000-1500,表示接收方收到了重复的数据包(1000-1500)。
12.TCP的流量控制(滑动窗口机制)
基于TCP协议的报文信息交换,以确认重传机制可以保证每个包都能到达发送方,但是还有个问题是接收方应用层的接收速度不是那么快的,有可能发送方发得太快了,那么接收方不能接收就会直接丢弃,效率就很低了,于是流量控制就是解决这个问题,让发送方的发送速度与接收方的接受速度相匹配。
流量控制主要是依靠滑动窗口机制来保证,发送方和接收方都维护一个滑动窗口,接受方的窗口大小由他自己决定,
发送方的窗口大小由拥塞窗口大小和接受窗口大小的最小值决定
,要区别一下拥塞窗口大小和接收窗口大小;拥塞窗口大小是发送方根据全局网络情况自己估计的,接收窗口大小是由接收方通告给发送方的;
发送窗口和接收窗口由下两图可见:
接收方的每一次ACK,都会捎带窗口大小或者说可用缓冲区大小(捎带技术),所以说这个窗口大小是动态的,是一直在变化的,发送方收到了接收方的ACK后,才敢删除保存在本地的缓存,(如果对方一直没回ACK就考虑重传机制),然后将滑动窗口前移滑动,同时根据最新通告的窗口大小来调整发送窗口大小。
如果接收方通告窗口大小为0,表示目前发送方就不能发送数据了,这时候为了防止死锁(接收方通告窗口不为0的报文丢失时出现),发送方会设置一个定时器,定时器一过就会主动发送一个探测报文询问接收方的窗口大小,如果不为0了,就正常开始发送,如果还是为0,就重置定时器继续等待。当然等待过程中如果接收方主动通告窗口非零的报文到达了那就直接开发,
这里设置定时器的原因就是担心接收方主动通告窗口非零的报文丢了就会陷入双方的死锁局面
。
13.TCP的拥塞控制
TCP协议被设计为一个无私的协议,发送方我不仅要照顾接收方能不能接受,还要照顾当前全局的网络状况,防止我发送的包把这个网络都堵了,因此他不仅会参考接收方的接受能力,还会考虑当前实时的一个网络情况。
拥塞控制大致来说包括几个部分:
慢启动、拥塞避免、拥塞发生
(超时重传、快速重传快速恢复),下面分别来介绍:
慢启动
:所谓慢启动其实并不慢,慢启动白话一点说就是刚开始传输的时候我不是一来就将拥塞窗口大小设置为很大的值,而是从1开始,每次收到了接收方的ACK,我就增加一个拥塞窗口大小,收到了2个ACK,就增加两个拥塞窗口,(一个拥塞窗口就是一个MSS:TCP段中载荷部分最大长度),因此慢启动其实是以指数的形式在增大发送窗口,当增大到门限值ssthresh,认为拥塞窗口已经挺大的了,就将增长速度放缓到线性增长,进入到拥塞避免阶段。
拥塞避免
:拥塞窗口大小达到了门限值后,进入拥塞避免阶段,拥塞窗口大小以线性速度+1+1,从慢启动的指数增长降到了线性增长,这个过程中如果发生了重传,那就是拥塞发生了。
拥塞发生
:如果在拥塞避免阶段发生了超时重传或者快速重传,那就是拥塞发生了,这时候就得分两种情况来看,一种是如果发生了超时重传,那么我们认为现在的全局网络状况应该是很糟糕的,那么这时候就将拥塞窗口从1开始重新来,并且将门限值降为原来的一半;如果是发生了快速重传,那么认为这时候网络状态也不是特别糟糕(因为还能收到三次相同的ACK),那么我们就不用这么激进地降低拥塞窗口大小,而是将门限值设置为原本的一半后,从拥塞避免阶段开始,以线性速度增长。而不是像超时重传那么激进直接将拥塞窗口回到解放前(cwnd=1,ssthresh_new = ssthresh_old/2)
这样看基本上面那个图各个阶段就清晰明了了,最开始慢启动,然后拥塞避免,拥塞避免过程中如果窗口直接降到1了,那就说明发生了超时重传,如果在此从拥塞避免最初开始,那就是在进行快速恢复算法。
14.糊涂窗口综合征
所谓糊涂窗口综合征,就是发送方发送小数据、接收方通告小窗口的情况,因为我们的网络包就包含了至少40个字节的头部字段(TCP头+IP头),因此如果发送的数据载荷很小的话传输的效率就显得很低,因此需要避免去发送小数据、通告小窗口。
怎么避免?很简单,发送方限制只发送一定阈值大小以上的数据就行,接收方的窗口大小如果很小的时候就干脆通告一个零窗口大小,小窗口不如零窗口先不发;
具体点来说,发送方的策略就是
1、如果没有已发送未确认数据的时候直接发送当前累计的数据;2、如果有已发送未确认数据时,就等待没有已发送未确认数据时 或者 当前累计的数据凑够了MSS(1460字节)再发送;
说白了就是最好要凑够了一车人再发车。但是如果实在紧急几个人发车也行。这个就是所谓的Nagle算法。
接收方的策略是,
接收窗口大小小于 min(MSS,缓存空间/2) 的时候,就会向发送方通告窗口为0,阻止了发送方发小数据过来。等到窗口大小到一定阈值的时候,再打开窗口让发送方发送。
还有个提高响应效率的机制是延迟确认机制,原因是接收方如果只是单纯地确认ACK,一个ACK也得用40个字节的头,消息传输效率也很低,因此采用以下策略:
1、当有响应数据要发送,ACK随着响应数据一起发送给对方(等到了数据,捎带技术发送)
2、没有响应数据发送,ACK会延迟一段时间,等待是否有响应数据可以一起发送(没等到第二个ACK和数据,再等等看看)
3、如果在延迟等待发送ACK期间,对方的第二个数据报文又到达了,这时候会立刻发送ACK(集齐了两个ACK直接发车)
图源参考自:www.xiaolincoding.com
个人理解总结记录