zoukankan      html  css  js  c++  java
  • 深入理解TCP协议及其源代码

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

    前言

    在前面实验我们分别实现了Socket 通信工具,探讨了Socket API、Socket 调用原理等。但是还没有针对某一实例进行讲解,在本实验我们将针对TCP协议进行详细分析,期待在Linux内核进行分析TCP原理。

    1.Tcp基本原理

    TCP是一种面向连接、可靠、基于字节流的传输协议,位于TCP/IP模型的传输层。

    • 面向连接:不同于UDP,TCP协议需要通信双方确定彼此已经建立连接后才可以进行数据传输;
    • 可靠:连接建立的双方在进行通信时,TCP保证了不会存在数据丢失,或是数据丢失后存在拯救丢失的措施;
    • 字节流:实际传输中,不论是何种数据,TCP都按照字节的方式传输,而非以数据包为单位。

    针对它的这三种特性,本小节我们将对其原理进行探究。

    1.1面向连接(三次握手)

    - 第一次握手。如图,TCP双方在进行连接时首先由Client(客户端)发起连接请求,请求中附带连接参数,包括随机数字起点Seq(预防传输时字节序列被预测收到攻击),连接请求标志位SYN(占用1字节序号)等。 - 第二次握手:当Server(服务器)分配资源打开监听请求,收到客户端请求后,对请求头进行解析。若连接建立成功则分配相应资源,并返回针对客户端请求的确认报文,其中响应报文头部参数包括:连接建立标志位SYN、Server端针对该通信过程的随机Seq、针对该请求的确认号ack、可附加接收窗口大小信息等。 - 第三次握手。客户端收到服务端的确认连接请求后将会发送对该确认请求的确认(简单来说也就是A请求B,B告诉A准许,A再告诉B我知道你准许了),试想若不对该请求进行响应那么服务端将白白分配资源并等待。 若以上三次握手都没问题则连接建立,在第三次握手的时候即可开始传送数据。

    1.2可靠(简单描述)

    • 检验和:TCP检验和的计算与UDP一样,在计算时要加上12byte的伪首部,检验范围包括TCP首部及数据部分,但是UDP的检验和字段为可选的,而TCP中是必须有的。计算方法为:在发送方将整个报文段分为多个16位的段,然后将所有段进行反码相加,将结果存放在检验和字段中,接收方用相同的方法进行计算,如最终结果为检验字段所有位是全1则正确(UDP中为0是正确),否则存在错误。
    • 序列号:TCP将每个字节的数据都进行了编号,这就是序列号。
      序列号的作用:
      a、保证可靠性(当接收到的数据总少了某个序号的数据时,能马上知道)
      b、保证数据的按序到达
      c、提高效率,可实现多次发送,一次确认
      d、去除重复数据
      数据传输过程中的确认应答处理、重发控制以及重复控制等功能都可以通过序列号来实现。
    • 确认应答机制(ACK):TCP通过确认应答机制实现可靠的数据传输。在TCP的首部中有一个标志位——ACK,此标志位表示确认号是否有效。接收方对于按序到达的数据会进行确认,当标志位ACK=1时确认首部的确认字段有效。进行确认时,确认字段值表示这个值之前的数据都已经按序到达了。而发送方如果收到了已发送的数据的确认报文,则继续传输下一部分数据;而如果等待了一定时间还没有收到确认报文就会启动重传机制。
    • 超时重传机制:当报文发出后在一定的时间内未收到接收方的确认,发送方就会进行重传(通常是在发出报文段后设定一个闹钟,到点了还没有收到应答则进行重传)
    • 流量控制:接收端处理数据的速度是有限的,如果发送方发送数据的速度过快,导致接收端的缓冲区满,而发送方继续发送,就会造成丢包,继而引起丢包重传等一系列连锁反应。因此TCP支持根据接收端的处理能力,来决定发送端的发送速度,这个机制叫做流量控制。
      在TCP报文段首部中有一个16位窗口长度,当接收端接收到发送方的数据后,在应答报文ACK中就将自身缓冲区的剩余大小,放入16窗口大小中。这个大小随数据传输情况而变,窗口越大,网络吞吐量越高,而一旦接收方发现自身的缓冲区快满了,就将窗口设置为更小的值通知发送方。如果缓冲区满,就将窗口置为0,发送方收到后就不再发送数据,但是需要定期发送一个窗口探测数据段,使接收端把窗口大小告诉发送端。
    • 拥塞控制:流量控制解决了 两台主机之间因传送速率而可能引起的丢包问题,在一方面保证了TCP数据传送的可靠性。然而如果网络非常拥堵,此时再发送数据就会加重网络负担,那么发送的数据段很可能超过了最大生存时间也没有到达接收方,就会产生丢包问题。
      为此TCP引入慢启动机制,先发出少量数据,就像探路一样,先摸清当前的网络拥堵状态后,再决定按照多大的速度传送数据。
      此处引入一个拥塞窗口:
      发送开始时定义拥塞窗口大小为1;每次收到一个ACK应答,拥塞窗口加1;而在每次发送数据时,发送窗口取拥塞窗口与接送段接收窗口最小者。
      慢启动:在启动初期以指数增长方式增长;设置一个慢启动的阈值,当以指数增长达到阈值时就停止指数增长,按照线性增长方式增加;线性增长达到网络拥塞时立即“乘法减小”,拥塞窗口置回1,进行新一轮的“慢启动”,同时新一轮的阈值变为原来的一半。
      “慢启动”机制可用图表示:

    2.基本原理探究

    在上次实验,我们通过追踪qemu底层的sys_call入口观察系统态和内核态之间的联系,理清了系统层面是怎样对底层接口进行调用的。在本小节,详细分析一下TCP协议在内核中的基本原理。

    TCP协议的初始化及socket创建TCP套接字描述符

    如图所示,上面展示了TCP调用系统内核中的相关函数进行资源分配和通信。经过上次实验对qemu的跟踪不难发现在建立连接及通信时在服务端经历了socket()->bind()->listen()->accept()四个步骤,在accet() 函数之后会进行客户端的数据通信。 为了验证连接建立的过程,我们对gdb跟踪的函数过程进行抓包,只要出现三次握手就能够捕捉到,同时对系统调用接口函数打断点,就能够知道在那个函数调用之间进行了三次通信。 初始执行过程,在wireshark中捕捉不到TCP通信,不断跳过断点,当客户端执行到connect(),服务端执行到accept()后捕获到TCP通信过程如下图。 ![](https://img2018.cnblogs.com/blog/1881275/201912/1881275-20191224195448552-560306073.png) 不难发现开始建立三次握手的过程发生在服务端accpet()后,当连接建立后,执行send()及rev()进行数据通信。 知道TCP建立连接的过程,接下来我们来探究一下它的初始化和套接字初始化过程。在文件/net/tcp /下找到accpet函数定义发现它最终调用了__sys_accept4,同时connect函数调用了__sys_connect函数。 上次实验我们知道与网络有关的代码放在net/文件下,打开net/socket.c文件,搜索```connect```,找到函数定义如下: ```c int __sys_connect(int fd, struct sockaddr __user *uservaddr, int addrlen) { struct socket *sock; struct sockaddr_storage address; int err, fput_needed;
    sock = sockfd_lookup_light(fd, &err, &fput_needed);
    if (!sock)
    	goto out;
    err = move_addr_to_kernel(uservaddr, addrlen, &address);
    if (err < 0)
    	goto out_put;
    
    err =
        security_socket_connect(sock, (struct sockaddr *)&address, addrlen);
    if (err)
    	goto out_put;
    
    err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen,
    			 sock->file->f_flags);
    out_put:
    fput_light(sock->file, fput_needed);
    

    out:
    return err;
    }

    分析代码能够发现,在内核socket接口层这两个socket API函数对应着sys_connect和sys_accept函数,进一步对应着sock->opt->connect和sock->opt->accept两个函数指针。
    进一步寻找TCP协议接口层函数定义,打开文件net/ipv4/tcp_ipv4.c,其中设定了TCP协议栈的访问接口函数,结果如下:
    ```c
    struct proto tcp_prot = {
        .name            = "TCP",
        .owner            = THIS_MODULE,
        .close            = tcp_close,
        .pre_connect        = tcp_v4_pre_connect,
        .connect        = tcp_v4_connect,
        .disconnect        = tcp_disconnect,
        .accept            = inet_csk_accept,
        .ioctl            = tcp_ioctl,
        .init            = tcp_v4_init_sock,
        .destroy        = tcp_v4_destroy_sock,
        .shutdown        = tcp_shutdown,
        .setsockopt        = tcp_setsockopt,
        .getsockopt        = tcp_getsockopt,
        .keepalive        = tcp_set_keepalive,
        .recvmsg        = tcp_recvmsg,
        .sendmsg        = tcp_sendmsg,
        .sendpage        = tcp_sendpage,
        .backlog_rcv        = tcp_v4_do_rcv,
        .release_cb        = tcp_release_cb,
        .hash            = inet_hash,
        .unhash            = inet_unhash,
        .get_port        = inet_csk_get_port,
        .enter_memory_pressure    = tcp_enter_memory_pressure,
        .leave_memory_pressure    = tcp_leave_memory_pressure,
        .stream_memory_free    = tcp_stream_memory_free,
        .sockets_allocated    = &tcp_sockets_allocated,
        .orphan_count        = &tcp_orphan_count,
        .memory_allocated    = &tcp_memory_allocated,
        .memory_pressure    = &tcp_memory_pressure,
        .sysctl_mem        = sysctl_tcp_mem,
        .sysctl_wmem_offset    = offsetof(struct net, ipv4.sysctl_tcp_wmem),
        .sysctl_rmem_offset    = offsetof(struct net, ipv4.sysctl_tcp_rmem),
        .max_header        = MAX_TCP_HEADER,
        .obj_size        = sizeof(struct tcp_sock),
        .slab_flags        = SLAB_TYPESAFE_BY_RCU,
        .twsk_prot        = &tcp_timewait_sock_ops,
        .rsk_prot        = &tcp_request_sock_ops,
        .h.hashinfo        = &tcp_hashinfo,
        .no_autobind        = true,
    #ifdef CONFIG_COMPAT
        .compat_setsockopt    = compat_tcp_setsockopt,
        .compat_getsockopt    = compat_tcp_getsockopt,
    #endif
        .diag_destroy        = tcp_abort,
    };
    

    不难发现在TCP协议中这两个函数指针对应着tcp_v4_connect函数和inet_csk_accept函数。分析到这里,应该能够想到我们可以通过MenuOS的内核调试环境设置断点跟踪tcp_v4_connect函数和inet_csk_accept函数来进一步验证三次握手的过程。

    接下来针对tcp_v4_connect函数和inet_csk_accept函数进行进一步分析。

    2.1tcp_v4_connect函数

    继续在net/ipv4/tcp_ipv4.c文件中找到connect函数定义如下:

    int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
    {
       struct sockaddr_in *usin = (struct sockaddr_in *)uaddr;
       struct inet_sock *inet = inet_sk(sk);
       struct tcp_sock *tp = tcp_sk(sk);
       __be16 orig_sport, orig_dport;
       __be32 daddr, nexthop;
       struct flowi4 *fl4;
       struct rtable *rt;
       int err;
       struct ip_options_rcu *inet_opt;
       struct inet_timewait_death_row *tcp_death_row = &sock_net(sk)-ipv4.tcp_death_row;
       if (addr_len < sizeof(struct sockaddr_in))
       	return -EINVAL;
       if (usin->sin_family != AF_INET)
       	return -EAFNOSUPPORT;
       nexthop = daddr = usin->sin_addr.s_addr;
       inet_opt = rcu_dereference_protected(inet->inet_opt,
       				     lockdep_sock_is_held(sk));
       if (inet_opt && inet_opt->opt.srr) {
       	if (!daddr)
       		return -EINVAL;
       	nexthop = inet_opt->opt.faddr;
       }
       orig_sport = inet->inet_sport;
       orig_dport = usin->sin_port;
       fl4 = &inet->cork.fl.u.ip4;
       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);
       if (IS_ERR(rt)) {
       	err = PTR_ERR(rt);
       	if (err == -ENETUNREACH)
       		IP_INC_STATS(sock_net(sk), IPSTATS_MIB_OUTNOROUTES);
       	return err;
       }
       if (rt->rt_flags & (RTCF_MULTICAST | RTCF_BROADCAST)) {
       	ip_rt_put(rt);
       	return -ENETUNREACH;
       }
       if (!inet_opt || !inet_opt->opt.srr)
       	daddr = fl4->daddr;
       if (!inet->inet_saddr)
       	inet->inet_saddr = fl4->saddr;
       sk_rcv_saddr_set(sk, inet->inet_saddr);
       if (tp->rx_opt.ts_recent_stamp && inet->inet_daddr != daddr) {
       	/* Reset inherited state */
       	tp->rx_opt.ts_recent	   = 0;
       	tp->rx_opt.ts_recent_stamp = 0;
       	if (likely(!tp->repair))
       		tp->write_seq	   = 0;
       }
       inet->inet_dport = usin->sin_port;
       sk_daddr_set(sk, daddr);
       inet_csk(sk)->icsk_ext_hdr_len = 0;
       if (inet_opt)
       	inet_csk(sk)->icsk_ext_hdr_len = inet_opt->opt.optlen;
       tp->rx_opt.mss_clamp = TCP_MSS_DEFAULT;
       /* 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);
       err = inet_hash_connect(tcp_death_row, sk);
       if (err)
       	goto failure;
       sk_set_txhash(sk);
       rt = ip_route_newports(fl4, rt, orig_sport, orig_dport,
       		       inet->inet_sport, inet->inet_dport, sk);
       if (IS_ERR(rt)) {
       	err = PTR_ERR(rt);
       	rt = NULL;
       	goto failure;
       }
       /* OK, now commit destination to socket.  */
       sk->sk_gso_type = SKB_GSO_TCPV4;
       sk_setup_caps(sk, &rt->dst);
       rt = NULL;
       if (likely(!tp->repair)) {
       	if (!tp->write_seq)
       		tp->write_seq = secure_tcp_seq(inet->inet_saddr,
       					       inet->inet_daddr,
       					       inet->inet_sport,
       					       usin->sin_port);
       	tp->tsoffset = secure_tcp_ts_off(sock_net(sk),
       					 inet->inet_saddr,
       					 inet->inet_daddr);
       }
       inet->inet_id = tp->write_seq ^ jiffies;
       if (tcp_fastopen_defer_connect(sk, &err))
       	return err;
       if (err)
       	goto failure;
       err = tcp_connect(sk);
       if (err)
       	goto failure;
       return 0;
    }
    

    分析源码不难发现 tcp_v4_connect函数的主要作用就是发起一个TCP连接,建立TCP连接的过程自然需要底层协议的支持,因此我们从这个函数中可以看到它调用了IP层提供的一些服务,比如ip_route_connect和ip_route_newports从名称就可以简单分辨,这里我们关注在TCP层面的三次握手,不去深究底层协议提供的功能细节。我们可以看到这里设置了TCP_SYN_SENT并进一步调用了 tcp_connect(sk)来实际构造SYN并发送出去。
    在tcp_connect函数具体负责构造一个携带SYN标志位的TCP头并发送出去,同时还设置了计时器超时重发。
    其中tcp_transmit_skb函数负责将tcp数据发送出去,这里调用了icsk->icsk_af_ops->queue_xmit函数指针,实际上就是在TCP/IP协议栈初始化时设定好的IP层向上提供数据发送接口ip_queue_xmit函数,这里TCP协议栈通过调用这个icsk->icsk_af_ops->queue_xmit函数指针来触发IP协议栈代码发送数据。

    2.2inet_csk_accept函数

    
    struct sock *inet_csk_accept(struct sock *sk, int flags, int *err, bool kern)
    {
        struct inet_connection_sock *icsk = inet_csk(sk);
        struct request_sock_queue *queue = &icsk->icsk_accept_queue;
        struct request_sock *req;
        struct sock *newsk;
        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);
    
            /* If this is a non blocking socket don't sleep */
            error = -EAGAIN;
            if (!timeo)
                goto out_err;
    
            error = inet_csk_wait_for_connect(sk, timeo);
            if (error)
                goto out_err;
        }
        req = reqsk_queue_remove(queue, sk);
        newsk = req->sk;
    
        if (sk->sk_protocol == IPPROTO_TCP &&
            tcp_rsk(req)->tfo_listener) {
            spin_lock_bh(&queue->fastopenq.lock);
            if (tcp_rsk(req)->tfo_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;
            }
         
    

    不难发现服务端调用inet_csk_accept函数会从请求队列中取出一个连接请求,如果队列为空则通过inet_csk_wait_for_connect函数处理,其代码如下:

    static int inet_csk_wait_for_connect(struct sock *sk, long timeo)
    {
        struct inet_connection_sock *icsk = inet_csk(sk);
        DEFINE_WAIT(wait);
        int err;
        for (;;) {
            prepare_to_wait_exclusive(sk_sleep(sk), &wait,
                          TASK_INTERRUPTIBLE);
            release_sock(sk);
            if (reqsk_queue_empty(&icsk->icsk_accept_queue))
                timeo = schedule_timeout(timeo);
            sched_annotate_sleep();
            lock_sock(sk);
            err = 0;
            if (!reqsk_queue_empty(&icsk->icsk_accept_queue))
                break;
            err = -EINVAL;
            if (sk->sk_state != TCP_LISTEN)
                break;
            err = sock_intr_errno(timeo);
            if (signal_pending(current))
                break;
            err = -EAGAIN;
            if (!timeo)
                break;
        }
        finish_wait(sk_sleep(sk), &wait);
        return err;
    }
    

    可以看到,inet_csk_wait_for_connect函数是一个for死循环,它循环等待连接到来,只有队列中有连接请求才会跳出循环。

    三次握手过程总结:可以将整个过程描述为,client端不断初始化资源,调用到connect函数进行连接请求,当server端收到连接请求后进行处理,利用accept函数进行连接建立。
    进一步的,client端的connect的请求会被server端放入请求队列,当有请求到来inet_csk_wait_for_connect函数对请求出队,进行三次握手的建立过程。按如上思路跟踪调试代码的话,会发现connect之后将连接请求发送出去,accept等待连接请求,connect启动到返回和accept返回之间就是所谓三次握手的时间。

    三次握手详细过程


    引用这里5

  • 相关阅读:
    UIPasteboard 粘贴板
    UIViewController没有随着设备一起旋转的原因
    UIButton 应用选择状态(附:UIButton 常用状态)
    WebService 中参数为枚举时引发的血案
    设计模式(1)之面向对象设计原则 阿正
    2012年年终总结 阿正
    生活工作如登山 阿正
    感谢我的技术总监 阿正
    尽孝要尽早 阿正
    我老了吗?不 你依然年轻 阿正
  • 原文地址:https://www.cnblogs.com/xshun/p/12093145.html
Copyright © 2011-2022 走看看