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

2.基本原理探究

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

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

技术分享图片
如图所示,上面展示了TCP调用系统内核中的相关函数进行资源分配和通信。经过上次实验对qemu的跟踪不难发现建立连接时在服务端经历了socket()->bind()->listen()->accept()四个步骤,在accet() 函数之后会等待客户端的连接建立请求。

 

3.通过GDB调试对TCP原理深度分析

再次用之前配合好的menuOS和gdb调试环境

以调试模式运行Menu OS系统

 

 打开一个命令行运行GDB进行Menu OS的调试,设置如下四个断点,并显示断点信息

 

b __sys_socket
b __sys_connect
b __sys_listen
b __sys_accept4
info breakpoints

socket相关的所有系统调用函数都在socket.c中

我们之前就曾分析过的bind()也是在其中定义的,其中socket接口函数定义:

listen():

 

 

bind():

 

connect():

 

 

其余接口就不一一展示了。

 

了解接口定义,下面我们可以从源代码的角度深入探究TCP的三次握手了

我们从conne()开始

int __sys_connect(int fd, struct sockaddr __user *uservaddr, int addrlen)
{

 

该函数根据文件描述符找到指定的socket对象,将地址信息从用户空间拷贝到内核空间,并调用指定类型套接字的connect函数。

对应流式套接字的connect函数是inet_stream_connect,接着我们分析该函数:

在GDB中设置断点找到源文件在net/ipv4/af_inet.c

 

 分析源码,

该函数首先检查socket地址长度和使用的协议族,然后再检查socket的状态,必须是SS_UNCONNECTED或SS_CONNECTING,检查完毕后调用实现协议的connect函数,对于流式套接字,实现协议是tcp,调用的是tcp_v4_connect();对于阻塞调用,等待后续握手的完成;对于非阻塞调用,则直接返回 -EINPROGRESS

tcp_v4_connect()在net/ipv4/tcp_ipv4.c中

 

 

 

在该函数主要完成:

  1. 路由查找,得到下一跳地址,并更新socket对象的下一跳地址;

  2. 将socket对象的状态设置为TCP_SYN_SENT;

  3. 如果没设置序号初值,则选定一个随机初值;

  4. 调用函数tcp_connect()完成报文构建和发送。

tcp_connect()在net/ipv4/tcp_output.c中

 

 

该函数初始化套接字跟连接相关的字段,将sk_buff初始化为syn报文,实质是操作tcp_skb_cb,在初始化TCP头的时候会用到,

调用tcp_connect_queue_skb()函数将报文sk_buff添加到发送队列sk->sk_write_queue,调用tcp_transmit_skb()函数构造tcp头,

然后交给网络层,最后初始化了重传定时器。

下面进入tcp_connect_queue_skb():

static void tcp_connect_queue_skb(struct sock *sk, struct sk_buff *skb)
{
    struct tcp_sock *tp = tcp_sk(sk);
    struct tcp_skb_cb *tcb = TCP_SKB_CB(skb);
 
    tcb->end_seq += skb->len;
    __skb_header_release(skb);
    sk->sk_wmem_queued += skb->truesize;
    sk_mem_charge(sk, skb->truesize);
    tp->write_seq = tcb->end_seq;
}
接下来的事情就交给网络层把报文发送出去,这样第一次握手完成客户端状态变为TCP_SYN_SEND

对于TCP协议的服务端,数据到达网卡的时候,将大致要经过这个一个调用链:

网卡驱动 ---> netif_receive_skb() ---> ip_rcv() ---> ip_local_deliver_finish() ---> tcp_v4_rcv()

第二次握手服务端层面完成

数据报到达客户端网卡,同样经过:

网卡驱动-->netif_receive_skb()--->ip_rcv()--->ip_local_deliver_finish()---> tcp_v4_rcv() --> tcp_v4_do_rcv()

至此第二次握手完成,客户端sock状态变为TCP_ESTABLISHED,第三次握手开始。

我们之前说到服务端的sock的状态为TCP_NEW_SYN_RECV,报文到达网卡:

网卡驱动-->netif_receive_skb()--->ip_rcv()--->ip_local_deliver_finish()---> tcp_v4_rcv()

最后将sock的状态设置为TCP_ESTABLISHED,

到这里三次握手完成。等待用户调用accept调用,取出套接字使用。

 

 

 

 

 

posted @ 2019-12-26 18:55  zjce  阅读(219)  评论(0编辑  收藏  举报