题外话:刚刚过去的半个月实在是忙得我喘不过来气,虽然手里还压着几个项目得在期末考试之前做完,但是想想还是更新一下随笔,稍微换个心情。另外小吐槽一下那些在博客园里原封不动抄书当随笔的人,唉真是....算了我不吐槽了哈哈,进入正题!
TCP/IP协议有关的书籍我在图书馆里翻看了很多,虽说每本侧重都不大一样,但是有一点是一样的:TCP协议讲义的篇幅都是其他协议的三到五倍!
下面总结一下TCP协议里最核心的知识和一些细节,首先从Overview开始:
一. 一张图--TCP FSM
这张图是TCP协议基础之基础之基础,凝练了TCP连接宏观的状态的迁移,没记住这个?嗯,那么你就肯定不懂TCP咯~盯着tcpdump和netstat里的一条条数据或许可以帮你背下来这个图,要注意的是这张图描述的是TCP连接的状态,从编程的角度来看就是一对套接字(socket)组建起来的连接的角度来看的,实线是TCP客户端连接状态经过的状态变迁,虚线是服务器端经过的状态变迁,其中“主动打开”和“被动打开”大部分是从应用程序发起的操作,如果你是用的C/C++的套接字,那么主动打开就是connect()的动作,而被动打开是listen()的操作,图中箭头上的描述是连接过程中的数据的流动,关于细节下文再细说。
二. 另一张图--Connect Process
对于一次正常的TCP连接我们要从两个角度分别来看连接的过程,这和TCP协议的“双全工”完全是两码事,每条单向的“数据通路”连接的两端都会同时进行以下的状态变迁:
客户端:CLOSED->SYN_SENT->SYN_RCVD->ESTABLISHED->( FIN_WAIT1->FIN_WAIT2 | FIN_WAIT1 )->TIME_WAIT->CLOSED
服务端:CLOSED->LISTEN->SYN_RCVD->ESTABLISHED->CLOSE_WAIT->LACK_ACK->CLOSED
但是只讨论正常情况下的TCP协议机制是个没用意义的话题,真正值得思考的是在特殊情况下的TCP协议状态的转换过程,里面有一些细节是不得不提的,篇幅限制我就罗列一些关键字及其信息摘要供大家温习回忆:
链接建立三次握手:
这个真的没有太多好说的,有点不是理解为什么有人非常强调,这个过程非常的清晰明了,和社会中人沟通的方式是相似的:
“阿军你真帅!” -> “谢谢呀,你也很帅!” -> “谢谢!”
SYN -> SYN,ACK -> ACK
但是有一点需要抛开来单独讨论的是一种叫做同时打开的状况,尽管这种状况非常的罕见,但是边界情况是坚决不能忽略的:
双方几乎同时发出SYN包,双方判断同时打开的条件就是:
在等待SYN+ACK包的时候却收到了一个SYN包,于是它就会进入SYN_RCVD状态并且发出一个ACK包,双方只进行了两组包的交换就完成了进入ESTABLISHED状态的过程。
链接关闭三次握手:
什么?这个也是三次握手?一定和上一个一样简单没内含? 大错特错,链接关闭的三次握手过程比链接建立复杂了一个数量级!
和刚才一样做个人之间沟通的情况模拟:
“阿军我走了阿” -> "那我也走了阿,再见" -> "再见"
FIN -> FIN,ACK -> ACK
是这个样子嘛?也许没错!但是大多数的情况是这样的:
“阿军我走了阿” -> "好的,但你先再听我说两句" ->(过了一会儿)-> “那我也走了阿” -> "再见"
FIN -> ACK -> .... -> FIN -> ACK
这是因为TCP协议是一种“双全工"的协议,TCP链接可以想象成有两条方向相反的数据通路之间相互沟通,当一方主动要求关闭链接的时候,另一方要回复答应关闭一个走向的数据通路但是维持另一个数据通路继续进行数据传输,直到想要传输的数据传输完毕再发出一个关闭数据通路的请求并且等待回应,这个单向数据通路关闭的状态叫做半关闭状态。
有半关闭状态就有半打开状态,我个人认为半打开状态应该放在关闭过程之中进行介绍,当一方已经关闭或者异常终止链接而另一方不知道,我们将这样的链接状态叫做半打开。
同样地有同时打开就有同时关闭,相似地当,一方发出FIN包进入FIN_WAIT1状态的时候如果接受到了另一方发送的FIN包那么就算作同时关闭了,双方直接再次交换ACK报文终止。
下面进入TCP协议的主要细节:
首先要看以下TCP的报文格式,
几个最关键的东西:
(1)窗口大小:
用“滑动”来描述窗口的变化过程是最恰当不过的,无论是发送窗口还是接受窗口都有左壁和右壁的概念,窗口是在报文缓冲内容上进行所谓滑动的。窗口从客户端(发送端)的角度来讲,窗口应该分为两部分,一部分是已经发送但是并未收到确认的报文内容,另一部分是待发送的报文,在未受到确认的报文内容在计时器(RTO计数器)计数完毕之后仍未受到确认则会启用报文重发机制重新发送一份报文内容,所以TCP协议是一个可靠的协议,在接受端的接受窗口也会随着不断地接受报文的同时发送确认报文,这里有一个特殊情况就是当收到的序列号之间出现了断点或者内容校验错误的时候,会再次发送一个ACK报文使序列号等于下一个希望接受的报文段的序列号(重复的ACK)。
可能产生的死锁:
有一种可能发生的特殊情况,确认的丢失可能会引起系统的死锁。当接受方发送了确认,同时把窗口大小调整为0,即请求关闭发送窗口时就会发生这样的情况。过了一段时间之后,接受方打算取消这一限制,但是如果它没有数据要发送,就会发送确认包,并且利用一个窗口大小非零的数值来取消这个限制。如果这个确认丢失了,那么就会产生问题,发送方一直在等待确认一个非零的窗口大小,而接收方则认为发送方已经收到了这个确认,因而正在等待数据,这种情况就是典型的死锁。双方都是在等待一个纯粹的ACK确认,不涉及窗口内报文的确认,所以并没有启用RTO机制来重新发送ACK。要避免死锁,就要设计一种持续计时器来处理这个问题。
(2)控制字段:六种不同的标志分别用于流量控制,链接建立和终止,链接异常终止及数据传输的确认
| URG | ACK | PSH | RST |SYN | FIN |
URG:紧急指针有效
ACK:确认是有效的
PSH:请求确认
RST:连接复位
SYN:同步序号
FIN:终止连接
(3)序号:本报文段第一个字节的编号,保证连接传送数据的正确性。
(4)确认号:报文段的接受方期望从对方接受的字节编号。
三. FSM模拟伪代码
TCP-FSM有限状态机模拟伪代码:
TCP_Main_Module(segment){ 查找 TCB(TransmitControlBlock) if(相应的TCB未找到) 创建TCB,其状态为CLOSED 找到TCB表中相应表项的状态 swith(状态){ /// case CLOSED 状态: if(收到 被动打开 报文)进入LISTEN状态 if(收到 主动打开 报文){ 发送SYN报文段 进入SYN_SENT状态 } if(收到任何报文段)发送RST报文段 if(收到其他任何报文)发出差错报文 break /// case LISTEN 状态: if(收到 发送数据 报文){ 发送SYN报文段 进入SYN_SENT状态 } if(收到 任何SYN报文段 ){ 发送SYN+ACK报文段 进入SYN_RCVD状态 } if(收到任何其他报文端或者报文){ 发出发错报文 } break /// case SYN_SENT 状态: if(超时)进入CLOSED状态 if(收到SYN报文段){ 发送SYN+ACK报文段 进入SYN+RCVD状态 } if(收到SYN+ACK报文段){ 发送ACK报文段 进入ESTABLISHED状态 } if(收到任何其他报文段或者报文){ 发出差错报文 } break case SYN_RCVD 状态: if(收到ACK报文)进入ESTABLISH状态 if(超时){ 发送RTS报文 进入CLOSED状态 } if(收到 关闭 报文){ 进入FIN报文段 进入FIN_WAIT1状态 } if(收到RTS报文段){ 进入LISTEN状态 } if(收到任何其他报文段或者报文){ 发出差错报文 } break // case ESTABLISHED 状态: if(收到FIN报文段){ 发送FIN报文段 进入CLOSED-WAIT状态 } if(收到 关闭 报文){ 发送FIN报文段 进入FIN-WAIT1状态 } if(收到RTS或者SYN报文段)发出差错报文 if(收到数据或者ACK报文段)调用输入模块 if(收到 发送 报文)调用输出模块 break //// case FIN-WAIT1 状态: if(收到FIN报文段){ 发送ACK报文段 进入CLOSING状态 } if(收到FIN+ACK报文段){ 发送ACK报文段 进入TIME-WAIT状态 } if(收到ACK报文段){ 进入FIN-WAIT2状态 } if(收到任何其他报文段或者报文){ 发出差错报文 } break //// case FIN-WAIT2 状态: if(收到FIN报文段){ 发送ACK报文段 进入TIME-WAIT状态 } break case CLOSING 状态: if(收到ACK报文段){ 进入TIME-WAIT状态 } if(收到任何其他报文段或者报文){ 发出差错报文 } break case TIME-WAIT 状态: if(超时){ 进入CLOSED状态 } if(收到任何其他报文段或者报文){ 发出差错报文 } break case CLOSED-WAIT 状态: if(收到 关闭 报文){ 发送FIN报文段 进入LAST-ACK状态 } if(收到任何其他报文段或者报文){ 发出差错报文 } break case LAST-ACK 状态: if(收到ACK报文段){ 进入CLOSED状态 } if(收到任何其他报文段或者报文){ 发出差错报文 } break ///Ending }}