NYC's Blog - 网络 http://niyanchun.com/tag/%E7%BD%91%E7%BB%9C/ zh-CN Fri, 02 Sep 2016 19:04:00 +0800 Fri, 02 Sep 2016 19:04:00 +0800 TCP状态变迁图 http://niyanchun.com/tcp-state-transition-diagram.html http://niyanchun.com/tcp-state-transition-diagram.html Fri, 02 Sep 2016 19:04:00 +0800 NYC TCP的状态变迁图囊括了TCP的所有状态,这里我们详细学习一下。在此之前,我们先简单回顾一下TCP的三步握手和“四步分手”。

1. TCP的三步握手

因为TCP是全双工的(即数据可以在两个方向上同时传递),所以建立一个TCP连接需要三步握手。如下图,是TCP三步握手的状态图:

establish.png

  1. 服务器端通过调用一系列接口(通常是socket、bind、listen)准备好接受外来的连接,我们称之为被动打开(passive open)。
  2. 客户端通过调用connect发起主动打开(active open)。这将导致客户TCP发送一个SYN分节,它告诉服务器客户将在(待建立的)连接中发送的数据的初始序列号。通常SYN分节不携带数据,其所在IP数据报只包含一个头部、一个TCP首部及可能有的TCP选项。
  3. 服务器必须确认(ACK)客户的SYN,同时自己也发送一个SYN分节,它含有服务器将在同一连接中发送的数据的初始序列号。服务器在单个分节中发送SYN和对客户SYN的ACK(确认)。
  4. 客户必须确认服务器的SYN。

下面是我用wireshark抓的一个TCP三步握手的图:

三步握手.png

2. TCP的四步分手

同理,因为TCP是全双工的,并且又有半关闭(half-close)状态的存在,所以终止一个TCP连接需要4次握手,即每一个方向都必须单独的进行关闭。这个原则就是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。当一端收到一个FIN,它必须通知应用层另一端已经终止了那个方向的数据传送。发送FIN通常是应用层进行关闭的结果。下图是TCP四步分手的状态图

close.png

  1. 某个应用进程(既可以是客户端,也可以是服务端)首先调用close,我们称该端执行主动关闭(active close)。该端的TCP便发送一个FIN分节,表示数据发送完毕。
  2. 接收到FIN的对端执行被动关闭(passive close)。这个FIN由TCP确认。它的接收也作为一个文件结束符(end-of-file)传递给接收端应用进程(放在已排队列等候应用进程接收的任何数据之后),因为FIN的接收意味着接收端应用进程在相应连接上再无额外数据可以接收。
  3. 一段时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字。这导致它的TCP也发送一个FIN。
  4. 接收这个最终FIN的原发送端TCP(即执行主动关闭的那一端)确认这个FIN。

需要注意的是,通常情况下断开一个TCP连接需要4个分节,但是某些情况下第一步的FIN随着数据一起发送;另外第二步和第三步可能被合成一个分节发送。比如下面我用wireshark抓的一个TCP四步分手的图里面就是这种场景:

四步分手.png

OK,TCP的连接和建立过程简单介绍完以后,我们来看一下TCP的状态变迁图。

3. TCP的状态变迁图

TCP的状态变迁图更细致的描述了TCP连接建立和连接终止中的各个状态。下图是一个完整的TCP状态变迁图(图片截自《UNIX网络编程》电子版):

tcp_status.png

TCP为一个连接定义了11种状态,并且TCP规则规定如何基于当前状态及在该状态下所接收的分节从一个状态转换到另一个状态。举例来说,当某个应用进程在CLOSED状态下执行主动打开时,TCP将发送一个SYN,且新的状态是SYN_SENT。如果这个TCP接着接收到一个带ACK的SYN,它将发送发送一个ACK,且新的状态是ESTABLISHED。这个状态是绝大多数数据传送发生的状态。

自ESTABLISHED状态引出的两个箭头处理连接的终止。如果某个应用进程在接收到一个FIN之前调用close(即主动关闭),那就转换到FIN_WAIT_1状态。但如果某个应用进程在ESTABLISHED状态期接收到一个FIN(即被动关闭),那就转到CLOSE_WAIT状态。

TCP的11种状态:

  • ESTABLISHED: The socket has an established connection.
  • SYN_SENT: The socket is actively attempting to establish a connection.
  • SYN_RECV: A connection request has been received from the network.
  • FIN_WAIT1: The socket is closed, and the connection is shutting down.
  • FIN_WAIT2: Connection is closed, and the socket is waiting for a shutdown from the remote end.
  • TIME_WAIT: The socket is waiting after close to handle packets still in the network.
  • CLOSE: The socket is not being used.
  • CLOSE_WAIT: The remote end has shut down, waiting for the socket to close.
  • LAST_ACK: The remote end has shut down, and the socket is closed. Waiting for acknowledgement.
  • LISTEN: The socket is listening for incoming connections.

当然netstat命令可能还能看到两种状态:

CLOSING: Both sockets are shut down but we still don't have all our data sent.
UNKNOWN: The state of the socket is unknown.

下面是TCP正常连接建立和终止所对应的状态:

state.png

结合上面各个状态的解释以及上图,TCP状态变迁图中的各个状态(除TIME_WAIT状态)出现的时机都比较容易理解。这里我们主要说一下FIN_WAIT_2状态和不太容易理解的TIME_WAIT状态。

FIN_WAIT_2状态

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

这意味着我们这端可能永远保持这个状态。另一端也将处于CLOSE_WAIT状态,并一直保持这个状态直到应用层决定进行关闭。不过许多TCP实现时,为了防止这种无限等待状态,如果执行主动关闭的应用层将进行全关闭,而不是半关闭来说明它还想接收数据,就设置一个定时器。如果这个连接空闲一段时间(常见的伯克利实现是10min75秒),TCP将进入CLOSED状态。

TCP的半关闭(Half-Close):连接的一端在结束它的发送后还能接收来自另一端数据的能力。socket API的close一般是终止两个方向的连接,即全关闭。而shutdown提供了半关闭的功能。

TCP半打开(Half-Open):一方关闭或者异常终止连接而另一方却不知道。这种状态在实际场景中挺常见的,比如一端突然掉电,网络突然中断等。一般可以通过TCP的Keepalive机制或者应用层心跳等机制去检测这种状态。

TIME_WAIT状态

TIME_WAIT状态又称为2MSL等待状态。所谓MSL(Maximum Segment Lifetime)指的是报文段在网络内存活的最大时间。RFC 793建议的MSL为2分钟,具体的实现中一般为30秒或1分钟。不过IP数据报在网络中传输时一般是以跳数限制的,即TTL。TIME_WAIT状态的存在主要是基于两个考虑:

1. 可靠的实现TCP全双工连接的终止。
2. 允许老的重复分节在网络中消逝。

对于第1个考虑:当TCP执行一个主动关闭,并发回最后一个ACK(即上图中的ACK N+1),该连接必须在TIME_WAIT状态停留的时间为2倍的MSL。这样可让TCP再次发送最后的ACK以防这个ACK丢失(如果丢失的话,另一端超时并重发最后的FIN)。

对于第2个考虑:我们假设在12.106.32.254的1500端口和206.168.112.219的21端口之间有一个TCP连接。我们关闭这个连接,过一段时间后在相同的IP和端口之间建立另外一个连接。后一个连接称为前一个连接的化身(incarnation),因为他们的IP地址和端口号相同。TCP必须防止某个连接的老的分组在该连接终止后再现,从而被误解成属于同一个连接的某个新的化身。为做到这一点,TCP将不给处于TIME_WAIT状态的连接发起新的化身。既然TIME_WAIT状态的持续时间是MSL的2倍,这就足以让某个方向上的分组最多存活MSL秒即被丢弃,另一个方向上的应答最多存活MSL秒被丢弃。通过实施这一个规则,我们就能保证每成功建立一个TCP连接时,来自该连接先前化身的老的重复分组都已经在网络中消逝了。

对于TIME_WAIT的第2个考虑点,还有两个注意事项:

  1. 只有执行主动关闭的一方才会进入TIME_WAIT状态,被动关闭方不会。一般来说,都是客户端执行主动关闭,服务端都是被动关闭的。因为客户端重新连接时,一般会使用一个新的端口号(通常都是系统分配的),所以不会受TIME_WAIT的限制。
  2. 为了防止TIME_WAIT对服务端的影响——服务端一般都使用的是固定的知名端口,如果重启的话(主动关闭),很可能因为TIME_WAIT的限制无法创建建立连接。所以socket API提供了SO_REUSEADDR选项,它让调用者可以对处于2MSL状态(即TIME_WAIT状态)的本地端口进行赋值。

当然,关于TIME_WAIT状态还有一些细节性的东西,有兴趣的可以阅读《TCP/IP详解 卷1:协议》第18.6.1章节。

至此,TCP的状态变迁就介绍完了。当然,TCP是一个比较复杂的协议,上面的状态变迁图中还包含了同时打开和同时关闭等一些比较少见但却存在的场景,这里就不再细述了。

]]>
0 http://niyanchun.com/tcp-state-transition-diagram.html#comments http://niyanchun.com/feed/tag/%E7%BD%91%E7%BB%9C/
TCP Keepalive基本知识 http://niyanchun.com/tcp-keepalive-howto.html http://niyanchun.com/tcp-keepalive-howto.html Sun, 08 Mar 2015 20:38:00 +0800 NYC TCP keepalive在很多场景下都不是必须的,但是在某些特定的场景下,这个特性却会是非常有用。从TCP keepalive(后面称为keepalive)这个名字就可以大概理解这个特性的作用:keep tcp alive。我们可以通过检查我们的socket(TCP sockets)来判断网络连接是不是正常的(running or broken)。

1. 什么是TCP Keepalive

keepalive的概念非常简单:当你建立一个TCP连接的时候,便有一组定时器与之绑定(associate)在一起。其中的一些定时器就用于处理keepalive过程。当keepalive定时器到0的时候,就会给对端发送一个不包含数据部分的keepalive探测包(probe packet),然后打开ACK标志。可以这样做(发送一个不包含数据部分的数据包)是因为TCP/IP规格里面有重复确认(duplicate ACK)机制,而且由于TCP是基于流的协议,所以对端也是没有任何参数的。另一方面,会收到对端(对端可以不支持keepalive选项,只要是TCP/IP就行)的确认消息,该消息也没有数据部分,只有ACK。

如果收到了keepalive探测包的回复消息,那么我们就可以断定连接依然是OK的,也不用担心用户层的实现。实际上,TCP允许我们处理没有数据包的流,而且数据部分长度为0的数据包对于用户程序来说也是没有什么危险的。这个过程是非常有用的,如果对端已经失去连接(比如机器重启了)我们就可以知道。如果没有收到对端keepalive探测包的回复消息,便可以断定连接已经不可用,进而采取一些措施。Keepalive除了会额外产生一些网络数据包外(这些包将加大网络流量,对路由器和防火墙造成一定的负担),没有什么坏处。

2. TCP Keepalive使用场景

下面介绍两种使用keepalive机制可以解决的网络异常场景。

2.1 检测对端的死连接

这里所谓的对端连接已经挂掉的场景有两种:(1)对端还没有来得及通知我们就已经死掉了。比如系统内核突然挂掉,或是进程被直接终止等。(2)对端进程虽然是正常的,但是网络链路却出了故障。这种场景下,如果网络链路不恢复正常的话,对我们来说,对端依旧是“挂掉”的。在这两种场景下,对端在挂掉之前都是无法通知我们的,而且一般的TCP操作是检测不出来连接状态的。

我们假设一种A和B的连接场景,如图1:A和B已经通过三步握手(A给B发送一个SYN,B给A回复了一个SYN/ACK,A给B回了一个ACK)建立了连接,此时认为连接已经稳定,我们可以在这条链路上面发送数据包了。但突然发生了一个意外:B端机器突然断电了,而B还没有来得及通知A连接出问题了。而再看此时的A端,A已经准备好接收B端发来的数据,却根本不知道B端已经crash了。这时我们再恢复B端的电源等待系统重启。此时的状态就是A和B都正常运行,并且A知道它和B之间有一条已经建立好的连接,但是B却不知道。这个时候,如果A试图通过这条连接向B发送数据,B将回复一个RST数据包(在一个已关闭的socket上收到数据时,将发送RST数据包,要求对端关闭异常连接且对端不需要回复ACK),这样将导致A最终关闭这个连接。至此,这个死连接才算清理掉。

1

Keepalive可以帮助我们判断出对端变得不可达(unreachable),并且不会误报。实际上,如果是因为两端的网络导致的问题,keepalive会过一些时间再重试一下,多次尝试之后才会将这个连接标记为不可用。

2.2 防止因为网络不活动而断连

Keepalive的另外一个目标就是防止因为网络不活动而断开网络连接。这是一个很常见的问题:当我们使用NAT代理或者防火墙的时候,经常会出现这种问题。这是由代理和防火墙内部的实现导致的:代理和防火墙一般会记录所有通过他们的连接。但由于机器的物理资源限制,它们只能在内存中保存有限数量的连接。最常见的策略就是保持最新的连接,丢掉老的和不活动的连接。

继续以刚才的场景为例,如图2,A端和B端已经通过三步握手建立了稳定的连接。但是过了很长一段时间,A和B才有事件发生,需要向对端发送数据。此时,A和B的连接是有效的(建立连接后,如果不断开,则该连接一直保持),但是代理或者防火墙却并不知道(该连接已经在他们的内存中被新的连接淘汰掉了)。当我们发出数据后,代理将不能正确处理我们的数据,最终导致连接断开。

2

3. Linux中的TCP Keepalive

3.1 与TCP Keepalive相关的三个参数

Linux中已经内建支持Keepalive,我们需要使能TCP/IP网络来使用它。并且需要使用procfs和sysctl来在内核运行过程中配置相关的几个参数。同时,也可以使用三个用户可配置的参数在程序中使用Keepalive。

  • tcp_keepalive_time :这个参数是最后一次包含数据(data)的数据包(data packet)发送(这里我们不认为简单的ACKs是数据)到第一个keepalive探测包发送的时间间隔。当一个连接被标记为需要keepalive的时候,这个值就不再使用了。
  • tcp_keepalive_intvl :这个参数是连续keepalive探测包发送的时间间隔,不管在这个期间这个连接进行过什么数据交换。
  • tcp_keepalive_probes :这个参数是keepalive在认为一个连接是死连接并且通知应用层之前尝试发送(未答复的)keepalive探测包的次数。

虽然Linux内核里面提供了对keepalive的支持,但是一般默认是未开启的。所以写程序的时候我们需要使用setsockopt接口来打开keepalive。如果在某次keepalive探测包发了之后收到了对端的回复,则说明对端是正常的。定时器重置。过一段时间后再进行检测。

3.2 查看与设置系统中三个参数的值

查看系统中的默认值:

allan@ubuntu:workspace$ cat /proc/sys/net/ipv4/tcp_keepalive_time
7200
allan@ubuntu:workspace$ cat /proc/sys/net/ipv4/tcp_keepalive_intvl 
75
allan@ubuntu:workspace$ cat /proc/sys/net/ipv4/tcp_keepalive_probes 
9

改变系统的默认值:

root@ubuntu:/home/allan/workspace# echo 600 > /proc/sys/net/ipv4/tcp_keepalive_time
root@ubuntu:/home/allan/workspace# echo 60 > /proc/sys/net/ipv4/tcp_keepalive_intvl
root@ubuntu:/home/allan/workspace# echo 20 > /proc/sys/net/ipv4/tcp_keepalive_probes

我们可以在任何时候更改这几个参数的值,因为keepalive会在每次用到这几个参数的时候重新读取他们的值。所以,如果在连接还正常使用的时候修改这几个参数的值,内核便会在下一次使用这个新值。但一般我们会选择在三个地方去设置这几个参数的值:(1)第一次配置网络的时候(2)rc.local脚本里面。一般的发行版里面都包含该脚本,一般认为该脚本执行后用户的设置就算完成了。(3)/etc/sysctl.conf配置文件里面。sysctl工具就是读取和设置该配置文件。

并不是所有的网络应用都需要keepalive的支持,只有TCP支持keepalive,所以我们也只能在TCP套接字中使用keepalive。

4. 在C网络编程中使用Keepalive

4.1 setsockopt函数

函数原型:

#include <sys/socket.h>

int setsockopt(int socket, int level, int option_name, 
      const void *option_value, socklen_t option_len);

我们在需要使能Keepalive的socket上面调用setsockopt函数便可以打开该socket上面的keepalive。第一个参数是要设置的套接字;第二个参数是SOL_SOCKET;第三个参数必须是SO_KEEPALIVE;第四个参数必须是一个布尔整型值,0表示关闭,1表示打开;最后一个参数是第四个参数值的大小。

还有其他三个socket选项可供我们在程序中设置keepalive,它们都是在SOL_TCP层使用,而不是SOL_SOCKET,而且它们只在当前socket中覆盖系统的变量值。如果在设置之前就读的话,将返回系统预设值的值。这三个选项如下:

  • TCP_KEEPCNT : 在当前socket中覆盖系统的tcp_keepalive_probes值
  • TCP_KEEPIDLE : 在当前socket中覆盖系统的tcp_keepalive_time值
  • TCP_KEEPINTVL : 在当前socket中覆盖系统的tcp_keepalive_intvl值

4.2 程序实例

下面程序实例先检测系统的Keepalive默认设置,然后再打开Keepalive:

/* --- begin of keepalive test program --- */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main(void);

int main()
{
   int s;
   int optval;
   socklen_t optlen = sizeof(optval);

   /* Create the socket */
   if((s = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
      perror("socket()");
      exit(EXIT_FAILURE);
   }

   /* Check the status for the keepalive option */
   if(getsockopt(s, SOL_SOCKET, SO_KEEPALIVE, &optval, &optlen) < 0) {
      perror("getsockopt()");
      close(s);
      exit(EXIT_FAILURE);
   }
   printf("SO_KEEPALIVE is %sn", (optval ? "ON" : "OFF"));

   /* Set the option active */
   optval = 1;
   optlen = sizeof(optval);
   if(setsockopt(s, SOL_SOCKET, SO_KEEPALIVE, &optval, optlen) < 0) {
      perror("setsockopt()");
      close(s);
      exit(EXIT_FAILURE);
   }
   printf("SO_KEEPALIVE set on socketn");

   /* Check the status again */
   if(getsockopt(s, SOL_SOCKET, SO_KEEPALIVE, &optval, &optlen) < 0) {
      perror("getsockopt()");
      close(s);
      exit(EXIT_FAILURE);
   }
   printf("SO_KEEPALIVE is %sn", (optval ? "ON" : "OFF"));

   close(s);

   exit(EXIT_SUCCESS);
}
/* ---  end of keepalive test program  --- */

编译运行结果:

allan@ubuntu:workspace$ ./a.out 
SO_KEEPALIVE is OFF
SO_KEEPALIVE set on socket
SO_KEEPALIVE is ON

原文参见:《TCP Keepalive HOWTO

]]>
0 http://niyanchun.com/tcp-keepalive-howto.html#comments http://niyanchun.com/feed/tag/%E7%BD%91%E7%BB%9C/