传输层概述
通过网络层、数据链路层和物理层可以建立起主机和主机之间的连接,但是实际上网络上的通讯都是主机应用程序和应用程序之间的通讯,IP数据报达到目的地之后要分发给不同的应用程序,这就是传输层的工作。
Socket
Socket套接字位于应用层和传输层之间,作为应用层和传输层通讯的通道。
多路复用和多路分解
多路复用:应用层产生的应用报文通过传输层的TCP/UDP协议会被封装成为应用数据报(UDP)、报文段(TCP),之后通过网络层的IP协议进一步封装成为IP数据报,IP数据报头部的协议字段会标记这个数据报是TCP还是UDP,取值为6表示是TCP报文段,取值为17表示的是UDP应用数据报。从应用报文封装到应用数据报/报文段这个过程称为UDP复用/TCP复用,之后把应用数据报/报文段封装成为IP数据报称为IP复用。
多路分解:IP数据报到达目的主机之后,首先通过头部的协议字段分发给UDP或者TCP,之后根据标识元组(UDP为源端口号,目的端口号二元组;TCP为源端口号,源IP地址,目的端口号,目的IP地址四元组)进行分发该具体的Socket。
TCP和UDP对比
TCP和UDP主要有以下几点的区别,我们分别来看:
无连接与面向连接
无连接:UDP是无连接的,也就是使用UDP作为通讯协议的双方可以随时发送数据。
面向连接:TCP是面向连接的协议,使用TCP作为通讯协议的双方,在数据发送之前要通过三次握手建立连接,之后才可以发送数据,发送完毕之后要使用四次挥手来断开连接。
单播、广播、多播
UDP支持单播、广播、多播:使用UDP协议进行信息的传输之前不需要建议连接。换句话说就是客户端向服务器发送信息,客户端只需要给出服务器的ip地址和端口号,然后将信息封装到一个待发送的报文中并且发送出去。至于服务器端是否存在,或者能否收到该报文,客户端根本不用管。
TCP仅支持单播:使用TCP作为通讯协议的双方,要建立连接之后才可以正常通讯,所以TCP只支持单播(一对一)。
封装方式
UDP:UDP不会对应用层传输的报文进行分割等操作,而是直接把应用层交付的报文直接打包封装成UDP应用数据报。
TCP:TCP发送端存在发送缓存,发送缓存存储应用层要传输数据的字节,接收端有接收缓存。TCP把多个字节封装成为TCP报文段之后发送,所以TCP是面向字节流的。
可靠性
UDP是不可靠的:我们说IP协议向上提供的是无连接不可靠的传输服务(尽力交付),UDP并没有增加其他的功能,仅仅只是增加了基于端口的应用程序定位功能,如果接受端收到的应用数据报出现误码(通过检验和判断是否误码),接受端会直接丢弃,同时对于传输过程出现的丢包也不会做处理。
TCP是可靠的:相对于UDP,TCP提供了很多功能来保证数据的可靠传输,如流量控制、拥塞控制等。
头部大小
UDP头部小:因为UDP只有定位应用程序的功能,所以UDP的头部非常简单。(检验和是用来判断数据报是否有误码的)
TCP头部大:TCP功能复杂,需要比较大的头部记录信息来支持这些功能。
TCP流量控制
一般来说,我们总是希望传输的速度越快越好,可以大量的、快速的传输数据。但是实际上网络资源是有限的(带宽、缓存等),接收端可以接收的数据量和发送端发送的数据量也不相同。通过滑动,我们可以很简单的做到流量控制。
各种窗口
接收窗口:TCP接收端的窗口,表示自己可以接收的数据是多大的,接收端会根据自己的资源和缓存情况动态的调整接收窗口。
拥塞窗口:通过拥塞控制确定的窗口。
发送窗口:接收窗口和拥塞窗口都是为了用来确定发送端可以发送的数据量的,也就是发送窗口,它会取接收窗口和拥塞窗口的较小值作为自己的值。
举例来说流量控制
主机A为发送端,主机B为接收端,两个主机通过三次握手建立TCP连接,主机B在握手成功时会把自己的接收窗口大小发送给主机A,也就是说流量控制,是接收端对发送端的控制。
主机A拿到接收窗口大小之后,会和拥塞窗口进行比较,这里假设一直是接收窗口更小,也就是接收窗口等于发送窗口。
我们现在假设我们有900字节的数据,每一个TCP报文段大小是100字节,TCP连接建立完毕之后,确定的发送端口的大小为400字节,也就是4个TCP报文段。
刚开始发送窗口如下:
- 主机A发送1-100字节的数据,此时还可以发送300字节的数据。(这里的seq是这个TCP报文段最小的起始字节,DATA表示这是一个TCP报文段)
- 主机A发送101-200字节的数据,此时还可以发送200字节的数据。
- 主机A发送201-300字节的数据,但是丢失了,此时还可以发送100字节的数据。
- 主机B对主机A发送的数据进行累积确认,其中ACK=1表示这是一个TCP的确认报文,ack是确认报文号字段(201表示201字节之前的数据已经全部准确接受,主机A通过这个值来移动字节的发送窗口,也就是1-200字节已经全部确定接收,那么发送缓存就可以删除这些数据了,发送窗口也可以移动),rwnb是动态调整之后的接收窗口的值,收到这个rwnb之后主机A也要调整自己的发送窗口。
- 主机A发送301-400字节的数据,此时还可以发送100字节的数据。
- 主机A发送401-500字节的数据,此时还可以发送0字节的数据,此时不能再发送数据了。
由于主机A不再发送数据了,过了旧数据的超时重传的时间之后,确认201 - 300的TCP报文段丢失,重传。 - 主机A发送201-300字节的数据,此时还可以发送0字节的数据。
- 主机B对主机A发送的数据进行累积确认,ACK = 1,ack = 501,rwnb = 100,收到确认之后,主机A调整自己的发送窗口
- 主机A发送501-600字节的数据,此时还可以发送0字节的数据。
- 主机B对主机A发送的数据进行累积确认,ACK = 1,ack = 601,rwnb = 0,主机A收到确认,调整自己的发送窗口,但是发现已经是0了。
超时重传
TCP发送端在数据发送之后会开启计时器,如果在规定时间内没有收到接收端关于这个TCP报文段的确认报文,就会进行重发。如上面的第3步。
发送窗口滑动与大小
发送窗口滑动:根据接收端的确认报文中的ack信息,ack中显示的字节数是接收端后面想要的起始字节数(比如201,表示之前1-200字节的数据已经确认收到了,但是还没有收到201字节的数据),发送端根据ack的值,就可以把发送缓存已经确认收到的数据删除,并且移动窗口。
窗口大小:每次接受的确认报文都会携带rwnb表示接受端口的大小,发送端口要根据这个值和拥塞窗口来确定自己的发送窗口大小。
rwnb为零之后
如果确认报文中rwnb为零,则发送端窗口也会调整为零,此时不再发送数据,发送端会启动计时器,在计算器到时的时候会发送一个零窗口探测报文
,接收端收到这个报文之后,返回自己的接收窗口大小。
死锁问题:我们说发送端会发送一个零窗口探测报文,接收端返回自己的接收窗口大小,如果接收端的返回报文丢失了,那发送端就会一直等待非0通知。
为了解决这种问题,每一次发送端发送零窗口探测报文之后,就会重新计时,时间到了又会重新发送零窗口探测报文,计时器知道收到非零的确认报文后停止。
零探测报文只有一个字节
TCP可靠传输的实现
TCP基于以字节为单位的滑动窗口来实现可靠性传输。
累计确认和捎带确认
- 累计确认:接收方在收到TCP报文段之后可以推迟发送确认,这样做的好处是后面如果收到连续的TCP报文段可以只发送一次确认,比如收到31报文段,不立刻确认,收到32,33,34之后,发送确认报文35。不过TCP要求推迟时间不能超过0.5秒,这是为了减少超时重传和充分利用快重传机制。
- 捎带确认:TCP的捎带确认是建立TCP是全双工通信的基础之上的,TCP的接收方同时也可以是发送端,而它的确认报文也可以携带在自己要发送的数据中发送。
拥塞控制
我们说过发送端的发送窗口由拥塞窗口和接收窗口中的较小值决定,其中拥塞窗口就是通过TCP的拥塞控制决定的。理想情况下,发送窗口和接收窗口只要保持一样就可以了,但是在实际的网络传输中,如果对某一资源的需求超过了该资源所能提供的可用部分,网络性能就要变坏,这种情况就叫做拥塞,在计算机网络中的链路容量(即带宽)、交换结点中的缓存和处理机等,都是网络的资源。通过拥塞控制可以动态的控制拥塞窗口的大小。
拥塞窗口变化以及拥塞判断
拥塞窗口cwnd的维护原则:只要网络没有出现拥塞,拥塞窗口就再增大一些;只要网络出现拥塞,拥塞窗口就减少一些。
判断是否网络出现拥塞:在超时重传的限定时间内没有按时收到应当达到的确认报文。
慢开始门限值
发送端要维护一个慢开始门限值ssthresh,这个值是慢开始和拥塞避免算法切换的限定点:
- 当cwnd < ssthresh时,使用慢开始算法;
- 当cwnd > ssthresh时,停止使用慢开始算法而改用拥塞避免算法;
- 当cwnd = ssthresh时,既可使用慢开始算法,也可使用拥塞避免算法。
拥塞控制的四种算法
慢开始
刚开始拥塞窗口的大小为1,我们假定慢开始的门限值为16,在拥塞窗口达到门限值之前,拥塞窗口大小会变成上一次传输轮次的两倍,所以在刚开始的四轮传输轮次,拥塞窗口的变化为 1 -> 2 -> 4 -> 8 -> 16。
拥塞避免
在达到慢开始门限值之后,就开始使用拥塞避免算法,拥塞窗口大小会变成上一次传输轮次的窗口大小 + 1。
在5-12传输轮次,由于拥塞避免算法,拥塞窗口会一点点增大,假如在窗口为24的时候发生了超时,那么就判断网络很可能是出现了拥塞,就要进行一些操作:
- 把慢开始门限值设置为发送拥塞时候拥塞窗口大小的一半,也就是12。
- 把拥塞窗口的大小设置为1,重新开始慢开始算法。
慢开始的慢值得拥塞窗口从1开始,并不是增长慢。拥塞避免的避免也不是说可以避免拥塞。
TCP Tahoe版本和TCP Reno版本
慢开始和拥塞避免是1988年Tahoe版本TCP的拥塞控制算法。这个版本存在着问题:
- 实际的网络传输过程中,经常有个别报文会丢失,但是实际上网络并未发生拥塞。
- 在发生拥塞的时候会把拥塞窗口设置为1,并重新开始慢开始算法,这样会降低传输效率。
因此,在1990年的Reno版本TCP中增加了两个新的拥塞控制算法(改进TCP的性能)。这就是快重传和快恢复。
快重传
快重传的作用就是让发送方尽早知道发生了个别报文段的丢失,而不用等到传出的报文出现超时再重传。为了实现块重传,接收端和发送要满足以下几个特点:
- 要求接收方不要等待自己发送数据时才进行捎带确认,而是要立即发送确认;
- 即使收到了失序的报文段也要立即发出对已收到的报文段的重复确认。比如TCP报文段2丢失了,接收端收到TCP报文段3要发送TCP报文段1的确认,并要求TCP报文段2;接收端收到TCP报文段4也是要发送TCP报文段1的确认,并要求TCP报文段2;
- 发送方一旦收到3个连续的重复确认,就将认为相应的报文段丢失了,需要立即重传,而不是等该报文段的超时重传计时器超时再重传。
如图,M3报文段丢失之后,即使接收端收到了M4、M5、M6报文,发送的确认报文也是对M2的确认,并请求M3。而接受端在收到连续3次重复对M2的确认之后,就可以确定M3已经丢失了,就对M3进行重传,而不会判断是发生了拥塞。接收端在收到M3之后,由于M1-M6都已经收到,就可以发送对M6的确认报文了。
连续3次重复对同一个TCP报文的确认使用的时间没有超过超时重传的时间,才会发生快重传。
快恢复
在发送端收到连续三个确认报文之后会发生快重传,并且要进行快恢复。发生快恢复,会被慢开始门限值设置为发生重传时候拥塞窗口的一半,之后把拥塞窗口设置为慢开始门限值,然后使用拥塞控制算法。
总结
快重传和快恢复是在1990年的Reno版本的TCP中增加了两个新的拥塞控制算法。在这一版本的TCP中,发生非拥塞导致报文丢失时,会使用快重传的算法,不会认为是发生了拥塞,在发生快重传之后会采用快恢复来增加性能。
注意:快重传和快恢复并不是发生拥塞的时候使用的算法,TCP发生拥塞时,仍然会把拥塞窗口设置为1,重新使用慢开始算法。快重传和快恢复是在发生非拥塞导致个别报文丢失时使用的。
TCP三次握手和四次挥手
TCP是面向连接的传输协议,在传输数据时,TCP客户端要先和TCP服务器通过三次握手建立连接,传输完成之后,要通过四次挥手的方式断开连接。
三次握手
TCP的三次握手是指需要通过客户端和服务器之间发送三次报文来建立起连接。
三次握手中客户端和服务端的状态
- LISTENING :TCP服务器进入这个状态,证明此时服务器开始监听TCP客户端的连接请求,当提供的服务没有被连接时,处于LISTENING状态,端口是开放的,等待被连接。
- SYN_SENT:客户端在发送SYN连接请求报文段之后就会进入到这个状态,处于该状态的客户端需要等待服务器发送的TCP连接请求确认报文段,如果在指定时间内没有收到TCP连接请求确认报文段,会重发连接请求报文段。
- SYN-RCVD:服务端在收到SYN连接请求报文段之后,会给客户端发送一个TCP连接请求确认报文段,之后进入SYN-RCVD等待客户端的连接确认报文,如果在规定时间内没有收到客户端的连接确认报文,服务端会重新发送TCP连接请求确认报文段,在重试一定次数之后,就如还没有收到客户端的连接确认报文,这会把连接状态置为CLOSED。处于这个状态的连接称之为半连接。
- ESTABLISHED:连接已建立,当客户端和服务端都处于这个状态时,两者就可以进行数据传输了。
三个报文段解析
- TCP连接请求报文段:当客户端要和服务端建立连接的时候,需要发送一个TCP连接请求报文段给服务端,其中SYN=1,表示这是一个TCP连接请求报文段,而seq = x则是客户端随机的一个初始序号,TCP规定SYN=1的报文不会携带数据,但是会占用一个序号,所以在客户端下一次发送报文段时,序号seq为x + 1。
- TCP连接请求确认报文段:当服务端收到TCP连接请求报文段之后,会给客户端返回一个TCP连接请求确认报文段,之后把状态置为SYN-RCVD。其中SYN=1,ACK=1表示这个报文段是一个TCP连接请求确认报文段,seq=y是服务器初始化的一个序号,ack=x+1表示已经收到客户端序号为x的报文段了,请求序号为x+1的报文段。
- TCP连接确认报文段:收到服务器的TCP连接请求确认报文段,客户端要发送TCP连接确认报文段给服务端,之后客户端就可以进入ESTABLISHED状态了。
假如TCP连接确认报文段丢失(第三个报文丢失)
服务端发送TCP连接请求确认报文段之后进入SYN_RECV状态,此时等待TCP连接确认报文段,如果等待超时,会重新发送TCP连接请求确认报文段,如果多次尝试都没有收到客户端的TCP连接确认报文段,那么会给客户端发RTS报文,进入CLOSED状态,这个时候客户端应该也会关闭连接。
客户端在发送TCP连接确认报文段之后,不管服务端有没有收到,客户端会优先进入ESTABLISHED状态,假如这个时候TCP连接确认报文段丢失,并且客户端发送了数据,服务端会给客户端发送RST报文。
RTS报文
RTS标志的报文段,即复位报文段,以通知对方关闭连接或重新建立连接。在以下三种情况,TCP连接的一端会向另一端发送携带RTS标志的报文段:
- 访问不存在的端口,当客户端程序访问一个不存在的端口时,目标主机将给他发送一个复位报文段。
- 异常终止连接,TCP提供了异常终止连接的方法,即给对方发送一个复位报文段。
- 处理半打开连接,服务器关闭或异常终止了连接,而对方没有接受到结束报文段,此时,客户端还维持着原来的连接,而服务器即使重启,也已经没有该连接的任何信息了。这种状态被称为半打开状态,处于这种状态的连接叫做半打开连接。如果客户端往处于半打开状态的连接里写入数据,则对方会回应一个复位报文段。
为什么是三次握手,而不是两次握手
假如连接的建立只通过两次握手,也就是客户端发送一个SYN报文,服务端响应一个ACK报文连接就算建立。我们可以假设这样的场景,客户端发送一个SYN报文1,这个报文1由于网络原因在网络上传输了很久,一段时间之后客户端认为报文丢失了,重新发送了一个SYN报文2,服务端收到SYN报文2,并返回一个ACK给客户端进入ESTABLISHED状态,客户端收到ACK报文也进入ESTABLISHED状态,之后两端正常发送数据,然后断开连接。此时在网络中滞留的SYN报文1达到了服务端,服务端收到之后发送ACK报文进入ESTABLISHED状态,此时客户端已经CLOSED了,但是服务端却进入了ESTABLISHED状态,这样就造成了服务端资源的浪费。
四次挥手
TCP四次挥手客户端和服务端都可以发起,为了方便理解,举例都是从客户端发起的。
四次挥手中客户端和服务端的状态
请求断开端状态
- FIN-WAIT-1(终止等待1):客户端发送TCP连接断开请求报文段(FIN = 1,ACK =1报文段)之后会处于这个状态等待服务端的TCP确认报文,如果在规定的超时时间没有收到确认报文就会重发FIN = 1,ACK =1报文段,收到确认报文退出该状态进入FIN-WAIT-2。
- FIN-WAIT-2(终止等待2):客户端发送FIN = 1,ACK =1报文段之后并收到服务端的确认报文之后就会进入这个状态,此时客户端到服务端的连接已经断开,客户端无法在发送数据,但是服务端到客户端的连接依旧存在,服务端可以继续发送数据,客户端也要正常处理这些数据,处于这个状态的客户端要等到服务端发送TCP连接断开请求报文段(FIN = 1,ACK =1报文段)来断开服务端到客户端的连接,只要收到来自服务端的FIN = 1,ACK =1报文段,客户端就会响应确认报文,然后进入TIME-WAIT状态。
- TIME-WAIT(时间等待):客户端收到服务端的FIN = 1,ACK =1报文段(请求断开服务端到客户端的连接),返回确认报文之后就会进入该状态,持续2MSL = 4分钟,为什么要等待这么长的时间,是因为服务端没有收到客户端的确认段报文不会进入CLOSED状态,如果客户端的确认段报文丢失了,还可以有时间处理服务端重发的FIN = 1,ACK =1报文段。
被动断开端状态
- CLOSE-WAIT(关闭等待):服务端收到客户端发送的TCP连接断开请求报文段(FIN = 1,ACK =1报文段)之后,会响应一个TCP确认报文,然后就处于这个状态,该状态客户端到服务端的连接已经断开,但是服务端到客户端的连接依旧存在,服务端可以继续进行数据传输,在确认可以断开连接的时候,服务端也需要发送TCP连接断开请求报文段(FIN = 1,ACK =1报文段)去请求客户端断开服务端到客户端的连接,服务端在发送了FIN = 1,ACK =1报文段之后,就会进入LAST-ACK状态。
- LAST-ACK(最后确认):服务端等待来自客户端的TCP确认报文段,就可以进入CLOSED状态了。
四个报文段解析
- 客户端发起的TCP连接断开请求报文段(FIN = 1,ACK =1报文段),也就是第一个报文FIN = 1表示这是一个断开请求,带ACK =1,是因为断开之前肯定存在数据交互,所以断开报文也可以作为确认报文;seq=u是序号,一个FIN = 1,ACK =1报文段占一个序号,所以下次再从客户端发去请求序号应该是u+1;ack=v表示服务器发来的v序号之前的数据客户端已经确认接收,希望收到下一个报文的序号为v。
- 服务端发送的TCP确认报文:ACK=1表示这是一个TCP确认报文;seq=v为序号,第一个请求的ack也是v;ack=u+1表示客户端发送的u+1序号之前数据已经全部收到,服务端希望接下来收到的报文段序号为u+1。
- 第三次报文依旧是由服务端发出的TCP连接断开请求报文段(FIN = 1,ACK =1报文段),请求断开服务端到客户端的连接,这个报文与第一个报文类似,不再赘述。
- 第四个报文与第二个报文功能类似,不再赘述。
如果没有TIME-WAIT
如果没有TIME-WAIT,也就是客户端在发送了TCP确认报文之后就进入CLOSED状态,假如该确认报文丢失了,服务端就会超时重发FIN = 1,ACK =1报文段,由于客户端已经CLOSED,不会响应,所以一样会超时重发,就这样反复,直到达到最大次数,服务端断开连接。这个过程是很浪费服务端资源的,因此需要进行TIME-WAIT。
过多的TIME-WAIT
服务器维持处于TIME-WAIT状态的连接也是要消耗资源的,过多的TIME-WAIT必定会影响服务器性能以及新连接的建立。解决过多TIME-WAIT的方法有如下几种:
- 可以改为长连接,但代价较大,长连接太多会导致服务器性能问题;
- 修改系统参数。
[root@master ~]# cat /etc/sysctl.conf
# sysctl settings are defined through files in
# /usr/lib/sysctl.d/, /run/sysctl.d/, and /etc/sysctl.d/.
#
# Vendors settings live in /usr/lib/sysctl.d/.
# To override a whole file, create a new file with the same in
# /etc/sysctl.d/ and put new settings there. To override
# only specific settings, add a file with a lexically later
# name in /etc/sysctl.d/ and put new settings there.
#
# For more information, see sysctl.conf(5) and sysctl.d(5).
net.ipv4.tcp_syncookies = 1 #开启SYN Cookies 当出现SYN等待队列溢出时启用 可防范少量SYN攻击
net.ipv4.tcp_tw_reuse = 1 #开启重用 允许TIME-WAIT sockets重新用于新的TCP连接
net.ipv4.tcp_tw_recycle = 1 #开启TCP连接中TIME-WAIT sockets的快速回收
net.ipv4.tcp_fin_timeout = 30 #修改系统默认的TIMEOUT时间 单位/秒
[root@master ~]# /sbin/sysctl -p #让参数生效
TCP报文头部格式
- 源端口:用来确认发送报文的应用程序。
- 目的端口:用来确认接收报文的应用程序。
发送响应的时候要把源端口作为目的端口,把目的端口作为源端口 - 序号:是TCP数据载荷的第一个字节的序号。
- 确认后:在ACK=1的时候才有用,表示已经确认收到确认号之前序号的数据,同时希望收到下一个序号为确认号的报文段。
- 数据偏移:记录首部与实际数据的偏移量,该字段可以得出首部大小。如果这个值是15,这表示首部长度为60字节,这个值如果是5,则表示首部长度是20。
- 窗口:接收端发送字节的接收窗口大小给发送端就是通过该字段。
- 检验和:可以检验数据包是否存在误码。
- 紧急指针:记录紧急数据的位置。
- 选项:如记录时间戳等等。
- 填充:保证一整个长度是4的倍数,才可以被数据偏移记录。
- 标志位:
FIN:表示这是一个断开报文。
SYN:表示这是一个连接建立报文。
ACK:表示这是一个确认报文。
URG:表示这是一个紧急报文,紧急报文会被优先发送,也会被优先接收。
PSH:表示这是一个推送报文,推送报文需要尽快上交给应用程序,不需要等待接收缓存满。
RST:表示这是一个复位报文,复位报文可以用来释放一个异常连接,然后重新建立;还可以用来拒绝非法的报文段;拒绝打开一个TCP连接。
参考学习
《计算机网络》-- 谢希仁
《计算机网络自顶向下方法》-- [美]库罗斯
《图解TCP/IP》-- [日]竹下隆史 / [日]村山公保 / [日]荒井透 / [日]苅田幸雄
《计算机网络微课堂》-- 湖科大教书匠