linux TCP协议(1)---连接管理与状态机

前言:TCP是传输层协议,实现了一种可靠的通信。它从不同角度提供了多种可靠性保障措施来为网络传输提供确定性。连接性就是其中之一,不像UDP的无连接状态,TCP在数据传输之前会进行连接,只有双方都协调完成后,才会进行数据传输;同样的,在结束时,又会断开连接,通告传输的完成;在数据传输过程中,又会对每个传输进行确认。更多的可靠性措施在后面的系列中会仔细说明,这一篇,重点从连接这个角度看看TCP协议。

一. TCP状态机的运转

二. TCP的连接与断开

2.1 TCP连接处理

2.1.1 listen()调用

listen()系统调用是服务器侧编程的一个必要动作,主要是把创建的主动socket变成被动socket,那么这里主动和被动有什么区别呢?通过代码一探里面的操作:当调用socket()时,会调用对应的协议无关层的接口

sock->ops->listen(sock, backlog);

这里的ops->listen最终调用的就是inet_listen(),在这个函数中,我们看到

old_state = sk->sk_state;
if (!((1 << old_state) & (TCPF_CLOSE | TCPF_LISTEN)))
	goto out;

/* Really, if the socket is already in listen state
* we can only allow the backlog to be adjusted.
*/
if (old_state != TCP_LISTEN) {
err = inet_csk_listen_start(sk, backlog);
if (err)
	goto out;
}

sk->sk_max_ack_backlog = backlog;
err = 0;

在开始就是检查套接字的状态,如果已经是listen状态,则只要更改backlog的值;如果不是listen状态,就启动设置listen。看这个inet_csk_listen_start(sk, backlog);,里面把套接字状态设置为listen:

sk->sk_state = TCP_LISTEN;

所以,所说的把主动套接字变成被动套接字主要就是改变TCP的初始状态,从closed状态转为listen状态。从第一节的状态机上可以看出不同的起始状态后续的处理也不同。

2.1.2 TCP发起连接

TCP的连接过程主要是3次握手,如下图所示:

这是一张截取《TCP/IP 详解》中的握手图,其中的序列号注意一下。左端为客户端,右端为服务端。

一般来说是客户端主动发起连接,而服务端则接收并建立连接。服务端是在调用connect()时发起SYN分节,那接下来就来看看这个connect系统调用里面都做了哪些事:

err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen,
				 sock->file->f_flags);

一样的,这里的ops->connect在INET域就是inet_stream_connect(),在这个函数中,看到在检查了套接字还没有连接的前提下,就调用TCP的连接函数:

err = sk->sk_prot->connect(sk, uaddr, addr_len);
if (err < 0)
  goto out;

这里的sk_prot->connect就是tcp_v4_connect(),在这个函数中,查找路由,填充各种信息等,最后调用tcp_connect(),这个函数主要就是构建一个SYN报文发送出去。后面是tcp_transmit_skb(),填充一下消息头等各种操作,最后发送到队列中:

err = icsk->icsk_af_ops->queue_xmit(skb, 0);
if (likely(err <= 0))
  return err;

最后会调用icsk_af_ops->queue_xmit()将数据包往IP层发送,那么这个queue_xmit是什么呢?如果我们去搜这个icsk_af_ops注册的地方,就会发现在TCP操作集的初始化tcp_v4_init_sock()中,icsk_af_ops->queue_xmit()被设置为:

icsk->icsk_af_ops = &ipv4_specific;

const struct inet_connection_sock_af_ops ipv4_specific = {
	.queue_xmit	   = ip_queue_xmit,
	.send_check	   = tcp_v4_send_check,
	.rebuild_header	   = inet_sk_rebuild_header,
	.conn_request	   = tcp_v4_conn_request,
	.syn_recv_sock	   = tcp_v4_syn_recv_sock,
	.remember_stamp	   = tcp_v4_remember_stamp,
	.net_header_len	   = sizeof(struct iphdr),
	.setsockopt	   = ip_setsockopt,
	.getsockopt	   = ip_getsockopt,
	.addr2sockaddr	   = inet_csk_addr2sockaddr,
	.sockaddr_len	   = sizeof(struct sockaddr_in),
	.bind_conflict	   = inet_csk_bind_conflict,
#ifdef CONFIG_COMPAT
	.compat_setsockopt = compat_ip_setsockopt,
	.compat_getsockopt = compat_ip_getsockopt,
#endif
};

可以看出来,最后的queue_xmit就是ip_queue_xmit()。然后就交给IP层处理。

那么这个tcp_v4_init_sock()是什么时候初始化的呢?我们看到这个函数是作为TCP操作集init的回调函数。它是在创建socket的时候,初始化的。系统调用socket会调用inet_init(),在这个函数中:

if (sk->sk_prot->init) {
  err = sk->sk_prot->init(sk);
if (err)
  sk_common_release(sk);

当创建的是流式套接字时,对应于INET族的就是TCP协议,就会调用tcp_v4_init_sock()

这就是客户端主动发起连接的过程,当然里面还有复杂的多种其他任务的处理,自行根据需要分析。

2.1.3 TCP接收连接

对于tcp接收,首先要确认它的处理函数—tcp_v4_rcv(),这是根据接收的报文头层层传递上来的,具体的过程,在讲设备无关层是具体说明。忽略其他的处理,然后就到了tcp_v4_do_rcv(),在这里进行报文的实际处理,主要分为三条线:

  1. 对于已经建立的tcp连接,此时接收的就是数据报文,进入到内层处理:

    if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */
      TCP_CHECK_TIMER(sk);
      if (tcp_rcv_established(sk, skb, tcp_hdr(skb), skb->len)) {
        rsk = sk;
        goto reset;
      }
      TCP_CHECK_TIMER(sk);
      return 0;
    }
    

    对于内层的tcp_rcv_established()就不进行详细说明了,对于它的工作,很显然的是拷贝报文到用户态应用程序,是在里面的tcp_copy_to_iovec()中做的,就是拷贝报文。

  2. 对于是tcp listen状态的,则进入建立连接的处理。最终进入到tcp_rcv_state_process()状态机处理。

  3. 对于不是以上两种状态的,则直接进入到状态机处理。

从以上可以看出,对于接收,最重要的处理过程就是tcp状态机。理解了状态机的转换过程,也就明白了代码处理的逻辑。

2.2 TCP断开处理

tcp的断开过程形象的说是4次挥手的过程,如下图所示:

这里一共有4次交互过程,关于为什么中间的ack M+1和FIN N不合并成一条呢?在《unix 网络编程》中作者曾提到过,其中一个原因就是另一方暂时不想断开,也就是说tcp是双工的,允许一个方向断开,而另个方向暂时不断开的情形,就是所说的半关闭状态。

其中主动关闭的一方可以在调用close()时发送FIN报文,开始关闭过程。如果调用shutdown()会触发单向关闭,可以去查看源代码:在用户调用close()时,最终根据套接字类型,会找到tcp_close()函数。在其中最后调用了tcp_send_fin()发送FIN报文。然后就是接收报文进入状态机进行处理。

对于断开连接有一个问题:TIME_WAIT状态会保持2MSL时间,其中的解释如下(取自网络)

主动发起关闭连接的操作的一方将达到TIME_WAIT状态,而且这个状态要保持Maximum Segment Lifetime的两倍时间。为什么要这样做而不是直接进入CLOSED状态?

原因有二:

一、保证TCP协议的全双工连接能够可靠关闭

二、保证这次连接的重复数据段从网络中消失

先说第一点,如果Client直接CLOSED了,那么由于IP协议的不可靠性或者是其它网络原因,导致Server没有收到Client最后回复的ACK。那么Server就会在超时之后继续发送FIN,此时由于Client已经CLOSED了,就找不到与重发的FIN对应的连接,最后Server就会收到RST而不是ACK,Server就会以为是连接错误把问题报告给高层。这样的情况虽然不会造成数据丢失,但是却导致TCP协议不符合可靠连接的要求。所以,Client不是直接进入CLOSED,而是要保持TIME_WAIT,当再次收到FIN的时候,能够保证对方收到ACK,最后正确的关闭连接。

再说第二点,如果Client直接CLOSED,然后又再向Server发起一个新连接,我们不能保证这个新连接与刚关闭的连接的端口号是不同的。也就是说有可能新连接和老连接的端口号是相同的。一般来说不会发生什么问题,但是还是有特殊情况出现:假设新连接和已经关闭的老连接端口号是一样的,如果前一次连接的某些数据仍然滞留在网络中,这些延迟数据在建立新连接之后才到达Server,由于新连接和老连接的端口号是一样的,又因为TCP协议判断不同连接的依据是socket pair,于是,TCP协议就认为那个延迟的数据是属于新连接的,这样就和真正的新连接的数据包发生混淆了。所以TCP连接还要在TIME_WAIT状态等待2倍MSL,这样可以保证本次连接的所有数据都从网络中消失。

2.3 异常处理

  1. 超时

    由于tcp是可靠的协议,因此,在一方发送数据后,期望能够确认数据发送成功或者失败。在linux中提供了重传定时器,用于在数据超时的时候进行重传。比如在tcp_connect()中,发送SYN连接时,设置的超时定时器:

    inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
    				  inet_csk(sk)->icsk_rto, TCP_RTO_MAX);
    

三. TCP连接的特殊状态

3.1 半打开状态

半打开状态是指连接的一方由于异常关闭,而另一端对此并不知情的场景。常见的触发原因有:主机掉电等。在这个时候,另一端如果发送数据,必然是不会通的,因为此时掉电重启的主机已经没有了连接的信息,会回复一个RST报文复位连接。对于检测半打开状态,可以使用keepalive保活定时器,默认是120分钟。也可以自己实现心跳来保持。

3.2 半关闭状态

由于tcp是全双工的,因此双方都可以关闭自己的连接。而有一种特殊的情况就是:主动发起关闭的一方发送FIN后,被动一方对其进行了确认,然而并没有接着发送自己的FIN,此时,表示被动一方仍然想要传输数据,当然,主动的一方虽然关闭了发送通道,但是仍然可以接收被动方的数据。

posted @ 2017-07-01 15:24  AISEED  阅读(3022)  评论(0编辑  收藏  举报