TCP/IP(二)TCP连接的建立和终止、MSL、状态变迁图

1. UDP:用户数据报协议

1.1. 概述细节

  1. UDP是一个简单的面向数据报的运输层协议:进程的每个输出操作都正好产生一个UDP数据报,并组装成一份待发送的IP数据报。这与面向流字符的协议不同,如TCP,应用程序产生的全体数据与真正发送的单个IP数据报可能没有什么联系。
  2. UDP数据报封装成一份IP数据报的格式:
  3. UDP不提供可靠性:它把应用程序传给IP层的数据发送出去,但是并不保证它们能到达目的地
  4. 应用程序必须关心IP数据报的长度,如果它超过网络的MTU(,那么就要对IP数据报进行分片。源端到目的端之间的每个网络都要进行分片,并不只是发送端主机连接第一个网络才这样做
  5. UDP首部(8个字节):
    • 4字节(源、目标端口各两个字节)
    • 2字节UDP长度(UDP首部+UDP数据的字节长度)、2字节UDP校验和
  6. IP基于协议号对IP包分用,TCP端口号与UDP端口号是相互独立的(基于约定应用应该选用不同,避免混淆)
  7. IP首部的检验和仅校验IP首部,UDP、TCP校验覆盖到数据(另外,UDP校验是可选的,TCP校验是必须的)
  8. UDP校验:
    • UDP数据报的长度可以为奇数字节,但是检验和算法是把若干个16 bit字相加。解决方法是必要时在最后增加填充字节0,这只是为了检验和的计算(也就是说,可能增加的填充字节不被传送)。
    • UDP数据报和TCP段都包含一个12字节长的伪首部,它是为了计算检验和而设置的。
  9. 尽管UDP检验和是可选的,但是它们应该总是在用,在单个局域网中这可能是可以接受的,但是在数据报通过路由器时,通过对链路层数据帧进行循环冗余检验(如以太网或令牌环数据帧)可以检测到大多数的差错,导致传输失败。路由器中也存在软件和硬件差错,以致于修改数据报中的数据。如果关闭端到端的UDP检验和功能,那么这些差错在UDP数据报中就不能被检测出来。
  10. Socket编程中可以开启校验
  11. 因为太网和IP层还使用其他的协议。例如,不是所有的以太网数据帧都是IP数据报,至少以太网还要使用ARP协议。不是所有的IP数据报都是UDP或TCP数据,因为ICMP也用IP传送数据。不要完全相信数据链路(如以太网,令牌环等)的CRC检验。应该始终打开端到端的检验和功能。而且,如果你的数据很有价值,也不要完全相信UDP或TCP的检验和,因为这些都只是简单的检验和,不能检测出所有可能发生的差错。
    • 链路层:CRC循环冗余校验(16 bit字的二进制反码求和)
    • 传输层:CRC循环冗余校验
  12. 首部检验和字段是根据IP首部计算的检验和码。它不对首部后面的数据进行计算。ICMP、IGMP、UDP和TCP在它们各自的首部中均含有同时覆盖首部和数据检验和码。(3.2章节)
  13. IP分片:物理网络层一般要限制每次发送数据帧的最大长度。任何时候IP层接收到一份要发送的IP数据报时,它要判断向本地哪个接口发送数据(选路),并查询该接口获得其MTU。IP把MTU与数据报长度进行比较,如果需要则进行分片。分片可以发生在原始发送端主机上,也可以发生在中间路由器上。把一份IP数据报分片以后,只有到达目的地才进行重新组装,重新组装由目的端的IP层来完成,其目的是使分片和重新组装过程对运输层(TCP和UDP)是透明的,除了某些可能的越级操作外。已经分片过的数据报有可能会再次进行分片(可能不止一次)。IP首部中包含的数据为分片和重新组装提供了足够的信息。
    • IP首部,标志字段用其中一个比特来表示“更多的片”,另外标志字段中有一个比特称作“不分片”位
    • 当数据报被分片后,每个片的总长度值要改为该片的长度值。
    • 当IP数据报被分片后,每一片都成为一个分组,具有自己的IP首部,并在选择路由时与其他分组独立。这样,当数据报的这些片到达目的端时有可能会失序,但是在IP首部中有足够的信息让接收端能正确组装这些数据报片。
  14. 即使只丢失一片数据也要重传整个数据报,因为IP层本身没有超时重传的机制——由更高层来负责超时和重传(TCP有超时和重传机制,但UDP没有。一些UDP应用程序本身也执行超时和重传),就这个原因,经常要避免分片
  15. 在一个以太网上,数据帧的最大长度是1500字节,其中1472字节留给数据,假定IP首部为20字节,UDP首部为8字节,可以基于tcpdump查看分片明细
  16. 术语:
    • IP数据报:是指IP层端到端的传输单元(在分片之前和重新组装之后),对上而言;
    • 分组:是指在IP层和链路层之间传送的数据单元。一个分组可以是一个完整的IP数据报,也可以是IP数据报的一个分片,对下而言;
  17. 发生ICMP不可达差错的另一种情况是,当路由器收到一份需要分片的数据报,而在IP首部又设置了不分片(DF)的标志比特。
  18. 基于traceroute确定路径MTU,要做的是发送分组,并设置“不分片”标志比特(需要自己改写traceroute,类似于试探MTU大小)
  19. 现在许多但不是所有的广域网都可以处理大于512字节的分组。利用路径MTU发现机制,应用程序就可以充分利用更大的MTU来发送报文。
  20. 最大UDP数据报长度:理论上,IP数据报的最大长度是65535字节,这是由IP首部16比特总长度字段所限制的。去除20字节的IP首部和8个字节的UDP首部,UDP数据报中用户数据的最长长度为65507字节。但是,大多数实现所提供的长度比这个最大值小,遇到两个限制因素:
    • 系统调用库的限制:socket API提供了一个可供应用程序调用的函数,以设置接收和发送缓存的长度。大部分系统都默认提供了可读写大于8192字节的UDP数据报
    • TCP/IP的内核限制:使IP数据报长度小于65535字节。
  21. 数据报截断:由于IP能够发送或接收特定长度的数据报并不意味着接收应用程序可以读取该长度的数据。因此,UDP编程接口允许应用程序指定每次返回的最大字节数。如果接收到的数据报长度大于应用程序所能处理的长度,取决于编程接口和实现来处理超出的内容。
  22. UDP服务器设计:通常一个客户启动后直接与单个服务器通信,然后就结束了。而对于服务器来说,它启动后处于休眠状态,等待客户请求的到来。对于UDP来说,当客户数据报到达时,服务器苏醒过来,数据报中可能包含来自客户的某种形式的请求消息。UDP服务涉及内容:
    • 源IP地址和端口号,方便给每个发送请求的客户发回应答
    • 目的IP地址:一些应用程序需要知道数据报是发送给谁的,即目的IP地址,并非所有的OS实现都提供这个功能(这要求操作系统从接收到的UDP数据报中将目的IP地址交给应用程序)。
    • UDP输入队列:程序所使用的每个UDP端口都与一个有限大小的输入队列相联系,这意味着,来自不同客户的到达的请求将由UDP自动排队,接收到的UDP数据报以其接收顺序交给应用程序,排队溢出造成内核中的UDP模块丢弃数据报的可能性是存在的。关于UDP输入队列的几个要点:
      • 应用程序并不知道其输入队列何时溢出。只是由UDP对超出数据报进行丢弃处理。
      • 没有发回任何信息告诉客户其数据报被丢弃,这里不存在像ICMP源站抑制这样发回发送端的消息。
      • 最后,看来UDP输出队列是FIFO(先进先出)的,所看到的ARP输入却是LIFO(后进先出)
    • 限制本地IP地址:
      • 比如限制服务器在en0接口(140.252.1.29)处接收数据报
      • 有可能在相同的端口上启动不同的服务器,需要开启端口重用
    • 限制远端IP地址
    • 每个端口有多个接收者
    • 指明socket选项SO_REUSEADDR,告诉系统应用程序重用相同的端口号(man 7 socket)
  23. UDP的RFC仅3页,RFC 768

2. TCP:传输控制协议

2.1. 关键内容点

  1. 可靠性:分段、定时器-失败重发、接收ack确认、延迟确认、首部+数据数据校验、差错不确认丢弃、IP失序重排、IP包去重、发送,接收buffer
  2. 字节流服务:对应用透明,数据可能被分多包、不对内容解释(二进制或ASCII字符或其他UTF8字符),应用层做协议解释(雷同Linux对文件系统交给应用程序处理)
  3. tcp首部,20字节(IP首部也是20字节):
    • 4字节(源、目标端口)- 进程关联
    • 4字节(2^32-1)seq序号 - ISN(Initial Sequence Number)、FIN,SYN包都消耗一个ISN
    • 4字节(2^32-1)ack序号 - 期望下次收到的序号
    • 2字节(4bit首长、6bit保留、6bit标志位-URG|ACK|PSH|RST|SYN|FIN),2字节滑动窗口
    • 2字节校验和、2字节URG指针
    • 4字节可选字段
  4. 全双工、没有选择确认或否认滑动窗口协议:
    • 后续包先到,SEQ序列跳过,TCP仅发送之前确认接收到的段
    • 校验出错,TCP也ACK之前确认接收的段
  5. TCP流控:滑动窗口、窗口大小(期望接收的字节大小)、16bit(合计65535字节)
  6. 校验和:发端计算和存储,收端验证
  7. URG指针:仅在URG标志为1可用,ISN+URG正向偏移=最后一个字段的序号
  8. 可选项:MSS(Maximum Segment Size),最长报文大小,首个SYN报文段中指明
  9. 数据项:可选,TCP连接建立、终止、处理超时,不会发生任何报文段
  10. 报文段:TCP对用户数据的打包

2.2. 概述细节

  1. 如何建立和终止一个TCP连接、数据传输过程,批量数据传送、TCP超时及重传的技术细节、定时器、TCP新的特性以及TCP的性能
  2. 尽管TCP和UDP都使用相同的网络层(IP),TCP却向应用层提供与UDP完全不同的服务。TCP提供一种面向连接的、可靠的字节流服务。
  3. 打电话:面向连接意味着两个使用TCP的应用(通常是一个客户和一个服务器)在彼此交换数据之前必须先建立一个TCP连接。
  4. 在一个TCP连接中,仅有两方进行彼此通信,广播和多播不能用于TCP
  5. 可靠性保障通过:
    • 应用数据被分割成TCP认为最适合发送的数据块(UDP数据报长度保持不变),TCP传递给IP的信息单位称为报文段或段(segment)
    • 超时及重传策略(启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。)
    • 发送接收确认(当TCP收到发自TCP连接另一端的数据,稍后它将发送一个确认)
    • 校验和差错检测,出错TCP丢弃该包(首部和数据的检验和)
    • 乱序重排(TCP将对收到的数据进行重新排序,将收到的数据以正确的顺序交给应用层)
    • 丢弃重复IP数据包
    • 流量控制,避免较慢主机缓冲区溢出
  6. 字节流服务:两个应用程序通过TCP连接交换字节构成的字节流,但TCP不在字节流中插入记录标识符,我们将这称为字节流服务(byte stream service),一端将字节流放到TCP连接上,同样的字节流将出现在TCP连接的另一端。TCP对字节流的内容不作任何解释。TCP不知道传输的数据字节流是二进制数据,还是ASCII字符、EBCDIC字符或者其他类型数据。对字节流的解释由TCP连接双方的应用层解释。
  7. TCP数据被封装在一个IP数据报中,TCP首部的数据格式通常为20字节
  8. 每个TCP段都包含源端和目的端的端口号,用于寻找发端和收端应用进程。这两个值加上IP首部中的源端IP地址和目的端IP地址唯一确定一个TCP连接。
  9. TCP包首部结构:
  10. TCP首部包结构说明:
    • 伯克利编程接口:一个IP地址和一个端口号也称为一个插口(socket),插口对(socketpair)(包含客户IP地址、客户端口号、服务器IP地址和服务器端口号的四元组)可唯一确定互联网络中每个TCP连接的双方。(RFC793)
    • 序号:用来标识从TCP发端向TCP收端发送的数据字节流,它表示在这个报文段中的的第一个数据字节。(序号是32 bit的无符号数,序号到达232-1后又从0开始。)
    • 确认序号:包含发送确认的一端所期望收到的下一个序号,因此,确认序号应当是上次已成功收到数据字节序号加1,且仅当ACK标识为1时候确认序号字段才有效
    • 首部长度:指明TCP首部长度(任选字段的长度是可变,正常是TCP首部是20字节)
    • 首部6bit标志:URG、ACK、PSH、RST、SYN、FIN
    • 16bit滑动窗口:用于流量控制,故最大可以65535字节
    • 校验和
    • 指针偏移量:紧急指针是一个正的偏移量,和序号字段中的值相加表示紧急数据最后一个字节的序号。TCP的紧急方式是发送端向另一端发送紧急数据的一种方式。
    • 可选字段:MSS(Maximum Segment Size),通常在SYN段中指定,指明本端所能接收的最大长度的报文段
  11. 建立一个新的连接时,SYN标志变1,初始序号ISN(Initial Sequence Number),由于SYN标志消耗了一个序号,故发送数据的第一个字节序号为这个ISN加1;同一样,断开连接FIN标志也要占用一个序号。
  12. TCP为应用层提供全双工服务。这意味数据能在两个方向上独立地进行传输。因此,连接的每一端必须保持每个方向上的传输数据序号。
  13. TCP可以表述为一个既不确认也不否认的滑动窗口协议:
    • 发端虽然在TCP首部包含了确认序号,但当前还无法对数据流中选定的部分进行确认。
    • 收端无法对一个报文段进行否认,比如检验和错,TCP接收端所能做的就是发回上一个确认序号的ACK,让发端重传。
  14. TCP提供了一种可靠的面向连接的字节流运输层服务,TCP将用户数据打包构成报文段;它发送数据后启动一个定时器;另一端对收到的数据进行确认,对失序的数据重新排序,丢弃重复数据;TCP提供端到端的流量控制,并计算和验证一个强制性的端到端检验和。

3. TCP:连接的建立和终止

3.1. 关键内容点

  1. TCP、面向连接的协议、必须建立连接,无连接协议UDP发送数据报时,无需握手
  2. 连接建立:
    • 三次握手
    • tcpdump输出说明,win4096:窗口大小、mss1024:由发端指明的最大报文段长度选项,避免分段
    • ISN、初始化SYN报文、ACK确认标识
    • 发送SYN主动打开,接收SYN被动打开、同时执行主动打开
    • ISN,32bit(TCP首部),ISN每4ms加1
  3. 连接终止:
    • 4次挥手,TCP的半关闭(halfclose)、全双工
    • FIN报文也占一个1ISN
    • FIN的ACK是由TCP软件自动产生的,同时向应用程序交付EOF
  4. 连接建立超时:默认超时时间,75s, BSD版的TCP软件采用一种500 ms的定时器
  5. 最大报文长度MSS只能出现在SYN报文段中,期望接收的MSS,不接收,则设置为默认值536字节
    • MSS越大,每个报文段传送的数据就越多,有更高的网络利用率
    • MSS值设置为外出接口上的MTU长度减去固定的IP首部(20)和TCP首部(20)长度。对于一个以太网MTU(1500),MSS值可达1460字节。
    • MSS默认值为536字节
    • MSS让主机限制另一端发送数据报的长度,方便以较小MTU连接到一个网络上的主机避免分段
  6. TCP半关闭,全双工,仅关闭发送端,还可以接收数据
  7. 半关闭可以作为,客户通知服务器完成了它的数据传送的一种方式
  8. 状态变迁:主动打开、被动打开、主动关闭、被动关闭
    • SYN,SYN_SENT
    • SYNACK, SYN_RCVD
    • ACK, ESTABLISHED
    • FIN, FIN_WAIT_1, CLOSE_WAIT
    • ACK, FIN_WAIT_2, LAST_ACK
    • TIME_WAIT、CLOSING、LISTEN、CLOSED
  9. TIME_WAIT状态也称2MSL(Maximum Segment Lifetime)等待状态
    • 2min、30s、1min
    • 作用:可让被动断开一方的FIN失败重传时候,可以有机会再次发送最后的ACK以防这个ACK丢失,若重复的FIN会得到确认,2MSL定时器重新开始
    • 副作用:
      • Socket无法重用,需要过2MSL时间限制(或者开启SO_REUSEADDR,不推荐)
      • 服务器通常执行被动关闭,不会进入TIME_WAIT状态,客户使用本地端口,也并不关心这个端口号是什么
      • 服务器使用熟知端口,重新启动服务器程序前,它需要在1~4分钟(MSL为30s~2min)。
    • SO_REUSEADDR, 允许一个进程重新使用仍处于2MSL等待的端口,但TCP不能允许一个新的连接建立在相同的插口对上,因为定义这个连接的Socket对仍处于2MSL等待状态。
    • 违背原则:通过修改socket四元组,允许一个新的连接请求到达仍处于TIME_WAIT状态的连接,只要新的序号大于该连接前一个替身的最后序号(比如更换主机)
    • TIME_WAIT状态时忽略RST段
  10. 平静时间:RFC 793指出TCP在重启动后的MSL秒内不能建立任何连接
  11. 防止这种在FIN_WAIT_2状态的无限等待,连接空闲10分钟75秒,TCP将进入CLOSED状态(违背协议规范)
  12. 基准的连接(socket连接)出现错误,引发TCP发送RST复位报文段:
    • 到不存在的端口的连接请求:UDP收到ICMP端口不可达,TCP收到RST的ACK响应
    • 异常终止连接:非有序释放,异常释放(丢弃未发送数据,发送RST段,RST接收区分是否正常,应用程序使用的API必须提供产生异常关闭而不是正常关闭的手段。)
    • 半打开连接(Half-Open):
      • 一方已经关闭或异常终止连接而另一方却还不知道,注意区别于半关闭连接(关闭方知情)(掉电、断网)
      • 可以通过keepalive选项发现
  13. 同时打开(simultaneous open),双方都执行被动打开,仅开启一条通路(TCP特意为止)
  14. 同时关闭(simultaneous close),双方都执行主动关闭,均从ESTABLISHED变为FIN_WAIT_1、CLOSING、TIME_WAIT
  15. TCP选项
  16. TCP服务器的设计
    • 并发设计:客户请求进入,调用一个新进程或线程来处理这个新的客户请求(fork进程或线程处理)
    • tcp服务端口:TCP使用由本地地址和远端地址组成的4元组:目的IP地址、目的端口号、源IP地址和源端口号来处理传入的多个连接请求
    • 限定的本地IP地址:非本地主机请求,导致连接被内核中的TCP模块拒绝
    • 限定的远端IP地址,大多数API都不支持这么做
  17. 呼入连接请求队列,服务进程始终准备处理下一个呼入的连接请求,当到达多个连接请求,当服务器正处于忙时,TCP是如何处理这些呼入的连接请求?(伯克利的TCP原则:) 1. 等待连接请求的一端,有固定长度的连接队列,队列存放tcp握手完成但应用层未接受连接(注意:TCP接受,放入队列,应用层接受,移除队列) 2. 应用层通过backlog积压值,指定队列最大长度(BSD被设置成backlogx3/2+1),积压值对系统所允许的最大连接数,或者并发服务器所能并发处理的客户数,并无影响 4. 当连接请求SYN到达,TCP根据算法是否接收该连接:
    • 若队列未满,TCP模块接收SYN,完成握手,放入请求队列(此时若应用未处理该请求连接,客户端发送的数据被存储在缓冲队列中)
    • 若队列已满,TCP模块不处理SYN(不接收,也不拒绝 - 软错误),可能被服务端处理(按LIFO的模式),也可能客户端主动打开SYN超时 5. 通常队列已满是由于应用程序或操作系统忙造成的,这样可防止应用程序对传入的连接进行服务。

3.2. TCP首部标识

  1. TCP是一个面向连接的协议,无论哪一方向另一方发送数据之前,都必须先在双方之间建立一条连接,因此搞清楚TCP连接是如何建立的以及通信结束后是如何终止是必须知道事情。
  2. 使用UDP向另一端发送数据报时,无需任何预先的握手。
  3. TCP首部标识,多个可能同时出现在同一个报文:
    • S: SYN,同步序号
    • F: FIN,发送方完成数据发送
    • R: RST,复位连接
    • P: PSH,尽可能快地将数据推送到接收进程
    • .: SFRP标志位皆置为0
    • A: ACK
    • U: URG

3.3. 常规TCP连接建立和终止

    • 源>目的:标志位
    • 1415531521:1415531521(0):开始的序号、一个冒号、隐含的结尾序
    • 结尾序显示(1、2、4、6报文),在这个例子中通信双方没有交换任何数据。
      • 报文段中至少包含一个数据字节
      • SYN、FIN或RST被设置为1时才显示
    • 第2行中,字段ack1415531522表示确认序号(ACK标志比特被设置1时才显示)
    • 每行显示的字段win4096表示发端通告的窗口大小(默认情况下的4096)
    • 表示由发端指明的最大报文段长度选项,发端将不接收超过这个长度的TCP报文段,这通常是为了避免分段。
  1. 建立TCP连接(三次握手),三个报文段完成连接的建立,
    • 请求端/客户端,发送SYN段,指明客户打算连接的服务器的端口,以及初始序号ISN(报文段1)
    • 服务器发回包含服务器的初始序号的SYN报文段(报文段2)作为应答,同时,将确认序号设置为客户的ISN加1以对客户的SYN报文段进行确认,一个SYN将占用一个序号。
    • 客户必须将确认序号设置为服务器的ISN加1以对服务器的SYN报文段进行确认(报文段3)
  2. 发送第一个SYN的一端将执行主动打开(active open),接收这个SYN并发回下一个SYN的另一端执行被动打开(passive open)
  3. 当一端为建立连接而发送它的SYN时,它为连接选择一个初始序号。ISN随时间而变化,因此每个连接都将具有不同的ISN。RFC 793[Postel 1981c]指出ISN可看作是一个32比特的计数器,每4ms加1。这样选择序号的目的在于防止在网络中被延迟的分组在以后又被传送,而导致某个连接的一方对它作错误的解释。在4.4BSD(和多数的伯克利的实现版)中,系统初始化时初始的发送序号被初始化为1,每0.5秒增加64000,并每隔9.5小时又回到0(对应这个计数器每8 ms加1,而不是每4 ms加1),另外,每次建立一个连接后,这个变量将增加64000。
  4. 断开TCP连接(四次挥手),这由TCP的半关闭(halfclose)造成的:
    • 键入quit命令后发生,导致TCP客户端发送一个FIN,用来关闭从客户到服务器的数据传送。(报文4)
    • 当服务器收到这个FIN,它发回一个ACK,确认序号为收到的序号加1(报文段5)(和SYN一样,一个FIN将占用一个序号),同时TCP服务器还向应用程序(即丢弃服务器)传送一个文件结束符。
    • 接着这个服务器程序就关闭它的连接,导致它的TCP端发送一个FIN(报文段6)
    • 客户必须发回一个确认,并将确认序号设置为收到序号加1(报文段7)
  5. TCP连接是全双工(即数据在两个方向上能同时传递),因此每个方向必须单独地进行关闭。这原则就是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向连接。当一端收到一个FIN,它必须通知应用层另一端几经终止了那个方向的数据传送。发送FIN通常是应用层进行关闭的结果。
  6. TCP的一端收到一个FIN只意味着在这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。
  7. 首先进行关闭的一方(即发送第一个FIN)将执行主动关闭,而另一方(收到这个FIN)执行被动关闭。
  8. 有很多情况导致无法建立TCP连接:
    • 服务器主机没有处于正常状态,客户端将进行SYN连接重试,大多数伯克利系统将建立一个新连接的最长时间限制为75秒

3.4. TCP的状态变迁图

  1. 说明:
    • 粗的实线箭头表示正常的客户端状态变迁,用粗的虚线箭头表示正常的服务器状态变迁。
    • 两个导致进入ESTABLISHED状态的变迁对应打开一个连接,而两个导致从ESTABLISHED状态离开的变迁主动打开对应关闭一个连接,ESTABLISHED状态是连接双方能够进行双向数据传递的状态。
    • 左下角4个状态放在一个虚线框内,并标为“主动关闭”
    • 右下两个状态(CLOSE_WAIT和LAST_ACK)也用虚线框住,并标为“被动关闭”
    • CLOSED状态不是一个真正的状态,而是这个状态图的假想起点和终点,11个状态的名称(CLOSED,LISTEN,SYN_SENT等)是有意与netstat命令显示的状态名称一致。
    • 从LISTEN到SYN_SENT的变迁是正确的,但伯克利版的TCP软件并不支持它。
    • 当SYN_RCVD状态是从LISTEN状态(正常情况),而不是从SYN_SENT状态(同时打开)进入时,从SYN_RCVD回到LISTEN的状态变迁才是有效的。这意味着如果我们执行被动关闭(进入LISTEN),收到一个SYN,发送一个带ACK的SYN(进入SYN_RCVD),然后收到一个RST,而不是一个ACK,便又回到LISTEN状态并等待另一个连接请求的到来。

3.5. 同时主动打开TCP连接

两个应用程序同时彼此执行主动打开的情况是可能的,尽管发生的可能性极小。每一方必须发送一个SYN,且这些SYN必须传递给对方,这需要每一方使用一个对方熟知的端口作为本地端口,这又称为同时打开(simultaneous open)。 一个同时打开的连接需要交换4个报文段,比正常的三次握手多一个。此外,要注意的是我们没有将任何一端称为客户或服务器,因为每一端既是客户又是服务器。

  • 两端几乎在同时发送SYN,并进入SYN_SENT状态
  • 当每一端收到SYN时,状态变为SYN_RCVD,同时它们都再发SYN并对收到的SYN进行确认
  • 当双方都收到SYN及相应的ACK时,状态都变迁为ESTABLISHED。

3.6. 同时主动关闭TCP连接

双方都执行主动关闭也是可能的,TCP协议也允许这样的同时关闭(simultaneous close)。

  • 当应用层发出关闭命令时,双方各发送一个FIN,两个FIN经过网络传送后分别到达另一端,两端均从ESTABLISHED变为FIN_WAIT_1
  • 当每一端收到FIN后,状态由FIN_WAIT_1变迁到CLOSING,并发送最后的ACK,对FIN进行确认
  • 当双方都收到最后的ACK时,状态变化为TIME_WAIT

3.7. 最大报文段长度(MSS)

  1. 服务类型字段,IP数据报内的服务类型(TOS)字段,BSD/386中的Telnet客户进程将这个字段设置为最小时延。
  2. 最大报文段长度(MSS)
    • MSS表示TCP传往另一端的最大块数据的长度,当一个连接建立时,连接的双方都要通告各自的MSS,MSS选项只能出现在SYN报文段中,起到协商左右,但如果一方不接收来自另一方的MSS值,则MSS就定为默认值536字节(这个默认值允许20字节的IP首部和20字节的TCP首部以适合576字节IP数据报)。
    • IP数据报通常是40字节长:20字节的TCP首部和20字节的IP首部。
    • 当TCP发送一个SYN时,或者是因为一个本地应用进程想发起一个连接,或者是因为另一端的主机收到了一个连接请求,它能将MSS值设置为外出接口上的MTU长度(1500)减去固定的IP首部(20)和TCP首部长度(20)。对于一个以太网,MSS值可达1460字节。使用IEEE 802.3的封装(参见2.2节),它的MSS可达1452字节。
    • 许多BSD的实现版本需要MSS为512的倍数,其他的系统,如SunOS 4.1.3、Solaris 2.2和AIX 3.2.2,当双方都在一个本地以太网上时都规定MSS为1460。

3.8. TCP的半关闭

  1. TCP的半关闭:TCP提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力
    • 如果应用程序不调用close而调用shutdown,且第2个参数值为1,则插口的API支持半关闭。然而,大多数的应用程序通过调用close终止两个方向的连接。
  2. 执行排序命令: sun % rsh bsdi sort < datafile
    • 在主机bsdi上执行sort排序命令,rsh命令的标准输入来自文件datafile。rsh将在它与在另一主机上执行的程序间建立一个TCP连接。rsh的操作很简单:它将标准输入(datafile)复制给TCP连接,并将结果从TCP连接中复制给标准输出(我们的终端)。
    • 在远端主机bsdi上,rshd服务器将执行sort程序,它的标准输入和标准输出都是TCP连接。
    • sort程序只有读取到所有输入数据后才能产生输出。所有的原始数据通过TCP连接从rsh客户端传送到sort服务器进行排序。当输入(datafile)到达文件尾时,rsh客户端执行这个TCP连接的半关闭。
    • 接着sort服务器在它的标准输入(这个TCP连接)上收到一个文件结束符,对数据进行排序,并将结果写在它的标准输出上(TCP连接)。
    • rsh客户端继续接收来自TCP连接另一端的数据,并将排序的文件复制到它的标准输出上。
    • 没有半关闭,需要其他的一些技术让客户通知服务器,客户端已经完成了它的数据传送,但仍要接收来自服务器的数据。使用两个TCP连接也可作为一个选择,但使用半关闭的单连接更好。

3.9. 2MSL等待状态

  1. TIME_WAIT状态也称为2MSL等待状态,每个具体TCP实现必须选择一个报文段最大生存时间MSL(Maximum Segment Lifetime)。它是任何报文段被丢弃前在网络内的最长时间。我们知道这个时间是有限的,因为TCP报文段以IP数据报在网络内传输,而IP数据报则有限制其生存时间的TTL字段。
  2. RFC 793 [Postel 1981c]指出MSL为2分钟。然而,实现中的常用值是30秒,1分钟,或2分钟。
  3. 2MSL处理的原则是:当TCP执行一个主动关闭,并发回最后一个ACK,该连接必须在TIME_WAIT状态停留的时间为2倍的MSL,这样可让TCP再次发送最后的ACK以防这个ACK丢失(若另一端超时并重发最后的FIN)
  4. 弊端:在2MSL等待期间,定义这个连接的插口(客户的IP地址和端口号,服务器的IP地址和端口号)不能再被使用。这个连接只能在2MSL结束后才能再被使用。大多数TCP实现(如伯克利版)强加了更为严格的限制。在2MSL等待期间,插口中使用的本地端口在默认情况下不能再被使用。
  5. SO_REUSEADDR选项可以配置地址重用(有风险,违反了TCP规范),当要建立一个有效的连接时,来自该连接的一个较早替身(incarnation)的迟到报文段作为新连接的一部分不可能被曲解。
  6. 客户执行主动关闭并进入TIME_WAIT是正常的。服务器通常执行被动关闭,不会进入TIME_WAIT状态。这暗示如果我们终止一个客户程序,并立即重新启动这个客户程序,则这个新客户程序将不能重用相同的本地端口。(如果很高频次,容易导致客户端的端口资源耗尽)
  7. 平静时间:TCP在重启动后的MSL秒内不能建立任何连接。这就称为平静时间(quiet time)。

3.10. FIN_WAIT_2状态

在FIN_WAIT_2状态我们已经发出了FIN,并且另一端也已对它进行确认。除非我们在实行半关闭,否则将等待另一端的应用层意识到它已收到一个文件结束符说明,并向我们发一个FIN来关闭另一方向的连接。只有当另一端的进程完成这个关闭,我们这端才会从FIN_WAIT_2状态进入TIME_WAIT状态。这意味着我们这端可能永远保持这个状态。另一端也将处于CLOSE_WAIT状态,并一直保持这个状态直到应用层决定进行关闭。

许多伯克利实现采用如下方式来防止这种在FIN_WAIT_2状态的无限等待,设置了定时器,如果这个连接空闲10分钟75秒,TCP将进入CLOSED状态(这个实现代码违背协议的规范)。

3.11. RST复位报文段

  1. TCP首部中的RST比特是用于“复位”的。
  2. 无论何时一个报文段发往基准的连接(referenced connection)出现错误,TCP都会发出一个复位报文段(这里提到的“基准的连接”是指由目的IP地址和目的端口号以及源IP地址和源端口号指明的连接。这就是为什么RFC 793称之为插口)。
  3. 复位报文段产生场景:
    • 当连接请求不存在的端口
    • 异常释放,异常终止一个连接,
    • 检测半打开连接
  4. 有序释放:终止一个连接的正常方式是一方发送FIN。有时这也称为有序释放(orderly release),因为在所有排队数据都已发送之后才发送FIN,正常情况下没有任何数据丢失。
  5. 异常释放:可能发送一个RST复位报文段而不是FIN来中途释放一个连接,这为异常释放(abortive release)
    • 丢弃任何待发数据并立即发送复位报文段
    • RST的接收方会区分另一端执行的是异常关闭还是正常关闭
    • RST报文段不会导致另一端产生任何响应,另一端根本不进行确认,收到RST的一方将终止该连接,并通知应用层连接复位。
    • 场景异常释放的错误:read error: Connect reset by peer
  6. 任何一端的主机异常都可能导致发生半打开(Half-Open)的情况,只要不打算在半打开连接上传输数据,仍处于连接状态的一方就不会检测另一方已经出现异常。(比如客户主机突然掉电、网络断开,服务器将永远不知道客户程序已经消失了)
  7. TCP的keepalive选项能使TCP的一端发现另一端已经消失

3.12. TCP选项

  1. 常规的选项表结束、无操作和MSS最大报文段长度
  2. 最新的TCP实现中才能见到新的TCP选项(RFC1323)

3.13. TCP服务器的设计

  1. TCP服务器进程是并发的,当一个新的连接请求到达服务器时,服务器接受这个请求,并调用一个新进程来处理这个新的客户请求(不同的操作系统使用不同的技术来调用新的服务器进程)。
  2. 在Unix系统下,常用的技术是使用fork函数来创建新的进程,如果系统支持,也可使用轻型进程,即线程(thread)
  3. 当一个服务器进程接受一来自客户进程的服务请求时是如何处理端口的?如果多个连接请求几乎同时到达会发生什么情况?
    • TCP服务器端口号、本地地址、远端地址、LISTEN状态,等待连接请求的到达
    • 当传入的连接请求到达并被接收时,处于LISTEN状态的服务器进程仍然存在,同时系统内核中的TCP模块就创建一个处于ESTABLISHED状态的进程。
    • TCP使用由本地地址和远端地址组成的4元组:目的IP地址、目的端口号、源IP地址和源端口号来处理传入的多个连接请求。
  4. 限定的本地IP地址:程序指明一个IP地址(或主机名),并将它作为服务器,那么该IP地址就成为处于LISTEN服务器的本地IP地址。(错误的连接请求,将被拒绝)
  5. 限定的远端IP地址:RFC 793中显示的接口函数允许一个服务器在执行被动打开时,可指明远端插口(等待一个特定的客户执行主动打开),也可不指明远端插口(等待任何客户);大多数API都不支持这么做,因为没有什么太多意义,服务器必须不指明远端插口,而等待连接请求的到来,然后检查客户端的IP地址和端口号。

3.14. 呼入连接请求队列

伯克利的TCP实现中采用以下规则,解决连接请求队列问题:

  1. 正等待连接请求的一端有一个固定长度的连接队列,该队列中的连接已被TCP接受(即三次握手已经完成),但还没有被应用层所接受。(TCP接受一个连接是将其放入这个队列,而应用层接受连接是将其从该队列中移出。)
  2. 应用层将指明请求队列的最大长度,这个值通常称为积压值(backlog),积压值说明的是TCP监听的端点已被TCP接受而等待应用层接受的最大连接数。
  3. 当一个连接请求(即SYN)到达时,TCP使用一个算法,根据当前连接队列中的连接数来确定是否接收这个连接。
  4. 如果对于新的连接请求,该TCP监听的端点的连接队列中还有空间(连接队列未满),TCP模块将对SYN进行确认并完成连接的建立。但应用层只有在三次握手中的第三个报文段收到后才会知道这个新连接时。另外,当客户进程的主动打开成功但服务器的应用层还不知道这个新的连接时,它可能会认为服务器进程已经准备好接收数据了(如果发生这种情况,客户端可能在发送数据了,服务器的TCP仅将接收的数据放入缓冲队列)。
  5. 如果对于新的连接请求,连接队列中已没有空间,TCP将不理会收到的SYN。也不发回任何报文段(即不发回RST)。如果应用层不能及时接受已被TCP接受的连接,这些连接可能占满整个连接队列,客户的主动打开最终将超时。

  • 1090、1091都是正常的TCP成功
  • 1092、1093由于TCP请求队列已满,出现SYN重试
  • 1093率先重试SYN连接成功,后续1092也重试成功(我们期望接收连接队列按先进先出顺序传递给应用层,许多伯克利的TCP实现都出现按后进先出的传递顺序,这个错误已存在了多年)

当队列已满时,TCP将不理会传入的SYN,也不发回RST作为应答,因为这是一个软错误,而不是一个硬错误。通常队列已满是由于应用程序或操作系统忙造成的,这样可防止应用程序对传入的连接进行服务。这个条件在一个很短的时间内可以改变。但如果服务器的TCP以系统复位作为响应,客户进程的主动打开将被废弃(如果服务器程序没有启动我们就会遇到)。由于不应答SYN,服务器程序迫使客户TCP随后重传SYN,以等待连接队列有空间接受新的连接。

3.15. 小结

  1. 两个进程在使用TCP交换数据之前,它们之间必须建立一条连接,完成后,要关闭这个连接,详解了该过程;
  2. 利用tcpdump程序显示了TCP首部中的各个字段,也了解了连接建立是如何超时,连接复位是如何发送,使用半打开连接发生的情况以及TCP是如何提供半关闭、同时打开和同时关闭。
  3. 弄清TCP操作的关键在于它的状态变迁图。我们跟踪了连接建立与关闭的步骤以及它们的状态变迁过程。还讨论了在设计TCP并发服务器时TCP连接建立的具体实现方法。
  4. 一个TCP连接由一个4元组唯一确定:本地IP地址、本地端口号、远端IP地址和远端端口号。
  5. 无论何时关闭一个连接,一端必须保持这个连接,我们看到TIME_WAIT状态将处理这个问题。处理的原则是执行主动打开的一端在进入这个状态时要保持的时间为TCP实现中规定的MSL值的两倍。

4. 辅助命令

4.1. tcpdump

  • -S: 输出序列号,而非相对偏移地址

4.2. netstat

  • -a: 显示网络中的所有主机端,而不仅仅是处于ESTABLISHED的主机端
  • -n: 以点分十进制的形式显示IP地址,而不是通过DNS将地址转化为主机名,同时还要求显示端口号
  • -f inet: 仅要求显示使用TCP或UDP的主机