• 深入理解TCP协议及其源代码


    深入理解TCP协议及其源代码

    本文参考了《TCP/IP协议族》第四版

    进程到进程的通信

    与UDP一样,TCP也是使用端口号提供进程到进程之间的通信。下表是我们常见的TCP使用的熟知端口号。

    端口协议说明
    7 Echo 把收到的数据报回送到发送方
    9 Discard 丢弃收到的任何数据报
    11 Users 活跃的用户
    20和21 FTP 文件传输协议
    23 TELNET 终端网络
    25 SMTP 简单邮件传送协议
    53 DNS 域名服务器
    80 HTTP 超文本传输协议

    面向字节流

    TCP创造了一个环境使得两个进程之间好像有一个管道连接,而中间流动着的,就是字节流。在发送进程写入字节流,而在另一端的接收进程则读取字节流。

    因为在发送进程和接收进程的读取和写入的速度可能不一样,那么可能存在发送进程发送的太快,而接收进程来不及接收就使得一些数据丢失。那么在这里就设置了TCP缓存。这种协调发送者和接收者之间速度的控制方式也叫做流量控制。在发送方有发送缓存,接收方有接收缓存。

     

    在发送方,缓存有三种类型的槽,白色区域是空槽,也就是可以让发送进程填入数据的地方,深灰色区域保存的是已经发送出去但是没有接收到ACK的字节,发送TCP的缓存中还需要保存这些字节,在必要的时候进行重传。灰色区域表示发送进程即将发送的字节。在深灰色的槽中的字节被确认后,这些位置就可以被回收并且被发送进程再次利用。在接收方呢,这里的缓存就被划分成了两种颜色,白色区域也是空槽,灰色区域是已经接收到的字节,还未被读取的,而这些字节即将被接收进程读取,在一个槽的数据被读取后就可以加入到白色区域,也就是槽被回收。这就是环形缓冲区域的好处了。

    TCP的连接建立阶段三向握手

    TCP的连接建立阶段也可以称为是三向握手,是一个客户的应用程序希望使用TCP作为传输层协议来和服务器的应用程序建立连接。客户端发送请求,所以客户端的行为也可以称为是主动打开,服务器程序告诉它的TCP自己已经准备好接受连接,这个过程也叫做被动打开

    • step1:首先客户发送第一个报文段,也就是一个请求连接报文段,这个报文段的SYN标志位置为1,SYN也就是同步序号。仿佛就是客户端向服务器说:“嘿,我想给你发信息啦。”这里选择了随机的序号8000.SYN报文段不携带任何数据,但是要消耗一个序号。

    • step2:服务器如果同意连接,就会返回一个SYN+ACK报文段,这里的SYN也指的是同步,ACK表示对刚才接收到的SYN报文段的确认。这个报文还有一个很重要的功能就是定义了接受窗口的大小rwnd。SYN+ACK报文段不懈怠数据,但是要消耗一个序号。这里的序列号也是随机序列号,ack是期待对方下一个发过来的报文段的序列号。这也就是服务器在跟客户端说:”好了,我直到你要发信息了,我期待你下一次发ack这个报文段过来,对了,我的接收窗口大小是rwnd。“

    • step3:此时发送方返回一个ACK报文段,是对第二个报文段的确认,这里使用了ACK标志和确认号。值得注意的是,这里的序号和刚开始还发过去的SYN报文段的序号一样,也就是如果这个ACK不携带任何数据就不消耗序列号。这里也定义了窗口大小。

      状态转换图

    为了理解这个状态转换图,我们要分为以下几种情况:

    连接建立和半关闭终止

    • 客户端

      客户进程发送一个连接请求,主动打开。这时TCP发送一个SYN报文段,进入到SYN-SENT状态,在接收到SYN+ACK报文段后,TCP发送一个ACK报文段,进入到ESTABLISHED状态。进入数据传送阶段。

      当客户端数据传送结束后,就发出主动关闭的请求,于是TCP发送FIN报文段,进入到FIN-WAIT-1状态。一直等待接收到对刚才的FIN报文段的ACK后,就进入到FIN-WAIT-2状态,直到服务器也结束数据发送,发送过来一个FIN报文段后,客户端就发送对这个FIN报文段的ACK报文段,此时进入到TIME-WAIT状态,启动2MSL计时器。设置这个计时器的目的是为了防止在最后一个ACK报文段丢失的情况下,此时如果客户端已经关闭,那么服务器就会陷入到盲等的状态。

    • 服务器

      服务器是在客户端主动打开后被动打开的,这是服务器TCP进入到LISTEN状态,被动接收客户端发来的SYN报文段。当服务器TCP接收到SYN报文段后,就发送SYN+ACK报文段,进入到SYN+RCVD状态,等待客户端发送ACK报文段。在接收到ACK报文段后,进入到ESTABLISHED状态,进入传送数据阶段。

      收到客户的TCP的FIN报文段后,服务器发送ACK报文段,进入CLOSE-WAIT状态。如果此时发送队列中还有未发送数据,就继续发送。因为TCP提供的是全双工服务,此时仅仅关闭了客户端到服务器的发送数据方向,但是在服务器到客户端的方向还未关闭,所以这就是半关闭终止。在发送数据接收后,服务器TCP发送一个FIN报文段,进入到LAST-ACK状态。并且等待最后从客户发来的ACK报文段,接下来进入CLOSED状态。

      此处的终止阶段称为四向握手

    常见情况

     

    • 在数据传送阶段完成后,客户端发出关闭命令。命令TCP发送FIN报文段,进入到FIN-WAIT-1状态。服务器在收到这个FIN报文段后,继续向客户端发送剩余数据,最后加上EOF标记,表示这个连接要关闭了。此时服务器TCP进入到CLOSE-WAIT状态,此处推迟对客户端发来的FIN报文段的确认,直到自己收到关闭命令时,服务器TCP就向客户端发送FIN+ACK报文段,进入到LAST-ACK状态,等待最后的ACK。客户取消了FIN-WAIT-2状态直接进入了TIME-WAIT状态。

    此处的终止阶段采用的是三向握手

    同时打开

    这种情况下双方都主动发出打开命令。此时通信的双方是对等的,双方的TCP同时发出SYN报文段,此后进入SYN-SENT状态,在收到SYN+ACK后双方同时进入SYN-RCVD状态,接下来进入ESTABLISHED状态。

    同时关闭

    这种情况下,双方都主动发出主动关闭,双方的TCP都发送FIN报文段,进入FIN-WAIT-1状态。在收到FIN报文段后,双方进入CLOSING状态,并且发送ACK报文段。此处的CLOSING状态取代了FIN-WAIT-2CLOSE-WAIT。在收到ACK后双方进入TIME-WAIT状态。

    拒绝连接

    当服务器TCP拒绝连接时,服务器在收到SYN报文段后会发送一个RST报文段,拒绝这条连接。客户在收到这个报文段后进入CLOSED状态。

    异常终止连接

    进程可以异常终止连接,在进程出现故障的时候,TCP发送RST+ACK报文段异常终止,把队列中的所有数据都丢弃,双方的TCP立即进入CLOSED状态。

    理解TCP源代码

    • 在TCP建立连接的过程中主要调用的函数是connect和accept,在前面我们已经分析过了,客户端主动发起connect,而服务器是被动接收accept的。accept和connect最终调用了__sys_accept4,sys_connect两个内核处理函数。这两个函数对应着sock->opt->connet和sock->opt->accept两个函数指针,而这两个函数指针对应着tcp_v4_connect和inet_csk_accept函数。

    tcp_v4_connect

    /* This will initiate an outgoing connection. */
    int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
    {
    ...
        rt = ip_route_connect(fl4, nexthop, inet->inet_saddr,
                      RT_CONN_FLAGS(sk), sk->sk_bound_dev_if,
                      IPPROTO_TCP,
                      orig_sport, orig_dport, sk);
    ...
        /* Socket identity is still unknown (sport may be zero).
         * However we set state to SYN-SENT and not releasing socket
         * lock select source port, enter ourselves into the hash tables and
         * complete initialization after this.
         */
        tcp_set_state(sk, TCP_SYN_SENT);//调用tcp_connect(sk)构造SYN
    ...
        rt = ip_route_newports(fl4, rt, orig_sport, orig_dport,
                       inet->inet_sport, inet->inet_dport, sk);
    ...
        err = tcp_connect(sk);
    ...
    }
    EXPORT_SYMBOL(tcp_v4_connect);

    tcp_connect

    /* 构造SYN并且发出去 */
    int tcp_connect(struct sock *sk)
    {
    ...
        tcp_init_nondata_skb(buff, tp->write_seq++, TCPHDR_SYN);
        tp->retrans_stamp = tcp_time_stamp;
        tcp_connect_queue_skb(sk, buff);
        tcp_ecn_send_syn(sk, buff);
    ​
        /* Send off SYN; include data in Fast Open. */
        err = tp->fastopen_req ? tcp_send_syn_data(sk, buff) :
        tcp_transmit_skb(sk, buff, 1, sk->sk_allocation);
        if (err == -ECONNREFUSED)
            return err;
    ​
        /* We change tp->snd_nxt after the tcp_transmit_skb() call
         * in order to make this packet get counted in tcpOutSegs.
         */
        tp->snd_nxt = tp->write_seq;
        tp->pushed_seq = tp->write_seq;
        TCP_INC_STATS(sock_net(sk), TCP_MIB_ACTIVEOPENS);
    ​
        /* Timer for repeating the SYN until an answer. */
        inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
                      inet_csk(sk)->icsk_rto, TCP_RTO_MAX);//超时计时器
        return 0;
    }

    inet_csk_accept

    /*
     * This will accept the next outstanding connection.
     */
    struct sock *inet_csk_accept(struct sock *sk, int flags, int *err)
    {
        struct inet_connection_sock *icsk = inet_csk(sk);
        struct request_sock_queue *queue = &icsk->icsk_accept_queue;
        struct sock *newsk;
        struct request_sock *req;
        int error;
    ​
        lock_sock(sk);
    ​
        /* We need to make sure that this socket is listening,
         * and that it has something pending.
         */
        error = -EINVAL;
    if (sk->sk_state != TCP_LISTEN)
            goto out_err;
    ​
        /* Find already established connection */
        if (reqsk_queue_empty(queue)) {
            long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);
    ...
             error = inet_csk_wait_for_connect(sk, timeo);
            if (error)
                goto out_err;
        }
        req = reqsk_queue_remove(queue);
        newsk = req->sk;
    ​
        sk_acceptq_removed(sk);
        if (sk->sk_protocol == IPPROTO_TCP && queue->fastopenq != NULL) {
            spin_lock_bh(&queue->fastopenq->lock);
            if (tcp_rsk(req)->listener) {
                /* We are still waiting for the final ACK from 3WHS
                 * so can't free req now. Instead, we set req->sk to
                 * NULL to signify that the child socket is taken
                 * so reqsk_fastopen_remove() will free the req
                 * when 3WHS finishes (or is aborted).
                 */
                req->sk = NULL;
                req = NULL;
            }
    ...
        return newsk;
    ...
    }
    • 客户端通过tcp_v4_connect函数调用到tcp_connect函数,将请求发送数据包出去,服务器端则通过inet_csk_accept函数调用inet_csk_wait_for_connect函数中的for循环进入阻塞,直到监听到请求才跳出循环。connect启动到返回和accept返回之间就是所谓三次握手的时间。

    实验验证

    首先我们用抓包工具来验证三次握手过程中的报文段相应字段的值

    这是在访问百度的时候抓包过程,这里记录了运用http协议中的传输层TCP建立连接的过程。

    我们清晰的可以看到在前面三个报文段中,第一个有SYN标记,是客户端发来的第一次握手,第二次是服务器返回的SYN+ACK报文段,也就是第二次握手过程。第三次是一个客户端返回的ACK。

     

  • 相关阅读:
    Java 条件语句
    Java循环
    Java 变量
    Java 数据类型
    nginx+php发布网站
    安装MySQL5.7
    docker-compose参数
    部署
    dockerfile编写
    在VMware中安装CentOS7
  • 原文地址:https://www.cnblogs.com/raoxinyue/p/12103966.html
Copyright © 2020-2023  润新知