深入理解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。

 

posted @ 2019-12-26 19:26  Euterpe0511  阅读(445)  评论(0编辑  收藏  举报