注:TCP不是只有十个机制
TCP
可靠传输是tcp最为重要的核心(初心)
可靠传输,并不是发送方把数据能够100%的传输给接收方
而是退而求其次
让发送方发送出去数据之后,能够知道接收方是否收到数据。
一但发现对方没有收到,就可以通过一系列的手段来进行补救
1.确认应答
确认应答就是用来判断接收方是否能够收到数据。
发送方,把数据发送给接收方之后,接收方收到一个数据之后就会返回一个应答报文。
发送方如果收到了这个应答报文就知道自己的数据是否发送成功了。
在网络中可能会出现一些信息丢失的情况如下
A先发给B一个咱们下午去吃自助餐,然后又发了一个咱们组队学习吧
B回应给好啊,又发送一个滚吧。
正常的情况下A收到回复明白了咱们下午去吃自助餐,但是不组队学习
但是不知道什么原因A先收到了滚吧,在收到好啊就理解为咱们组队学习下午不吃自助餐 。
这样收到的回应的顺序不同造成的误解很使人困扰。在实际的网络传输中就也有可能会出现这样的后发先致的情况(一个数据包在传输的过程中,走的路径可能是非常复杂的,不同的数据报走的不同的路线)。
TCP解决上述个问题需要完成两个工作
1.确认应答报文和发送出去的数据,能够对上号,不要出现歧义。
2.确保在出现后发先致的现象下,能够让应用程序这边仍然按照正确的顺序来理解数据。
TCP中的32位序号位和32位确认号位就会在发送信息时进行标记,然后如果出现后发先致,就让其等待至先发的到来。如下述例子
A先发给B一个咱们下午去吃自助餐(标记为1),然后又发了一个咱们组队学习吧(标记为2)
B回应给好啊(针对1的确认序号1),又发送一个滚吧(针对2的确认序号2)。
这样就通过序号了描述了数据的先后顺序。
此图只是粗略的形容了一下大概的工作流程实际中序号并不是按照这样一条两条来进行编号,而是按照字节来编号(TCP面向字节流)
如图所示,一个TCP包中一共有1000个字节的载荷数据。其中第一个字节的序号是1,就在TCP报头的序号字段中写1.
由于一共是1000个字节,最后一个就是1000,但是1000这样的数据并没有在TCP报头中记录。TCP报头中记录的序号,是这一次传输的载荷数据中的第一个字节的序号。其他字节的序号需要一次的推出。
在应答报文中就会在确认序号字段填写1001
因为收到的数据是1-1000,所以1001之前的所有数据,都被B收到了或者也可以理解为B向A要从1001开始的数据。
通过特殊的ack数据包,里面携带的”确认序号“告诉发送方,哪些数据已经被确认收到了.
此时发送方,心中就有数了,知道自己刚发的数据是到了还是没到。也就构成了可靠传输的核心部分。
TCP的初心是为了实现可靠传输 达成可靠传输的最核心的机制就是确认应答。
如何区分一个数据包是普通的数据还是ack应答数据?
如图所示
当ACK这一位为1时,表示当前的数据包是一个应答报文,此时该数据包中的确认序号字段就能够生效。
这一位为0时,表示当前数据包是一个普通报文此时数据包是一个普通报文。此时数据包中的确认序号字段是不生效的。
TCP这里的常见面试题:通过确认应答为核心借助,借助其他机制辅助,最终完成可靠传输。
不是三次握手四次挥手保证了可靠传输,这是建立连接的。
2.超时重传
确认应答,描述的是一个比较理想的情况
如果网络传输过程中,出现了丢包操作??怎么解决
发送方肯定就无法接收到ACK。
为什么会出现丢包??
网络如同现实生活中的公路一样,错综复杂,并且有很多的收费站
平常如果车流量不大的时候,车会很快通过收费站,很少出现堵车
但如果到节假日,收费站肯定会出现堵车
网络中的收费站可以理解为是一些路由器/交换机
如果数据包太多了就会在这些路由器/交换机上出现堵车
但是路由器针对堵车的处理往往粗暴,不会把这些积压得数据包都保存好,而是将其中得大部分数据包之间丢弃掉,这个数据包直接消失了在网络中。
丢包是一个随机得事情,在上述tcp传输过程中,丢包存在两种情况
1.传输的数据丢失了
2返回的ACK丢失了
发送方无法区分这两种情况,所以无论出现哪种情况发送方都会进行重新传输。
第一次是丢失了,重传一下试试,很大概率能传过去
假设丢包的概率是20%,两次都丢包的概率是4%,重传就大幅度的提升了数据会被传输过去的概率,重传操作是一个很好的丢包补救措施。
注:引入了可靠性,会付出代价。最明显的代价两方面
1.传输效率
2.复杂程度
发送方何时进行重传?等待时间(初始的等待时间是可以配置的。不同系统的时间不一定一样,也可以通过修改一些内核参数来进行改变时间,等待的时间也会进行动态变化,每多经历一次超时,等待时间都会变长)
发送方发出数据之后,会等待一段时间。如果这个时间之内,ack到达了,被视为数据到达。如果到达这个时间,数据还没到就会触发超时重传
例如
A->B发了一条数据,
第一次,A等待ACK的时间,假设是50ms
此时如果达到50ms,还没有ack,A就重传.
当A重传的数据,还是没有收到ack,第二次等待的时间就会比第一次更长
拉长也不是无限拉长,重传若干此时,时间拉长到一定程度,认为数据再怎么重传也没用了,就放弃 tcp连接(准确的说是会触发tcp的重置连接操作)
根据上图站在B的角度来看,收到了两条一样的数据,收到了重复的数据,是否会给程序带来一些bug,比如发一条,收的时候收到了两条一模一样的数据。
TCP帮我们解决了这个问题,TCP有一个接收缓冲区(内存空间),会保存当前已经收到的数据,以及数据的序号。接收方如果发现,当前发送方发来的数据,是已经在接收缓冲区中存在的(收到过重复的数据),接收方就会直接把后来这个数据给直接丢弃掉,确保应用程序读的时候,只能读到一条数据。
接受缓冲区不仅能进行去重,还能进行重新排序。确保发送的顺序和接受的顺序是一致的。
3.连接管理
建立连接+断开连接
三次握手和四次挥手
tcp这里的握手,就是给对方传输一个简短的,没有业务数据的数据包,通过这个数据包,来唤起对方的注意,从而触发后续的操作。
握手这个操作,不是TCP独有的,甚至不是网络通信独有的,计算机中的很多操作,都会涉及到握手
TCP的三次握手,TCP在建立连接的过程中,需要通信双方一共”打三次招呼“才能完成建立连接的。
A想和B建立连接,A就会主动发起握手操作,实际开发过程中,主动发起的一方,就是客户端,被动接受的一端就是服务器。
同步报文段,就是一个特殊的TCP数据包,没有载荷的(不携带业务数据的应用层数据包)
握手完成,A和B记录了对方的信息(构成了逻辑上的连接)。
建立连接的过程,其实是通信双方都要给对方发起syn,也要给对方反馈ack。一共是4次握手了,但是中间两个可以合并成一次。
上图中的syn这一位为1表示这个报文段是同步报文段,如果是1不是同步报文段。
三次握手是要解决什么问题??
TCP初心,是为了实现"可靠传输”
进行确认应答和超时重传有个大前提,当前的网络环境是基本可用的,通畅的.
如果当前网络已经存在重大故障了,此时,可靠传输,无从谈起.
三次握手的核心作用一:
确认当前网络是否是通畅的
三次握手核心作用二:
要让发送方和接受方都能够确认自己的发送能力和接受能力均正常
三次握手核心作用三:
让通信双方,在握手过程中,针对一些重要的参数,进行协商
握手这里要协商的信息,其实是有好几个的。
大家要知道, tcp通信过程中的序号从几开始,就是双方协商出来的(一般不是从1开始的)
每次连接建立的时候,都会协商出一个比较大的,和上次不太一样的值.|
上图中的客户端应用层和服务器端应用层是操作系统原生socket api对应的执行过程
右侧TCP层的LISTEN等类似的都是服务器的状态,服务器这边的socket创建好并且把端口号绑定好,此时就会进入listen状态。此时就允许客户端来建立连接。ESTABLISHED是客户端和服务器都会有的状态。
注:
FIN叫做结束报文段。
建立连接一般都会是客户端主动发起的。
断开连接客户端和服务器都可以发起。
此时连接断开,这个时候就相当于A和B都把对端的信息删除了。
和三次握手不同的是这里的四次挥手不一定可以将中间的两次交互合二为一
不一定的原因在于ACK的触发时机和FIN的触发时机是不同的。
ACK是内核响应的。B收到FIN,就会立即返回ACK
第二个FIN是程序的代码触发的,B这边调用了close方法才会触发FIN
从服务器到收到FIN,同时返回ACK,在到执行到close发起FIN,中间的时间是不确定的。
像前面的三次握手,ACK和第二个syn都是内核触发的.同一个时机.可以合并.
这里的四次挥手,ACK是内核触发的,第二个FIN是应用程序执行close触发的.时机不相同,不能合并.
那是否意味着,如果我这边代码close没写/没执行到,是不是第二个FIN就一直发不出去??(有可能的)
如果是正常的四次挥手,"好聚好散”,正常的流程断开的连接.
如果是不正常的挥手(没有挥完四次),异常的流程断开连接.(也是存在的)
TCP中有一个机制延时应答,能够拖延ACK的回应时间。一旦ACK滞后了,就会有机会和下一个FIN合并在一起。
如上图如果哪一方主动断开连接,哪一方就会进入到TIME_WAIT
TIME_WAIT存在的主要意义,防止最后一个ACK丢失
如果最后一个ACK丢失了,从右边来看,右边就会触发超时重传。
重新传送给左边FIN。
如果没有TIME_WAIT状态,就意味着左边这个时候已经进入到CLOSED状态,释放连接了。
此时重传的FIN没有人可以进行处理,没有人能返回ACK了,右边也永远收不到ACK,关闭不了了。
左边使用TIME_WAIT状态进行等待,等待的这个时间,就是为了处理后续右边可能重传的FIN
此时有重传的FIN来了,就可以正常返回ACK了。
右边的重传才有意义
TIME的等待时间???
假设网络上的两个节点通信消耗的最大时间为MSL(可自己配置的参数),
那么TIME_WAIT的时间就是2MSL(上限,绝大部分的数据包不会到达这个时间)
4.滑动窗口
TCP的可靠传输,会影响到传输的效率。因为多出来一些等待ack的时间,单位时间内能够传输的数据就变小了。
滑动窗口可以让可靠传输对性能的影响小一些。
TCP引入可靠机制导致传输效率是不可能超过没有可靠传输的UDP的。
TCP的效率机制都是为了尽可能减少两者之间的差距。
缩短确认应答的时间
如上图每收到一个应答报文,再发下一个数据,这个过程中,等待时间比较长。
如上图进行批量传输数据,不等待ack返回,直接在发下一个数据。
批量传输,也不是无限的进行传输。
存在一定上限,达到上限,统一等待ack。
不等待的情况下,批量最多能发多少个数据,这个数据量被称为窗口大小。
如上图
当前A->B是批量的发了四份数据.此时B也要给A回应四组ACK
此时A已经达到窗口大小,再收到ACK之前,不能继续往下发了.
需要等待有ACK回来了之后,才能继续往下发.
这里是怎么继续发的?是等待四个ack都回来了,在继续发四条?还是回来一个ack就继续发一个呢?
回来一个ack,就立即继续发一个.
这里的直观效果,看着就像这个窗口在往后滑动一样
TCP的初心是可靠传输,上述滑动窗口中,确认应答是可以正常工作的。
但是出现丢包的话怎么办??
情况一
这种情况不需要任何重传。确认 序号表示的含义是,当前序号之前的数据,已经确认收到了。下一个你应该从确认序号这里,继续发送。
如1001这个ack丢了,但是2001ack到了。证明2001之前的数据都已经确认传输成功了,包括了1001的情况。
情况二
主机A需要知道哪个数据丢了 主机B告诉主机A哪个数据丢了
如上图所示B下面一直返回下一个是1001就是给A说1001之前的都没有传给我并且无论当前传输的数据具体是几,都在索要1001这个数据。
A收到了B的连续索要1001,知道了这个数据之前的包丢了,进行重传。
1001到达之后直接就返回了7001
上述的重传过程,没有额外的冗余操作,哪个数据丢了,就重传哪个数据,没丢的数据不需要重传(快重传:滑动窗口下的超时重传的变种)
如果通信双方,传输数据的量比较小也不频繁,就仍然是普通的确认应答和普通的超时重传
如果通信双方,传输数据量更大,也比较频繁,就会进入到滑动窗口模式,按照快速重传的方式处理.
通过滑动窗口的方式传输数据,效率是会提升的.
窗口越大,传输效率就越大.(一份时间,等待的ack 更多了,总的等待时间更少了)
但是滑动窗口设置的越大越好吗
如果传输的速度太快,就可能会使接收方,处理不过来了.此时,接收方也会出现丢包.发送方还得重传
TCP前提是可靠性.可靠性的基础上,再提高传输效率.
5.流量控制
站在接收方的角度,反向制约发送方的传输速率
发送方发送的速率,不应该超过接收方的处理能力.
如图
数据到达B的系统内核中.
tcp socket对象上带有接收缓冲区.
A->B发的数据,就会先到达B的接收缓冲区.
B这边还有应用程序,就会调用read这样的方法,把数据从接收缓冲区中读出来,进一步的进行处理.
(—旦数据被read了,就可以从接收缓冲区删除了)
类似于生产者消费者模型
接收方每次收到数据之后,都会把接收缓冲区剩余空间大小通过ack返回给发送方.发送方就会按照这个数值来调整下一轮的发送速度.
接收缓冲区的剩余空间大小 根据16位窗口大小来进行划分的 并且TCP报头中,选项部分里有一项是叫做"窗口扩展因子" ,通过扩展因子,就可以让窗口大小表示一个更大的值.
如图
接收缓冲区,总的空间是4000收到1000数据之后,还剩3000.
于是就把3000放到应答报文中,告诉发送方了.
依据收到的3000确定了这一轮发送的窗口大小
假设在这个过程中,接收方的应用程序还没来得及处理任何数据呢, 此时收到一个数据,接收缓冲区的剩余空间就缩小—分
反馈0,意味着告诉A我接收方这边已经满了.你暂时先别发数据了.
此时,A确实就要暂停发送
那么暂停多长时间???
此时,虽然不传输业务数据了
仍然会周期性的发送一个"窗口探测包”,并不携带具体的业务数据.
探测包就只是为了触发ack,为了查询当前接收方这边的接收缓冲区剩余空间.
6.拥塞控制
流量控制,是考虑的接收方的处理能力.不仅仅是接收方,还有你整个通信的路径.
通信过程中的任何一个节点处理能力达到上限,都可能会对发送方产生影响,影响到可靠传输。
拥塞控制就是考虑/衡量通信过程中的中间节点的情况
由于中间节点,结构更复杂,难以直接的进行量化.因此就可以使用“实验"的方式,来找到个合适的值.
让A先按照比较低的速度先发送数据(小的窗口)如果数据传输过程非常顺利,没有丢包,
再尝试使用更大的窗口,更高的速度进行发送.(一点一点变化)
随着窗口大小不停的增大,达到一定程度,可能中间节点就会出现问题了.此时这个节点就可能会出现丢包.发送方发现丢包了,就把窗口大小调整小。此时如果发现还是继续丢包,继续缩小.如果不丢包了,就继续尝试变大
再这个过程中,发送方不停的调整窗口大小,逐渐达成"动态平衡"
如图
拥塞窗口:拥塞控制下,发送方应该按照多快的速度(多大的窗口大小)来进行传输
传输轮次:TCP第几次发送数据
指数规律增长:翻倍每次
网络拥塞:丢包,一但触发丢包,就把窗口大小在缩小。重新开始前面的慢开始,指数增长,线性增长(为了避免指数增长一下达到丢包的极限)
注:按照指数增长的过程中,如果达到阈值,就要从指数增长,变为线性增长
流量控制和拥塞控制都是在限制发送方的发送窗口的大小
最终时机发送的的窗口大小,是取流量控制和拥塞控制的窗口的较小值
7.延时应答
A把数据传给B,B就会立即返回ack给A(正常)
也有的时候,A传输给B,此时B等一会再返回ack给A(延时应答)
其本质上也是为了提升传输效率
发送方的窗口大小,就是传输效率的关键
流量控制,就是根据接收缓冲区的剩余空间,来决定发送速率。如果可以使流量控制得到的这个窗口更大一些,发送速率就会更快一些(大也要在接收方能处理的极限之内)
而延时应答就是给接收方更多的时间,来读取接收缓冲区的数据,此时接收方读取了比之前更多的数据之后,缓冲区的剩余空间变大,返回的窗口值也会变大。
如下假设
初始情况下,接收缓冲区剩余空间是10kb,如果立即返回ack,返回了10kb这么大的窗口.如果延时个200ms再返回,这200ms的过程中,接收方的应用程序的,又读了2kb,
此时,返回的ack,就可以返回12kb的窗口了.
8.捎带应答
在延时应答的基础之上,进一步的来提高效率。
ack是内核立即返回的
response则是代码返回的
两者返回的时机不一样
由于tcp 引入了延时应答.上面的ack,不一定是立即返回,可能要等一会.在等一会的过程中,B就正好把response给计算好了.计算好了之后就会把response返回,于此同时顺便就把刚才要返回的ack 也带上了.
这样就将两个数据合成称为一个数据
本来要传输两个数据包,通过上述操作,将两个包合并成一个,得到更高的效率。
9.面向字节流
还有一个关键的问题,粘包问题(不是 tcp独有的,而是面向字节流的机制都有类似的情况)
目前,接收缓冲区中,这三个应用层数据包的数据,就是以字节的形式紧紧挨在一起的.
接收方的应用程序,读取数据的时候,
可以一次读一个字节,也可以读两个字节,也可以读N个字节......
但是最终的目标都是为了得到一个完整的应用层数据包.
B应用程序,就不知道,缓冲区里的数据,从哪里到哪里是一个完整的应用数据包了.
如何解决粘包问题?
核心思路:通过定义好应用层协议。明确应用层数据包之间的边界.
1.引入分隔符.
可以在输入的数据的时候加上特殊符号来进行分隔,比如回车当读入到回车的时候证明这个数据是一个完整的单独的数据。
2.引入长度.|
在输入数据时,加上各自字节的长度,当读到长度时截止。
10.异常情况的处理
在使用TCP过程中如果出现意外怎样进行处理?
1.进程崩溃
进程没了,异常终止了.文件描述符表,也就释放了.相当于调用socket.close()
此时就会触发FIN,对方收到之后,自然就会返回FIN和ACK,这边再进行ACK(正常的四次挥手断开连接的流程)TCP的连接,可以独立于进程存在.(进程没了,TCP连接不一定没)
2主机正常关机
在进行关机的时候,就是会先触发强制终止进程操作.(相当于1)
此时就会触发FIN,对方收到之后,自然就会返回FIN和ACK.
不仅仅是进程没了,整个系统也可能关闭了.如果在系统关闭之前,对端返回的ACK和FIN到了,此时系统还是可以返回ACK,进行正常的四次挥手的.如果系统已经关闭了,ACK和FIN迟到了,无法进行后续ACK的响应.站在对端的角度,对端以为是自己的FIN丢包了,重传 FIN.重传几次都没有响应,自然就会放弃连接.(把持有的对端的信息就删了)
3.主机非正常关机(停电等)
假设A和B A停电了
此时,是一瞬间的事情,来不及杀进程,也来不及发送FIN,主机直接就停机了.
站在B的角度,B不一定知道这个事情咋搞~~
如果B是在发送数据(接收方掉电)发送的数据就会一直等待 ack .触发超时重传﹒触发TCP连接重置功能.发起“复位报文段'
如果复位报文段发过去之后,也没有效果,此时就会释放连接了.
如果B是在接收数据(发送方掉电),B还在等待数据到达.等了半天没消息,此时其实无法区分,是B没法消息,这
还是A挂了
TC中提供了心跳包机制.(形象的比喻)
接收方也会周期性的给发送方发起一个特殊的,不携带业务数据的数据包.并且期望对方返回一个应答.如果对方没有应答.并目重复了多次之后.仍然没有,就视为对方挂了.
4.网线断开
当前假设,是A正在给B发送数据,一旦网线断开,
A就相当于就会触发超时重传->连接重置->单方面释放连接.B就会触发心跳包->发现对端没响应->单方面释放连接.