搜索
您的当前位置:首页正文

TCP(四)

来源:好走旅游网


  TCP重点:三次握手、四次挥手、timewait状态、为何挥手不是三次(全双工)、拥塞避免(慢启动)、可靠性(超时重传、快速重传)、滑动窗口。

一 TCP报文封装

二 TCP头部

三 TCP和UDP的区别

  • TCP是稳定、可靠、面向连接的传输层协议,它在传递数据前要三次握手建立连接,在数据传递时,有确认机制、重传机制、流量控制、拥塞控制等,可以保证数据的正确性和有序性。UDP是无连接的数据传输协议,端与端之间不需要建立连接,且没有类似TCP的那些机制,会发生丢包、乱序等情况。
  • 从头部结构来说,TCP因为有选项部分,所以有首部长度字段;而UDP没有选项部分,所以不需要首部长度字段。
  • TCP避免分段,因为有重传机制本来就浪费了一些带宽,一旦出现分段,那么重传会大量增加,将会浪费大量带宽并且会严重降低传输效率;而UDP则不关心分不分段,且因为UDP的头部小(只有8字节,而TCP头部最小也得20字节),故可以携带更多的数据。
  • TCP是数据流模式,所以应用程序产生的全体数据与真正发送的单个IP数据报没有什么联系;而UDP是数据报模式,进程的每个操作产生都正好产生一个UDP数据报,并组装成一个IP数据报发送。
  • 所谓的“流模式”,是指TCP发送端发送几次数据和接收端接收几次数据是没有必然联系的,比如你通过 TCP 连接给另一端发送数据,你只调用了一次 write,发送了100个字节,但是对方可以分10次收完,每次10个字节;你也可以调用10次 write,每次10个字节,但是对方可以一次就收完。
  • 所谓的“数据报模式”,是指UDP发送端调用了几次 write,接收端必须用相同次数的 read 读完。UDP 是基于报文的,在接收的时候,每次最多只能读取一个报文,报文和报文是不会合并的,如果缓冲区小于报文长度,则多出的部分会被丢弃。

原因:这是因为UDP是无连接的,只要知道接收端的 IP 和端口,任何主机都可以向接收端发送数据。 这时候, 如果一次能读取超过一个报文的数据, 则会乱套。

四 三次握手

  在socket编程中,这一过程由客户端执行connect来触发,整个流程如下图所示:

五 四次挥手

  socket编程中,这一过程由客户端或服务端任一方执行close来触发,流程如下图所示:

六 TCP Api和三次握手对应

七 TCP滑动窗口

  TCP是通过滑动窗口来进行流量控制。我们知道,在TCP头部里有一个字段叫 Advertised-Window(即窗口大小)。这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据,于是发送端就可以根据这个剩余空间来发送数据,而不会导致接收端处理不过来。

八 拥塞控制

  我们知道TCP通过一个定时器(timer)采样了RTT并计算RTO,但是,如果网络上的延时突然增加,那么,TCP对这个事做出的应对只有重传数据,然而重传会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,这就导致了恶性循环,最终形成“网络风暴” —— TCP的拥塞控制机制就是用于应对这种情况。
  首先需要了解一个概念,为了在发送端调节所要发送的数据量,定义了一个“拥塞窗口”(Congestion Window),在发送数据时,将拥塞窗口的大小与接收端ack的窗口大小做比较,取较小者作为发送数据量的上限。
  拥塞控制主要是四个算法:
  慢启动:意思是刚刚加入网络的连接,一点一点地提速,不要一上来就把路占满。1、连接建好的开始先初始化cwnd = 1,表明可以传一个MSS大小的数据。2、每当收到一个ACK,cwnd++; 呈线性上升。3、每当过了一个RTT,cwnd = cwnd*2; 呈指数上升。4、阈值ssthresh(slow start threshold),是一个上限,当cwnd >= ssthresh时,就会进入“拥塞避免算法”。
  拥塞避免:当拥塞窗口 cwnd 达到一个阈值时,窗口大小不再呈指数上升,而是以线性上升,避免增长过快导致网络拥塞。1、每当收到一个ACK,cwnd = cwnd + 1/cwnd。2、每当过了一个RTT,cwnd = cwnd + 1。
  拥塞发生:当发生丢包进行数据包重传时,表示网络已经拥塞。分两种情况进行处理:1.等到RTO超时,重传数据包:sshthresh = cwnd /2,cwnd 重置为 1,进入慢启动过程;2.在收到3个duplicate ACK时就开启重传,而不用等到RTO超时:sshthresh = cwnd = cwnd /2;进入快速恢复算法——Fast Recovery;
  快速恢复:至少收到了3个Duplicated Acks,说明网络也不那么糟糕,可以快速恢复。cwnd = sshthresh + 3 * MSS (3的意思是确认有3个数据包被收到了);重传Duplicated ACKs指定的数据包;如果再收到 duplicated Acks,那么cwnd = cwnd +1;如果收到了新的Ack,那么,cwnd = sshthresh ,然后就进入了拥塞避免的算法了。

九 TCP可靠传输的实现

  1、应用数据被分割成TCP认为最合适发送的数据块。称为段(Segment)传递给IP层
  2、当TCP收到发自TCP连接另一端的数据,它将发送一个确认。这个确认不是立即发送的,通常将推迟几分之一秒
  3、当TCP发出一个段后,它会启动一个定时器,等待目的端确认收到这个报文段。若没有及时收到确认,将重新发送这个报文段
  4、TCP将保持它首部和数据的校验和,这是一个端到端的校验和,目的是检测数据在传输过程中的任何变化。如果收到段的校验和有差错,TCP将丢弃这个报文也不进行确认(对方就会重复发送了)
  5、TCP承载与IP数据报来传输,而IP数据报可能会失序,所以TCP的报文段到达时也可能会失序。但是TCP收到数据后会重新排序到正确的顺序(通过序号)
  当队列已满时,TCP将不理会传入的SYN,也不发回RST作为应答,因为这是一个软错误,而不是一个硬错误。

十 TCP定时器

  BSD Reno版本(reno算法,目前广泛使用的,很多都是基于此实现的)
  Tcp提供可靠的传输层,是通过确认机制保证的,当然确认包与数据包都有可能丢失,解决的办法是设置定时器,当定时器溢出时,就重传数据包或者ack;实现的关键是如何确定超时时间以及重传的频率。
  对于每个TCP连接,维持4个定时器:1、重传定时器;2、坚定定时器,保证窗口的流动;3、保活定时器;4、2msl的维持TIME_WAIT状态的定时器。

1 保活定时器

  保活定时器:当一端主动关闭时,处于半连接状态,另一端设置保活定时器,在交互数据流中,Nagle算法保证的是小块数据流的发送,避免拥塞的产生。它是自适应的,确认到达的越快,发送速率越快。规则是:a长度大于mss的数据直接发送;b小于mss的数据只能存在一个为被确认的发送包;c超时发送,一般为200ms;d设置了TCP_NODEAY,取消nagle算法;e设置了TCP_CORK,开启算法;f有fin,直接发送。
  保活定时器时间长为2小时,并坚持发送10个探测段,间隔时间75秒;对端四种情况:1、正常;2、重启,发送rst复位;3、对端崩溃等情况,发送10次探测;4、路由问题,探测不可达,收到icmp不可达报文;
  Connect超时时间为75秒;msl一般为2分钟,2msk30秒到2分钟。

2 重传定时器

  指数退避:即倍乘关系,从开始的比如1.5秒,到发生重传后的3,6,12,24,48,64,直到最多的64秒将会放弃发送,从而发送一个复位报文段,并通知主机断开信息。
  往返时间的测量:(由于路由器与网上流量的动态变化,需要动态的进行测定,通常使用RTT进行测量)
  分组重传机制:
  当发生分组丢失时,接收端发送一个丢失数据包起始序列号的ACK,当接收端收到三次(为什么是三呢,可能发生丢包或者接收端顺序调整,若三次还是收到重复的ACK,就表示可能发生了包丢失)的丢失数据包的序列号的ACK时就会重传该序列号对应的数据包,当重传报文发送后,不会等待重传报文的确认(当再次丢失呢?重复收到该序列号的ACK,再次等待3次后重传。。。)
  拥塞避免算法和慢启动算法是两个不同的算法,但是当发生拥塞的时候,就需要减少降低分组在网络中的传输速率,此时需要用到慢启动就行控制。
  二者都需要为每个链接维持两个变量,一个是拥塞窗口(cwnd)、另个时慢启动的门阀值(ssthresh)。
  拥塞避免(拥塞窗口是发送方的流量控制,通告窗口是接收端的流量控制)
  当拥塞发生时(超时或者是收到重复的确认),就会将ssthresh设置为当前窗口(拥塞窗口以及通告窗口的最小值,但是最小为2个报文段长度)的一半大小,此外,如果是超时引起的,那就会将cwnd设置为一个报文段长度(慢启动)
  当新的数据被确认时,就会增加cwnd,但增加的方法依赖于当前是在进行拥塞避免或者慢启动(指数增长)。当拥塞窗口cwnd的值小于ssthresh的值时则证明在进行慢启动(小于阀值),否则在进行拥塞避免。慢启动一直持续到我们回到发生拥塞窗口的一般的时候才停止,此时将进行拥塞避免。
  拥塞避免算法要求每次收到一个ACK,将会增加1/cwnd大小,希望在一个往返的时间内,最多增加一个cwnd(无论收到了多少个ACK);

  快速重传与快速恢复算法:
  当发生收到重复的ACK时有两种可能,一种是顺序性错乱(当接收端通过调整后可以不用重传),一般会收到一两个重复的ACK属于正常,另一种是发生丢包,当重复ACK的数量达到三或者三个以上时,就非常有可能发生丢包,此时不用考虑定时器是否溢出,采用的算法就是快速重传,此外不需要等待重传数据报的ACK。当发生快速重传后采用的是拥塞避免而不是慢启动。这就是快速恢复算法。
  当出现分组丢失时我们的速率将减半(执行的是拥塞避免)。
  总结:当超时引发的阻塞,不仅会修改当前ssthresh为当前窗口的一半(前边有述),还会进行慢启动,当是由于收到重复的ack(顺序调整或者丢包),就会进行拥塞避免,而不是慢启动。
  然后就是快速重传(不用等待定时器是否溢出)以及快速恢复算法(不进行慢启动,原因是,能收到重复的ACK,表明数据包已经离开网络,而不需要采用慢启动从一开始,只需要采用拥塞避免进行控制快速恢复即可)。
  按每条路由进行初始化:
  当一个tcp链接断开,同时这条链接已经发送了足够多的信息用与提取有用信息时,我们就可以将:被平滑RTT,被平滑的均值偏差以及慢启动阀值记录在路由表项中,这样当建立一个新的链接时,如果路有表项中存在信息,将会以这写信息进行初始化。
  ICMP的差错
  多种ICMP差错对tcp链接的影响,在旧的实现版本上,当出现ICMP 显示主机不存在时,直接会关闭tcp链接,但是,这个信息是不完整的,又是可能只是延时(路由替换)。
  重新分组
  当T C P超时并重传时,它不一定要重传同样的报文段。相反, T C P允许进行重新分组而发送一个较大的报文段,这将有助于提高性能(当然,这个较大的报文段不能够超过接收方声明的M S S)。

十一 关闭链接过程中的TCP状态和SOCKET处理,及可能出现的问题

1 TIME_WAIT

  TIME_WAIT是主动关闭 TCP 连接的那一方出现的状态,系统会在TIME_WAIT状态下等待2MSL(maximum segment lifetime)后才能释放连接(端口)。通常约合4分钟以内。
  TIME_WAIT状态等待2MSL的意义:1、确保连接可靠地关闭,即防止最后一个ACK丢失。2、避免产生套接字混淆(同一个端口对应多个套接字)。
   为什么说可以用来避免套接字混淆呢?
  一方close发送了关闭链接请求,对方的应答迟迟到不了(例如网络原因),导致TIME_WAIT超时,此时这个端口又可用了,我们在这个端口上又建立了另外一个socket链接。 如果此时对方的应答到了,怎么处理呢?其实这个在TCP层已经处理了,由于有TCP序列号,所以内核TCP层,就会将包丢掉,并给对方发包,让对方将sockfd关闭。所以应用层是没有关系的。即我们用socket API编写程序,就不用处理。
  通常调用close(),或者closesock()的时候,会立即返回,不考虑缓冲区的数据有没有被完全发送完,所以应用程序也不知到这些数据是否被发送成功。
  为了避免这种情况的发生,设置了套接字选项,SO_LINGER,填写linger结构

Struct longer
{
	Int l_onoff;
	Int l_linger;
};

  如果成员l_onoff为0,标示被关闭;非零标示开启,其行为由l_linger控制,当该值非零时,就将其作为内核等待挂起的数据被发送出去并被确认时逗留的时间间隔。也就说当close等调用在缓冲区数据全部发送完毕或者时间间隔超时之前是不会返回的。当时间到期后,返回EWOULDBLOCK,成功返回0;如果成员l_linker为0,就直接丢弃链接,返回一个 rst的复位报文段,不经过TIME_WAIT状态就直接关闭链接(TIEM_WAIT暗杀)。
  如果要立即重用链接,可以使用SO_REUSEADDR选项,保证使用一个处于TIME_WAIT状态的端口。
  将写操作合并起来:禁止Nagle算法一部分原因是一系列小规模的写操作来发送逻辑上相互关联的数据造成性能问题(比如与ack延时交互后,造成大量的延时,其原理为:当客户端需要发送逻辑上相互关联的数据,比如两份数据构成一个完整的请求,当使用nagle算法时,由于发送出前半部分请求后,后半部分作为小分组将不会被发送直到收到ack,对于服务端程序中的ack延时(也用来减少网络上的分组),等待服务端的响应信息,使得ack与响应信息同时发送,但是服务端没有收到完整的请求信息,所以只有在ack延时定时器到期后才会发送ack,这样nagle与ack延时相互交互造成了大量的延时)使用writev与readv可以避免Nagle与ack延时算法的交互。
即将数据合并起来再一起发送出去。
  总结:尽量使用大规模的写操作,因为小规模的写操作可能会引起相当严重的性能问题。
  注意:TIME_WAIT是指操作系统的定时器会等2MSL,而主动关闭sockfd的一方,并不会阻塞。(即应用程序在close时,并不会阻塞)。
  当主动方关闭sockfd后,对方可能不知道这个事件。那么当对方(被动方)写数据,即send时,将会产生错误,即errno为: ECONNRESET。
  服务器产生大量 TIME_WAIT 的原因:(一般我们不这样开发Server,但是web服务器等这种多客户端的Server,是需要在完成一次请求后,主动关闭连接的,否则可能因为句柄不够用,而造成无法提供服务。)
   服务器存在大量的主动关闭操作,需关注程序何时会执行主动关闭(如批量清理长期空闲的套接字等操作)。
  一般我们自己写的服务器进行主动断开连接的不多,除非做了空闲超时之类的管理。(TCP短链接是指,客户端发送请求给服务器,客户端收到服务器端的响应后,关闭链接)。

2 CLOSE_WAIT

  CLOSE_WAIT是被动关闭 TCP 连接时产生的,如果收到另一端关闭连接的请求后,本地(Server端)不关闭相应套接字就会导致本地套接字进入这一状态。(如果对方关闭了,没有收到关闭链接请求,就是下面的不正常情况)
按TCP状态机,我方收到FIN,则由TCP实现发送ACK,因此进入CLOSE_WAIT状态。但如果我方不执行close(),就不能由CLOSE_WAIT迁移到LAST_ACK,则系统中会存在很多CLOSE_WAIT状态的连接。
  如果存在大量的 CLOSE_WAIT,则说明客户端并发量大,且服务器未能正常感知客户端的退出,也并未及时 close 这些套接字。(如果不及时处理,将会出现没有可用的socket描述符的问题,原因是sockfd耗尽)。
  正常情况下:一方关闭sockfd,另外一方将会有读事件产生, 当recv数据时,如果返回值为0,表示对端已经关闭。此时我们应该调用close,将对应的sockfd也关闭掉。
  不正常情况下:一方关闭sockfd,另外一方并不知道,(比如在close时,自己断网了,对方就收不到发送的数据包)。此时,如果另外一方在对应的sockfd上写send或读recv数据。
  recv时,将会返回0,表示链接已经断开。
  send时, 将会产生错误,errno为ECONNRESET。

十二 关于三次握手和四次挥手的问题

  1、为什么建立连接是三次握手,而关闭连接却是四次挥手呢?
  这是因为服务端在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。而关闭连接时,当收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,己方也未必全部数据都发送给对方了,所以己方可以立即close,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送。
  2、TIME_WAIT状态的产生、危害、如何避免?
  TCP协议在关闭连接的四次挥手中,为了应对最后一个ACK丢失的情况,Client(即主动关闭连接的一方)需要维持time_wait状态并停留 2个MSL(个数据报文在网络中能够生存的最长时间通常为2分钟)的时间。
  3、为什么TIME_WAIT需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?
  ①、为了保证A发送的最后一个ACK报文段能够到达B。即最后这个确认报文段很有可能丢失,那么B会超时重传,然后A再一次确认,同时启动2MSL计时器,如此下去。如果没有等待时间,发送完确认报文段就立即释放连接的话,B就无法重传了(连接已被释放,任何数据都不能出传了),因而也就收不到确认,就无法按照步骤进入CLOSE状态,即必须收到确认才能close。
  ②、防止“已失效的连接请求报文段”出现在连接中。经过2MSL,那些在这个连接持续的时间内,产生的所有报文段就可以都从网络中消失。即在这个连接释放的过程中会有一些无效的报文段滞留在楼阁结点,但是呢,经过2MSL这些无效报文段就肯定可以发送到目的地,不会滞留在网络中。这样的话,在下一个连接中就不会出现上一个连接遗留下来的请求报文段了。
  4、为什么要采用三次握手,两次不行吗?
  若A发送的第一个请求报文滞留,延误到连接已经释放的某个时间点到达B,而B以为是新的连接,进行连接,但是A不会建立连接,则B一直处于等待转态。
  5、我们如何判断有一个建立链接请求或一个关闭链接请求:
  建立链接请求:connect将完成三次握手,accept所监听的fd上,产生读事件,表示有新的链接请求。
  关闭链接请求:close将完成四次挥手,如果有一方关闭sockfd,对方将感知到有读事件,如果read读取数据时,返回0,即读取到0个数据,表示有断开链接请求。(在操作系统中已经这么定义)

十三 串包

  有时候,我们以API的方式为客户提供服务,如果此时你提供的API采用TCP长连接,而且还使用了TCP接收超时机制(API一般都会提供设置超时的接口,例如通过setsockopt设置SO_RCVTIMEO或这select),那你可能需要小心下面这种情况(这里姑且称之为“窜包”,应用程序没有将应答包与请求包正确对应起来):
  如果某一笔以TCP接收的请求超时(例如设置为3秒)返回客户,此时客户继续使用该链接发送第二个请求,此时后者就有可能收到前一笔请求的应答(前一笔的应答在3秒后才到达),倘若错误的将此应答当做后者的应答处理,那就可能会导致严重的问题。如果网络不稳定,或者后台处理较慢,超时严重,其中一笔请求应答窜包了,很可能导致后续多个请求应答窜包。例如网上常见的抽奖活动,第一个用户中了一个iPad,而第二个用户在后台中仅为一个虚拟物品,若此时出现窜包,那第二个用户也会被提示中了iPad。

  这个问题,初看起来最简单的解决办法就是:一旦发现有请求超时,就断开并重新建立连接,但这种方案理论上是不严谨的,考虑下面这种情况:
  1、应答超时的原因是因为应答包在网络中游荡(例如某个路由器崩溃等原因,这类在网络中游荡的包,俗称迷途的分组);
  2、API在检测到超时后,断开并重新建立的连接的IP和Port与原有连接相同(新连接为被断开连接的化身);
  3、在新连接建立后,立即发送了一个新的请求,但随后那个迷途的应答包又找到了回家的路,重新到达,此时新连接很有可能将这个不属于自己的包,当做第二个请求的应答(该包的TCP Sequence恰好是新连接期望的TCP Sequence,这种情况是可能的,但是基本不可能发生)。
  注:正常情况下,TCP通过维持TIME_WAIT状态2MSL时间,以避免因化身可能带来的问题。但是在实际应用中,我们可以通过调整系统参数,或者利用SO_LINGER选项使得close一个连接时,直接到CLOSE状态,跳过TIME_WAIT状态,又或者利用了端口重用,这样就可能会出现化身。在实际应用中,上面这种情况基本不会发生,但是从理论上来说,是可能的。
再仔细分析,就会发现这个问题表面上看是因为“窜包”导致,但本质原因是程序在应用层没有对协议包效验。例如另外一种情况:A、B两个客户端与Server端同时建立了两个连接,如果此时Server端有BUG,错将A的应答,发到B连接上,此时如果没有效验,那同样会出现A请求收到B应答的情况。所以这个问题解决之道就是:在应用层使用类似序列号这类验证机制,确保请求与应答的一一对应。

十四 问题

  tcpdump中发现icmp host unreachable admin prohibited,分析导致这个问题的原因是防火墙中阻止了ICMP消息,在iptables中开放icmp就没有问题了。
  打开/etc/sysconfig/iptables,注释以下行:-A RH-Firewall-1-INPUT -j REJECT --reject-with icmp-host-prohibited,然后重新启iptables,再刷新postfix队列。
  iptables-save -c > /etc/iptables-rules:将规则保存到iptables-rules文件,重启一个服务:service iptables restart
  iptables:静态防火墙,过滤数据包,可以设置规则。
  RTT:给定连接的往返时间。
  重传多义性:分组在rtt时间内没有到达,增减rtt的值,下次到达的ack是上次的还是第二次的,不能更行rtt的值。

十五 sockaddr、sockaddr_un、sockaddr_in、sockaddr_in6、struct in_addr、struct in6_addr的区别与使用

因篇幅问题不能全部显示,请点此查看更多更全内容

Top