计算机网络 | Linux | 解析TCP协议——进阶篇

目录

进阶篇

一.TCP报文段的首部格式

二.TCP的连接建立

1.三次握手

2.连接过程中出现的几种状态

3.为什么是三次握手

4.SYN flood攻击

5.在三次握手过程中,如果服务器一直收不到客户端的ack会发生什么?

6.初始序列号Seq为什么要随机初始化?

三.TCP的连接释放

扫描二维码关注公众号,回复: 11096452 查看本文章

1.四次挥手

2.为什么建立连接是三次握手,而关闭连接却是四次挥手

3.socket中的close是一次就关闭的吗?

4.如果已经建立了连接, 但是客户端突发故障了怎么办?

5.服务端不调用close会发生什么?

四.TIME_WAIT状态

1.为什么需要TIME_WAIT状态以及为什么 TIME_WAIT 状态还需要等 2MSL

2.为什么是2MSL?

3、TIME_WAIT会带来哪些问题?

4.TIME_WAIT状态如何产生

五.connect()、listen()和accept()三者之间的关系

1.connect()函数分析

2.listen()函数分析

3.accept()函数分析

六.拥塞控制

1.什么是拥塞控制

2.TCP拥塞控制的算法

3.慢开始和拥塞避免

4.快重传和快恢复

七.粘包问题

1.TCP的粘包问题

2.UDP的粘包问题


进阶篇

一.TCP报文段的首部格式

首部固定部分各字段的意义如下:

  1. 源端口和目的端口各占2个字节,分别写入源端口号和目的端口号。
  2. 序号占4字节。在一个TCP连接中传送的字节流中的每一个字节都按顺序编号。
  3. 确认号占4字节,是期望收到对方下一个报文段的第一个数据字节的序号。
  4. 数据偏移占4位,它指出TCP报文段的数据起始处距离TCP报文段的起始处有多远。这个字段实际上是指出TCP报文段的首部长度。
  5. 保留占6位,保留为今后使用,但目前应置为0。
  6. 紧急URG,当URG=1时,表明紧急指针字段有效。它告诉系统此报文段中有紧急数据,应尽快传送(相当于高优先级的数据),而不要按原来的排队顺序来传送。
  7. 确认ACK,仅当ACK=1时确认号字段才有效。当ACK=0时,确认号无效。TCP规定,在连接建立后所有传送的报文段都必须把ACK置1。
  8. 推送PSH很少使用。
  9. 复位RST,当RST=1时,表明TCP连接中出现严重差错(如由于主机崩溃或其他原因),必须释放连接,然后再重新建立运输连接。
  10. 同步SYN,在连接建立时用来同步序号。
  11. 终止FIN用来释放一个连接。当FN=1时,表明此报文段的发送方的数据已发送完毕,并要求释放运输连接。
  12. 窗口占2个字节。窗口大小标志着TCP缓冲区内部剩余空间的大小,起到一个流量控制的作用。如果窗口满了,那么这个时候是不允许数据接收的。后面到达的数据会被丢失。
  13. 校验和占2个字节,这里的校验和由发送端填充,CRC校验。接收端校验数据的时候如果校验不通过,那么认为数据有问题。此处的校验和不仅仅校验TCP首部,还校验数据部分。
  14. 紧急指针占2字节它指出本报文段中的紧急数据的字节数(紧急数据结束后就是普通数据)。因此,紧急指针指出了紧急数据的末尾在报文段中的位置。

二.TCP的连接建立

1.三次握手

TCP建立连接的过程叫做握手,握手需要在客户和服务器之间交换三个TCP报文段,因此我们又将TCP的连接建立过程称为三次握手。

假定主机A运行的是TCP客户程序,而B运行TCP服务器程序。注意,在本例中,A主动打开连接,而B被动打开连接。

开始,A的TCP客户进程也是首先创建传输控制模块TCB,在打算建立TCP连接时,才向B发出连接请求报文段。B的TCP服务器进程先创建传输控制块TCB,准备接受客户进程的连接请求。然后服务器进程就处于 LISTEN(收听)状态,等待客户的连接请求。如有,即作出响应。

  • 第一次握手:客户端A打算建立TCP连接,于是向B发出连接请求报文段。这时请求报文段首部中的同步位SYN=1,同时选择一个初始序号seq=x。这时,TCP客户进程进入 SYN-SENT(同步已发送)状态。
  • 第二次握手:B收到连接请求报文段后,如同意建立连接,则向A发送确认。在确认报文段中应把SYN位和ACK位都置1,确认号是ack=x+1,同时也为自己选择一个初始序号seq=y。这时TCP服务器进程进入 SYN-RCVD(同步收到)状态。
  • 第三次握手:A收到B的确认后,还要向B给出确认。确认报文段的ACK置1,确认号ack=y+1,而自己的序号seq=x+1。这
    时,TCP连接已经建立,A进入 ESTABLISHED(已建立连接)状态,当B收到A的确认后,也进入 ESTABLISHED状态。

通过这样的三次握手,客户端与服务端建立起可靠的双工的连接,开始传送数据。 三次握手的最主要目的是保证连接是双工的,可靠更多的是通过重传机制来保证的。 

2.连接过程中出现的几种状态

  • 半连接状态:发生在TCP三次握手过程中,客户端向服务器发起连接,服务器也进行了回应,但是客户端却不进行第3次握手。
  • 半打开状态:在TCP连接中,如果某一端关闭了连接或者是异常关闭,则该连接处于半打开状态。解决半打开问题:引入心跳机制就可以察觉半打开状态。
  • 半关闭状态:当TCP链接中客户端向服务器发送 FIN 请求关闭,服务端回应ACK之后,并没有立即发送 FIN 给客户端,客户端就处于半关闭状态,此时客户端可以接收服务器发送的数据,但是客户端已经不能再向服务器发送数据。

3.为什么是三次握手

为什么A最后还要发送一次确认呢?这主要是为了防止已失效的连接请求报文段突然又传送到了B,因而产生错误。

例如,现假定出现一种异常情况,即A发出的第一个连接请求报文段在某些网络结点长时间滞留了,以致延误到连接释放以后的某个时间才到达B.本来这是一个早已失效的报文段。但B收到此失效的连接请求报文段后,就误认为是A又发出一次新的连接请求。于是就向A发出确认报文段,同意建立连接。假定不采用三次握手,那么只要B发出确认,新的连接就建立了。由于现在A并没有发出建立连接的请求,因此不会理睬B的确认,也不会向B发送数据。但B却以为新的运输连接已经建立了,并一直等待A发来数据。B的许多资源就这样白白浪费了。

采用三报文握手的办法,可以防止上述现象的发生。例如在刚才的异常情况下,A不会向B的确认发出确认。B由于收不到确认,就知道A并没有要求建立连接。

4.SYN flood攻击

Syn攻击就是攻击客户端在短时间内伪造大量不存在的IP地址,向服务器不断地发送syn包(也就是进行第一次握手),服务器回复确认包,并等待客户的确认(进行第二次握手),但由于源地址是不存在的,服务器需要不断的重发直至超时,由于一台服务器可用的TCP连接是有限的,如果恶意攻击方快速连续地发送此类连接请求,则服务器可用TCP连接队列很快将会阻塞,系统资源和可用带宽急剧下降,无法提供正常的网络服务,从而造成拒绝服务。

5.在三次握手过程中,如果服务器一直收不到客户端的ack会发生什么?

服务端会给每个待完成的半连接都设一个定时器,如果超过时间还没有收到客户端的ACK消息,则重新发送一次SYN-ACK消息给客户端,直到重试超过一定次数时才会放弃。这个时候服务器需要分配内核资源维护半连接。

6.初始序列号Seq为什么要随机初始化?

这样做主要是为了保证网络安全,如果不是随机产生初始序列号,黑客将会以很容易的方式获取到你与其他主机之间通信的初始化序列号,并且伪造序列号进行攻击,这已经成为一种很常见的网络攻击手段。

三.TCP的连接释放

1.四次挥手

  • 第一次挥手:数据传输结束后,通信的双方都可释放连接。现在A和B都处于 ESTABLISHED状态。A的应用进程先向其TCP发出连接释放报文段,并停止再发送数据,主动关闭TCP连接。A把连接释放报文段首部的终止控制位FIN置1,其序号seq=u这时A进入FⅠNWAI-1(终止等待1)状态,等待B的确认。
  • 第二次挥手:B收到连接释放报文段后即发出确认,确认号是ack=u+1,而这个报文段自己的序号是seq=ⅴ。然后B就进入 CLOSEWAT(关闭等待)状态。TCP服务器进程这时应通知高层应用进程,因而从A到B这个方向的连接就释放了,这时的TCP连接处于半关闭状态,即A已经没有数据要发送了,但B若发送数据,A仍要接收。也就是说,从B到A这个方向的连接并未关闭。
  • A收到来自B的确认后,就进入 FIN-WAIT2(终止等待2)状态,等待B发出的连接释放报文段。
  • 第三次挥手:若B已经没有要向A发送的数据,其应用进程就通知TCP释放连接。这时B发出的连接释放报文段必须使FIN=1。现假定B的序号seq=w。B还必须重复上次已发送过的确认号ack=u+1.这时B就进入 LAST-ACK(最后确认)状态,等待A的确认。
  • 第四次挥手:A在收到B的连接释放报文段后,必须对此发出确认。在确认报文段中把ACK置1,确认号ack=w+1,而自己的序号是seq=u+1。然后进入到 TIME-WAIT(时间等待)状态。

请注意,现在TCP连接还没有释放掉。必须经过时间等待计时器设置的时间2MSL后,A才进入到 CLOSED状态。时间MSL叫做最长报文段寿命。B只要收到了A发出的确认,就进入 CLOSED状态。同样,B在撤销相应的传输控制块TCB后,就结束了这次的TCP连接。我们注意到,B结束TCP连接的时间要比A早一些。

2.为什么建立连接是三次握手,而关闭连接却是四次挥手

在三次握手时,服务端可以把 ACK 和 SYN(ACK 起应答作用,而 SYN 起同步作用)放在一个报文里来发送。但是四次挥手时,服务端发送的FIN与ACK是分开发送的。

原因在于:首先FIN信号是由于调用close所以才发送的,而ACK是由内核发送的,所以ACK报文和FIN报文在发送的时间上都是分开的,不一定能同时发送。但是三次握手的时候发送SYN是由内核直接完成的,所以这就可以达到一个同步发送的情况。

3.socket中的close是一次就关闭的吗?

使用close中止一个连接,但它只是减少文件描述符的引用计数,并不直接关闭连接,只有当描述符的引用计数为0时才关闭连接。

4.如果已经建立了连接, 但是客户端突发故障了怎么办?

TCP设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75分钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。

5.服务端不调用close会发生什么?

如果服务器的代码没有调用close,那么意味着并没有发送FIN结束报文段。那么也就是说,此连接的服务器长期保持在CLOSE_WAIT状态,这会有什么影响?

服务器长期保持在CLOSE_WAIT状态,也就是说分配的文件描述符并没有关闭并归还。那么大量的CLOSE_WAIT存在的话,就会导致一种资源的泄漏,可能到最后就没有可分配的文件描述符了,那么就会使一些客户端无法连接,从而造成不可估量的影响。

四.TIME_WAIT状态

1.为什么需要TIME_WAIT状态以及为什么 TIME_WAIT 状态还需要等 2MSL

  1. 第一,为了保证A发送的最后一个ACK报文段能够到达B。这个ACK报文段有可能丢失,因而使处在 LAST-ACK状态的B收不到对已发送的FIN+ACK报文段的确认。B会超时重传这个FN+ACK报文段,而A就能在2MSL时间内收到这个重传FIN+ACK报文段。接着A重传一次确认,重新启动2MSL计时器。最后,A和B都正常进入到CLOSED状态。如果A在 TIME- WAIT状态不等待一段时间,而是在发送完ACK报文段后立即释放连接,那么就无法收到B重传的FIN+ACK报文段,因而也不会再发送一次确认报文段。这样,B就无法按照正常步骤进入 CLOSED状态。
  2. 第二,防止“已失效的连接请求报文段”出现在本连接中。A在发送完最后一个ACK报文段后,再经过时间2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样就可以使下一个新的连接中不会出现这种旧的连接请求报文段。

2.为什么是2MSL?

我们知道服务端收到ACK,关闭连接。但是客户端无法知道ACK是否已经到达服务端,于是开始等待?等待什么呢?假如ACK没有到达服务端,服务端会为FIN这个消息超时重传 timeout retransmit ,那如果客户端等待时间足够,又收到FIN消息,说明ACK没有到达服务端,于是再发送ACK,直到在足够的时间内没有收到FIN,说明ACK成功到达。这个等待时间至少是:服务端的timeout + FIN的传输时间,为了保证可靠,采用更加保守的等待时间2MSL。

客户端发出ACK,等待ACK到达对方的超时重传时间 MSL(最大报文生存时间),等待FIN的超时重传,也是MSL,所以如果2MSL时间内没有收到FIN,说明对方安全收到ACK。

3、TIME_WAIT会带来哪些问题?

  • 作为服务器,短时间内关闭了大量的Client连接,就会造成服务器上出现大量的TIME_WAIT连接,占据大量的tuple,严重消耗着服务器的资源。

  • 作为客户端,短时间内大量的短连接,会大量消耗Client机器的端口,毕竟端口只有65535个,端口被耗尽了,后续就无法再发起新的连接了。

4.TIME_WAIT状态如何产生

主动发起关闭连接(调用close)的一方最后才会出现TIME_WAIT状态。

五.connect()、listen()和accept()三者之间的关系

1.connect()函数分析

对于客户端的 connect() 函数,该函数的功能为客户端主动连接服务器,建立连接是通过三次握手,而这个连接的过程是由内核完成,不是这个函数完成的,这个函数的作用仅仅是通知 Linux 内核,让 Linux 内核自动完成 TCP 三次握手连接,最后把连接的结果返回给这个函数的返回值(成功连接为0, 失败为-1)。通常的情况,客户端的 connect() 函数默认会一直阻塞(可以使用fcntl()函数或者ioctl()函数把connect()变为非阻塞),直到三次握手成功或超时失败才返回(正常的情况,这个过程很快完成)。

2.listen()函数分析

对于服务器,它是被动连接的。这里需要注意的是,listen()函数不会阻塞,它主要做的事情为,将该套接字和套接字对应的连接队列长度告诉 Linux 内核,然后,listen()函数就结束。

所以,只要 TCP 服务器调用了 listen(),客户端就可以通过connect() 和服务器建立连接,而这个连接的过程是由内核完成。

在被动状态的socket有两个队列,一个是正在进行三次握手的socket队列,一个是完成三次握手的socket队列。在握手完成后会从正在握手队列移到握手完成的队列,此时已经建立连接。

3.accept()函数分析

accept()函数功能是,从连接队列头部取出一个已经完成的连接,如果这个队列没有已经完成的连接,accept()函数就会阻塞,直到取出队列中已完成的用户连接为止。TCP 的连接队列满后,会延时连接。

六.拥塞控制

1.什么是拥塞控制

在某段时间,若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络的性能就要变坏。这种情况就叫做拥塞。所谓拥塞控制就是防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不致过载。拥塞控制所要做的都有一个前提,就是网络能够承受现有的网络负荷。

在TCP连接中,两个端点只要迟迟不能收到对方的确认信息,就猜想在当前网络中的某处很可能发生了拥塞,但这时却无法知道拥塞到底发生在网络的何处,也无法知道发生拥塞的具体原因。因此才有了TCP拥塞控制。

2.TCP拥塞控制的算法

TCP进行拥塞控制的算法有四种

  1. 慢开始
  2. 拥塞避免
  3. 快重传
  4. 快恢复

3.慢开始和拥塞避免

发送方维持一个叫做拥塞窗口的状态变量。拥塞窗口的大小取决于网络的拥塞程度,并且动态地在变化。发送方让自己的发送窗口等于拥塞窗口。发送方控制拥塞窗口的原则是:只要网络没有出现拥塞,拥塞窗口就可以再增大些,以便把更多的分组发送出去,这样就可以提高网络的利用率。但只要网络出现拥塞或有可能出现拥塞,就必须把拥塞窗口减小一些,以减少注入到网络中的分组数,以便缓解网络出现的拥塞。

慢开始算法的思路是这样的:当主机开始发送数据时,由于并不清楚网络的负荷情况,所以如果立即把大量数据字节注入到网络,那么就有可能引起网络发生拥塞。经验证明,较好的方法是先探测一下,即由小到大逐渐增大发送窗口,也就是说,由小到大逐渐增大拥塞窗口数值。使用慢开始算法后,每经过一个传输轮次,拥塞窗口cwnd就加倍。

拥塞避免算法的思路是让拥塞窗口cwnd缓慢地增大,即每经过一个传输轮次就把发送方的拥塞窗口cwnd加1,而不是像慢开始阶段那样加倍增长。这表明在拥塞避免阶段,拥塞窗口cwnd按线性规律缓慢增长,比慢开始算法的拥塞窗口增长速率缓慢得多。但请注意,“拥塞避免”并非完全能够避免了拥塞。“拥塞避免”是说把拥塞窗口控制为按线性规律增长,使网络比较不容易出现拥塞。

4.快重传和快恢复

有时,个别报文段会在网络中丢失,但实际上网络并未发生拥塞。如果发送方迟迟收不到确认,就会产生超时,就会误认为网络发生了拥塞。这就导致发送方错误地启动慢开始,把拥塞窗口cwnd又设置为1,因而降低了传输效率。

采用快重传算法可以让发送方尽早知道发生了个别报文段的丢失。快重传算法首先要求接收方不要等待自己发送数据时才进行捎带确认,而是要立即发送确认,即使收到了失序的报文段也要立即发出对已收到的报文段的重复确认。

如上图所示,接收方收到了M1和M2后都分别及时发出了确认。现假定接收方没有收到M3但却收到了M4本来接收方可以什么都不做。但按照快重传算法,接收方必须立即发送对M2的重复确认,以便让发送方及早知道接收方没有收到报文段M3。发送方接着发送M5和M6.接收方收到后也仍要再次分别发出对M2的重复确认。这样,发送方共收到了接收方的4个对M2的确认,其中后3个
都是重复确认。快重传算法规定,发送方只要一连收到3个重复确认,就知道接收方确实没有收到报文段M3,因而应当立即进行重传(即“快重传”),这样就不会出现超时,发送方也不就会误认为出现了网络拥塞。

此时数据段只是单纯的丢失,而不是因为网络拥塞导致,所以此时不需要拥塞窗口更新为最小值进行慢启动,此时需要设置拥塞窗口大小为:门限值大小+3,更新之后按照拥塞避免算法继续进行。也就是下图的④~⑤的过程。

七.粘包问题

1.TCP的粘包问题

TCP是一个基于字节流的传输服务,"流"意味着TCP所传输的数据是没有边界的。这不同于UDP提供基于消息的传输服务,其传输的数据是有边界的。TCP的发送方无法保证对等方每次接收到的是一个完整的数据包。

粘包问题本质上要在应用层维护消息与消息的边界。解决方案如下:

  • 在接收端接收的时候采用定长的方式接收;

  • 在数据包尾添加一些分隔符;

  • 在数据包头部加上数据包长度;

  • 更复杂的应用层协议。

关于粘包问题的相关博客: https://blog.csdn.net/zhangxinrun/article/details/6721495

2.UDP的粘包问题

为什么udp不会粘包

  • TCP协议是面向流的协议,UDP是面向消息的协议
  • UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据。

UDP具有保护消息边界,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样对于接收端来说就容易进行区分处理了。传输协议把数据当作一条独立的消息在网上传输,接收端只能接收独立的消息。接收端一次只能接收发送端发出的一个数据包,如果一次接受数据的大小小于发送端一次发送的数据大小,就会丢失一部分数据,即使丢失,接受端也不会分两次去接收

发布了88 篇原创文章 · 获赞 40 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/ThinPikachu/article/details/105152911
今日推荐