深入理解TCP协议及其源代码
三次握手如图所示:
TCP协议通过三次握手来建立一个可靠地连接。
第一次握手:客户端尝试连接服务器,向服务器发送连接请求报文(SYN包),其首部中的同步比特SYN置为1,seq=x,客户端进入SYN-SEND状态,等待服务器确认。
第二次握手:服务器接收到连接请求报文后,若同意,则发送确认报文。确认报文中,将SYN置为1,ack=x+1,同时也为自己随机选择一个序号y,seq=y,即SYN+ACK包,此时服务器进入SYN-RECV状态。
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包,其确认序号ack=y+1。此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。
从C语言代码考虑,客户端的connect和服务端的accept分别对应着sys_connect 和 sys_accept4函数。结构体变量struct proto tcp_prot指定了TCP协议栈的访问接口函数。通过查看该结构体可知,sys_connect和sys_accept4实际上调用的是tcp_v4_connect和inet_csk_accept。该结构体位于linux-5.0.1/net/ipv4/tcp_ipv4.c中。
tcp_v4_connect代码如下,在tcp_v4_connect中,调用了ip_route_connect和ip_route_newports,分析代码,可以看出tcp_v4_connect所做的工作如下:
1.检查目的ip长度、协议,如果设置了源路由选项而且数据包目的地址不空,则从用户给定的源路由列表中取一个ip地址赋给网关地址。
2.调用ip_route_connect选路由,路由结构保存到rt->rt_dst中,实际调用的函数是ip_route_output_flow,如果是广播地址、组地址就返回。
3.调用tcp_set_state设置套接字状态为TCP_SYN_SENT。并将套接字加入到连接管理哈希链表中,为连接分配一个临时端口。
4.初始化序列号,调用tcp_connect函数完成建立连接,其中调用了tcp_transmit_skb将数据包发送到IP层。
5.若连接建立失败,就将TCP状态设为CLOSE,将套接字删除。
代码注释如下:
/* This will initiate an outgoing connection. */tcp int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len) {
//将通用地质结构转换为IPv4的地址结构 struct sockaddr_in *usin = (struct sockaddr_in *)uaddr;
//变量inet指向套接字struct sock中的inet选项 struct inet_sock *inet = inet_sk(sk);
//变量tp指向套接字struct sock中的TCP选项 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记录下来 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; } //如果没有设置源路由ip选项,就使用路由表寻址的路由 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); //初始化tcp选项 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; //序号初始化为0 } inet->inet_dport = usin->sin_port; //目的端口地址 sk_daddr_set(sk, daddr); //目的ip地址 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_SYN_SENT tcp_set_state(sk, TCP_SYN_SENT);
//将套接字sk放入tcp连接管理哈希链表中 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)//初始化tcp数据段序列号 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; //构建SYN包调用tcp_transmit_skb发送到IP层 err = tcp_connect(sk); if (err) goto failure; return 0; failure: /* * This unhashes the socket and releases the local port, * if necessary. */
//失败设置套接字状态为CLOSED tcp_set_state(sk, TCP_CLOSE); ip_rt_put(rt); sk->sk_route_caps = 0; inet->inet_dport = 0; return err; } EXPORT_SYMBOL(tcp_v4_connect);
tcp_connect代码如下,在tcp_connect中,主要完成的操作位构造SYN报文并将其发送出去,但是由于TCP要提供可靠性的服务,故发送完成后,还需要将该SYN报文加入到发送队列,并启动重传定时器,只有等对方收到SYN报文并回发ACK报文,待我方接收到才将该报文从发送队列中删除并停止重传定时器。
故三次握手的第一次握手完成。
/* Build a SYN and send it off. */ int tcp_connect(struct sock *sk) { struct tcp_sock *tp = tcp_sk(sk); struct sk_buff *buff; int err; tcp_call_bpf(sk, BPF_SOCK_OPS_TCP_CONNECT_CB, 0, NULL); if (inet_csk(sk)->icsk_af_ops->rebuild_header(sk)) return -EHOSTUNREACH; /* Routing failure or similar. */ //对tcp进行一定的初始化 tcp_connect_init(sk); if (unlikely(tp->repair)) { tcp_finish_connect(sk, NULL); return 0; } //分配一个SKB用于构造SYN段 buff = sk_stream_alloc_skb(sk, 0, sk->sk_allocation, true); if (unlikely(!buff)) return -ENOBUFS; tcp_init_nondata_skb(buff, tp->write_seq++, TCPHDR_SYN); tcp_mstamp_refresh(tp); tp->retrans_stamp = tcp_time_stamp(tp); tcp_connect_queue_skb(sk, buff); tcp_ecn_send_syn(sk, buff); tcp_rbtree_insert(&sk->tcp_rtx_queue, buff); /* Send off SYN; include data in Fast Open. */
//发送SYN请求报文 err = tp->fastopen_req ? tcp_send_syn_data(sk, buff) : tcp_transmit_skb(sk, buff, 1, sk->sk_allocation); if (err == -ECONNREFUSED) return err; /* We change tp->snd_nxt after the tcp_transmit_skb() call * in order to make this packet get counted in tcpOutSegs. */ tp->snd_nxt = tp->write_seq; tp->pushed_seq = tp->write_seq; buff = tcp_send_head(sk); if (unlikely(buff)) { tp->snd_nxt = TCP_SKB_CB(buff)->seq; tp->pushed_seq = TCP_SKB_CB(buff)->seq; } TCP_INC_STATS(sock_net(sk), TCP_MIB_ACTIVEOPENS);
//启动SYN重传定时器,初始超时时间为1s /* Timer for repeating the SYN until an answer. */ inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS, inet_csk(sk)->icsk_rto, TCP_RTO_MAX); return 0; } EXPORT_SYMBOL(tcp_connect);
当服务端收到第一次握手的请求后,服务端会调用tcp_v4_recv进行第二次握手的处理。
在tcp_v4_recv中,调用函数tcp_v4_do_rcv,该函数主要作用是防止洪泛和拥塞控制,接着是调用tcp_rcv_state_process,所有的收到的报文都会在该函数中处理。然后调用tcp_v4_conn_request,tcp_v4_conn_request又调用tcp_conn_request,之后调用tcp_v4_send_synack对客户端的请求报文进行确认。此时完成第二次握手。
static int tcp_v4_send_synack(const struct sock *sk, struct dst_entry *dst, struct flowi *fl, struct request_sock *req, struct tcp_fastopen_cookie *foc, enum tcp_synack_type synack_type) { const struct inet_request_sock *ireq = inet_rsk(req); struct flowi4 fl4; int err = -1; struct sk_buff *skb; if (!dst && (dst = inet_csk_route_req(sk, &fl4, req)) == NULL) return -1; //构建SYN+ACK段 skb = tcp_make_synack(sk, dst, req, foc, synack_type); //生成SYN+ACK段 if (skb) { __tcp_v4_send_check(skb, ireq->ir_loc_addr, ireq->ir_rmt_addr); err = ip_build_and_send_pkt(skb, sk, ireq->ir_loc_addr, ireq->ir_rmt_addr, ireq->opt); err = net_xmit_eval(err); } return err; }
客户端收到服务端的SYN+ACK报文后,会调用tcp_v4_rcv来进行处理,在该函数中调用tcp_child_process,在tcp_child_process中又调用tcp_rcv_state_process来完成第三次握手。