传输层中有两个重要的协议:TCP协议和UDP协议。本博文分享的是TCP协议,不仅分享其协议格式,特点等等,还有应答机制、超时传送机制、连接管理机制、滑动窗口、阻塞控制等等。
TCP协议
TCP全称为 "传输控制协议(Transmission Control Protocol")。人如其名, 要对数据的传输进行一个详细的控制。
tcp的特点
可靠性,面向连接等等。
TCP协议格式
在TCP协议格式中,其数据报包含了:
①16位源端口号、16位目的端口号
②32位序号,32位确认序号
③四位TCP报头长度
④六位标志位
⑤16位窗口大小
⑥16位校验和
⑦16位紧急指针
⑧40字节的头部选项
⑨数据
16位源端口号、16位目的端口号
表示数据是从哪个进程来, 到哪个进程去。
32位序号,32位确认序号/确认应答机制
TCP具有可靠性的特点,这个可靠性该如何去理解?对于TCP的可靠性,其核心的机制是基于序号的确认应答机制!接下来我们好好分析一下确认应答机制是怎么样的。
当客户端向服务器发送消息后,服务端需要返回消息表示已经收到,当客户端收到来自服务器的消息(表示服务端已经收到了客户端的消息了),那么客户端就再发一条应答信息回去......这样一来一回的向对方发送一条确认收到信息的行为,便是确认应答机制。
通过应答,发送方就可以保证自己的信息被对方百分之百的收到了。
但是这样的行为,我们都可以理解想明白,tcp的这种可靠性,并不是百分之百的可靠,但是有一点我们必须清楚的认识到:只要一条消息有应答,我们就能确认该消息被对方百分之百收到了!
在一般情况下,客户端会向服务端发送多条数据(报文),而服务端收到的数据,其顺序不一定是按照客户端发来的顺序是一致的,为了能够让客户端发送的信息和服务端接收到信息后能够返回对应的应答,就需要依靠报头中的32位序号,32位确认序号!
序号是表示这个是第几个,按顺序嘛。而确认序号是用于接收方给发送方一个应答,无论接收来的顺序是否跟发送的顺序一致不一致,有了确认序号就可以保证匹配得上相应的数据应答。
确认序号是使用方法是对历史确认报文的序号+1。比如说:客户端向服务端发送了序号为10,11和12的数据报,服务端接收后,就给客户端发送确认序号为13的数据报(不管有没有带有效载荷),表示13号之前的报文我服务端全部收到啦!
在tcp的报头中为什么要把序号和确认序号分开呢?因为无论是数据还是应答,本质都是TCP的完整报文,这个报文可以不带数据,但是必须具有一个完整的tcp报头。而tcp是一个全双工通信协议!客户端可以向服务端发送数据,带上序号的,同样的是,服务端在应答客户端的某条数据时,也能带上自己的序号,带上自己的数据发送给客户端!
那么序号从哪里来的呢?TCP将每个字节的数据都进行了编号,即为序列号。其实对于发送缓冲区和接收缓冲区来说,缓冲区是一个数组,因为TCP是面向字节流的,所有数据会拷贝到这个数组中,因此其下标便是序列号。
四位TCP报头长度
4位tcp的报头长度,即4位首部长度,转化为二进制即【0000,1111】-->【0,15】的范围。而首部的单位是4字节,tcp标准长度是20字节,因此,长度最大x的时候是x*4 = 20,即x = 5。因此,tcp报头长度为5,即0101。
16位窗口大小
tcp协议是自带发送缓冲区和接收缓冲区的,因为读写的接口比如write/read,recv/send我们可以理解为拷贝函数,比如在应用层中我们进行send的时候,并没有把数据直接发送到网络中,而是把数据拷贝到了tcp的发送缓冲区中了。
这样的好处:
①提高应用层的效率。
②做到应用层和TCP的解耦。只有tcp协议才可以知道网路,对方的状态明细,因此也只有tcp协议能处理数据如何发,怎么发,发多少,出错了怎么办等等的问题,真正意义上做到传输控制!而因为缓冲区的存在,应用层只需把数据交给缓冲区即可,其它的事情不需要去管!就好像我们寄快递,只需将快递写好单子交给快递公司即可,快递公司怎么寄过去不需要我们来管了。
缓冲区是有大小的,等满了就会刷新,那么在还没有满之前,发送方需要得知接收方的接收缓冲区目前的剩余空间大小,来决定如何去发送数据,因此在tcp报头中,就可以使用16为窗口大小来填入接收方目前的接收缓冲区剩余的空间大小,然后作为应答返回给发送方!
六个标志位
tcp是面向连接的,那么在通信之前就需要connect连接,而连接的操作,便是三次握手,关闭连接的操作便是四次挥手。而且,对于服务器来说,服务端有可能在任意时刻,都有成千上万的报文向其发送,因此服务端面临的是需要知道如何将这些成千上万的报文进行区分。
对于三次握手四次挥手,区分大量的报文类型,所用到的便是数据报中的标志位!所用到的标志位,就将其置为1,用不到置为0
六个标志位:URG、ACK、PSH、RST、SYN、FIN
①ACK:ACK即是应答,几乎在所有的TCP通信过程中,ACK标志位都会被设置起来。
②SYN:请求建立连接; 我们把携带SYN标识的称为同步报文段。
请求建立连接的动作:三次握手。第一次握手:客户端向服务器发起请求SYN;第二次握手:服务器应答客户端,并且也向客户端请求建立连接;第三次握手:客户端应答服务端。
③FIN:通知对方, 本端要关闭了, 我们称携带FIN标识的为结束报文段。
关闭连接的动作:四次挥手。第一次挥手:客户端通知服务端进行连接关闭FIN;第二次挥手:服务端应答(ACK)客户端;第三次挥手:服务端反过来通知客户端进行连接关闭FIN;第四次挥手:客户端应答(ACK)服务端。
④RST: 对方要求重新建立连接; 我们把携带RST标识的称为复位报文段。
在请求建立连接的时候,三次握手不一定会成功。在第一次握手时,服务端会应答,第二次握手时,客户端会应答,第三次握手时不会有应答,因此第三次握手出现错误时,会导致异常连接。只要出现异常连接,那么可以使用RST来重新建立连接。
建立连接时,服务器一般存在大量的连接,因此服务器需要管理这些连接。连接的本质是在三次握手成功之后,在通信双方的操作系统内都创建了对应的用于维护这些连接的数据结构!这说明了双方维护连接都是需要成本的(空间+时间)。
⑤PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走。
⑥URG和16位紧急指针: 紧急指针是否有效。因为TCP是有序到达的,每一个报文,什么时候被上层读取是基本上确定的了,但是如果想要让一个数据尽快被上层读取到,那么可以设置URG,表示该报文携带了紧急数据,需要被优先处理。设置了URG后,16位紧急指针就会指向报文数据中一个字节的数据,因此紧急指针只能传输1个字节。
为什么是三次握手?
三次握手为什么是三次握手,而不是两次四次五次六次等等?
三次握手是为了确保双方建立可靠的连接,说白了就是双方都需要确认对方好着没,对方的主机是否健康,而三次握手是最小化的可靠连接建立过程。如果只进行两次握手,那么会存在一种情况,即客户端发送的SYN包到达服务器后,但是由于某种原因(比如网络延迟等等),服务器没有及时应答(SYN+ACK)客户端,客户端就会认为连接建立失败,会重新发送SYN包,此时服务器就会收到两个SYN包,会误认为客户端想要建立两个连接,并且第二次握手到达客户端后,客户端没有应答给服务端,服务端并不知道自己的这个应答对方有没有收到,因此服务端并不确定自己是否拥有发送数据的能力!如果建立四次或以上的握手,那么会增加连接建立的时间和网络负载,第四次握手的没有必要的。三次握手是最小化的可靠连接建立过程,可以保证连接的可靠性和效率。
因此,三次握手可以:1.确认双方主机是否健康。2.验证双全工(双方都具有发送和接收的能力)
为什么是四次挥手
跟三次挥手一样,四次挥手是协商断开连接的最小次数。
TIME_WAIT和CLOSE_WAIT
一般来说,主动断开连接的那一方,要进入一个TIME_WAIT状态,此时对于主动断开连接的这一方来说,四次挥手已经完成了,但是连接并没有被释放,端口号依然被占用中,这说明在一段时间内这个端口号无法被重启!
理解TIME_WAIT状态
TCP协议规定,主动关闭连接的一方要处于TIME_ WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态。
TIME_WAIT为什么是两个MSL的等待时间?
①尽量历史发送的网络数据在网络中消散。MSL是TCP报文的最大生存时间, 因此TIME_WAIT持续存在2MSL的话就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(否则服务器立刻重启, 可能会收到来自上一个进程的迟到的数据, 但是这种数据很可能是错误的)。
②尽量保证最后一个ACK被对方收到。同时也是在理论上保证最后一个报文可靠到达(假设最后一个ACK丢失, 那么服务器会再重发一个FIN. 这时虽然客户端的进程不在了, 但是TCP连接还在, 仍然可以重发LAST_ACK)。
解决TIME_WAIT状态引起的bind失败的方法(作业)
如果一个端口号在四次挥手后,短时间内无法重启,会造成一些经济上的损失,比如如果某宝在双十一中,有上千万的用户同时连接了某宝的服务器,如果,我是说如果,如果某宝的服务器此时崩了,服务端与客户端的连接断开,但是由于服务端的状态变成了TIME_WIAT,无法立刻马上重启,需要等到2*MSL的时间,要知道,在双十一,一秒钟都是上千万的交易!由此可知,我们需要解决无法立刻重启的问题。
使用setsockopt()设置socket描述符的 选项SO_REUSEADDR为1, 表示允许创建端口号相同但IP地址不同的多个socket描述符。
理解 CLOSE_WAIT 状态
如果在关闭连接的时候,比如客户端发起了关闭连接请求,服务端应答后,但是服务端并没有调用close去关闭连接,此时服务器就会进入CLOSE_WIAT状态,该状态表明四次挥手没有完成。
总结三次握手四次挥手
滑动窗口
如果在发送报文的时候,一条一条的发,每发一条,接收方就应答一次,这样相当于串行的方式,效率比较低,解决方法很容易想到,就是一次性发送很多条(实是将多个段的等待时间重叠在一起了),这就用到了滑动窗口!
窗口大小指的是无需等待确认应答而可以继续发送数据的最大值。上图的窗口大小就是4000个字节(四个段)
一次发送多少?
一次性给接收方发送多条数据,就会自然而然地会有一个问题:一次发送多少才合适?
一句话:看对方的接收缓冲区。对方的接收缓冲区还能接收多少数据,那么你就可以一次性发多少数据。
在发送或接收数据的时候,发送数据本质上是将需要发送的数据先保存在自己的TCP发送缓冲区中,接收数据本质上是将接收到的数据保存在TCP的接收缓冲区中。
在发送缓冲区中,发送缓冲区可以分成三个区域:
第一个区域,存放了已经发送和已经确认应答的数据。
第二个区域,存放了已经或者可以发送的,但是还没有确认应答的数据。
第三个区域,存放的是还没被发送,或者还不可以发送的数据。
其中,第二个区域,就称为滑动窗口!因此,滑动窗口是一个名词,是发送缓冲区中的一个区域,本质就是发送缓冲区。
在上文中,我们讲述过16位窗口大小表示的是接收缓冲区中剩余空间的大小。
总之,滑动窗口,与对方的接受能力有关!
滑动窗口怎么滑?
因为TCP的缓冲区是一个数组,那么该区域(滑动窗口)就由两个指针去指着,假设左边指针叫win_start,右边指针叫win_end。
每当收到一个ACK时,需要根据对方接收缓冲区的接收能力,即对方报文中的16位窗口大小来判断该怎么滑动。
①左边指针wen_start往右移动,右边指针win_end不动。出现这种情况,一般就是当发送方发送数据A之后,接收方接收并返回ACK时,16位窗口大小显示对方剩余的空间比发送数据A时的小。比如发送数据A前,对方接收缓冲区还剩4KB,发送后,还剩3KB。那么此时,win_end不需要动了,甚至于,发送数据A后,还剩的空间为0。这样说明了滑动窗口的大小是可以改变的。
②左右指针都移动了。比如说,发送数据A时,对方接收后,缓冲区满了,然后上层应用刷一下,把缓冲区的数据全刷走了,此时接收缓冲区空了!此时,发送缓冲区中,左右指针都往右移动了!
滑动窗口发送数据发生丢包了怎么办?
如果出现了丢包, 如何进行重传? 这里分两种情况讨论。
情况一:数据包已经抵达, ACK被丢了。
这种情况下, 部分ACK丢了并不要紧, 因为可以通过后续的ACK进行确认。因为即使1001,3001和4001的AKC丢了,后续因为5001和6001都确认应答了,而6001ACK的意思是6001序号之前所有数据,我(接收方)都已经成功接收了!
情况二: 数据包就直接丢了。
如果有一个数据包丢了,比如上图中1001-2000的数据丢了,那么在后续的发送数据中,会进行重传,并且在后续的ACK中,确认序号都为1001。即使2001到7000的数据都成功发送,确认序号始终是1001,直到重传后1001-2000成功发送,这个时候接收端收到了 1001 之后, 再次返回的ACK就是7001了(因为2001 - 7000)接收端其实之前就已经收到了, 被放到了接收端操作系统内核的接收缓冲区中,这种机制被称为 "高速重发控制"(也叫 "快重传")。
快重传和超时重传的区别
流量控制
接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送,就会造成丢包, 继而引起丢包重传等等一系列连锁反应。因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制(Flow Control)。
那么在第一次发送数据的时候,怎么知道接收端的接收缓冲区的接受能力呢?其实在三次握手的时候,就已经协商好了窗口大小了,因此在第一次发送数据的时候,就可以根据这个窗口大小来设置自己的滑动窗口的初始值。
拥塞控制
虽然TCP有了滑动窗口这个大杀器, 能够高效可靠的发送大量的数据. 但是如果在刚开始阶段就发送大量的数据, 仍然可能引发问题:因为网络上有很多的计算机, 可能当前的网络状态就已经比较拥堵,在不清楚当前网络状态下, 贸然发送大量的数据,是很有可能引起雪上加霜的,比如一个场景:
场景:一个服务器,跟许许多多个客户端连接起来,那么假设有一个客户端发送了1千条数据,但是有999条数据发送失败,丢了!那么,这样很大可能,连其他的客户端也是如此!在这种情况下,如果这些客户端进行快重传或者超时重传,一下子有许许多多个客户端同时发送了999条数据,服务器就会崩溃掉!因此,拥塞控制可以出场了!
TCP引入 慢启动 机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据。
上文中,我们一直讨论的,是两台主机之间的通信问题,但是此时我们需要加上网络,即两台主机之间的网络。发送方发送数据到接收方中,先会通过网络,再到达接收方,那么在网络中,有一个叫做拥塞窗口的东西,每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际发送的窗口。
因此,对于发送方的滑动窗口来说,上文我们说了它是跟接收方的接收缓冲区的接收能力有关,现在,它也跟拥塞窗口有关。
滑动窗口 = min(拥塞窗口大小,16位窗口大小)。
慢启动
在上图中,我们看到,可以发送的数据越来越多,像上面这样的拥塞窗口增长速度, 是指数级别的. "慢启动" 只是指初使时慢, 但是增长速度非常快,但是并不能让它无限增长,因此慢启动有一个阈值,当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长。
当TCP开始启动的时候, 慢启动阈值等于窗口最大值,在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回1。
因此,拥塞控制可以控制每次的重传,让其不能马上就重传太多的数据,导致服务器崩溃。
总结一下:
少量的丢包, 仅仅是触发超时重传,大量的丢包, 就认为网络拥塞,当TCP通信开始后, 网络吞吐量会逐渐上升;,随着网络发生拥堵, 吞吐量会立刻下降,拥塞控制, 归根结底是TCP协议想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案。
延迟应答
延迟应答可以提高效率,如果接收数据的主机立刻返回ACK应答, 这时候返回的窗口可能比较小。假设接收端缓冲区为1M,一次收到了500K的数据, 如果立刻应答,,返回的窗口就是500K,但实际上可能处理端处理的速度很快, 10ms之内就把500K数据从缓冲区消费掉了,在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来,如果接收端稍微等一会再应答, 比如等待200ms再应答, 那么这个时候返回的窗口大小就是1M。这就是延迟应答!
窗口越大, 网络吞吐量就越大, 传输效率就越高. 我们的目标是在保证网络不拥塞的情况下尽量提高传输效率。
一般而言,不会什么情况都会采用延迟应答,一般是以下两种情况才会采用延迟应答:
数量限制: 每隔N个包就应答一次。
时间限制: 超过最大延迟时间就应答一次。具体的数量和超时时间, 依操作系统不同也有差异; 一般N取2, 超时时间取200ms。
捎带应答
在延迟应答的基础上, 我们发现, 很多情况下, 客户端服务器在应用层也是 "一发一收" 的。意味着客户端给服务器说了 "How are you", 服务器也会给客户端回一个 "Fine, thank you"。那么这个时候ACK就可以搭顺风车, 和服务器回应的 "Fine, thank you" 一起回给客户端。
面向字节流
创建一个TCP的socket, 同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区。
粘包问题
首先要明确, 粘包问题中的 "包" , 是指的应用层的数据包,在TCP的协议头中, 没有如同UDP一样的 "报文长度" 这样的字段, 但是有一个序号这样的字段,站在传输层的角度, TCP是一个一个报文过来的,按照序号排好序放在缓冲区中。站在应用层的角度, 看到的只是一串连续的字节数据,那么应用程序看到了这么一连串的字节数据, 就不知道从哪个部分开始到哪个部分, 是一个完整的应用层
数据包。
解决粘包问题,本质就是明确两个包之间的边界。
对于定长的包, 保证每次都按固定大小读取即可,例如上面的Request结构, 是固定大小的, 那么就从缓冲区从头开始按sizeof(Request)依次读取即可。对于变长的包, 可以在包头的位置, 约定一个包总长度的字段, 从而就知道了包的结束位置。对于变长的包, 还可以在包和包之间使用明确的分隔符(应用层协议, 是程序猿自己来定的, 只要保证分隔符不和正文冲突即可)。
对于UDP协议来说,如果还没有上层交付数据, UDP的报文长度仍然在. 同时, UDP是一个一个把数据交付给应用层. 就有很明确的数据边界,站在应用层的站在应用层的角度, 使用UDP的时候, 要么收到完整的UDP报文, 要么不收. 不会出现"半个"的情况。