TCP的报头
- Source Port,Dest Port:源端口和目的端口,和IP层的源IP和目的IP来唯一确定一个socket。
- Sequence Number:包的序列号,主要用来解决乱序的问题。
- Acknowledgement Numbe:就是ACK——用于确认收到,用来解决不丢包的问题,也会用来进行窗口更新。
- Window:滑动窗口,通过接受方来控制发送方,达到流量控制的目的。
- TCP Flags:TCP的标志比特位。
TCP状态机
TCP是一个面向连接、可靠的字节流协议,其实在网络传输中是没有连接的概念,TCP所谓的连接,提现在它的两端所维护的一个状态机上。
TCP建立过程
三次握手过程,主要是通信的双方要互相通知对方自己的初始化的Sequence Number(缩写为ISN:Inital Sequence Number)——所以叫SYN,全称Synchronize Sequence Numbers。也就上图中的 x 和 y。这个号要作为以后的数据通信的序号,以保证应用层接收到的数据不会因为网络上的传输的问题而乱序。
- 讨论:为什么是三次握手过程,而不是两次握手?
- 首先,必须至少两次才能建立连接。而且最后一个ACK始终有可能丢失,奇数次建立链接和偶数次建立连接是不同的
- 讨论为什么不是偶数次,试想如果偶数次发送最后一个ACK,那么此时服务器端建立了连接,客户端还没有建立连接。假设此时客户端下线,那么服务端并不知道,只能保持这个连接,极大地浪费服务端的资源。
- 如果是奇数次连接,就不会出现上述情况。所以奇数次握手是为了保护服务端。
- 连接建立超时
TCP的三次握手协议等价于两次等待,第一次等待是客户端发送了SYN后,等到ACK和对端SYN报文,第二次等待是服务端发送了ACK和SYN后,等待客户端的ACK。一般来讲,等待就意味着可能超时。
如果是客户端发送SYN以后的等待超时,那么客户端每隔几秒重发SYN报文,直到经过75秒。
如果是服务器发送SYN以后等到超时,服务器会以1S,2S,4S,8S,16S,32S为间隔重发,整个过程一共63秒,这对服务器资源是一种极大的浪费。
断连过程
- 为什么是四次挥手
由于TCP是全双工的,允许在一段关闭发送通道,表示不再有数据的发送,而接受数据的通道是可以继续使用的。
- socket的优雅关闭和暴力关闭,close和shutdown的区别
- close实际做的事是将socket_fd的内部引用计数值减1,如果内部引用计数不用0,则不做任何其他操作;当引用计数为0时,释放掉sock_fd所占资源,关闭对应的TCP连接。
- 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状态的必要性
- 保证最后一个ACK能够到达被动断开方。如果不保持这个状态,假设最后一个ACK丢包,那么超时后被动断开方又发送一个FIN,这是因为连接已经终止,这样不够优雅。
- 最重要的原因是,让这个连接所有的数据都在网络中消失。防止产生一个相同四元组的连接,
TCP的数据交互
TCP是可靠的传输协议,那么必须要有重传的机制。TCP的重传机制是基于ACK的。我们知道ACK表示的意思是对已经收到数据的确认,并且还表示接下来想要收到的数据的SEQ。
超时与重传
TCP的基础重传策略是基于定时器的超时机制的,每当TCP发送了一个报文时,会为这个报文设置一个大小为RTO(重传超时时间)的定时器。
通过RTT(一个数据包从发送出去到ACK回来的时间)动态改变RTO的时间。
- RTT的测量:
在图(a)中,如果按照第一次发送和收到ACK之间的时间差,那么显然,由于第一次发送的报文的ACK丢失,所以,ACK其实去重传报文的ACK,这个时间差显然太大。如果按照重传和ACK之间的时间差,那么会出现(b)中的情况,又会出现时间差太小的情况。
因此,为了简化RTT的测量:
- 忽略重传的报文,只计算正常的报文。
- 在一个TCP连接上同时只能有一个计时器在测量RTT。
补充
- TCP还有另外的重传机制,例如连续接受三次ACK.
- SACK汇报已经接受到的序列号
流量控制
TCP头里有一个字段叫Window,这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。
TCP的滑动窗口协议主要有两个作用,一是提供TCP的可靠性,二是提供TCP的流控特性。同时滑动窗口机制还体现了TCP面向字节流的设计思路。TCP会话的双方都各自维护一个“发送窗口”和一个“接收窗口”。
发送窗口:
发送缓冲区一共有4段:已经发送并得到对端ACK的”,“已经发送但还未收到对端ACK的”,“未发送但对端允许发送的”,“未发送且对端不允许发送”。“已经发送但还未收到对端ACK的”和“未发送但对端允许发送的”这两部分数据称之为发送窗口(中间两部分)
发送窗口更新规则:
- 当ACK=36且window size = 14,左边沿滑动到35后面,右边沿滑动到49后面。
- 当ACK=36且window size = 10,左边沿滑动到35后面,右边沿不动。
- 当发送了两个字节后,中间蓝绿区域中间的线移动到47后面。
接受窗口
接收方每次向发送方发送ACK时,都会向发送方汇报己方的接收窗口大小,发送方会根据接收方的窗口大小(实际上会去拥塞窗口和接收窗口的最小值)来控制发送数据的大小,以保证接收方可以及时处理。
接受窗口为0
坚持定时器
当接收窗口大小为0时,由于不能接收到任何数据,不能接收数据也就意味着不能发送ACK,那么当服务器处理了部分数据,接收窗口不为0时如何通知发送方。发送方会定时使用一个坚持定时器来周期性地向接收方查询,以便发现窗口是否增大,这种报文叫窗口探查,该报文包含一个字节的伪数据,这一个字节的数据是必须的,因为TCP不会对一个不带数据的报文进行ACK确认。接收方收到这个报文后,返回一个ACK,ACK中带有接收窗口大小。
糊涂窗口综合症
如果服务器返回的接受窗口只有很小的字节,按照发送原则,只能发送很小的字节。但是我们的TCP/IP头部都有40个字节,这样发送效率太低。所以,接受方要有足够大的空间才通知发送方,而发送方有比较多的数据才发送。
- 对于接受方
- 只有可用空间大于等于一个MSS时才通知发送方。
- 接收缓冲区有超过一半为空时才通知发送方。
- 对于发送方
- 要发送的报文长度大于一个MSS才发送。
- 可以发送至少是接收方通告窗口大小一般的报文段才发送。
- 没有还没被确认的数据,也就是收到之前发送数据的所有ACK。
发送方的策略就是著名的Nagle算法。但是对于一些实时交互应用,Nagle算法需要关闭。
拥塞处理
流量控制依赖于发送端和接收端,和网络中的状态无关。拥塞控制用于在网络状态不好,发生丢包的时候处理。
慢启动(指数增长)
- 对于刚建立好的连接,先初始化拥塞窗口cwnd=MSS,即一个报文段的大小,初始化慢启动门限ssthresh,一般为65535,都以字节为单位。
- 每收到一个ACK,拥塞窗口就增加一个MSS。
- 当cwnd>=ssthresh时,启动拥塞避免算法。
拥塞避免算法(线性增长)
每收到cwnd/MSS个ACK,cwnd增加MSS个字节
拥塞发生时算法
分为两种情况,一为未收到ACK,即超时重传;二为收到3次重复的ACK
- 超时重发
- ssthredsh = cwnd / 2
- cwnd = MSS
- 进入慢启动的过程
- 收到3次重复的ACK(说明网络并不是那么糟糕)
- cwnd = cwnd / 2
- ssthresh = cwnd
- 进入快速恢复算法
- 快速恢复算法
- 设置cwnd = cwnd + 3 * MSS,3个MSS对应3个重复的ACK。
- 以后每收到一个重复的ACK,cwnd = cwnd + MSS。
- 如果收到的是一个新的ACK,那么cwnd = ssthresh,进入拥塞避免算法。
- 讨论:快速重传为什么选择三次重复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的大小。
名词解释
- RTT:一个数据包从发送出去到ACK回来的时间
- RTO:超时重传时间,根据RTT计算
- MSL:TimeWait等待时间
- MTU:链路最大传输字节数
- 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秒