TCP建立连接

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

wireshark分析DNS 协议_tcp

图1


客户端发送SYN


wireshark分析DNS 协议_tcp_02


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


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

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




wireshark分析DNS 协议_wireshark分析DNS 协议_03


图5 connect调用关系图



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

int __inet_stream_connect(struct socket *sock, struct sockaddr *uaddr,

int addr_len, int flags)  

{  

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

err = sk->sk_prot->disconnect(sk, flags);  

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(sk, uaddr, addr_len);  

if (err < 0)  

goto  out;  

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

sock->state = SS_CONNECTING;    

err = -EINPROGRESS;  

break;  

}

 

timeo = sock_sndtimeo(sk, flags & 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(sk, timeo, writebias))

goto  out;  

}  




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

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

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

下面接着分析tcp_v4_connect函数:

int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int 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(fl4, nexthop, inet->inet_saddr,  

RT_CONN_FLAGS(sk), sk->sk_bound_dev_if,  

IPPROTO_TCP,  

orig_sport, orig_dport, sk);  

 

 

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

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

ip_rt_put(rt);  

return -ENETUNREACH;  

   

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

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

err = inet_hash_connect(&tcp_death_row, sk);

 

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

rt = ip_route_newports(fl4, rt, orig_sport, orig_dport,  

inet->inet_sport, inet->inet_dport, sk);  

 

       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 + 15, sk->sk_allocation);    

skb_reserve(buff, MAX_TCP_HEADER);  /*为tcp头预留空间*/  

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

tcp_init_nondata_skb(buff, tp->write_seq++, TCPHDR_SYN);  

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

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

TCP_ECN_send_syn(sk, buff);  /*设置ECN*/

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

err = tp->fastopen_req ? tcp_send_syn_data(sk, buff) :  

tcp_transmit_skb(sk, buff, 1, sk->sk_allocation);   

/*重传定时器*/

inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,  inet_csk(sk)->icsk_rto, TCP_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 *sk, struct sk_buff *skb, int clone_it,  

         gfp_t gfp_mask)  

 {  

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

   if (clone_it) {  

              if (unlikely(skb_cloned(skb)))  

                  skb = pskb_copy(skb, gfp_mask);  

              else  

                  skb = skb_clone(skb, gfp_mask);  

   } 

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

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

tcp_options_size = tcp_syn_options(sk, skb, &opts, &md5);  

else  

tcp_options_size = tcp_established_options(sk, skb, &opts,  &md5);  

tcp_header_size = tcp_options_size + sizeof(struct tcphdr);  

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

if (tcp_packets_in_flight(tp) == 0)  

tcp_ca_event(sk, CA_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);  序列号

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_wnd, 65535U));  

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

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

}  

th->check  = 0;  校验和

th->urg_ptr  = 0;  //紧急指针

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

icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl);


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


wireshark分析DNS 协议_网络_04

图6 握手示意图1


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