TCP建立连接
图1中编号为3、4、5的是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,序列号seq为0,Flags标志为SYN,表示这是一个SYN段。
PC1首先调用connect来发送SYN段,将执行主动打开。connect对应的系统调用是sys_connect。大致的一个函数调用流程如图5所示。
图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的回来。
图6 握手示意图1
到目前为止客户端发送SYN包已经完成,下面看下服务器在收到客户端发送过来的sync包之后的情况。