首页 » 网络 » TCP协议

TCP总览

TCP状态转换图

TCP状态转换图

TCP报文头

TCP报文头

三次握手

            client                  server
SYN_SENT    --------- SYN J --------->
            <-------- SYN K/ACK J+1 --  ESTABLISHED
            --------- ACK K+1 ------->
                                        ESTABLISHED

SYN包中可能携带以下信息

  1. MSS(Maximum Segment Size),可由TCP_MAXSEG获取或设置。
  2. Window Size,由SO_RCVBUF决定。原为65535,后增加14bit的扩展窗口,因此最大值为65535 * 2^14 (1GB)。
  3. 时间戳选项。

其中,2/3由RFC1323定义。Window Size的扩展,也称为Long Fat Pipe(即所谓长肥管道)。

半开连接

收到客户端的SYN报文后,Server处于半开half-open状态,SYN攻击即Client不执行下一步响应ACK。内核中半开连接数有限,查看Linux内核backlog队列的默认长度:

# sysctl net.ipv4.tcp_max_syn_backlog
net.ipv4.tcp_max_syn_backlog = 1024

在Socket API中,listen(sockfd, backlog)的第二个参数设置即为半开连接的队列长度,显然超过内核限定的队列长度没有意义。对于半开连接攻击(SYN flood),Linux中有一种名为SYN Cookie的保护措施。

SYN初始序列号

当TCP实现发送SYN报文时,将为之选择一个ISN(Initial Sequence Number,初始序列号),RFC 793中指出ISN是一个32bit的计数器,每4ms加1。4.4BSD中,系统初始化时ISN被设置为1(与标准建议不符),每隔0.5秒增加64000(这个计数器每8ms加1),另外每次建立连接后,ISN将增加64000。

选择ISN的目的在于防止网络中被延迟的分组在以后又被传送,导致连接的另一端对其做出错误的解释。

SYN报文的超时

在Linux上可见超时时间大约为3分30秒:

$ date; telnet facebook.com 80; date
Sat Nov 10 11:05:43 CST 2012
Trying 203.98.7.65...
telnet: connect to address 203.98.7.65: Connection timed out
Sat Nov 10 11:09:12 CST 2012

以下是对应的tcpdump抓包输出:

11:06:03.180678 IP 192.168.1.105.49026 > 203.98.7.65.http: Flags [S], seq 2074592499, win 5840, options [mss 1460,sackOK,TS val 9954586 ecr 0,nop,wscale 4], length 0
11:06:06.180978 IP 192.168.1.105.49026 > 203.98.7.65.http: Flags [S], seq 2074592499, win 5840, options [mss 1460,sackOK,TS val 9957587 ecr 0,nop,wscale 4], length 0
11:06:12.181220 IP 192.168.1.105.49026 > 203.98.7.65.http: Flags [S], seq 2074592499, win 5840, options [mss 1460,sackOK,TS val 9963587 ecr 0,nop,wscale 4], length 0
11:06:24.180853 IP 192.168.1.105.49026 > 203.98.7.65.http: Flags [S], seq 2074592499, win 5840, options [mss 1460,sackOK,TS val 9975587 ecr 0,nop,wscale 4], length 0
11:06:48.180291 IP 192.168.1.105.49026 > 203.98.7.65.http: Flags [S], seq 2074592499, win 5840, options [mss 1460,sackOK,TS val 9999587 ecr 0,nop,wscale 4], length 0
11:07:36.180339 IP 192.168.1.105.49026 > 203.98.7.65.http: Flags [S], seq 2074592499, win 5840, options [mss 1460,sackOK,TS val 10047587 ecr 0,nop,wscale 4], length 0

从tcpdump输出可见每次的超时时间:3、6、12、24、48、(96),超时规律即为: 3 * 2n,n取值为[0,5]。建立TCP连接的超时时间最长为:

timeout = 3 * (26 - 1) = 3 * 63 = 189

对比下《TCP/IP详解卷1》关于TCP连接超时的描述:典型伯克利实现有3次超时,第1次超时时间在5.59-5.93秒之间变化,第2次超时时间总是24.00秒,第3次超时时间为48秒。大多数伯克利实现的TCP连接总的超时时间约为75秒。

TIME_WAIT状态

主动关闭连接的peer会进入TIME_WAIT状态。TIME_WAIT通常又叫做2MSL,MSL(Maximum Segment Lifetime)指报文的最大生存周期。TCP建立在IP之上,而IP报文的生存周期由TTL限制,IP头中的TTL字段占8bit,最大值为255,表示一个IP报文可被传播的最大跳数(hop)。虽然TTL不是一个时间值,但它确实限制了IP报文在网络中的生存时间。RFC 1122建议MSL是2分钟,伯克利版本的TCP实现通常设置为30秒,通常我们认为MSL的取值为30秒到2分钟,因此TIME_WAIT的时间为1分钟到4分钟。

TCP连接处于TIME_WAIT状态,可以防止最后发出的ACK报文丢失,此时另一端的TCP实现会重发最后的FIN。而处于TIME_WAIT状态的Socket Pair不能再被使用。所谓Socket Pair其实由5个元素组成:

(local-ip, local-port, remote-ip, remote-port, protocol)

在这里协议始终是TCP,也可认为Socket Pair由4元组构成。如果TCP连接处于TIME_WAIT,则4元组构成的连接只能在2MSL结束后才能使用。现实中这个限制还更为严格一些:处于TIME_WAIT状态的端口默认情况下也不能使用。虽然可以通过设置SO_REUSEADDR来绕开这个限制,但TCP原则上仍需避免使用处于TIME_WAIT状态的端口。

在Server开发中,如果中断Server进程,将导致TIME_WAIT连接的出现;而Server必然绑定在一个已知端口(如HTTP的80),因此Server程序通常必然会设置SO_REUSEADDR。对Client程序而言则影响不大,虽然Client通常是主动关闭连接方,但OS总可以选择一个新的端口建立与Server的连接(除非端口耗尽,即所有可用端口都处于TIME_WAIT)。

这里再总结下TIME_WAIT存在的意义:

  • 防止上一次连接的报文出现在新连接中。经过2MSL的等待时间,这些报文都已失效(超过生存周期)。
  • 如果没有TIME_WAIT,主动关闭方将直接进入CLOSED状态,如果对端没有收到最后我们最后发出的ACK报文,将重发FIN,此时我们已进入CLOSED,就会响应RST而非ACK,这将导致对端非正常关闭连接。

TCP的半开与半关闭状态

所谓TCP半开状态(half-open),即指收到SYN报文,并响应SYN/ACK后,对端不发送ACK。

所谓TCP半关闭状态(half-close),即指TCP连接的一方可以选择部分关闭连接。因为TCP是全双工连接,在一条连接上的读/写操作是分开的,因此可以显示地关闭写通道,但还可以继续读取数据。应用程序用shutdown()而非close()关闭连接时,可选择关闭读或写或都关闭:

shutdown(sockfd, SHUT_WR);

如此调用将给对端发送FIN,说明我没有数据可写了,但还可以读数据(进入TIME_WAIT状态)。

参考

Linux Socket编程

分享

0