点击左上方蓝色“一口Linux”,选择“设为星标”
我本人是比较抗拒背八股的,我主张去理解知识,让它真的成为你的一部分,这样在表达的时候就会非常流利,我常和我朋友说每次我面试的时候对于这些八股都是在简单的吟诵,因为我真的理解掌握了这些知识。但是网络、计算机组成的具体内容确实不像代码一样容易理解,所以很多人只能死记硬背。
其实这些所谓的八股,除了设计者和专精的工程师,又有多少人能真正确定自己对这些知识的理解是没有错误的呢?只要在面试中表达出自己的理解,而不是死记硬背,相信就能够和普通的候选人拉开差距了。
闲话少说,下面是我对于TCP协议的一些个人理解,可能会带有错误,欢迎大家指正。
TCP内部的实现是很复杂的,我会从流量控制、可靠交付、拥塞控制、TCP报文、三次握手四次挥手、重传时间等内容和大家稍微聊聊TCP的具体实现,以及为什么要这么做。
TCP的地位:是整个网络协议族中最重要的一个,TCP在网络协议族中是很特别的,因为整个网络,我们姑且分为课本上的五层吧,从下到上即物理层、数据链路层、网络层、传输层、应用层五层,只有传输层的TCP能做到可靠交付,这也是为什么很多应用都是用TCP的原因,所以在面试中TCP绝对是重点中的重点。
TCP:传输控制协议(英语:Transmission Control Protocol,缩写:TCP)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793定义。在简化的计算机网络OSI模型中,它完成第四层传输层所指定的功能。用户数据报协议(UDP)是同一层内另一个重要的传输协议。
在因特网协议族(Internet protocol suite)中,TCP层是位于IP层之上,应用层之下的中间层。不同主机的应用层之间经常需要可靠的、像管道一样的连接,但是IP层不提供这样的流机制,而是提供不可靠的包交换。
应用层向TCP层发送用于网间传输的、用8位字节表示的数据流,然后TCP把数据流分割成适当长度的报文段(通常受该计算机连接的网络的数据链路层的最大传输单元(MTU)的限制)。之后TCP把结果包传给IP层,由它来透过网络将包传送给接收端实体的TCP层。TCP为了保证不发生丢包,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的包发回一个相应的确认信息(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据包就被假设为已丢失并进行重传。TCP用一个校验和函数来检验数据是否有错误,在发送和接收时都要计算校验和。(维基百科)
1.源端口,目的端口:每个两字节
2.序号:两字节,TCP的每个字节都按照序号编序(这也是面向字节流的一个标志),建立连接时序号是随机的 而后的序号都在此基础上递增。比如我第一次握手seq=10086 第三次握手seq=10087 之后又传了一百字节的数据,那么再发数据,seq=10087+100+1=10188。
3.确认号:两字节,希望对方下一个报文段的序号 这也是为什么握手的时候ack=seq+1 确认号代表前面序号的字节都已经成功按序收到(捎带确认机制),当客户端发送过来的序号seq=10086时,报文长度为200,那么服务端的ack应该是12087,表示对之前所有报文的确认。
4.数据偏移:四位,实际上表示的是首部长度,最大十进制数转换过来对应的是15,代表头部最多大小为60字节,所以TCP头部的长度应该是20-60间以4字节为单位变化的,不可能出现30字节长度的TCP头部
5.保留:六位,今后使用
然后就是6个控制位,每个1为,数据偏移+保留+6控制位=2字节
6.紧急URG:表明该报文优先级很高,当URG=1时,报文会被放在发送方此次发送报文的最前端进行优先发送
7.确认ACK:ACK=1 确认号有效 建立连接之后 每次ACK=1
8.推送PUSH:PUSH=1 立即创建一个报文段发送出去(用得少)
9.复位RST:RST=1 表示TCP连接出现严重错误,需要重新连接
10.同步SYN:SYN=1 表明这是一个连接请求报文
11.窗口:2字节,滑动窗口的大小 我们后面细说
12.检验和:2字节,这个用途和UDP的一样,加上12字段伪首部之后进行校验
13.紧急指针:2字节,只有当URG=1时才有意义,指出本报文段中紧急数据的字节数
14.选项:长度可变,最大40字节(窗口扩大选项、时间戳选项等等)
连接点对点
全双工 A可以给B发 B也可以给A发
面向连接 只有连接建立完成 才可以发数据
拥塞控制 主要是通过以下机制来完成的(慢开始 拥塞避免 快恢复和快重传)
可靠交付
面向字节流
这些下面都会一一介绍
TCP是一个复杂的协议 而UDP比较简单
1.连接 TCP是一个面向连接的协议 而UDP是一个无连接协议 即TCP需要连接建立之后才能传输数据 而UDP在未建立连接时就能够传输数据
2.交付 TCP是一个可靠交付的协议 而UDP只能尽最大努力交付 可靠交付指的是数据按序到达 不丢失 不重复
3.数量 TCP是一个点对点的全双工协议 而UDP支持一对一 一对多 多对多
4.首部长度 TCP的首部长度更长 20-60字节 而UDP的首部固定为8个字节
5.面向 TCP面向的是字节流 即上层应用层交付下来的数据报 在TCP的眼中是一串字节流 而UDP是面向数据报的
6.是否具有拥塞控制 TCP是有拥塞控制的 在网络拥塞时源点主机的发送速率会降低 而UDP不具有拥塞控制 一些追求传输效率的应用通常使用UDP协议
所谓面向字节流 指的是TCP发送端发送几次数据和接收端接受几次数据时没有必然联系的
比如发送端调用一次write写了一百个字节 但是接受端可以分10次接受 也可以写10次100字节 接收端一次接收完
原因:
TCP面向连接 一个Socket收到的数据由同一台数据发出 并且有序到达 所以每次读取多少数据都可以 因为我知道前面的数据都是对的 某些时候为了节省资源 程序可能会选择"攒一会"再读
而UDP协议不知道前面的数据是否是错的 所以每次发送就对应着一次接收 如果错了就再发
应用区别:
UDP:追求高速度的应用一般用的是UDP(KCP),比如视频、音频、实时游戏、广播,而且通常这些应用对于数据的可靠性要求不是很高,视频丢点数据(丢几帧 后面再补上),你也看不出来,但是速度慢就不能接受了,玩英雄联盟慢一秒别人就把你头打爆了
TCP:可靠传输,HTTP用的就是TCP(后面用为了追求性能用UDP去了,我们之后详说),任何确保数据可靠的应用都应该使用TCP
相信很多同学都听说过三次握手 四次挥手的问题,也大概知道过程是什么样的,但是自己讲很难讲清楚,我们现在来捋一捋这到底是是个什么过程,以及为什么要三次握手四次挥手
注:下文的客户端和服务器是站在应用层角度的,可以理解成发送方和接收方
第一次握手:客户端向服务器发送TCP请求连接报文,SYN置1,seq=x (都知道SYN和seq是啥吧)
第二次握手:服务器收到报文,发送TCP确认报文,SYN=ACK=1,seq=y ack=x+1
第三次握手:客户端收到报文,发送ACK=1,seq=x+1,ack=y+1
怎么记忆这个过程呢?
第一次和第二次握手 都属于正儿八经连接建立的过程 我们之前说过SYN置一的情况是请求连接报文才有的 所以前两次握手是有SYN的
第三次握手是对前两次握手的确认过程 即确认 客户端感知到服务器的存在 服务器也知道客户端的存在 所以不需要SYN
因为第一次握手发送的报文是没有实体数据的,所以第三次握手seq=第一次握手seq+1
注:第三次握手可以带数据 前面两次不行
这个一般是和三次握手一起问的
我个人理解有两个原因
1.TCP是面向连接的 如果只进行两次握手 服务器其实是不知道连接是否建立成功的
2.如果只进行两次握手,那么会出现这么一种情况,因为网络延迟,客户端向服务器发了一次连接请求报文,但是服务器暂时没收到,然后客户端又发了一次,这次服务器收到了,两次握手成功了,这时第一次发送的请求来了,服务器也会做出响应,就会同时有两个TCP连接,浪费资源。但是如果有三次握手的话,服务器进行确认的时候,客户端会忽略掉这次确认(想一想为什么)
第一次:客户端主动发送断开请求,FIN=1(这是一个请求断开的报文),seq=x
第二次:服务器收到之后 发送 ACK=1 seq=v ack=x+1 然后处于半关闭状态Close-wait
第三次:服务器发送FIN=1 ACK=1 seq=w ack=x+1
第四次:客户端收到后 发送ACK=1 seq=u+1 ack=w+1
一些解释
1.第三次为什么seq=w 是因为我们假设第二次到第三次的期间 服务器还在发数据
2,为什么第三次也有ACK字段
个人理解:TCP报文规定了只要建立连接之后所有报文都有ACK字段,所以其实第一次也有ACK字段的,只是大部分教材没有画出来
(八股文的很多细节是无法深究的 不是TCP的设计者谁知道真相是什么呢 我觉得只要有自己的理解 就已经超越很多死背书的人了)
3.为什么服务器连着给客户端发两次 一次不行吗?
第一次是对客户端发来的确认
第二次是请求和客户端断开
而且在这两次发送报文之间 服务器是可以发送数据的 客户端不行
服务器决定进行第二次挥手和第三次挥手,某些时候把两次挥手合并,因为中间等不等待其实是由服务端应用程序决定的(handler),如果没有数据要发,同时又开启了tcp的延迟确认,那么就会合并挥手,这也是抓包的时候为什么能抓到三次挥手的包。
1.释放的端口可能会重连刚断开的服务器端口,而第二次挥手之后服务器还在给该端口发数据,如果不等的话,在这个通道里的老TCP报文可能和新的报文冲突,造成数据紊乱,2MSL(两倍生存时间,原因我推测可能是计算客户端发送给服务器,服务器处理之后再返回过来)是足够让客户端全部收到第二次挥手后的数据的。具体的时间不同版本其实也不一样,有兴趣的同学可以深入了解
2.保证确认断开,如果最后一次确认丢失了,服务器会重新进行第三次挥手,这样客户端还能够对第三次挥手的FIN报文进行处理
这个问题是一个很广泛的问题 基本把所有TCP的设计思想都包含了
什么是可靠交付?
我认为最起码要做到这几点:数据不丢 数据不重 数据按序 数据不错
1.数据不丢:TCP使用了确认-重传机制,对每一个收到的正确报文进行确认(ack),如果发现有数据没有被确认,就重传,其中重传又有超时重传和快重传
2.数据不重/不错:接收方会对数据进行检验。重复的数据丢弃,错误的数据丢弃,并不进行确认。
3.数据按序:流量控制,不至于一下发很多报文,而且会对数据包进行重新排序
相信大家已经懂什么是确认了吧,简单来说,接收端发送ack=10001的报文,就说明从连接开始的序列号(假设是100)到ack-1(10000)的报文都是可靠交付的。
重传也很简单,我们先来聊聊超时重传
什么时候重传呢?
其实TCP在传递数据的时候,会设置一个超时计时器,当超时计时器触发了,发送端还没有收到来自接收端的确认报文,那么就认为这个消息没成功传递,原因可能有很多,网络阻塞,丢包,数据出错等等
问题来了:超时计时器怎么设置呢?每一个数据的超时时间都不一样吗?怎么能做到既不浪费算力,又能高效确认超时呢?
首先超时时间肯定不能选的很短,不然会很容易重发,浪费很多资源。
也不能很长,因为发送端只有收到确认之后才回删除数据,这样会浪费发送端的资源。
所以重传时间RTO我们一般设为比平常报文的平均往返时间稍微长一点点
具体公式如下:
RTTS(加群啊平均往返时间)=(1-a) x 旧的RTTS+ a x RTTS RFC文档推荐 a=0.125
RTTD(RTT偏差加权值) =(1-b) x 旧的RTTD + b x (RTTS-RTTD) RFC文档推荐b=0.25
RTO=RTTS+4 * RTTD
而每次触发超时重传 RTO=2 X RTO
什么是流量控制?为什么要流量控制?
简单来说流量控制就是接收方通过参数控制发送方的速度,记得我们上文讲到的TCP报文的窗口字段吗,这个就是调控的因素,当接收方的缓存变少,就把窗口变小,发送方就会把数据发的慢一点,窗口的大小就是发送速度的大小
而我们提到的这种方案,就是TCP的滑动窗口,它的思想与算法中的滑动窗口类似,维护一个变长的窗口值,落在窗口里的就是可以发送的,在窗口后面的就是发完了的,窗口前面的就是还没发的,窗口是以字节为单位的,这也能体现TCP的面向字节流思想。
注意:发送端和接收端的窗口大小不可能做到完全同步,因为网络具有时延,当窗口为0的时候,发送端不再发送数据,待接收端缓冲区有空间了之后,会重新打开滑动窗口,让发送端继续发送数据。
什么是拥塞?
拥塞:对网络中的某种资源的需求超过了资源可提供的部分
TCP采用四种算法避免拥塞
我们把A设为发送方 B设为接收方
发送方维护一个cwnd(拥塞窗口)的动态变量,其值取决于网络的拥塞程度,还有一个慢启动门限ssthresh状态变量
原则:没有拥塞,把cwnd变大,拥塞了,把cwnd缩小 思想很简单,实现很复杂
现在是不是有点蒙,之前不是提到了发送窗口大小吗,怎么又来了个拥塞窗口,A到底用什么速度发数据
经查证:发送窗口大小等于min(cwnd,B的接收窗口)
1.慢开始:cwnd小于ssthresh时,采用这种算法,cwnd大小按照指数扩容,每次 x 2
2.拥塞避免:cwnd>=ssthresh时,采用这种算法,cwnd按照常数扩容,每次+1
判断拥塞:当网络中有一段数据重传,则认为网络拥塞,ss变为拥塞时的一半,cwnd变为1
大家是不是觉得很不科学,因为丢包是很正常的,再牛逼的网络也会丢包,重传一次就认为拥塞,cwnd可能永远都等于1
因此TCP还有一种重传机制
3.快重传:不需要等到超时才进行重传,当B给A连着发了三次ack一致的确认报文,A就认为数据丢失了,重发ack之后的数据
4.快恢复:当快重传被触发时,同时执行快恢复算法,ss=拥塞时的一半,cwnd=ss,执行拥塞避免算法
相信如果你听懂了上面的内容,你脑子里应该有这个疑问
TCP到底是两种都用,还是拥塞的时候用快重传,平常使用超时重传呢?
我们计网老师说的是后者,但是TCP是如何判断网络是否拥塞的呢,已经建立连接的TCP能动态修改重传策略吗?
从业务的角度来说我觉得答案更可能是前者,毕竟这么玩能提高速度。
今天的内容到这里就结束了,如果对您有所帮助,欢迎点赞、评论、收藏,您的支持就是对我最大的鼓励。
end
一口Linux
关注,回复【1024】海量Linux资料赠送
精彩文章合集
文章推荐