• 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,此时,表示被动一方仍然想要传输数据,当然,主动的一方虽然关闭了发送通道,但是仍然可以接收被动方的数据。

  • 相关阅读:
    JavaScript之六种排序法
    实习的意义
    HTML、JS、CSS之特殊字符
    CSS之全屏背景图
    Swiper之滑块1
    (转)Android之接口回调机制
    (转)Android之自定义适配器
    反射
    对数据库事务、隔离级别、锁、封锁协议的理解及其关系的理解
    get和Post区别
  • 原文地址:https://www.cnblogs.com/yhp-smarthome/p/7102488.html
Copyright © 2020-2023  润新知