网络编程相关知识杂记(TCP&UDP&IP理论相关)

Posted by W-M on December 3, 2017

本文主要记录了个人在学习网络编程相关知识时遇到的问题及自己的思考,主要用于备忘,错误难免,敬请指出!


TCP、UDP、IP理论相关

IP层相关

IP层采取的策略是尽力传递,不保证数据报能否到达目的端,也不能保证按照发送时的顺序到达目的端。

一个网络层的数据报可以通过下层网络中几个不同的网络封装成帧数据进行传输,如超级通道、以太网、ppp等。超级通道网络对于一帧数据最大字节数限制为65535,而以太网限制为1500个字节。

IP分片:为了使IPV4协议与物理网络无关,协议设计者决定使IPV4数据报的最大长度等于下层网络中各种协议中一帧支持的最大字节数,即超级通道中的65535个字节。如果我们数据报的长度大于1500个字节,那么在通过以太网时可能出现问题,因为以太网限制一帧长度最大为1500个字节。为了使数据报可以顺利通过类似于以太网这样的物理网络,我们就需要对数据报进行分割,使其能通过这些网络,这个过程称为分段

在IPV4中,数据报可能被主机或其路径中的任何路由器进行分段。然而,数据报的重组只能在目的主机上进行,因为每个分段都是一个独立的数据报。属于同一数据报的所有分段最后都必须到达目的主机,所以从逻辑上说数据报的重组是在目的端的传输层。对于重组后缺少某个分段的数据报,在进行校验时会检验出差错,之后传输层根据使用协议的不同可能会选择悄悄丢弃此数据报或者请求重传此数据报。

使用UDP很容易导致IP分片,而使用TCP则很难导致IP分片。这是因为在TCP建立连接的过程中会协商一个MSS字段,通过这个MSS字段来控制最大的TCP段长度,(比如基于以太网进行传输的TCP MSS字段大小一般为1460,一个TCP段大小最多为1460个字节)可以防止由于TCP段中数据过多导致在IP层进行分片传输,在目的端重组带来的性能损失。在UDP传输过程中,鉴于Internet上的标准MTU值为576字节,所以建议在进行Internet的UDP编程时,最好将UDP的数据长度控制在 (576-8(UDP头部)-20(IP头部))548字节以内以尽量减少分片。

关于MTU与MSS还可参考 :以太网中的MTU与MSS

UDP相关

(1)用户数据报协议(UDP)又称为无连接不可靠传输层协议,相比于网络层,除了提供进程到进程间通信和非常简单的差错检验机制外没有增加任何东西。UDP分组称为用户数据报,有8字节的固定头部。UDP数据报头部中TotalLength字段为16位,限制了用户数据报总长度为0到65535个字节,实际上总长度必须比这个要小,因为UDP数据报在向外传输时还要封装在总长度限制为65535字节的IP数据报中。一个UDP数据报示例如图1:
UDP数据报示意图

图1:UDP数据报示意图

(2)无连接服务:UDP提供无连接服务,这就表示UDP发送出去的每一个用户数据报都是一个独立的数据报。不同的用户数据报之间没有关系,即使它们都来自相同的源进程并发送到相同的目的程序。用户数据报不进行编号,也没有像TCP协议那样的连接建立和连接终止。

无连接的一个结果就是使用UDP的进程不能向UDP发送数据流,也不能期望UDP将这个数据流分割成许多不同的相关联的用户数据报。相反,每一个请求必须足够小(必须小于65535字节),使其能够装入到用户数据报中,只有那些发送短报文的进程才应当使用UDP。

问题:客户端定义了一个基于UDP协议的Socket,如果一次请求给服务器传送的字节数大于65535个,在网络层会不会被划分为多个数据报后进行传输?如果被划分为了多个数据报进行传输,服务器是不是要分多次接收才能获得完整的报文呢?

答:如果大于65535个字节,被划分为多个数据报传输是肯定的。即使小于65535个字节被放到了一个数据报中向对方传输,如果数据报大于路径中的最小MTU,传输过程中此数据报也会被分片,在目的端重组为一个数据报。虽然UDP数据报理论最大长度是65507字节,但实际上总是比此要少的多。在很多平台上,实际的限制往往是8KB,并且不要求具体实现接收总长度超过576字节的数据报。为保证最大的安全性,UDP数据报的数据部分应保持为512字节或更少。

(3)流量控制与差错控制:UDP是一个简单的不可靠的传输协议,没有流量控制,因而也没有窗口机制。如果到来的报文太多时,接收方可能会溢出。如果发送报文速度太快导致发送方出队列溢出,操作系统就要求客户进程在继续发送报文之前要等待。

除校验和外,UDP也没有差错控制机制,这就表示发送方不知道报文是否丢失还是重复传递。当接收方使用校验和检测出差错时,就悄悄的将此用户数据报丢弃。

(4)数据报排队:在UDP中,队列是与端口的实现联系在一起的,如图2:
UDP中的队列

图2:UDP数据报排队示意图

客户端:

在客户机端,当进程启动时,它从操作系统请求一个端口号。有些实现是创建一个入队列和一个出队列与每一个进程相关联,而有些实现只创建与每一个进程相关的入队列。入队列用来接收外部的数据,出队列用来通过端口向外发出数据。UDP中,即使一个进程想与多个进程通信,它也只得到一个端口号,最后也只有一个入队列与出队列。多数情况下,客户端打开的队列由暂时端口号来标识,只要进程在运行,这些队列就起作用,当进程终止时,队列就撤销。

在客户端发送数据的过程中,客户进程使用在请求中指明的源端口号将报文发送到出队列。UDP逐个将报文取出,将报文加上UDP头部后递交给IP层。出队列可能发生溢出,如果发生了溢出,操作系统就要求客户进程在继续发送报文之前要等待。

当报文到达客户端时,UDP要检查一下以确认对应于该用户数据报中目的端口号字段指明的端口号在客户端是否创建了入队列,如果有这样的入队列,UDP就将接收到的用户数据报放在该队列的末尾。如果没有这样的队列,UDP就丢弃该用户数据报,并请求ICMP协议向服务器发送端口不可达报文。所有发送给当前客户端特定客户程序的入报文,不管是来自相同的服务器还是不同的服务器,都被放在同一个入队列中。入队列可能会溢出,如果发生溢出,UDP就丢弃此用户数据报,并请求向服务器发送端口不可达报文。

服务器:

在服务器端,创建队列的机制是不同的。用最简单形式,服务器在它开始运行的时候就用它的熟知端口创建入队列和出队列,只要服务器运行,队列就一直是打开的。

当报文到达服务器进程时,UDP要检查一下以确认对应于该用户数据报中目的端口号字段指明的端口号是否创建了入队列,若有,则将接收到的用户数据报放在该队列的末尾。如果没有这样的队列,UDP就丢弃该用户数据报,并请求ICMP协议向客户端发送一个端口不可达报文。一个特定服务器程序的所有入报文,不管是来自相同的客户还是不同的客户机,都放入同一队列。入队列可能溢出,如果发生溢出,UDP就丢弃这个用户数据报,并请求向客户发送端口不可达报文。

当服务器想要回答客户端时,它就使用在请求报文中指明的源端口号将报文发送到出队列。UDP逐个将报文取出,加上UDP的头部,递交给IP。出队列可能溢出,如果发生溢出,操作系统就要求服务器进程在继续发送报文之前要等待。

问题:UDP客户端与服务端都是只占用操作系统的一个端口来提供服务吗?客户端想与多个不同服务器上的进程通信,也仅占用客户端的一个端口号?

问题:为什么UDP可以做到保留数据边界,是怎么实现的?UDP发送队列与接收队列中的数据都包含UDP请求头部吗?如果不包含,是如何区分数据边界的呢?

UDP应用举例:TFTP(简单文件传输协议基于UDP自行实现差错控制和基于停止等待协议的流量控制)

TCP相关

(1)简单来说,TCP被称为面向流的,有连接的、可靠的传输协议。它为IP服务增加了面向连接性和可靠性的特性。面向连接可以体现在建立连接时的三次握手、关闭连接时的四次挥手、对于每一个连接在服务器和客户端都有一套独立的数据发送与接收数据的缓冲区等,可靠性体现在基于发送与接收缓冲区实现的滑动窗口进行的流量控制,通过网络情况进行的拥塞控制,还有通过差错控制来防止数据的重复、丢失、乱序。

(2)下图所示是一个使用TCP协议进行进程到进程之间数据传输的宏观流程。 TCP数据传输过程

图3:TCP数据传输宏观流程

首先需要注意一点,TCP服务是全双工的,即数据可以在同一时间双向流动。所以对于建立连接的两个进程都既有发送缓冲区,也有接收缓冲区。

发送进程向发送缓冲区中写入字节流,接收进程从接收缓冲区中读出字节流。为什么需要发送缓冲区和接收缓冲区呢?因为发送和接收进程可能以不同的速度写入和读出数据,所以TCP需要用于存储的缓冲区来进行流量控制和差错控制。

发送缓冲区中数据可能分为三种类型:空存储单元:图中白色的部分,等待由发送进程(生产者)填充; 灰色部分用来保存已经发送的但还没有得到确认的字节,TCP在缓冲区中保留这些字节,直到收到确认为止;粉色缓冲区是将要由TCP发送的字节。 但是如果接收进程接收缓慢或者网络中产生拥塞,发送进程对于粉色区域的部分字节可能暂时不会发送。灰色存储单元中保存的已经发送出去的字节可以回收并且对发送进程可用,这就是图中表示为一个环形缓冲区的原因。

接收端的缓冲区比较简单。环形缓冲区分为两个区域,白色区域为等待接收数据进行填充的存储单元;粉色区域表示已经填充了数据但是还没被接收进程读出数据的存储单元。当粉色区域某个字节被接收进程读出之后,这个存储单元可被回收,并被加入到空存储单元池中。

(3)TCP分组

尽管缓冲能够处理生产进程速度与消费进程速度之间的不相称问题,但在发送数据之前,还需要多个步骤。IP层作为TCP服务的提供者,需要以分组的方式而不是字节流的方式发送数据。在传输层,TCP将缓冲区中多个字节分组合在一起成为一个分组。如图3所示,在使用TCP协议进行数据传输的过程中涉及到了Segment(段)的概念,TCP中的分组称为段,段的格式如图4所示:
TCP段格式

图4:TCP段格式

TCP给每个段添加头部(为了控制的目的),并将该段传递给IP层。段被封装到IP数据报中,然后再进行传输。

TCP段中涉及到的内容较多,下面仅记录其中一部分的作用,具体参见《数据通信与网络》一书。

  • 源端口地址与目的端口地址:均为16位的字段,定义了在源主机中发送该TCP段的应用程序的端口号和目的主机中接收该TCP段的应用程序的端口号。

  • 序号与确认号:TCP在段的头部采用称为序号(Sequence number)和确认号(Acknowledgment number)的两个字段。这两个字段指的是字节序号。字节序号即TCP为在一个连接中传输的所有字节编号,在每个方向上序号都是独立的。当TCP接收来自进程的一些数据字节时,将它们存储在发送缓冲区时并对它们编号。不必从0开始编码,TCP在0到2^32-1之间生成一个随机数作为第一个字节的序号,例如生成的随机数为1057,并且发送的全部字节个数是6000,那么这些字节序号是1057-7056。随机生成序列号的好处是可能防止网络中被延迟的分组在以后被重传。每个段的序号指的就是这个段中的第一个字节的字节序号。确认号代表接收方预期接收的下一个字节的编号。正是通过序号、确认号配合缓冲区机制来实现流量控制和差错控制。

  • 头部长度:此处TCP与UDP有些不同,TCP段头部中仅记录了头部长度,并没有记录头部加数据的总长度。TCP段头部长度不固定,为20-60字节。头部长度这个4位的字段指明了TCP头部中共有多少个4字节长的字,由于头部长度在20字节到60字节之间,因此这个字段的值在5到15之间。

  • 窗口大小:这个字段定义对方必须维持的窗口的大小(以字节为单位)。字段长度16位,意味着窗口的最大长度是65535字节。这个值通常称为接收窗口(rwnd),它由接收方确定。此时发送端必须服从接收端的支配。

(4)TCP连接建立、关闭与数据传输流程

在TCP中,面向连接的传输需要三个过程:连接建立、数据传输和连接终止。
连接建立过程:连接建立三次握手过程如图5所示:
三次握手建立TCP连接

图5:使用三次握手建立TCP连接

三个阶段具体步骤如下:

  • 客户端发送的第一个段是SYN段,这个段仅有SYN标志被置位,它用于序列号同步。它占用一个序列号,实际数据传输开始时,在此序列号基础上加1。我们说SYN段不能携带实际数据,但我们可以认为它是一个字节,占用一个字节序列号。
  • 服务器发送第二个段,此段中SYN与ACK标志均置位。这个段有两个目的,一是作为向另一方向通信的SYN段,另一个目的是使用ACK标志作为对第一个SYN段的确认,占用一个序列号,此段也不能携带要传输的实际数据。
  • 客户端发送第三个段,这个段仅仅是一个ACK段,使用ACK标志和确认序号字段来确认收到了第二个段,此段已经可以携带客户端向服务器端要发送的数据,需要注意的是仅含有ACK的段并不占用序列号。

为什么建立连接要进行三次握手而不是两次握手呢?

这主要是为了防止已失效的连接请求报文段突然又传送到服务器,产生错误。

已失效的连接请求报文段的产生原因:当客户A发送连接请求,但因连接请求报文丢失而未收到确认。于是A会再次重传一次连接请求,此时服务器端B收到再次重传的连接请求,建立了连接,然后进行数据传输,数据传输完了后,就释放了此连接。假设A第一次发送的连接请求并没有丢失,而是在网络结点中滞留了太长时间,以致在AB通信完后,才到达B。此时这个连接请求其实已经是被A认为丢失的了。如果不进行第三次握手,那么服务器B可能在收到这个已失效的连接请求后,进行确认,然后单方面进入ESTABLISHED状态,而A此时并不会对B的确认进行理睬,这样就白白的浪费了服务器的资源。

SYN洪泛攻击:恶意攻击者将大量SYN段发送到一个服务器并在数据报中通过伪装源IP地址假装这些段来自不同的客户端时就发生了这种情况。服务器对于每一个请求都分配必要的资源如生成通信表和设置计时器等,然后回送SYN+ACK段给这些假装客户,但这些段都丢失了。然而,在这段期间,许多资源被占用但没有被使用。如果短时间内,SYN段数量很大,服务器可能会因为资源耗尽而崩溃。解决这个问题可以通过限制一段时间内的连接请求的数量或者推迟资源分配的方式。在Linux对于TCP/IP协议的实现中,服务器默认会进行5次重发SYN-ACK包,重试的间隔时间从1s开始,下次的重试间隔时间是前一次的双倍,5次的重试时间间隔为1s, 2s, 4s, 8s, 16s,总共31s,第5次发出后还要等32s都知道第5次也超时了,所以,总共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 63s,TCP才会断开这个连接。由于,SYN超时需要63秒,当短时间内出现大量这种情况时就会出现上述的SYN洪泛攻击问题。对于应对SYN过多的问题,linux提供了几个TCP参数:tcp_syncookies、tcp_synack_retries、tcp_max_syn_backlog、tcp_abort_on_overflow 来调整应对。

TCP建立连接的三次握手阶段为什么要协商一个随机的初始序列号(ISN),而不是默认从0开始?
每次建立连接前重新初始化一个序列号主要是为了通信双方能够根据序号将不属于本连接的报文段丢弃。比如:如果连接建好后始终用1来做ISN,如果client发了30个segment过去,但是网络断了,于是 client重连,又用了1做ISN,但是之前连接的那些包到了,于是就被当成了新连接的包,此时,client的Sequence Number 可能是3,而Server端认为client端的这个号是30了,这就会出问题。RFC793中说,ISN会和一个假的时钟绑在一起,这个时钟会在每4微秒对ISN做加一操作,直到超过2^32,又从0开始。这样,一个ISN的周期大约是4.55个小时。因为,我们假设我们的TCP Segment在网络上的存活时间不会超过Maximum Segment Lifetime(根据实现的不同设置在30s到2min之间),所以,只要MSL的值小于4.55小时,那么,我们就不会重用到ISN。

个人认为随机序列号可以减小上述问题发生的概率,而不能从根本上杜绝。就算每次重连使用不同的初始序列号,上次传输未到达的报文到达时还是可能与本次传输的报文序列号恰好一致,可以通过在报文中增加时间戳来解决这一问题?

TCP序列号回绕问题
序列号回绕问题具体表现为回绕序列号结合报文段延迟导致的重复报文段问题,这样的问题只会发生在相对高速的连接中。具体可参考:TCP头时间戳选项与回绕序列号

数据传输过程:数据传输具体过程如图6所示:
数据传输具体过程

图6:数据传输具体过程

在TCP连接建立后即可进行双向数据传输,客户端与服务器双方都可发送数据和确认。图中连接建立后,客户端用两个段发送了两千字节的数据,之后服务器用一个段携带两千字节的数据发送给客户端并对客户端之前的两个段进行了确认。客户端发送的最后一个段仅携带确认。

在这个过程中需要注意两点:一是可以对多个段一次性确认,比如服务器一次性对客户端发过来的两个段进行了确认;二是在段内携带确认时,在同一段中也可以携带数据,即数据捎带确认。

连接关闭过程:连接关闭可分为三次挥手和带有半关闭选项的四次挥手,先来看图7中的三次挥手:
三次挥手连接关闭具体过程

图7:三次挥手关闭连接
  • 正常情况下,客户端进程接收到一个关闭命令后,客户的TCP发送第一个段:FIN段,即其中的FIN位置位。注意:FIN段可包含客户机要发送的最后数据块,或者是仅仅发送如图7所示的控制段,如果它只是控制段,该段仅占用一个序列号。
  • 服务器TCP接收到FIN段后,通知它的进程,并发送第二个段:FIN+ACK段,证实它接收到来自客户端的FIN段,同时告知另一端连接关闭。这个段还可以包含来自服务器端的最后数据块,如果其不携带数据,则该段仅占用一个序列号。
  • 客户端的TCP发送最后一段:ACK段,证实它接收到来自服务器的FIN段。这个段包含确认号,它是接收到来自服务器FIN段的序号加1,这个段不携带数据也不占用序列号。

连接关闭具体过程

图8:四次挥手关闭连接

需要四次挥手关闭连接的原因是在TCP中,一端可以停止发送数据后,还可以继续接收数据。虽然任一端都可以发出半关闭,但是通常都是由客户端发起的。当服务器在开始处理之前需要所有数据时,就会出现半关闭。以排序为例,当客户端发送数据给服务器进行排序时,在开始排序之前,服务器需要接收到全部数据。这就是说客户端发送完全部数据之后,它在对外方向的连接就可以关闭了,但在向内方向,为了接受排序数据必须保持打开。服务器在接收到数据之后还需要时间进行排序,它的向外方向必须保持打开。图8中具体过程解释如下:

  • TCP连接两端都还处于ESTABLISHED状态,客户端停止发送数据,并发出一个FIN报文段,之后客户端进入FIN-WAIT-1状态。此FIN数据段占用一个序列号,此段还可包含客户机要发送的最后一个数据块。
  • 服务端回复客户端的确认报文段,之后服务器进入CLOSE_WAIT状态,客户端接收到服务器的ACK数据报后进入到FIN-WAIT-2状态。此时在客户端关于FIN-WAIT-2状态会有一个计时器,超过一段时间内没有接收到服务器发来的任何数据,就会放弃这个TCP连接。
  • 服务器处于CLOSE_WAIT状态向处于FIN-WAIT-2状态的客户端继续发送数据。当服务器发送完所有数据之后,向客户端发出FIN报文段,之后服务器进入LAST-ACK状态。
  • 客户端接收到服务器传来的FIN报文,发出ACK段进行回复,之后客户端进入到TIME-WAIT状态,服务端接收到客户端的ACK报文后进入CLOSED状态并关闭连接。
  • 客户端在TIME_WAIT状态结束后进入到CLOSED状态关闭连接。TIME-WAIT状态一般持续时间是2MSL(MSL为最大报文段生存时间 一般可为30s 1分钟或两分钟)

为甚么在客户端连接关闭的时候需要进入到TIME-WAIT状态等待一段时间呢?一般有以下两个原因:

  • 保证TCP协议的全双工连接能够可靠关闭。如果Client不经过TIME-WAIT状态直接CLOSED了,那么由于IP协议的不可靠性或者是其它网络原因,导致Server没有收到Client最后回复的ACK。那么Server就会在超时之后继续发送FIN,此时由于Client已经CLOSED了,就找不到与重发的FIN对应的连接,最后Server就会收到RST而不是ACK,Server就会以为是连接错误把问题报告给高层。这样的情况虽然不会造成数据丢失,但是却导致TCP协议不符合可靠连接的要求。所以,Client不是直接进入CLOSED,而是要保持TIME_WAIT,当再次收到FIN的时候,能够保证对方收到ACK,最后正确的关闭连接。
  • 保证这次连接的重复数据段从网络中消失。TCP分段可能由于路由器异常而“迷途”,在迷途期间,TCP发送端可能因确认超时而重发这个分段,迷途的分段在路由器修复后也会被送到最终目的地,这个原来的迷途分段就称为lost duplicate。在关闭一个TCP连接后,马上又重新建立起一个相同的IP地址和端口之间的TCP连接,后一个连接被称为前一个连接的化身(incarnation),那么有可能出现这种情况,前一个连接的迷途重复分组在前一个连接终止后出现,从而被误解成从属于新的化身。为了避免这个情 况,TCP不允许处于TIME_WAIT状态的连接启动一个新的化身,因为TIME_WAIT状态持续2MSL,就可以保证当成功建立一个TCP连接的时候,来自连接先前化身的重复分组已经在网络中消逝。

TIME-WAIT状态有什么危害呢?

主动关闭方进入 TIME-WAIT 状态后,无论对方是否收到 ACK,都需要等待2MSL。这期间不仅占用内存(系统维护连接耗用的内存),耗用CPU,更为重要的是,宝贵的端口被占用,端口枯竭后,新连接的建立就成了问题。之所以端口宝贵,是因为在IPv4中,一个端口占用2个字节,端口最高可以到65535。例如当客户端需要同时需要和大量服务器建立TCP连接时,由于TIME-WAIT状态的存在就可能造成端口不够用的问题。注意:TIME-WAIT状态出现于连接主动关闭方,也就是说当客户端主动关闭连接时,出现于客户端;而服务器主动关闭时,TIME-WAIT会出现在服务器端,所以对于服务器端来讲,短时间内关闭了大量的Client连接,就会造成服务器上出现大量的TIME_WAIT连接,严重消耗着服务器的资源。

RST报文可能被发送的几种情况:TCP协议中无论何时一个报文段发往一个基准的连接(由源IP地址 源端口号 目的IP地址 目的端口号)出现错误,TCP都会发出一个复位报文段。发出RST报文段可能有以下几种情况:

  • 到不存在的端口的连接请求:当一个数据报到达目的端口时,该端口并没有任何进程在监听,对于UDP将产生一个ICMP端口不可达的信息,对于TCP将使用回复复位报文的方式。
  • 异常终止一个连接。终止连接的正常方式是使用FIN报文段,使用RST报文终止连接相比于使用FIN报文有以下两个优点:一、应用程序会丢弃缓冲区中的任何待发数据并立即发送复位报文。二、对于接收端会区分另一端执行的是正常关闭还是异常关闭。
  • 开始时Client端与Server端正常通信,之后Client端网络连接断开,Server端如果一直不向Client主动发数据,会等到
  • -alive时间到期之时通过发送检测存活报文收到的RST报文才能得知Client连接断开,在Client网络断开期间,Server段向Client端发送的报文均会得到RST的回复。Client重新联网之后,可以先检测之前的连接是否可用,如果可用,可以复用之前的连接,否则需要和Server重新建立连接。

(5)TCP一次连接过程中可能涉及到的状态转换问题:
TCP状态转换

图8:TCP状态转换
TCP端口状态 描述
LISTEN 等待从任何远端TCP和端口的连接请求
SYN_SENT 发送完一个连接请求后等待一个匹配的连接请求
SYN_RECEIVED 发送连接请求并且接收到匹配的连接请求以后等待连接请求确认
ESTABLISHED 表示一个打开的连接,接收到的数据可以被投递给用户。连接的数据传输阶段的正常状态
FIN_WAIT_1 等待远端TCP的连接终止请求,或者等待之前发送的连接终止请求的确认
FIN_WAIT_2 等待远端TCP的连接终止请求
CLOSE_WAIT 等待本地用户的连接终止请求
CLOSING (同时关闭时)等待远端TCP的连接终止请求确认
LAST_ACK 等待先前发送给远端TCP的连接终止请求的确认(包括它字节的连接终止请求的确认)
TIME_WAIT 等待足够的时间过去以确保远端TCP接收到它的连接终止请求的确认
CLOSED 不在连接状态(这是为方便描述假想的状态,实际不存在)

(6)TCP中的交互数据流与成块数据流

基于TCP实现的应用层应用有些需要传输的是交互数据流(每次传输分组较小 如telnet,rlogin等),有些需要传输的则是成块数据流(每次传输的分组较大,一般为MSS 如FTP等)。TCP对于这两类数据通常情况下使用的处理算法有所不同,对于TCP成块数据流,通常使用滑动窗口协议进行流量控制;对于TCP交互数据流,通常采用Nagle算法。以RLogin为例,客户一般每次发送一个字节到服务器,这就会产生一些包含20字节IP首部、20字节TCP首部和1字节数据的41字节长的微小分组。在广域网上,为防止这些小分组增加拥塞出现的可能,一种简单的方法就是使用Nagle算法对其进行传输。

Nagle算法要求一个TCP连接上最多只能有一个未被确认的未完成的小分组,在该分组的确认到达之前不能发送其它的小分组。算法在上一个发出的分组的确认到达之前收集客户端之后到达的待发送的分组,并在收到上一个发出的分组的确认数据之后,将目前收集的待发送的分组以一个分组的方式发送出去。

Nagle算法的优越之处在于它是自适应的,确认到达的越快,数据也就发送的越快。而在希望减少微小分组数目的低速广域网上,通过合并待发送分组的方式可以发送更少的分组。

对于一些对于时延性要求很高的应用,有时我们也需要关闭Nagle算法。一个典型的例子是X窗口系统服务器,鼠标的移动必须无时间延迟的发送,以便为进行某种操作的用户提供实时的反馈。在Socket API中可以通过TCP_NODelay选项来关闭Nagle算法。

TCP发送方通过对应用层数据分组大小进行多次判断(一般分组大小都是和MSS做比较的),以在Nagle和滑动窗口协议之间抉择到底选择哪一种控制方式进行发送。

(7)通过滑动窗口进行流量控制,滑动窗口具体算法举例?
滑动窗口协议加速数据传输:允许发送方在停止并等待确认前可以连续发送多个分组。由于发送方不必每发送一个分组就停下来等待确认,因此该协议可以加速数据的传输。简而言之,滑动窗口是通过控制窗口大小进而控制发送方待发送数据的多少达到流量控制的目的。
TCP连接双方滑动窗口示意图

图9:TCP连接双方滑动窗口示意图

(个人理解)在图9中可以看到,滑动窗口是基于TCP连接双方中的发送缓冲区抽象出来的,发送方每次仅发送滑动窗口中满足发送条件的数据;但滑动窗口的大小却不由发送缓冲区决定,而是由接收缓冲区通告的当前可接受的字节数与当前网络拥塞程度共同决定的。

滑动窗口可以是合拢的、张开的、或者是收缩的,也就是说窗口左边沿仅可以向右移动,窗口右边沿既可以向右又可以向左移动。左边沿不能向左移动是因为这宣告先前已经由接收方确认过的数据无效需要重新发送。 具体示意图如下:
滑动窗口状态转化示意图

图10:滑动窗口状态转化示意图

一个具体使用滑动窗口进行数据传输的具体示例如下(示例中假设网络状况良好,即窗口大小仅由接收缓冲区大小决定):
滑动窗口数据传输示意图

图11:滑动窗口数据传输示意图

图11传输过程中滑动窗口变化示意图如下:
滑动窗口变化示意图

图12:滑动窗口变化示意图

从图11与图12中可以看到滑动窗口合拢与张开的示例。合拢通常是因为接收方应用对于其接收缓冲区内的数据没有及时接收或者是接收方当前TCP协议的实现是仅当TCP接收缓冲区中数据满时才将其交给上层应用;张开的一种情况是TCP接收窗口中的数据被上层应用处理完成后告知发送方导致窗口张开;右边沿向左移动导致窗口收缩???

对于数据发送方,不必每次发送一个全窗口大小的数据,对于数据接收方,也不必等待接收缓冲区被填满才发送一个ACK。

由接收方提供的窗口大小通常可以由接收进程控制,上层应用可以通过socket API控制发送、接收缓冲区的大小来增加性能。

TCP Header中有一位代表PUSH标志,此标志的作用为通知接收方将当前缓冲区中的所有数据全部提交给接收进程而不是等到缓冲区满才提交。如果待发送数据将清空发送缓存(socket.flush或者close),则当前发送缓冲区中的所有数据在被发送给接收方之前可有可能被加上PUSH标志。

前面提到过滑动窗口大小是由接收方缓冲区大小和网络拥塞情况大小来共同控制的。上例就是一个由接收方控制发送方窗口大小的示例。那么为什么还要由网络拥塞情况来控制滑动窗口大小呢?又如何进行控制呢?

(个人理解)如果TCP协议对于网络拥塞情况没有流量控制,则可能发生这种情况:某一时间内使用网络的用户很多,导致网络中分组数量达到载荷,之后路由器由于处理不过来就会丢弃一些分组;TCP协议超时检测到这些分组被丢弃了之后选择重发这些分组,这样路由器即使丢弃了这些分组,网络载荷也不能减少,因为TCP协议还会重发它们,这就造成了一个恶性循环。为了防止这种情况的发生,TCP需要对网络状况进行检测,当检测到可能有拥塞时,就可以通过控制滑动窗口大小减少发送的分组数量来缓解网络拥塞。

TCP处理拥塞的一般策略基于三个阶段:慢启动、拥塞避免和拥塞检测。在慢启动阶段,发送方用很慢的传输速率启动,但迅速的增加到阈值。达到阈值后,为了避免拥塞而降低传输速率增加速度。最后,如果检测到拥塞,则发送方基于如何检测到拥塞而返回到慢速启动或拥塞避免阶段。

  • 慢启动:算法开始时设置拥塞窗口大小为一个最大段长度(MSS),这个MSS是连接建立期间由最大段长度选项所决定的。每次接收到一个确认时,窗口大小增加一个MSS值。正如前面所说,这里的慢指的是开始时的传输速率很慢,但之后的增加速度很快,是按指数规则增长的。具体示例如下(示例中假设rwnd比cwnd大得多,每段都是单独进行确认): 慢启动数据传输示意图
图13:慢启动数据传输示意图
  • 拥塞避免:加性增加 以慢启动算法开始,拥塞窗口大小按指数规律增长。为了在拥塞发生之前避免拥塞,必须降低指数增长的速度。TCP定义了另外一个算法即拥塞避免,这个算法是加性增加的。当拥塞窗口的大小达到慢速启动的阈值时,慢速启动阶段停止,加性增加阶段开始。在这个算法中,每次整个窗口所有段都被确认时,拥塞窗口才增加1。(个人理解:拥塞窗口线性增加直到检测到拥塞或者拥塞窗口大于接收方缓冲区大小),拥塞避免算法具体示例如下(拥塞避免算法通常开始时窗口大小是大于1的,下图中仅为示例):
    拥塞避免数据传输示意图
图14:拥塞避免数据传输示意图
  • 拥塞检测:乘性减少 如果检测到了发生拥塞,拥塞窗口大小必须减小。发送放能推测出发生拥塞现象的唯一方法是通过重传段的要求。重传可能在两种情况下发生:重传计时器到时或者接受到了三个ACK(见差错控制中的快速重传)。在这两种情况下阈值就减少一半,即乘性减少。大多数TCP实现包含两个反应:
    一、如果通过计时器到时检测到拥塞,那么存在着非常严重的拥塞的可能性;一个段可能已经在网络中丢失并且没有关于该发送段的消息。在这种情况下,TCP做出强烈的反应:

    (a)设置阈值为当前拥塞窗口大小的一半;
    (b)设置cwnd为一个段的大小;
    (c)启动慢速启动阶段。

  二、如果接收到三个ACK,那么存在轻度拥塞的可能性。一个段可能已经丢失,但自从接受到三个ACK后,在丢失段之后发送的一些段有些可能已经安全到达并由接受方保存。这称为快速重传和快速恢复。在这种情况下,TCP做出轻度的反应:

    (a)设置阈值为当前拥塞窗口大小的一半;
    (b)设置cwnd为阈值;
    (c)启动拥塞避免阶段。

  具体示例如下(假定最大窗口大小为32个段):
拥塞检测数据传输示意图

图15:拥塞检测数据传输示意图

(8)TCP差错控制

TCP中的差错控制机制通过三种工具配合完成:校验和、确认、超时。

  • 校验和:每个段都包含校验和字段,用来检查收到损坏的段。如果段被损坏,它将被目的端TCP丢弃,并认为是丢失了。
  • 确认:TCP使用确认方法来证实收到了数据段。不携带数据但占用序列号的一些控制段也要确认(如syn,fin),但ACK段若不捎带数据则不需要进行确认。
  • 重传:差错控制的核心机制是段的重传。当一个段损坏、丢失或延迟时,就重传这个段。在当前实现中,有两种情况要重传段:重传计时器到时或当发送方收到三个重复ACK时。

一、重传计时器到时导致重发
重传计时器到时导致段重发

图16:重传计时器到时导致段重发

在图16中可以看到第三个报文段丢失后由于直到其对应的RTO(重传计时器)到时仍没有收到其对应的ACK,所以选择重发此报文段。图中有两个地方需要注意,一是TCP中,RTO的值是动态的,根据段的往返时间(RTT)进行更新,二是接收方即使没有收到第三个段,对于第四个段会先进行暂存,要求重传第三个段,等到第三个段重传后接收缓冲区中段有序再将其提交给上层应用。

二、3个重复ACK段之后的重发(快速重传机制)

如果RTT的值不是很大,则仅依靠RTO计时器判断是否需要重传某个段是可行的,但假设RTT值很大,在某个段丢失之后接收方又收到了很多无序的段,它们不可能都被存储。这就要求发送方尽可能快的检测到丢失的段并进行重传,即接收到三个ACK之后不必等待RTO超时,直接重传ACK要求的报文段。
快速重传

图17:快速重传

更多关于TCP协议超时机制信息,可参考:TCP协议的那些超时

(9)TCP粘包拆包问题发生原因

TCP面向字节流传输,并且不在字节流中插入记录标识符。如果一方的应用程序先传10字节,又传20字节,再传50字节,连接的另一方无法了解发送方每次发送了多少字节。收方可以分4次接收这80个字节,每次接收20字节。对于应用程序来讲,发送方认为自己发送了3个数据包,每个包大小为10、20、50字节,而接收方接收到的是4个大小为20个字节的数据包,此时在应用层看来就发生了拆包/粘包问题。

(10)TCP长连接和短连接的区别?TCP长连接的保持消耗流量不,有什么优劣势?

TCP短连接示例:client向server发起连接请求,server接到请求,然后双方建立连接。client向server发送消息,server回应client,然后一次读写就完成了,这时候双方任何一个都可以发起close操作,不过一般都是client先发起close操作。为什么呢,一般的server不会回复完client后立即关闭连接的,当然不排除有特殊的情况。从上面的描述看,短连接一般只会在client/server间传递一次读写操作。

TCP长连接示例:client向server发起连接,server接受client连接,双方建立连接。Client与server完成一次读写之后,它们之间的连接并不会主动关闭,后续的读写操作会继续使用这个连接。

TCP长连接适用于并发量小、交流频繁、每次传输数据量小的应用场景,由于服务器保持长连接会一直占用服务器资源(需要为每个连接保存连接缓冲区、连接状态、计时器等资源),WEB网站并发量大时可能由于同时维持的连接数太多导致服务器资源耗尽而崩溃。举例来说,对于数据库的TCP连接一般使用长连接,而WEB网站一般使用TCP短连接。

(个人理解)由于TCP的保活机制,服务器对于长连接在指定一段时间(一般为2小时)内没有任何交互的情况下才会发送一个探查报文,服务器对于维持一个TCP的长连接流量上消耗应该是不大的。

更多关于TCP长短连接及TCP保活机制(受DHCP和NAT Session影响)可参考:TCP长连接与短连接的区别长连接及心跳保活原理简介TCP keepalive的探究 (1) : NAT和保活机制

问题:socket编程中如何为建立的TCP连接指定保活时间?Http的keep-alive如何复用一个TCP连接?

问题:考虑这种情况,客户端与服务器之间保持一个连接,之后客户端网络连接断开,由于TCP中keep-alive在指定一段时间过后客户端没有给服务器发消息,服务器才会检测客户端是否存活,尝试断开连接;也就是说在客户端网络连接断开与服务器检测其是否存活这段时间中服务端对于这个TCP连接是保留的;

如果在客户端网络连接断开到服务器检测客户端是否存活这段时间内客户端又有网络连接了,尝试重新连接服务器,能否复用之前的网络连接呢?(我觉得不能,因为这时客户端尝试建立新连接会给服务器发送syn包,而服务器与此客户端之前的连接握手阶段肯定已经完成了);如果不能复用之前的连接,客户端此次尝试建立新的连接能否成功?(我觉得是可以的);如果建立连接可以成功,客户端如何在(源IP、源端口、目的IP、目的端口、协议)都相同的情况下区分之前与此客户端的连接和新建立的连接,还是客户端建立新连接的时候一定会使用与之前不同的端口呢?

(11)相比于使用UDP,为什么TCP编程更占用内存空间,UDP效率更高?

(12)对于TCP服务端来讲,对于多个TCP客户端的连接,在服务端会使用同一个端口号进行服务;而对于客户端来说,若在客户端建立多个进程进行不同的TCP连接,会占用客户端的不同端口,若在客户端的同一进程内与多个不同TCP服务器建立连接,每个连接也会占用此客户端的一个端口吗?对于TCP服务器,建立的每一个与客户端的连接都会创建一个新的进程吗?

自写的一个TCP并发服务器应该调用一个新的进程或线程来处理每个客户请求来防止呼入连接请求队列超时。

(13)TCP段中的URG标志与PSH标志

  • TCP段中使用PSH标志可以使得发送端不必等待滑动窗口被填满,创建一个段就立即将其发送;接收端看到段中的PSH标志位,就明白这个段所包含的数据必须尽快的传递给接收应用程序,而不要等待更多数据的到来。
  • TCP中使用URG的一种情况是数据发送方在发送大量数据之后,发现出现了某些差错,希望终止此过程,这时发送端就可以发送一个URG置1的段,当接收端的TCP接收到此段时,就可以利用紧急指针的值从段中提取出紧急数据,并不按序的将其传递给接收应用程序。

应用程序中并不能设置PSH标志,比如java并没有提供设置PSH标志的API,一般PSH标志由操作系统内核设置;java中提供的sendUrgentData方法发送的也不是符合TCP规范的URG标志置位的数据。

(14)OSI模型运输层的实现是将连接请求的到达与接受分开,应用程序可以选择是否接收一个连接;但TCP/IP模型不是这样,当请求到达应用层时,TCP三次握手已经完成,连接已经建立起来了!如果此时应用程序不想为此客户端服务,服务器所能做的就是关闭连接(发送FIN),或者复位连接(发送RST)。

问题:OSI七层模型与TCP/IP四层模型在具体实现上有何差异???
目前网络中TCP/IP协议族成为占统治地位的业务体系结构,而OSI模型却从未实现过。


Http理论相关