TCP 的笔记
这一次的笔记是读TCPV1 记录一下TCP的相关知识,看到什么就记什么。
TCP头格式
先看一下TCP头的格式
- TCP 的一个传输单位为segment(段),但下面也叫包
- TCP的包是没有IP地址的,那是IP层上的事。但是有源端口和目标端口port。
- 一个TCP连接需要四个元组来表示是同一个连接(src_ip, src_port, dst_ip, dst_port
- 注意上图中的四个非常重要的东西:
- 序号(Sequence Number)是包的序号,用来解决网络包乱序(reordering)问题。
- 确认号(Acknowledgement Number)就是ACK——用于确认收到,用来解决不丢包的问题。
- 窗口大小(Window又叫Advertised-Window),也就是滑动窗口(Sliding Window),用于解决流量控制的。
- TCP Flag ,也就是包的类型,有ACK,SYN,FIN等,主要是用于操控TCP的状态机的。
TCP状态机
网络上的传输是没有连接的,都是属于虚电路,包括TCP也是一样的。
而TCP所谓的“连接”,其实只不过是在通讯的双方维护一个“连接状态”,让它看上去好像有连接一样。所以,TCP的状态变换是非常重要的
TCPv1 第一版上的图,所有TCP的状态如下
而发起连接的进程一般为客户端,响应连接的进程一般为服务端
一端(一般为客户端)的主动关闭,FIN_WAIT1,FIN_WAIT2和TIME_WAIT
一端(一般为服务端)被动关闭包括CLOSE_WAIT和 LAST_ACK
可以使用netstat 查看TCP端口的状态
TCP的三次握手过程,四次挥手过程。
建立连接协议(三次握手)
- (1)客户端发送一个带SYN标志,SEQ_NUM = S 的包packet到服务器。这是三次握手过程中的报文1。(客 -> SYN_SEND)
- (2) 服务器端回应客户端的,发含ACK,SYN位且ACK_NUM = S + 1,SEQ_NUM = P的包到客户机。这是三次握手中的第2个报文。(服 -> SYN_RECV)
- (3)客户机发送含ACK位,ACK_NUM = P + 1的包到服务器,这是报文段3。(客 -> ESTABLISH,服 -> ESTABLISH)
连接终止协议(四次挥手)
由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
- (1) 客户机发含FIN位,SEQ = Q的包到服务器。(报文段4)。(客 -> FIN_WAIT_1)
- (2) 服务器收到这个FIN,服务器发送含ACK且ACK_NUM = Q + 1的包到客户端。(报文段5)。(服 -> CLOSE_WAIT,客 -> FIN_WAIT_2)
- 此处有等待
- (3) 服务器关闭客户端的连接,发送含FIN且SEQ_NUM = R的包到客户机。(报文段6)。(服 -> LAST_ACK,客 -> TIME_WAIT)
- 此处有等待
- (4) 客户端发送最后一个含有ACK位且ACK_NUM = R + 1的包到客户机。(报文段7)。(服 -> CLOSED)
TCP的状态解释
下面为客户端主动发起连接,服务端被动响应连接;
客户端主动关闭,服务端被动关闭;
CLOSED:
表示初始状态。
LISTEN:
表示服务器端的某个SOCKET处于监听状态,可以接受连接了。
### SYN_SENT:
客户端socket执行CONNECT连接,发送SYN包,进入此状态.
这个状态与SYN_RCVD呼应,当客户端SOCKET执行CONNECT连接时,它首先发送SYN报文,因此也随即它会进入到了SYN_SENT状 态,并等待服务端的发送三次握手中的第2个报文。SYN_SENT状态表示客户端已发送SYN报文。
SYN_RCVD:
服务端收到SYN包并发送服务端SYN包,进入此状态。
在正常情况下,这个状态是服务器端的SOCKET在建立TCP连接时的三次握手会话过程中的一个中间状态,很短暂,基本 上用netstat很难看到这种状态的,除非特意写了一个客户端测试程序,故意将三次TCP握手过程中最后一个ACK报文不予发送。因此这种状态 时,当收到客户端的ACK报文后,它会进入到ESTABLISHED状态。
ESTABLISHED:
表示连接已经建立了。客户端发送了最后一个ACK包后进入此状态,服务端接收到ACK包后进入此状态。
FIN_WAIT_1:
终止连接的一方(通常是客户机)发送了FIN报文后进入。等待对方FIN。
FIN_WAIT_2:
客户端接收到服务器的ACK包,但并没有立即接收到服务端的FIN包,进入FIN_WAIT_2状态。 此时是半连接状态,即有一方要求关闭连接,等待另一方关闭。
半连接,也即有一方要求close连接,但另外还告诉对方,我暂时还有点数据需要传送给你,稍后再关闭连接。
FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。而这两种状态的区别 是:FIN_WAIT_1状态实际上是当SOCKET在ESTABLISHED状态时,它想主动关闭连接,向对方发送了FIN报文,此时该SOCKET即 进入到FIN_WAIT_1状态。而当对方回应ACK报文后,则进入到FIN_WAIT_2状态,当然在实际的正常情况下,无论对方何种情况下,都应该马 上回应ACK报文,所以FIN_WAIT_1状态一般是比较难见到的,而FIN_WAIT_2状态还有时常常可以用netstat看到。
TIME_WAIT:
客户端收到服务端的FIN包,并立即发出ACK包做最后的确认,在此之后的2MSL时间称为TIME_WAIT状态。
表示收到了对方的FIN报文,并发送出了ACK报文,就等2MSL后即可回到CLOSED可用状态了。如果FIN_WAIT_1状态下,收到了对方同时带 FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。
意义:
- 如果客户端直接进入CLOSED状态,如果服务端没有接收到最后一次ACK包会在超时之后重新再发FIN包,此时因为客户端已经CLOSED,所以服务端就不会收到ACK而是收到RST。所以TIME_WAIT状态目的是防止最后一次握手数据没有到达对方而触发重传FIN准备的。
在2MSL时间内,同一个socket不能再被使用,否则有可能会和旧连接数据混淆(如果新连接和旧连接的socket相同的话)。
关于TIME_WAIT数量太多。从上面的描述我们可以知道,TIME_WAIT是个很重要的状态,但是如果在大并发的短链接下,TIME_WAIT 就会太多,这也会消耗很多系统资源。见陈皓菊苣博客
CLOSING:
这种状态比较特殊,实际情况中应该是很少见,属于一种比较罕见的例外状态。正常情况下,当你发送FIN报文后,按理来说是应该先收到(或同时收到)对方的 ACK报文,再收到对方的FIN报文。但是CLOSING状态表示你发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。什 么情况下会出现此种情况呢?其实细想一下,也不难得出结论:那就是如果双方几乎在同时close一个SOCKET的话,那么就出现了双方同时发送FIN报 文的情况,也即会出现CLOSING状态,表示双方都正在关闭SOCKET连接。
CLOSE_WAIT:
这种状态的含义其实是表示在等待关闭。(假设服务器)接收到客户机FIN包之后等待关闭的阶段。在接收到对方的FIN包之后,自然是需要立即回复ACK包的,表示已经知道断开请求。但是本方是否立即断开连接(发送FIN包)取决于是否还有数据需要发送给客户端,若有,则在发送FIN包之前均为此状态。
LAST_ACK:
服务端发动最后的FIN包,等待最后的客户端ACK响应,进入此状态。
被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,也即可以进入到CLOSED可用状态了。
面试几个问题:
1、 为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?
- 这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的建连请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。
- 但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以未必会马上会关闭SOCKET,也即可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文 和FIN报文多数情况下都是分开发送的。
2、 为什么TIME_WAIT状态还需要等2MSL后才能返回到CLOSED状态?
这是因为tcp是全双工模式,虽然双方都同意关闭连接了,而且握手的4个报文也都协调和发送完毕,按理可以直接回到CLOSED状态(就好比从SYN_SEND状态到 ESTABLISH状态那样);但是因为网络是不可靠的,无法保证最后发送的ACK报文会一定被对方收到,因此对方处于 LAST_ACK状态下的SOCKET可能会因为超时未收到ACK报文,而重发FIN报文,所以这个TIME_WAIT状态的作用就是用来重发可能丢失的ACK报文。
3, tcp为什么要三次握手?两次不行吗?
client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达server。本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。于是就向client发出确认报文段,同意建立连接。假设不采用“三次握手”,那么只要server发出确认,新的连接就建立了。由于现在client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送ack包。但server却以为新的运输连接已经建立,并一直等待client发来数据。这样server的很多资源就白白浪费掉了。采用“三次握手”的办法可以防止上述现象发生。例如刚才那种情况,client不会向server的确认发出确认。server由于收不到确认,就知道client并没有要求建立连接。主要目的防止server端一直等待,浪费资源。
“三次握手”的目的是“为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误”。
两次握手可能产生死锁。作为例子,考虑计算机server和client之间的通信,假定client给server发送一个连接请求分组,server收到了这个分组,并发送了确认应答分组。按照两次握手的协定,server认为连接已经成功地建立了,可以开始发送数据分组。可是,client在server的应答分组在传输中被丢失的情况下,将不知道server是否已准备好,不知道server建立什么样的序列号,client甚至怀疑server是否收到自己的连接请求分组。在这种情况下,client认为连接还未建立成功,将忽略server发来的任何数据分组,只等待连接确认应答分组。而S在发出的分组超时后,重复发送同样的分组。这样就形成了死锁。
同时打开 和 同时关闭
同时打开
两个应用程序同时执行主动打开的情况是可能的,虽然发生的可能性较低。每一端都发送一个SYN,并传递给对方,且每一端都使用对端所知的端口作为本地端口。例如:
主机a中一应用程序使用7777作为本地端口,并连接到主机b 8888端口做主动打开。
主机b中一应用程序使用8888作为本地端口,并连接到主机a 7777端口做主动打开。
tcp协议在遇到这种情况时,只会打开一条连接。
这个连接的建立过程需要4次数据交换,而一个典型的连接建立只需要3次交换(即3次握手)但多数伯克利版的tcp/ip实现并不支持同时打开。
SYN_RCVD与SYN_SEND都是转换为ESTABLISHED的中间状态,目标是两端均转换到ESTABLISHED状态。
同时关闭
如果应用程序同时发送FIN,则在发送后会首先进入FIN_WAIT_1状态。在收到对端的FIN后,回复一个ACK,会进入CLOSING状态。在收到对端的ACK后,进入TIME_WAIT状态。这种情况称为同时关闭。
同时关闭也需要有4次报文交换,与典型的关闭相同。
TCP的超时重传等
一些基本概念
RTT(Retransmission TimeOut)即重传超时时间:
数据从发送到接收到对方响应之间的时间间隔,即数据报在网络中一个往返用时。大小不稳定。
RTO(Round Trip Time)
从上一次发送数据,因为长期没有收到ACK响应,到下一次重发之间的时间。就是重传间隔。
- 通常每次重传RTO是前一次重传间隔的两倍,计量单位通常是RTT。例:1RTT,2RTT,4RTT,8RTT……
- 重传次数到达上限之后停止重传。
二者区别和计算见blog http://blog.csdn.net/zhangskd/article/details/7196707
计算参考[陈皓TCP 的那些事儿(下)]https://coolshell.cn/articles/11609.html
超时重传:
发送端发送报文后若长时间未收到确认的报文则需要重发该报文。可能有以下几种情况:
- 发送的数据没能到达接收端,所以对方没有响应。
- 接收端接收到数据,但是ACK报文在返回过程中丢失。
- 接收端拒绝或丢弃数据。
也会有快速重传等,看 [陈皓这篇blog] https://coolshell.cn/articles/11564.html
流量控制
主要是使用滑动窗口
目的是接收方通过TCP头窗口字段告知发送方本方可接收的最大数据量,用以解决发送速率过快导致接收方不能接收的问题。所以流量控制是点对点控制。
TCP是双工协议,双方可以同时通信,所以发送方接收方各自维护一个发送窗和接收窗。
发送窗:用来限制发送方可以发送的数据大小,其中发送窗口的大小由接收端返回的TCP报文段中窗口字段来控制,接收方通过此字段告知发送方自己的缓冲(受系统、硬件等限制)大小。
接收窗:用来标记可以接收的数据大小。
TCP是流数据,发送出去的数据流可以被分为以下四部分:已发送且被确认部分 | 已发送未被确认部分 | 未发送但可发送部分 | 不可发送部分,其中发送窗 = 已发送未确认部分 + 未发但可发送部分。接收到的数据流可分为:已接收 | 未接收但准备接收 | 未接收不准备接收。接收窗 = 未接收但准备接收部分。
发送窗内数据只有当接收到接收端某段发送数据的ACK响应时才移动发送窗,左边缘紧贴刚被确认的数据。接收窗也只有接收到数据且最左侧连续时才移动接收窗口。
拥塞控制
拥塞控制目的是防止数据被过多注网络中导致网络资源(路由器、交换机等)过载。因为拥塞控制涉及网络链路全局,所以属于全局控制。控制拥塞使用拥塞窗口。
TCP拥塞控制算法:
慢开始(慢启动) & 拥塞避免:先试探网络拥塞程度再逐渐增大拥塞窗口。每次收到确认后拥塞窗口翻倍,直到达到阀值ssthresh,这部分是慢开始过程。达到阀值后每次以一个MSS为单位增长拥塞窗口大小,当发生拥塞(超时未收到确认),将阀值减为原先一半,继续执行线性增加,这个过程为拥塞避免。
快速重传 & 快速恢复:略。
最终拥塞窗口会收敛于稳定值。
区分流量控制和拥塞控制
流量控制属于通信双方协商;拥塞控制涉及通信链路全局。
流量控制需要通信双方各维护一个发送窗、一个接收窗,对任意一方,接收窗大小由自身决定,发送窗大小由接收方响应的TCP报文段中窗口值确定;拥塞控制的拥塞窗口大小变化由试探性发送一定数据量数据探查网络状况后而自适应调整。
实际最终发送窗口 = min{流控发送窗口,拥塞窗口}。
而如何TCP调用和C 语音socket结合参考之前的CSAPP chap 11网络编程