用户数据报协议(UDP)
UDP是一个简单的传输层协议(RFC 768)。
进程往一个UDP套接字写入一个消息,该消息随后被封装(encapsulating)到一个UDP数据报,该UDP数据报进而又被封装到一个IP数据报,然后发送到目的地。
(1) UDP的几个“不保证”
[1] 不保证UDP数据报会到达其最终目的地;
[2] 不保证各个数据报的先后顺序跨网络后保持不变;
[3] 不保证每个数据报只到达一次;
……
总之,UDP不提供可靠性,其本身不提供确认、序列号、RTT估算、超时、重传、流量控制等机制。如果想要确保一个数据报达到其目的地,可以往应用程序中添置一大堆的特性:来自对端的确认、本端的超时与重传等。即,UDP应用必须处理所有这些情况。
(2) UDP几个特点
[1] 每个UDP数据报都有一个长度,而TCP是一个字节流(byte-stream)协议,没有任何记录边界,这一点不同于UDP;
[2] UDP提供无连接的(connectionless)服务,因为UDP客户与服务器之间不必存在任何长期的联系。即,一个UDP客户可以创建一个套接字并发送一个数据报给一个给定的服务器,然后立即用同一个套接字发送另一个数据报给另一个服务器;同样,一个UDP服务器可以用同一个UDP套接字从若干个不同的客户接收数据报;
传输控制协议(TCP)
由TCP向进程提供的服务不同于由UDP提供的服务。
(1) TCP提供客户与服务器之间的连接(connection)。TCP客户先与某个给定服务器建立一个连接,再跨该连接与那个服务器交换数据,然后终止这个连接。
(2) TCP还提供了可靠性(reliability)。当TCP向另一端发送数据时,它要求对端返回一个确认。如果没有收到确认,TCP就自动重传数据并等待更长时间,在数次重传失败后,TCP才放弃,如此在尝试发送数据上所花的总时间一般为4~10分钟(依赖于具体实现)。即,TCP的可靠性是指“数据的可靠递送或故障的可靠通知”。
(3) TCP含有用于动态估算客户和服务器之间的往返时间(round-trip time, RTT)的算法,以便它知道等待一个确认需要多少时间。
(4) TCP通过给其中每个字节关联一个序列号,对所发送的数据进行排序(sequencing)。例如,假设一个应用写2048字节到一个TCP套接字,导致TCP发送2个分节:第一个分节所含数据的序列号为1~1024,第二个分节所含数据的序列号为1025~2048。(注意:分节是TCP传递给IP的数据单元。)如果这些分节非顺序到达,接收端TCP将先根据它们的序列号重新排序,再把结果数据传递给接收应用。如果接收端TCP接收到来自对端的重复数据(譬如说,对端认为一个分节已丢失并因此重传,而这个字节并没有真正丢失,只是网络通信过于拥挤),它可以根据序列号判定数据是重复的,从而丢弃重复数据。
(5) TCP提供流量控制(flowcontrol)。TCP总是告知对端在任何时刻它一次能够从对端接收多少字节的数据,这称为通告窗口(advertised window)。即,在任何时刻,该窗口指出接收缓冲区中当前可用的空间量,从而确保发送端发送的数据不会使接收缓冲区溢出。
该窗口时刻动态变化:当接收到来自发送端的数据时,窗口大小就减小;当接收端应用从缓冲区中读取数据时,窗口大小就增大。
通知窗口大小减小到0是有可能的:当TCP对应某个套接字的接收缓冲区已满,导致它必须等待应用从该缓冲区读取数据时,方能从对端再接收数据。
(6) TCP连接是全双工的(full-duplex)。即,在一个给定的连接上应用可以在任何时刻在进出两个方向上既发送数据又接收数据。因此,TCP必须为每个数据流方向跟踪诸如序列号和通知窗口大小等状态信息。
TCP连接的建立和终止
(1) TCP连接建立——三次握手
[1] 服务器必须准备好接收外来的连接,这通常通过调用socket、bind和listen这3个函数来完成,称之为“被动打开”(passive open);
[2] 客户调用connect发起“主动打开”(active open)。这导致客户TCP发送一个SYN(同步)分节,它告诉服务器客户将在(待建立的)连接中发送的数据的初始序列号。通常SYN分节不携带数据,其所在IP数据报只含有一个IP首部、一个TCP首部及可能有的TCP选项;
[3] 服务器必须确认(ACK)客户的SYN,同时自己也得发送一个SYN分节,它含有服务器将在同一连接中发送的数据的初始序号列。服务器在单个分节中发送SYN和对客户SYN的ACK(确认);
[4] 客户必须确认服务器的SYN;
这种交换至少需要3个分组,因此称之为TCP的三路握手(three-way handshake)。
(2) TCP选项
每一个SYN可以包含有多个TCP选项,一些常用的选项有:
[1] MSS选项
发送SYN的TCP一端使用本选项通告对端它的最大分节大小(maximum segmentsize, MSS),即,它在本连接的每个TCP分节中愿意接收的最大数据量。发送端TCP使用接收端的MSS值作为所发送分节的最大大小。TCP_MAXSEG
[2] 窗口规模选项
TCP连接任何一端能够通告对端的最大窗口大小是65535,因为在TCP首部中相应的字段占16位。但目前要求有更大的窗口以获得尽可能大的吞吐量,这个新选项指定TCP首部中的通告窗口必须扩大(即左移)的位数(0~14),因此所提供的最大窗口接近1GB(65535*2^14)。
在一个TCP连接上使用窗口规模的前提是它的两个端系统必须都支持这个选项。SO_RCVBUF
[3] 时间戳选项
这个选项对于高速网络连接是必要的,它可以防止由“失而复现的分组”可能造成的数据损坏。
PS: “失而复现的分组”不是超时重传的分组,而是由暂时的路由原因造成的迷途的分组,当路由稳定后,它们又会正常到达目的地,其前提是它们在此前尚未被路由器丢弃。高速网络中32位的序列号短时间内就可能循环一轮重新使用,若不用时间戳选项,失而复现的分组所承载的分节可能与再次使用相同序列号的真正分节发生混淆。
(3) TCP连接终止——四次握手(通常)
TCP终止一个连接则需4个分节。
[1] 某个应用进程首先调用close,称该端执行“主动关闭”(active close)。该端的TCP于是发送一个FIN分节,表示数据发送完毕。
[2] 接收到这个FIN的对端执行 “被动关闭”(passive close),这个FIN由TCP确认。
注意:FIN的接收也作为一个文件结束符(end-of-file)传递给接收端应用进程,放在已排队等候该应用进程接收的任何其他数据之后,因为,FIN的接收意味着接收端应用进程在相应连接上再无额外数据可接收。
[3] 一段时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字。这导致它的TCP也发送一个FIN。
[4] 接收这个最终FIN的原发送端TCP(即执行主动关闭的那一端)确认这个FIN。
既然每个方向都需要一个FIN和一个ACK,因此通常需要4个分节。
注意:
[1]“通常”是指,某些情况下,步骤1的FIN随数据一起发送,另外,步骤2和步骤3发送的分节都出自执行被动关闭那一端,有可能被合并成一个分节。
[2] 在步骤2与步骤3之间,从执行被动关闭一端到执行主动关闭一端流动数据是可能的,这称为“半关闭”(half-close),可参考shutdown。
[3] 当一个Unix进程无论自愿地(调用exit或从main函数返回)还是非自愿地(收到一个终止本进程的信号)终止时,所有打开的描述符都被关闭,这也导致仍然打开的任何TCP连接上也发出一个FIN。
[4] 无论是客户还是服务器,任何一端都可以执行主动关闭。通常情况是,客户执行主动关闭,但是某些协议,例如,HTTP/1.0却由服务器执行主动关闭。
TCP状态转换图
TCP涉及连接建立和连接终止的操作可以用状态转换图(state transitiondiagram)来说明。这些状态可使用netstat工具显示,方便监视状态的变化。
TCP为一个连接定义了11种状态,并且TCP规则规定如何基于当前状态及在该状态下所接收的分节从一个状态转换到另一个状态。
11种状态,含义如下:
状态 |
描述 |
(1) CLOSED |
关闭状态,没有连接活动或正在进行 |
(2) LISTEN |
监听状态,服务器正在等待连接进入 |
(3) SYN_RCVD |
收到一个连接请求,尚未确认 |
(4) SYN_SENT |
已经发送连接请求(SYN),等待确认 |
(5) ESTABLISHED |
连接建立,正常数据传输状态 |
(6) FIN_WAIT_1 |
(主动关闭)已经发送关闭请求(FIN),等待确认 |
(7) FIN_WAIT_2 |
(主动关闭)收到对方关闭确认(ACK),等待对方关闭请求(FIN) |
(8) TIME_WAIT |
完成双向关闭,等待所有分组死掉(目的是防止主动关闭端最后发出的ACK丢失,接收端处于LAST_ACK超时重发FIN,因此主动关闭端会进入TIME_WAIT状态,在压力测试时,这种状态会大量存在,进程所占用的端口号不能被释放,导致后来的连接建立失败) |
(9) CLOSING |
双方同时尝试关闭,等待对方确认 |
(10) CLOSE_WAIT |
(被动关闭)收到对方关闭请求,已经确认 |
(11) LAST_ACK |
(被动关闭)等待最后一个关闭确认(ACK),等待所有分组死掉 |
(1) 连接建立——三次握手
当某个进程在CLOSED状态下执行主动打开时,TCP将发送一个SYN,且新的状态是SYN_SENT,如果这个TCP接着接收到一个带ACK的SYN,它将发送一个ACK,且新的状态是ESTABLISHED,这个最终状态是绝大多数数据传送发生的状态。
(2) 连接终止——四次握手
自ESTABLISHED状态引出的两个“方向”处理连接的终止。如果某个进程在接收到一个FIN之前调用close(主动关闭),那就转换到FIN_WAIT_1状态;但如果某个进程在ESTABLISHED状态期间接收到一个FIN(被动关闭),那就转换到CLOSE_WAIT状态。
注意:
[1] 客户端和服务器端都可以执行主动关闭和被动关闭。
[2] 关于同时打开(simultaneous open),发生在两端几乎同时发送SYN并且这两个SYN在网络中交错的情形下;同时关闭(simultaneousclose),发生在两端几乎同时发送FIN的情形下。(在TCPv1的第18章有这两种情况的讨论)
分组交换
讨论一个完整的TCP连接所发生的实际分组交换情况,包括连接建立、数据传送和连接终止3个阶段,并且表明每个端点所历经的TCP状态。
(1) 客户通告一个值为536的MSS(最大分节大小),表明该客户只实现了最小重组缓冲区大小;服务器通告一个值为1460的MSS(以太网上IPv4的典型值)。不同方向上MSS值可以不同。
(2) 连接建立后,客户就构造一个请求并发送给服务器。这里假设该请求适合于单个TCP分节,即请求的大小小于服务器通告的值为1460字节的MSS;服务器处理完该请求并发送一个应答,假设该应答也适合于单个分节,即小于536字节。
注意:
[1] 服务器对客户请求的确认可以是伴随其应答一起发送的,即P+ACK,这种做法称为“捎带”(piggybacking),它通常在服务器处理请求并产生应答的时间少于200ms时发生。如果服务器耗用更长时间,譬如说1s,那么将看到先是确认后是应答。(参考TCPv1第19章和第20章的TCP数据流机理)
(3) 最后是终止连接的4个分节。执行主动关闭的一端,将进入TIME_WAIT状态。
TIME_WAIT状态
(1) 2MSL
执行主动关闭的那端会经历TIME_WAIT状态,该端点停留在这个状态的持续时间是:最长分节生命期的两倍(maximumsegment lifetime, MSL),即,2MSL。
任何TCP实现都必须为MSL选择一个值。RFC 1122[Braden 1989]的建议值是2分钟,不过源自Berkeley的实现传统上改用30秒这个值。这意味着:TIME_WAIT状态的持续时间在1分钟到4分钟之间。
MSL是任何IP数据报能够在因特网中存活的最长时间,这个时间是有限的,因为每个数据报含有一个称为跳限(hop limit)的8位字段,可见IPv4的TTL字段,它的最大值为255。注意:尽管这是一个跳数限制而不是真正的时间限制,我们仍然假设:具有最大跳限(255)
的分组在网络中存在的时间不可能超过MSL秒。
(2) TIME_WAIT状态存在的理由
有两个理由:
[1] 可靠地实现TCP全双工连接的终止。
[2] 允许老的重复分节在网络中消失。
理由1的解释:
假设最终的ACK丢失了,服务器将重新发送它的最终那个FIN,因此客户必须维护状态信息,以允许它重新发送最终那个ACK。要是客户不维护状态信息,它将响应以一个RST(另外一种类型的TCP分节),该分节将被服务器解释成一个错误。
如果TCP打算执行所有必要的工作以彻底终止某个连接上两个方向的数据流,即全双工关闭,那么它必须正确处理连接终止序列4个分节中任何一个分节丢失的情况。
为什么执行主动关闭的那一端是处于TIME_WAIT状态的那一端:因为可能不得不重传最终那个ACK的就是主动关闭端。
理由2的解释:
TCP必须防止来自某个连接的老的重复分组在该连接已终止后再现,从而被误解成属于同一连接的某个新的化身。既然TIME_WAIT状态的持续时间是2MSL,这就足以让某个方向上的分组最多存活MSL秒即被丢弃,另一个方向上的应答最多存活MSL秒也被丢弃。通过实施这个规则,就能保证每成功建立一个TCP连接时,来自该连接先前化身的老的重复分组都已在网络中消逝了。
关于TMIE_WAIT状态的进一步说明
在内核协议层通过设置以下两个参数来解决TIME_WAIT问题:
(1) TIME_WAIT状态可以重用,这样即使TIME_WAIT占满了所有端口,也不会拒绝新的请求;
echo 1 > /proc/sys/net/ipv4/tcp_tw_reuse
(2) 让TIME_WAIT尽快回收
echo 1 >/proc/sys/net/ipv4/tcp_tw_recycle
PS: According to Linux documentation
tcp_tw_recycle - BOOLEAN 460 Enable fast recycling TIME-WAIT sockets. Default value is 0. 461 It should not be changed without advice/request of technical 462 experts. 463 464tcp_tw_reuse - BOOLEAN 465 Allow to reuse TIME-WAIT sockets for new connections when it is 466 safe from protocol viewpoint. Default value is 0. 467 It should not be changed without advice/request of technical 468 experts.
As described here, The TCP_TW_RECYCLE could cause some problems when using load balancers...
好处:
其实只修改tcp_tw_recycle就可以解决问题,TIME_WAIT重用TCP协议本身是不建议打开的。tcp_tw_recycle打开后,能在很短的时间能将TIME_WAIT的端口回收,但是具体时间并未找到相应的资料,测试观察在1s左右。
问题:
打开加速回收或允许重用,也存在一定的问题。因为,TIME_WAIT状态需要等待2MSL的目的之一就是要保证老的重复分节在网络中死掉,而如果快速回收的话,例如,发送端在发出最后一个ACK后立即被回收,而此ACK丢失,接收端超时重发FIN,而由于快速回收,恰好此时发送端使用刚才的端口建立起新的连接,那新的连接将收到一个异常的FIN,从而可能引发新的连接被异常被动关闭。
TMIE_WAIT的解决方法
短连接最大的缺点是将占用大量的系统资源,例如:本地端口、socket句柄。导致这个问题的原因其实很简单:tcp协议层并没有长短连接的概念,因此不管长连接还是短连接,连接建立->数据传输->连接关闭的流程和处理都是一样的。
正常的TCP客户端连接在关闭后,会进入一个TIME_WAIT的状态,持续的时间一般在1~4分钟,对于连接数不高的场景,1~4分钟其实并不长,对系统也不会有什么影响,但如果短时间内(例如1s内)进行大量的短连接,则可能出现这样一种情况:客户端所在的操作系统的socket端口和句柄被用尽,系统无法再发起新的连接!
举例来说:假设每秒建立了1000个短连接(Web场景下是很常见的,例如每个请求都去访问memcached),假设TIME_WAIT的时间是1分钟,则1分钟内需要建立6W个短连接,由于TIME_WAIT时间是1分钟,这些短连接1分钟内都处于TIME_WAIT状态,都不会释放,而Linux默认的本地端口范围配置是:net.ipv4.ip_local_port_range = 32768 61000
不到3W,因此这种情况下新的请求由于没有本地端口就不能建立了。
可以通过如下方式来解决这个问题:
1)可以改为长连接,但代价较大,长连接太多会导致服务器性能问题,而且PHP等脚本语言,需要通过proxy之类的软件才能实现长连接;
2)修改ipv4.ip_local_port_range,增大可用端口范围,但只能缓解问题,不能根本解决问题;
3)客户端程序中设置socket的SO_LINGER选项;
4)客户端机器打开tcp_tw_recycle和tcp_timestamps选项;
5)客户端机器打开tcp_tw_reuse和tcp_timestamps选项;
6)客户端机器设置tcp_max_tw_buckets为一个很小的值;
附相关流程图片:
图1 三次握手建立连接
图2 四次握手关闭连接
图3 TCP状态转换图
图4 TCP连接的分组交换