tcpdump+wireshark分析数据笔记(2)

来源:互联网 发布:百电通打电话要网络 编辑:程序博客网 时间:2024/05/16 08:17

TCP建立连接

1中编号为345的是TCP建立连接的包,是TCP建立的三次握手的过程。PC2作为server端,启动监听程序,监听端口65044,一开始处于LISTEN状态。


图1

客户端发送SYN


图4 三次握手之第一次握手

TCP连接的建立需要三次的握手,图4是三次握手的第一次握手。PC1作为client端,发送一个SYN段指明打算连接的服务器端PC2。图4中红色框部分是IP首部,可以看到源地址为67.153.0.0,目的地址为67.153.0.10,协议为TCP。蓝色框部分是TCP首部,目标端口号为65044,序列号seq0Flags标志为SYN,表示这是一个SYN段。

PC1首先调用connect来发送SYN段,将执行主动打开。connect对应的系统调用是sys_connect。大致的一个函数调用流程如图5所示。

图5 connect调用关系图


下面结合着图5中的流程来分析下内核相关代码。sys_connect系统调用调用tcpBSD socket操作集inet_stream_ops对应的inet_stream_connect函数。

int __inet_stream_connect(struct socket *sockstruct sockaddr *uaddr,

      int addr_lenint flags 

 

  if (uaddr->sa_family == AF_UNSPEC) { /*地址簇未指定*/   

   err sk->sk_prot->disconnect(skflags);  

   sock->state = err SS_DISCONNECTING SS_UNCONNECTED 

   goto  out 

   

 

  switch (sock->state) {  

case SS_CONNECTED:     /* BSD socket已连接*/

   err = -EISCONN 

   goto  out 

  case SS_CONNECTING:   /* BSD socket正在连接*/

   err = -EALREADY 

   break 

  case SS_UNCONNECTED /* BSD socket还未连接*/

   err = -EISCONN 

   if (sk->sk_state != TCP_CLOSE 

    goto  out 

                 /*对于INET socket中的tcp连接,协议特有操作符集为tcp_prot 

INET SOCKET 调用协议特有connect操作符 tcp_v4_connect函数*/   

   err sk->sk_prot->connect(skuaddraddr_len);  

   if (err 0 

    goto  out 

   /* 上面的调用完成后,连接并没有完成*/

   sock->state = SS_CONNECTING   

   err = -EINPROGRESS 

   break 

  }

 

timeo sock_sndtimeo(skflags O_NONBLOCK);  /*获取链接超时时间*/

if ((1 << sk->sk_state(TCPF_SYN_SENT TCPF_SYN_RECV))  

   int writebias (sk->sk_protocol == IPPROTO_TCP&&  

     tcp_sk(sk)->fastopen_req &&  

     tcp_sk(sk)->fastopen_req->data ? 1 0 

 

   /*发送sync后进入等待状态*/  

   if (!timeo || !inet_wait_for_connect(sktimeowritebias)) 

    goto  out 

 


代码最开始会判断connect是指定的地址簇如果为AF_UNSPEC,则不会建立链接,直接返回。(不理解为什么?地址簇未指定不是可以接收ipv4ipv6的吗?)

接着判断如果该socket还未链接,就调用tcp操作集的tcp_v4_connect函数建立链接。下面会看下此函数具体做了些什么?

发送sync后,会先获取链接的超时时间,然后调用inet_wait_for_connect进入等待状态,等待超时时间内收到服务器段回的sync ack

下面接着分析tcp_v4_connect函数:

int tcp_v4_connect(struct sock *skstruct sockaddr *uaddrint addr_len 

  

   /*目标地址,也即路由的下一跳地址设置为connect参数指定值*/  

   nexthop daddr usin->sin_addr.s_addr 

   inet_opt rcu_dereference_protected(inet->inet_opt sock_owned_by_user(sk));  

   /*如果使用源地址路由,下一跳地址设置为ip选项中的faddr*/ 

   if (inet_opt && inet_opt->opt.srr) {  

            if (!daddr 

                return -EINVAL 

             nexthop inet_opt->opt.faddr 

    

/*调用路由相关函数获取出口信息*/  

  rt ip_route_connect(fl4nexthopinet->inet_saddr 

          RT_CONN_FLAGS(sk), sk->sk_bound_dev_if 

          IPPROTO_TCP 

          orig_sportorig_dportsk);  

 

 

/*获取的路由为广播或者多播,由于tcp不支持,直接返回不可达错误*/  

  if (rt->rt_flags & (RTCF_MULTICAST RTCF_BROADCAST))  

   ip_rt_put(rt);  

   return -ENETUNREACH 

 

   

  tcp_set_state(skTCP_SYN_SENT);  /*状态从CLOSING转到TCP_SYN_SENT*/

       /*获取合适的源端口号,将连接加入到bind链表中*/

  err inet_hash_connect(&tcp_death_rowsk); 

 

  /*找到合适源端口后重新建立路由表项*/  

  rt ip_route_newports(fl4rtorig_sportorig_dport 

           inet->inet_sportinet->inet_dportsk);  

 

       err tcp_connect(sk);  /*完成连接*/

此函数在调用tcp_connect完成连接之前,主要的工作就是路由相关的一些事情。首先是调用路由模块获取出口相关的信息;如果路由时广播或者多播,就返回不可达的错误信息,不建立连接;接着设置client端为SYNC_SENT状态;


inet_hash_connect函数的实现主要是查找合适的源端口,并将此端口与合适的bind表项绑定。Tcp内核表可参见inet_hashinfo结构体,由三个表项组成,分别是ehash, bhash, listening_hash。ehash表对应于socket处在tcp的ESTABLISHED状态,listening_hash表对应于socket处在tcp的LISTEN状态,bhash对应于socket已绑定了地址。这里socket还在建立,使用bhash表。


找到合适的源端口后,调用ip_route_newports函数重新建立路由表项。最后调用函数tcp_connect来发送sync段,完成连接建立。

下面接着分析tcp_connect函数的具体实现:

int tcp_connect(struct sock *sk 

   

tcp_connect_init(sk);  /*初始化此socket链接的参数*/  

  

/*分配一个skb的内存空间,sync包不包含数据,大小为tcp*/  

buff alloc_skb_fclone(MAX_TCP_HEADER 15sk->sk_allocation);    

skb_reserve(buffMAX_TCP_HEADER);  /*为tcp头预留空间*/  

    /*构建一个不包含数据的sync skb*/

tcp_init_nondata_skb(bufftp->write_seq++TCPHDR_SYN);  

tp->retrans_stamp = TCP_SKB_CB(buff)->when = tcp_time_stamp/*保存数据包发送时间*/ 

tcp_connect_queue_skb(skbuff);  /*将此skb包加入发送队列中*/

TCP_ECN_send_syn(skbuff);  /*设置ECN*/

    /*对于是否开启TFO,调用不同的函数来实现发送sync*/

err tp->fastopen_req ? tcp_send_syn_data(skbuff:  

tcp_transmit_skb(skbuff1sk->sk_allocation);   

/*重传定时器*/

inet_csk_reset_xmit_timer(skICSK_TIME_RETRANS inet_csk(sk)->icsk_rtoTCP_RTO_MAX);  

}


此函数功能就是构建一个不包含数据的sync包并发送出去。在发送sync报文之前,初始化此次socket连接的参数;分配sync报文的内存空间。

TFO(TCP fast open)是一种非标准的TCP行为,它利用三次握手的sync报文来传递数据。这里如果使用TFO,就调用tcp_send_syn_data来发送sync报文,否则调用tcp_transmit_skb来发送sync报文。


下面接着分析不支持TFO情况下tcp_transmit_skb函数的具体实现:

static int tcp_transmit_skb(struct sock *skstruct sk_buff *skbint clone_it 

         gfp_t gfp_mask 

  

   /*发送时要保留skb以备份重传,所以大部分时间都设置了clone_it,不过发送ack除外*/  

   if (clone_it) {  

              if (unlikely(skb_cloned(skb)))  

                  skb pskb_copy(skbgfp_mask);  

              else  

                  skb skb_clone(skbgfp_mask);  

  

/*获取tcp头的选项部分信息,分成sync包和已建立连接的普通包两种情况*/  

if (unlikely(tcb->tcp_flags & TCPHDR_SYN))  

  tcp_options_size tcp_syn_options(skskb&opts&md5);  

else  

  tcp_options_size tcp_established_options(skskb&opts &md5);  

tcp_header_size tcp_options_size + sizeof(struct tcphdr);  

       /*已发出但还未确认的数据包为0,初始化拥塞控制钩子*/

if (tcp_packets_in_flight(tp== 0)  

  tcp_ca_event(skCA_EVENT_TX_START);  

 /* Build TCP header and checksum it. */  

th tcp_hdr(skb);  

th->source  inet->inet_sport //源端口号

th->dest  inet->inet_dport   //目的端口号

th->seq  htonl(tcb->seq);      //seq序列号

th->ack_seq  htonl(tp->rcv_nxt);   //确认号

*(((__be16 *)th) 6) htons(((tcp_header_size >> 2<< 12) |  

     tcb->tcp_flags);  //首部长度和标志

if (unlikely(tcb->tcp_flags & TCPHDR_SYN))  //sync段报文,窗口设置为初始值  

  th->window =  htons(min(tp->rcv_wnd65535U));  

else  //其他报文,调用tcp_select_window计算当前窗口的大小

  th->window =  htons(tcp_select_window(sk));  

 

th->check  0    //TCP校验和

th->urg_ptr  0 //紧急指针

/*tcp协议中调用ip_queue_xmit发送报文,进入ip*/

icsk->icsk_af_ops->queue_xmit(skskb&inet->cork.fl);


前面在__inet_stream_connect函数中,发送sync包之后,会先获取链接超时时间,然后调用inet_wait_for_connect进入等待状态,等待sync Ack的回来。



图6 握手示意图1

到目前为止客户端发送SYN包已经完成,下面看下服务器在收到客户端发送过来的sync包之后的情况。



0 0