TCP协议

TCP的报头

image

  1. Source Port,Dest Port:源端口和目的端口,和IP层的源IP和目的IP来唯一确定一个socket。
  2. Sequence Number:包的序列号,主要用来解决乱序的问题。
  3. Acknowledgement Numbe:就是ACK——用于确认收到,用来解决不丢包的问题,也会用来进行窗口更新。
  4. Window:滑动窗口,通过接受方来控制发送方,达到流量控制的目的。
  5. TCP Flags:TCP的标志比特位。

TCP状态机

TCP是一个面向连接、可靠的字节流协议,其实在网络传输中是没有连接的概念,TCP所谓的连接,提现在它的两端所维护的一个状态机上。

image

TCP建立过程

image

三次握手过程,主要是通信的双方要互相通知对方自己的初始化的Sequence Number(缩写为ISN:Inital Sequence Number)——所以叫SYN,全称Synchronize Sequence Numbers。也就上图中的 x 和 y。这个号要作为以后的数据通信的序号,以保证应用层接收到的数据不会因为网络上的传输的问题而乱序。

  • 讨论:为什么是三次握手过程,而不是两次握手?
  1. 首先,必须至少两次才能建立连接。而且最后一个ACK始终有可能丢失,奇数次建立链接和偶数次建立连接是不同的
  2. 讨论为什么不是偶数次,试想如果偶数次发送最后一个ACK,那么此时服务器端建立了连接,客户端还没有建立连接。假设此时客户端下线,那么服务端并不知道,只能保持这个连接,极大地浪费服务端的资源。
  3. 如果是奇数次连接,就不会出现上述情况。所以奇数次握手是为了保护服务端。
  • 连接建立超时
    TCP的三次握手协议等价于两次等待,第一次等待是客户端发送了SYN后,等到ACK和对端SYN报文,第二次等待是服务端发送了ACK和SYN后,等待客户端的ACK。一般来讲,等待就意味着可能超时。
    如果是客户端发送SYN以后的等待超时,那么客户端每隔几秒重发SYN报文,直到经过75秒。
    如果是服务器发送SYN以后等到超时,服务器会以1S,2S,4S,8S,16S,32S为间隔重发,整个过程一共63秒,这对服务器资源是一种极大的浪费。

断连过程

image

  • 为什么是四次挥手

由于TCP是全双工的,允许在一段关闭发送通道,表示不再有数据的发送,而接受数据的通道是可以继续使用的。

  • socket的优雅关闭和暴力关闭,close和shutdown的区别
  1. close实际做的事是将socket_fd的内部引用计数值减1,如果内部引用计数不用0,则不做任何其他操作;当引用计数为0时,释放掉sock_fd所占资源,关闭对应的TCP连接。
  2. shutdown不管sock_fd的引用计数值是多少,只是单纯的向对方发送一个FIN报文,断开TCP连接。

当调用shutdown时,只是关闭了TCP连接的某个方向,具体方向视参数而定,对于sock_fd本身所占用的资源,不做任何工作。

当调用close时,如果引用计数变为0,那么会释放sock_fd所占用的资源,而TCP连接是sock_fd所占用资源的一部分,所以隐含了TCP的断连过程。

在客户端close一个sock_fd以后,会将这个sock_fd标记为INVALID SOCKET,但是服务器端收到FIN报文后仍然可以通过其所在端的sock_fd发送数据,但是当客户端TCP协议栈收到数据后,发现这是一个无效的socket,因此会返回一个RST报文。

TIME_WAIT状态

TIME_WATI状态主要是产生在TCP连接主动断开方,并且在发送最后一个ACK以后达到这个状态,并且TIME_WAIT会持续2MSL的时间,结束后,处于CLOSED状态。

  • 讨论:TIME_WAIT状态的必要性
  1. 保证最后一个ACK能够到达被动断开方。如果不保持这个状态,假设最后一个ACK丢包,那么超时后被动断开方又发送一个FIN,这是因为连接已经终止,这样不够优雅。
  2. 最重要的原因是,让这个连接所有的数据都在网络中消失。防止产生一个相同四元组的连接,

TCP的数据交互

TCP是可靠的传输协议,那么必须要有重传的机制。TCP的重传机制是基于ACK的。我们知道ACK表示的意思是对已经收到数据的确认,并且还表示接下来想要收到的数据的SEQ。

超时与重传

TCP的基础重传策略是基于定时器的超时机制的,每当TCP发送了一个报文时,会为这个报文设置一个大小为RTO(重传超时时间)的定时器。

通过RTT(一个数据包从发送出去到ACK回来的时间)动态改变RTO的时间。

  • RTT的测量:
    image

在图(a)中,如果按照第一次发送和收到ACK之间的时间差,那么显然,由于第一次发送的报文的ACK丢失,所以,ACK其实去重传报文的ACK,这个时间差显然太大。如果按照重传和ACK之间的时间差,那么会出现(b)中的情况,又会出现时间差太小的情况。

因此,为了简化RTT的测量:

  1. 忽略重传的报文,只计算正常的报文。
  2. 在一个TCP连接上同时只能有一个计时器在测量RTT。

补充

  1. TCP还有另外的重传机制,例如连续接受三次ACK.
  2. SACK汇报已经接受到的序列号

流量控制

TCP头里有一个字段叫Window,这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。

TCP的滑动窗口协议主要有两个作用,一是提供TCP的可靠性,二是提供TCP的流控特性。同时滑动窗口机制还体现了TCP面向字节流的设计思路。TCP会话的双方都各自维护一个“发送窗口”和一个“接收窗口”。

发送窗口:

image

发送缓冲区一共有4段:已经发送并得到对端ACK的”,“已经发送但还未收到对端ACK的”,“未发送但对端允许发送的”,“未发送且对端不允许发送”。“已经发送但还未收到对端ACK的”和“未发送但对端允许发送的”这两部分数据称之为发送窗口(中间两部分)

发送窗口更新规则:

  1. 当ACK=36且window size = 14,左边沿滑动到35后面,右边沿滑动到49后面。
  2. 当ACK=36且window size = 10,左边沿滑动到35后面,右边沿不动。
  3. 当发送了两个字节后,中间蓝绿区域中间的线移动到47后面。

接受窗口

image
接收方每次向发送方发送ACK时,都会向发送方汇报己方的接收窗口大小,发送方会根据接收方的窗口大小(实际上会去拥塞窗口和接收窗口的最小值)来控制发送数据的大小,以保证接收方可以及时处理。
image

接受窗口为0

  1. 坚持定时器
    当接收窗口大小为0时,由于不能接收到任何数据,不能接收数据也就意味着不能发送ACK,那么当服务器处理了部分数据,接收窗口不为0时如何通知发送方。

    发送方会定时使用一个坚持定时器来周期性地向接收方查询,以便发现窗口是否增大,这种报文叫窗口探查,该报文包含一个字节的伪数据,这一个字节的数据是必须的,因为TCP不会对一个不带数据的报文进行ACK确认。接收方收到这个报文后,返回一个ACK,ACK中带有接收窗口大小。

  2. 糊涂窗口综合症
    如果服务器返回的接受窗口只有很小的字节,按照发送原则,只能发送很小的字节。但是我们的TCP/IP头部都有40个字节,这样发送效率太低。所以,接受方要有足够大的空间才通知发送方,而发送方有比较多的数据才发送。

  • 对于接受方

    1. 只有可用空间大于等于一个MSS时才通知发送方。
    2. 接收缓冲区有超过一半为空时才通知发送方。
  • 对于发送方

    1. 要发送的报文长度大于一个MSS才发送。
    2. 可以发送至少是接收方通告窗口大小一般的报文段才发送。
    3. 没有还没被确认的数据,也就是收到之前发送数据的所有ACK。

发送方的策略就是著名的Nagle算法。但是对于一些实时交互应用,Nagle算法需要关闭。

拥塞处理

流量控制依赖于发送端和接收端,和网络中的状态无关。拥塞控制用于在网络状态不好,发生丢包的时候处理。

慢启动(指数增长)

  1. 对于刚建立好的连接,先初始化拥塞窗口cwnd=MSS,即一个报文段的大小,初始化慢启动门限ssthresh,一般为65535,都以字节为单位。
  2. 每收到一个ACK,拥塞窗口就增加一个MSS。
  3. 当cwnd>=ssthresh时,启动拥塞避免算法。

拥塞避免算法(线性增长)

每收到cwnd/MSS个ACK,cwnd增加MSS个字节

拥塞发生时算法

分为两种情况,一为未收到ACK,即超时重传;二为收到3次重复的ACK

  1. 超时重发
    1. ssthredsh = cwnd / 2
    2. cwnd = MSS
    3. 进入慢启动的过程
      image
  2. 收到3次重复的ACK(说明网络并不是那么糟糕)
    1. cwnd = cwnd / 2
    2. ssthresh = cwnd
    3. 进入快速恢复算法
  • 快速恢复算法

    1. 设置cwnd = cwnd + 3 * MSS,3个MSS对应3个重复的ACK。
    2. 以后每收到一个重复的ACK,cwnd = cwnd + MSS。
    3. 如果收到的是一个新的ACK,那么cwnd = ssthresh,进入拥塞避免算法。
      image
  • 讨论:快速重传为什么选择三次重复ACK来确定,而不是两次或者四次

重复ACK次数越多,由丢包引起重复的概率就越大,但是次数越多,丢包响应速度就越慢。3次是一种很好的折中。

TCP避免分段

MSS

TCP数据包每次能够传输的最大数据分段。通讯双方会根据双方提供的MSS值得最小值确定为这次连接的最大MSS值。

分片

在IP层有一个数据报要进行传输,但是数据的大小比链路上的MTU还要大,那么这是就必须将数据分成若干片进行传输。通常UDP是要进行分片的,因为UDP在传输的时候,本身不会将报文分段,报文长度可能会远远超过链路的MTU,所以UDP的传输依赖IP层的分片进行。

分片的重组

IP数据报进行分片以后,由到达目的端的IP层来进行重新组装,其目的是使分片和重新组装过程对运输层(TCP/UDP)是透明的。由于每一分片都是一个独立的包,当这些数据报的片到达目的端时有可能会失序,但是在IP首部中有足够的信息让接收端能正确组装这些数据报片。

由于IP分片数据再对端重组,但是一旦IP分片丢失任何一片,整个数据包将不能重组,只能被丢弃,所以,一旦TCP报文段进行分片,丢包率会大大上升,导致很多重传问题。所以TCP会尽量在自己的协议层分片。

总结

TCP不适用IP层分片,UDP使用IP层分片。ICMP报文控制MSS的大小。

名词解释

  1. RTT:一个数据包从发送出去到ACK回来的时间
  2. RTO:超时重传时间,根据RTT计算
  3. MSL:TimeWait等待时间
  4. MTU:链路最大传输字节数
  5. MSS:TCP最大传输字节数量,MTU-MSS

API

listen

int listen(int sockfd, int backlog);

Linux对正在建立连接的socket有两个队列,一个是正在建立连接的队列,一个是已经建立连接的队列。

  • 收到SYN,建立未完成的sock,加入正在建立连接的队列
  • 收到ACK,完成上面未完成的sock,加入已经建立连接的队列

backlog和somaxconn的最小值决定了已经建立连接的队列长度。

accept

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

accept只是从已建立连接的队列摘下来一个sock结构体,然后建立文件描述符sock关联起来。accept会一直阻塞,直到有一个sock返回

connect

int connect(int sockfd, const struct sockaddr *addr,
                   socklen_t addrlen);
  • 调用connect函数会发起三次握手建连过程
  • 通常阻塞直到建连成功或者发生错误
  • 非阻塞connect可以防止多次重发syn,等待75秒

对比

image

原文地址:https://www.cnblogs.com/biterror/p/6909853.html