上一讲我们讲了简单的UDP协议,UDP相信网络世界是简单的,所以UDP没有复杂的机制。
这次我们讲TCP,TCP是很复杂的,因为它认为网络世界是复杂的,丢包、乱序、拥堵是家常便饭,一不小心数据包就无法送达。
TCP包头格式
TCP头的格式比UDP复杂许多。
首先,源端口号和目的端口号必不可少。如果没有端口号就不知道该发给哪个应用。
接下来是序号。为什么需要序号字段?是为了解决乱序的问题,每个包编上号,就知道先后顺序不会乱。
还有就是确认序号,发出去的包应该有确认,不然我不知道你收到没有。如果没有确认就重新发送,直到送达。解决的是不丢包的问题。
TCP是可靠的协议,但是它不能决定真实的网络状况的好坏。如果IP层的网络状况确实不好,TCP也无能为力,它能做的只是努力不断地重传。
接下来是状态位。例如SYN是发起一个连接,ACK是回复,RST是重新连接,FIN是结束连接。TCP是面向连接的,因而双方要维护连接的状态,这些状态位的包的发送,会引起双方的状态的变更。
还有一个重要的字段是窗口大小。TCP要做流量控制,通信双方各声明一个窗口大小,标识自己的处理能力,别发得太快,要不然处理不过来,也别发太慢,不然效率太低。
要掌握TCP协议,需要重点关注以下问题:
- 顺序问题,稳重不乱;
- 丢包问题,承诺可靠;
- 连接维护,有始有终;
- 流量控制,把握分寸;
- 拥塞控制,知进只退。
TCP的三次握手
要发送数据,首先要建立连接。
TCP的连接建立,称为三次握手。
A:你好B,我是A。
B:你好A,我是B。
A:你好B。
为什么是三次,而不是两次?
两个人打招呼不是一来一回就可以了吗?
我们假设网络是非常不可靠的。A发起一个连接,当发了第一个请求之后,会有多种可能性,比如这个请求包走丢了,没有走丢超时了,B不响应,不想和我建连接。A不能确认结果,于是再发。终于B收到了包,但是B收到包这件事A是不知道的。如果B这时候不想和A建立连接,则不响应。A重试一段时间之后会放弃,连接建立失败。如果B乐意和A建立连接,则会发送应答包给A。
对于B来说,这个应答包也不确定是否会被A收到。所以B此时不能认为连接建立好了,因为应答包也可能会丢,或者A挂了也有可能。
B发送的应答可能会发送多次,但是只要有一个包到达A,A就认为连接已经建立了。因为对A来说,它的消息有去有回。A会给B发送应答,B也在等这个消息,才能确认连接的建立,对于B来讲,它的消息也是有去又回。
当然A发给B的应答也可能丢失。按理来说,还应该有个应答的应答,但这样下去就会没完没了,一万次握手也不能保证彻底可靠。所以,只要双方的消息有去有回,就基本可以了。
大部分情况下,A和B建立了连接之后,A会马上发数据,一旦A发数据,很多问题就解决了。例如A发给B的应答包丢了,当A发送后续的数据到达后,B认为这个连接已经建立,或者B挂了,A发送的数据会报错,说B不可达。
A也可以建立连接之后空着,不发送数据,程序设计的时候可以要求开启keepalivie机制。
对于服务端B来说,A这种长时间不发包的客户端,可以主动关闭,空出资源给其他客户端使用。
三次握手除了双方建立连接,还是为了沟通TCP包的序号问题。
A告诉B,我发送的序号是从哪个号开始的,B也要告诉A,B的包是从哪个号开始的。
每个连接都要不同的序号,这个序号的起始序号是随着时间变化的,可以看成一个32的计数器,每4ms加一,需要4个小时才能用完。
为了维护连接,双方要维护一个状态机,连接建立的过程中,双方状态变化的时序图像这样:
一开始,客户端和服务器都处于CLOSED状态。先是服务器监听端口,处于LISTEN状态。然后客户端主动发起连接SYN,之后处于SYN_SENT状态。服务端收到发起的连接,返回SYN,并且ACK客户端的SYN。之后处于SYN_RCVD状态。客户端收到服务端的ACK和SYN后,发送ACK的ACK,之后处于ESTABLISHED状态,因为它的一收一发成功了。服务端收到ACK的ACK之后,处于ESTABLISHED状态,因为它也一发一收了。
TCP四次挥手
说完了建连接,我们说说断连接。这称为四次挥手。
A:B,我不想玩了。
B:哦,你不想玩了,我知道了。
B:A,好吧,我也不想完了,再见。
A:好的,再见。
为什么不是两次就可以了,而是需要四次挥手?
走完前两步的时候,A不想玩了,即A不会再向B发数据,但是B能不能在ACK的时候直接关闭呢?不可以,A不能向B发数据,但是B还没有做完自己的事情,还是可以向A发数据,所以称为半关闭的状态。这个时候A可以选择不再接收数据,也可以选择最后再接收一段数据。等待B也主动关闭。
当然上面是正常的情况。
A开始说“不玩了”,B说“知道了”,这个回合没什么问题。如果A说“不玩了”,没有收到回复,则A会重新发送“不玩了”。这个回合结束之后,就有可能出现异常了。
一种情况是,A说完“不玩了”之后,直接跑路,B还没有发起结束,B发起结束后收不到回答,B就不知道该怎么办了。另一种情况是,A说完“不玩了”,B直接跑路,A不知道B是有事情要处理,还是过一会儿发送结束。
要怎么解决这些问题呢?TCP专门设计了几个状态来处理这些问题。我们看断开连接的状态时序图。
断开的时候,我们可以看到,A说“不玩了”,就进入了FIN_WAIT_1状态,B收到A“不玩了”的消息,发送知道了之后就进入CLOSED_WAIT状态。
A收到“B说知道了”,就进入FIN_WAIT_2状态,如果这个时候B跑路,则A将永远处在这个状态。TCP没有对这个状态的处理,Linux有,可以调整 tcp_fin_timeout 参数,设置一个超时时间。
如果B没有跑路,发送“我也不玩了”的请求到A,A发送“知道B不玩了”后从FIN_WAIT_2状态结束。按说A可以跑路了,但是最后的这个ACK万一B收不到,则B会重新发送一个“B不玩了”,这个时候A已经跑路了,因为TCP协议要求等待最后一段时间TIME_WAIT,这个时间要足够长,长到如果B没收到ACK的话,“B说不玩了”会重发的,A会重新发一个ACK并且足够时间到达B。
A跑路还有一个问题是,A的端口会空出来,但B不知道,B发送的很多包可能还在路上,此时A的端口被一个新的应用占用,新的应用会收到B发过来的包,为了防止混乱,需要等待足够长的时间,等到B发送的所有的包都死掉了,再空出端口来。
等待的时间是2MSL,MSL是Maximum Segment Lifetime,报文最大生存时间。它是任何报文再网络上存在的最长时间,超过这个时间的报文将被丢弃。协议规定MSL位2分钟,实际应用中常用的是30秒,1分钟和2分钟。
还有一种异常情况是,B超过了2MSL依然没有收到它的FIN的ACK,按照TCP的原理,B会重新发送FIN,之后A再收到这个包之后,A表示,我已经等了这么长时间,之后的我就不认了,于是就直接发送RST,B就知道A跑路了。
TCP状态机
将连接建立和断开的两个时序图综合起来就是著名的TCP的状态机。
小结
- TCP的包头很复杂,要关注五个问题,顺序问题,丢包问题,连接维护,流量控制,拥塞控制;
- 连接的建立是经过三次握手,断开的时候经过四次挥手。
两个思考题:
1. TCP的连接有这么多的状态,你知道如何在系统中查看某个连接的状态吗?
2. 这一节只讲了连接维护问题,为了维护连接,还有其他的数据结构来处理其他四个问题,你知道是什么吗?