zoukankan      html  css  js  c++  java
  • 深入理解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。

     

  • 相关阅读:
    Nodejs下载和第一个Nodejs示例
    永久关闭Win10工具栏的TaskbarSearch控件
    对称加密,非对称加密,散列算法,签名算法
    【转】TTL和RS232之间的详细对比
    zlg核心板linux系统中查看系统内存等使用信息
    Power BI后台自动刷新数据报错 The operation was throttled by Power BI Premium because there were too many datasets being processed concurrently.
    剪切板和上传文件内容获取
    CSS, LESS, SCSS, SASS总结
    文字程序
    electron 打包“ERR_ELECTRON_BUILDER_CANNOT_EXECUTE”
  • 原文地址:https://www.cnblogs.com/raoxinyue/p/12103966.html
Copyright © 2011-2022 走看看