前言
之前我们简单了解了一下Tcp是什么及它的套接字如何使用:基于UDP和TCP套接字实现简单的回显客户端服务器程序_Crystal_bit的博客-CSDN博客
因为要给大家介绍Tcp的十大特性,所以这里给出Tcp报头结构:
目录
1. 确认应答
2. 超时重传
3. 连接管理
3.1 建立连接
3.2 断开连接
4. 滑动窗口
5. 流量控制
6. 拥塞控制
7. 延时应答
8. 捎带应答
9. 面向字节流
10. 异常情况
1. 确认应答
Tcp协议有个特点就是可靠传输,而这里我们介绍的确认应答,就是实现可靠传输的重要机制.为了让大家更好理解确认应答这个特性,请看如下示例:
这里小丽的回复就相当于应答报文,也称ACK(acknowledge)报文.但是网络上信息传递真有这么简单吗?大家请看下个示例:
背景:论小明的求生欲.
正常情况下:
非正常情况:
这种情况下,小丽收到信息和小明发送的信息顺序发生了一定的改变,但是内容就大不一样了,他怕是要完蛋.为了帮小明解决这个问题,我们就可以对他发送的消息进行编号,这里我们把这个编号称为“序号”,同时分配一个“确认序号”。如上述示例问题作出解决:
这样小丽就可以根据确认序号知道,小明回复的消息是针对她发送的哪条消息进行回复。但在TCP协议中,不是针对一条多条消息进行编号,而是针对每个字节进行编号,如图:
确认序号是取发送方发过来的所有数据,最后一个字节的下一个字节。如果它< 1001则表示数据已经收到,并向发送方索要当前1001之后的数据,如图:
这样发送方就可以通过确认序号知道哪些数据已被接收方接收,那为什么会出现这种后发先至的问题呢?因为在网络传输过程中,很难保证数据的到达顺序,tcp有一个接收缓冲区,每个socket都有自己的一份缓冲区,tcp按照序号给收到的消息进行整队(优先级队列),那么就能保证应用数据读到的数据和发送数据顺序一致。
注:可靠传输不代表数据能100%传过去,只是说尽可能地传,如果传不过去,也会让发送方知道自己发送的数据没有发送成功.就比如我们微信发消息有的时候就会出现感叹号,我们就知道我们发送的消息,对方没有收到,但是大部分情况下,我们的消息都是发送成功了的.
2. 超时重传
超时重传用于解决一个经典问题,那就是丢包问题。在网络传输环境中,发送方到接收方会涉及到多个结点的传输。如图:
当然实际情况可能远远不止这些结点。
如果中间任何一个结点出现问题,都可能会导致丢包。为什么中间结点会出现问题呢?那是因为每个设备(结点),都在承担很多转发任务,而它们转发能力有上限。在某一时刻,某个中间设备的流量达到峰值,就可能会引起部分数据丢包。包如果丢了,那么接收方就不会返回ack应答报文。那么发送方等待一端时间后,如果还是没有收到ack,就会视为刚才发送的数据包已经丢失,最后就会重新发一次,这就叫超时重传。如果发生多次丢包,每丢包一次,超时等待的时间会变长,连续多次重传,都没有得到ack,那么tcp就会重置连接,如果重置连接失败,tcp就会关闭连接,放弃网络通信(只能保证尽可能地完成传输,不能保证100%完成传输)。
注:丢包有两种情况,一种是发送方发送的数据包丢了,ack未返回,另一种就是发送方发送的数据包收到了,但是返回的ack丢了。这两种情况都会超时重传。如果是第二种,发送方重传的数据包在接收方的缓冲区会根据收到数据的序号过滤,避免收到重复数据。
3. 连接管理
3.1 建立连接
Tcp中客户端和服务器通过三次握手建立连接关系。握手也就是指通信双方进行网络交互,三次握手也就是三次网络交互,如图:
类似于男女双方相互告白的过程,女方害羞,男方会先问女方你愿意做我的girlfriend吗?(syn)女方说她愿意(ack),但是她不确定男方是不是愿意,所以也会向男方发出疑问(syn),男方也返回一个我愿意(ack),这样男女双方关系就确定好了。一次连接中,通信双方都是唯一的。那我们怎么确定某个报文是syn报文还是ack报文呢?通过前言中给定的TCP报头结构的6个特殊比特位,它们一般默认是0,如果设为1,则表示特定的含义,第二位是ACK,第五位是SYN,只要某个报文中这两位为1,则分别表示该报文为应答报文或同步报文,当然如果它们都为1,则当前这个报文为syn+ack 报文。
大家这时候可能就会有疑问:为什么第2次交互和第3次交互要合并到一起?其实我们可以把它们拆开发送,但是没有必要,分两次发还不如一次就发完,可以提高封装分用的效率。
注:本质上三次握手这个过程就是在投石问路,验证客户端和服务器双方各自发送和接收能力是否正常。
3.2 断开连接
和三次握手类似,断开连接我们通过四次挥手,通信双方各自给对方发送一个fin(结束报文),再各自给对方返回ack。如图:
这时候大家就会有一个问题,这里的第2步和第3步为什么大部分情况下没有合并呢?那是因为三次握手,ack和syn是同一个时机触发的(在内核中完成),而四次挥手,ack和fin则是不同时机触发的。ack是内核完成的,会在收到fin的时候第一时间返回,fin则是通过应用程序代码控制的,只有在调用了socket的close方法后才会触发fin(收到fin时立即调用close方法是可以合并的)。如果tcp客户端没有显式调用close,那等进程结束,会自动调用close方法,也会触发fin。
注:断开连接,客户端和服务器都有可能先发起,而建立连接一定是客户端主动先发起。断开连接的整个过程,内核会一直将tcp连接进行维护,直到四次挥手完成。
4. 滑动窗口
滑动窗口是为了提高传输效率的特性。主要是靠缩短发送方等待时间,让发送方批量去发送数据,一次发多条数据,一次等待多个ack应答报文。我们将批量传输数据的过程称为滑动窗口。如图:
批量不是无限发送,当发送到了一定程度就等待ack返回,而不等待直接发送的数据量是有限的,每回来一个ack就立即发送下一条,相当于总的要批量等待的数据是一致的。我们可以把要批量等待数据的数量称为“窗口大小”,如图:
从上图中可以看出窗口大小没变,但是它在不断往后挪动,如果发送方很快就收到ack,那么这个窗口就在快速往后滑动。
批量发送的丢包情况:
情况一:数据包已经抵达,但是ack丢失。如图:
这种情况下,是没问题的,确认序号的意思就是该序号之前的数据已经收到,在图例中,虽然大部分的ack都丢失了,但是当6001确认序号返回的时候,发送方就会知道6001之前的数据已经收到,那么1001,2001,4001,5001....这些确认序号丢失也没有关系,这时候大家就会说那如果最后一个确认序号丢失了怎么办?当然就靠我们的超时重传了啊~滑动窗口是建立在可靠性的基础上,对传输效率的提高。
情况二:数据包丢失,接收方未收到数据。如图:
当1001-2000段的数据丢失后,接收方会一直索要1001-2000段的数据,不会因为收到2001-3000的数据就直接返回3001。发送方连续收到多个1001确认序号就可以确定1001-2000段的数据丢失,就会重传1001-2000段的数据,此外,由于1-1000,2001-7000的数据已经接收,所以重传后返回的确认序号7001,表示7001之前的数据已经全部接收到。类似于在主机B的接收缓冲区里拼图,每收到一端数据放到对应位置,差哪块就要持续索要哪块。上述重传称为快速重传,它和超时重传是不一样的,它只有丢了数据才会重传,不丢的数据不会重传,整体速度比较快。
一般传输少量数据,就无需使用滑动窗口,直接确认应答+超时重传。
注:在可靠性的基础上,使用一份时间等待多个ack,缩短总等待时间从而提升整体效率。如果主机B的缓冲区满了,Tcp也有机制来控制这种情况尽量避免丢包,类似于阻塞等待的效果。
5. 流量控制
在滑动窗口(批量发送)的条件下,窗口越大,批量发送的数据越多,整体的速度就越快。但是别忘了,Tcp是可靠传输,批量发送太快,接收方缓冲区一下子就满了,那就会导致数据丢包。所以这就需要Tcp另一种特性——流量控制,让接收方去限制发送方的速度(有时候可能会让发送方阻塞一下)。在TCP报头结构中,当第二位ack为1时,窗口大小字段就会生效,这里的值就是建议发送方发送的窗口大小(接收缓冲区剩余的空间大小):
当接收缓冲区没有满的时候,发送方会根据接收缓冲区剩余空间大小进行数据发送,直到缓冲区满了,就停止发送并且会隔一段时间就触发一个窗口探测报文,如果探测一会发现接收缓冲区剩余空间不为0,腾出空间了就会继续发送数据。应用程序向接收方缓冲区读取数据时,接收缓冲区内容就会被消耗,从而空间就可以腾出来了。
注:这里返回的窗口大小与实际可能会有出入,实际上发送方的窗口大小 = 流量控制 + 拥塞控制.
6. 拥塞控制
上述说到的流量控制主要衡量接收方的处理能力,而拥塞控制特性主要衡量中间节点,传输的能力(传输路径的处理能力)。发送方到接收方有多条路径,且每条路径有多少个结点,每个结点当前的情况如何,都是未知的。一条路径上的某个结点处理情况出现问题都会影响整个传输速率。拥塞控制就是不断通过实验的方式,找到一个合适的发送速率,达到发送和消耗两者的动态平衡。
如图:
从图中可以看出,以一个非常小的窗口(比较小的速度)慢开始,每次进行指数增长,让窗口大小短时间内就达到比较大的值,快速接近当前网络传输路径的能力瓶颈,达到阈值,就变成线性增长,逐渐接近传输上限。如果出现丢包,那么当前窗口大小已经超过传输上限。此时又将窗口大小恢复到一个比较小的初始值。重复上诉过程,直到找到一个合适的发送速率。
注:实际发送方的窗口大小 = min(拥塞窗口,流量控制窗口)。不同的系统实现,阈值很可能存在差别。
7. 延时应答
上述中我们可以知道决定传输效率的关键元素就是窗口大小。而延时应答就是通过延时让接收方应用程序尽可能多消耗数据,那么返回的窗口大小就会大一些同时接收方也能处理得过来,发送方的发送速率也能更快一些。举个例子:吃自助的过程中,我一般都是不建议一直不间断地吃的,吃一会儿等肚子消化一会儿,等胃空间空出来了,才能吃得更多。来自吃货的自豪~
注:不是所有的包都可以延迟应答。分两种情况:一种是数量限制,另一种是时间限制。前者是每隔N个包就延迟应答一次,后者是超过最大延迟时间就延迟应答一次。
8. 捎带应答
捎带应答基于延时应答,客户端服务器之间的通信模型有四种:
① 一问一答。大多数情况下都是这种。
② 多问一答。上传大文件。
③ 一问多答。下载大文件。
④ 多问多答。游戏串流。
在四次挥手过程中,ack应答报文由内核负责,收到请求后会被立即返回,而fin结束报文是通过应用程序调用close方法触发。这俩时机是不同的,但是在延时应答的条件下,ack应答报文就可能稍等一会儿再发送,那么就很可能和fin结束报文合并成一个数据报一起发送,这就是捎带应答。这也是为什么四次挥手也有可能是三次的原因。
9. 面向字节流
TCP特性之一面向字节流进行数据传输,那么这里就涉及到一个经典问题——粘包问题。当主机A向主机B发送了多个应用层数据报,那么这些数据报就会在主机B的接收缓冲区紧紧挨在一起,那么B的应用程序读数据就无法将这多个数据报完整地区分出来。对于上述这种问题,我们可以定义分隔符(比如每条信息都以;或。结尾) 或者 约定整个数据报长度(由产品经理约定的需求决定)。
10. 异常情况
① 进程关闭/崩溃:进程没了,socket是文件,随之被关闭。虽然进程没了,但是连接还在,仍然可以四次挥手。
② 主机关机(正常流程关闭):会杀死所有用户进程,触发四次挥手,如果没挥完,ack应答报文没返回给发送方,那么发送方就会重传,如果几次都没有收到ack,就尝试重置连接,重置连接失败则释放连接。
③ 主机掉电(非正常关闭):对端如果是发送方,那么就跟②步骤一样,但是如果对端是接收方,我们还没发数据就over了,这时候就涉及到另一个机制——心跳包保活机制(周期性+心跳没了就挂了)。对方给我们发的心跳包(ping),我们关机了则无法返回pong,对端就会知道我们这边已经挂了。就类似于人的心跳,心在跳动,人就还活着,不跳了人也就没了。但一般心跳的确定不会很严格,会设置连续多少次ping了却没有pong才视为连接异常。
④ 网线断开:和③一致
除上述十大核心机制外,TCP还有别的很多机制,大家有兴趣可以去TCP REC标准文档中了解。