小甜、
Published on 2017-09-02 11:31 in 暂未分类 with 小甜、

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

posted @ 2019-12-24 19:25  小甜、  阅读(560)  评论(0编辑  收藏  举报