TCP三次握手源码分析(客户端发送SYN)
一、环境说明
内核版本:Linux 3.10
内核源码地址:https://elixir.bootlin.com/linux/v3.10/source (包含各个版本内核源码,且网页可全局搜索函数)
二、TCP协议格式
各字段的作用:
- 源端口号:用于指定本地程序绑定的端口;
- 目的端口号:用于指定远端程序绑定的端口;
- 序列号:用于本地发送数据时所使用的序列号;
- 确认号:用于本地确认接收到远端发送过来的数据序列号;
- 首部长度:指示 TCP 头部的长度;
- 标志位:用于指示 TCP 数据包的类型;
- 窗口大小:用于流量控制,表示远端能够接收数据的能力;
- 校验和:用于校验数据包是否在传输时损坏了;
- 紧急指针:一般比较少用,用于指定紧急数据的偏移量(URG 标志位为1时有效);
- 可选项:TCP的选项部分;
Linux内核怎定义TCP头部的结构:
// file: include/uapi/linux/tcp.h struct tcphdr { __u16 source; // 源端口 __u16 dest; // 目的端口 __u32 seq; // 序列号 __u32 ack_seq; // 确认号 __u16 doff:4, // 头部长度 res1:4, // 保留 cwr:1, ece:1, urg:1, // 是否包含紧急数据 ack:1, // 是否ACK包 psh:1, // 是否Push包 rst:1, // 是否Reset包 syn:1, // 是否SYN包 fin:1; // 是否FIN包 __u16 window; // 滑动窗口 __sum16 check; // 校验和 __u16 urg_ptr; // 紧急指针 };
其中,CWR和ECE用于拥塞控制,ACK、RST、SYN、FIN用于连接管理及数据传输。tcphdr结构与TCP头部的各个字段一一对应。
三、TCP三次握手
所谓三次握手(Three-Way Handshake)即建立TCP连接,就是指建立一个TCP连接时,需要客户端和服务端总共发送3个包以确认连接的建立。整个流程如下图所示:
(1)第一次握手:Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。
(2)第二次握手:Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。
(3)第三次握手:Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。
在socket编程中,这一过程涉及到listen、accept以及connect。
listen函数初始化设置半连接、全连接队列,将套接字变成被动的连接监听套接字(被动等待客户端的连接);具体参考《Linux内核listen系统调用源码分析》
accept函数从处于established状态的全连接队列头部取出一个连接,创建一个通信套接字,返回用户进程;
然后是connect函数,这里是真正tcp三次握手的开始,由客户端调用connect主动发起连接。
四、客户端发送SYN
/** * sockfd:socket描述字 * addr:要连接的远端IP地址和端口 * addrlen: 指定参数addr的长度b */ int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
通过网络栈专用操作函数集的总入口函数(sys_socketcall函数),请求会分发到sys_connect()函数。
sys_connect()函数,主要工作:
- 根据fd查找套接字struct socket *sock;
- 拷贝用户地址到内核空间;
- 调用四层协议连接接口(tcp、udp在此处分流)
// file: net/socket.c SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *, uservaddr, int, addrlen) { // 获取fd对应的socket结构 struct socket *sock; sock = sockfd_lookup_light(fd, &err, &fput_needed); // // 从用户空间复制要连接的远端IP地址和端口信息 err = move_addr_to_kernel(uservaddr, addrlen, &address); // 调用具体协议的连接函数 err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen,sock->file->f_flags); ...... }
sock->ops在socket初始化时,挂载的是inet_stream_ops结构,其对应的connect函数为inet_stream_connect()。
inet_stream_connect函数调用了__inet_stream_connect函数。
__inet_stream_connect()函数的主要工作:
- 判断服务器地址必须指定协议类型;
- 判断当前套接字连接状态为SS_UNCONNECTED,防止重连等情况;
- 调用tcp_v4_connect()函数进行 TCP 协议的连接操作;
- 如果socket设置了非阻塞,并且连接还没建立完成,那么返回 EINPROGRESS 错误;
- 调用inet_wait_for_connect()函数等待连接服务端操作完成;
- 设置socket 的状态为 SS_CONNECTED,表示连接已经建立完成;
int __inet_stream_connect(struct socket *sock, struct sockaddr *uaddr, int addr_len, int flags) { struct sock *sk = sock->sk; int err; long timeo; if (addr_len < sizeof(uaddr->sa_family)) return -EINVAL; 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) { //判断客户端socket状态 default: err = -EINVAL; goto out; case SS_CONNECTED: //如果已经连接就直接返回 err = -EISCONN; goto out; case SS_CONNECTING: //如果正在连接就跳出语句 err = -EALREADY; /* Fall out of switch with err, set for this state */ break; case SS_UNCONNECTED: //如果未连接就执行协议结构的连接函数 err = -EISCONN; if (sk->sk_state != TCP_CLOSE) //若不是TCP_CLOSE状态,返回错误(sock初始化时,设置的状态为TCP_CLOSE) goto out; // sk->sk_prot挂载的是tcp_prot,对应的connect函数是tcp_v4_connect() err = sk->sk_prot->connect(sk, uaddr, addr_len); if (err < 0) goto out; sock->state = SS_CONNECTING; //设置socket的状态为正在连接状态 /* Just entered SS_CONNECTING state; the only * difference is that return value in non-blocking * case is EINPROGRESS, rather than EALREADY. */ err = -EINPROGRESS; break; } // 根据是否阻塞操作,确定connect()调用的超时时间 // 阻塞模式下,connect()返回时三次握手已经完成了 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; /** * 如果是非阻塞模式,那么直接返回错误码 * socketw为阻塞模式,使用inet_wait_for_connect()来等待协议栈的处理 */ if (!timeo || !inet_wait_for_connect(sk, timeo, writebias)) //定时等待连接 goto out; err = sock_intr_errno(timeo); //确定超时错误码 if (signal_pending(current)) //是否有信号等待处理 goto out; } /* Connection was closed by RST, timeout, ICMP error * or another process disconnected us. */ if (sk->sk_state == TCP_CLOSE) goto sock_error; /* sk->sk_err may be not zero now, if RECVERR was ordered by user * and error was received after socket entered established state. * Hence, it is handled normally after connect() return successfully. */ sock->state = SS_CONNECTED; //设置客户端socket状态为连接状态 err = 0; out: return err; sock_error: err = sock_error(sk) ? : -ECONNABORTED; sock->state = SS_UNCONNECTED; if (sk->sk_prot->disconnect(sk, flags)) sock->state = SS_DISCONNECTING; goto out; }
sk->sk_prot挂载的是tcp_prot,对应的connect函数是tcp_v4_connect()。
- tcp_v4_connect()函数,主要工作:
- 校验远端地址的合法性;
- 相关路由操作;
- 自动分配客户端端口;
- 设置tcp_sock的状态为SYN_SENT;
- 调用tcp_connect发送SYN包;
// file: net/ipv4/tcp_ipv4.c int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len) { struct sockaddr_in *usin = (struct sockaddr_in *)uaddr; struct inet_sock *inet = inet_sk(sk); 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; if (addr_len < sizeof(struct sockaddr_in)) //地址长度是否相等 return -EINVAL; if (usin->sin_family != AF_INET) //是否属于INET协议族 return -EAFNOSUPPORT; nexthop = daddr = usin->sin_addr.s_addr; //记录服务器IP地址 inet_opt = rcu_dereference_protected(inet->inet_opt, sock_owned_by_user(sk)); if (inet_opt && inet_opt->opt.srr) { //是否设置了IP选项结构,并指定了源路由 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 = ip_route_connect(fl4, nexthop, inet->inet_saddr, RT_CONN_FLAGS(sk), sk->sk_bound_dev_if, IPPROTO_TCP, orig_sport, orig_dport, sk, true); if (IS_ERR(rt)) { err = PTR_ERR(rt); if (err == -ENETUNREACH) IP_INC_STATS_BH(sock_net(sk), IPSTATS_MIB_OUTNOROUTES); return err; } if (rt->rt_flags & (RTCF_MULTICAST | RTCF_BROADCAST)) { //路由表是组播或者广播类型,就放弃使用 ip_rt_put(rt); return -ENETUNREACH; } if (!inet_opt || !inet_opt->opt.srr) //查找IP选项 daddr = fl4->daddr; 使用路由表中的地址作为目标地址 if (!inet->inet_saddr) //没有指定源地址 inet->inet_saddr = fl4->saddr; //使用路由表中的地址作为源地址 inet->inet_rcv_saddr = inet->inet_saddr; //接收地址与源地址相同 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; } if (tcp_death_row.sysctl_tw_recycle && !tp->rx_opt.ts_recent_stamp && fl4->daddr == daddr) tcp_fetch_timewait_stamp(sk, &rt->dst); inet->inet_dport = usin->sin_port; //目标端口 inet->inet_daddr = daddr; //目标地址 inet_csk(sk)->icsk_ext_hdr_len = 0; //初始化网络层头部 if (inet_opt) inet_csk(sk)->icsk_ext_hdr_len = inet_opt->opt.optlen; //记录IP选项规定长度 tp->rx_opt.mss_clamp = TCP_MSS_DEFAULT; //设置MSS最大分段值 /* 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_set_state(sk, TCP_SYN_SENT); //修改sock状态为SYN_SENT状态 err = inet_hash_connect(&tcp_death_row, sk); //检查端口是否可用(sock会链入ehash) if (err) goto failure; 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); //设置分段标识和分段值 if (!tp->write_seq && likely(!tp->repair)) //计算发送序号 seq tp->write_seq = secure_tcp_sequence_number(inet->inet_saddr, inet->inet_daddr, inet->inet_sport, usin->sin_port); inet->inet_id = tp->write_seq ^ jiffies; //设置inet_sock结构的ID err = tcp_connect(sk); //发送SYN数据包 rt = NULL; if (err) goto failure; return 0; failure: /* * This unhashes the socket and releases the local port, * if necessary. */ tcp_set_state(sk, TCP_CLOSE); //设置sock关闭状态 ip_rt_put(rt); //放弃路由表 sk->sk_route_caps = 0; //清除兼容标识 inet->inet_dport = 0; //清除端口 return err; }
确定tcp四元组之后,调用tcp_connect()函数发送SYN包。
tcp_connect构造一个SYN报文,加入到发送队列,将其发送出去,并启动SYN重传定时器。
// file:net/ipv4/tcp_output.c int tcp_connect(struct sock *sk) { struct tcp_sock *tp = tcp_sk(sk); struct sk_buff *buff; int err; tcp_connect_init(sk); //初始化tcp_sock结构内容 if (unlikely(tp->repair)) { tcp_finish_connect(sk, NULL); return 0; } buff = alloc_skb_fclone(MAX_TCP_HEADER + 15, sk->sk_allocation); //为SYN准备数据包 if (unlikely(buff == NULL)) return -ENOBUFS; /* Reserve space for headers. */ skb_reserve(buff, MAX_TCP_HEADER); //在缓冲块中开辟TCP头部空间 tcp_init_nondata_skb(buff, tp->write_seq++, TCPHDR_SYN); //构建SYN非数据类型的通用控制块(设置SYN),并自增应答序号 tp->retrans_stamp = TCP_SKB_CB(buff)->when = tcp_time_stamp; //初始化重发时间 tcp_connect_queue_skb(sk, buff); //将数据包链入发送队列 TCP_ECN_send_syn(sk, buff); /* Send off SYN; include data in Fast Open. */ 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; //上一次push的序号 TCP_INC_STATS(sock_net(sk), TCP_MIB_ACTIVEOPENS); /* 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); //设置用于重发SYN的定时器(重传机制) return 0; } // file: net/ipv4/tcp_output.c 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; //计算end seq序号 skb_header_release(skb); //设置没有头部的nohdr标志位 __tcp_add_write_queue_tail(sk, skb); //将数据包链入发送队列 sk->sk_wmem_queued += skb->truesize; //调整队列长度 sk_mem_charge(sk, skb->truesize); //调整预分配长度 tp->write_seq = tcb->end_seq; tp->packets_out += tcp_skb_pcount(skb); //调整"飞行中"的数据包计数器 }
具体tcp_transmit_skb发送数据包的过程,后续章节详细分析。
inet_wait_for_connect()函数,阻塞等待连接。展示了进程因为某个条件而睡眠,当满足条件后又被唤醒的用法。
// file: net/ipv4/af_inet.c static long inet_wait_for_connect(struct sock *sk, long timeo, int writebias) { DEFINE_WAIT(wait); //初始化等待任务 prepare_to_wait(sk_sleep(sk), &wait, TASK_INTERRUPTIBLE); //把等待任务加入到socket的等待队列头部,把进程的状态设为TASK_INTERRUPTIBLE(可打断) sk->sk_write_pending += writebias; /*完成三次握手后,状态就会变为TCP_ESTABLISHED,从而退出循环*/ while ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) { release_sock(sk); /* 进入睡眠,直到超时或收到信号,或者被I/O事件处理函数唤醒。 * 1.如果是收到信号退出的,timeo为剩余的jiffies。 * 2.如果使用了SO_SNDTIMEO选项,超时退出后,timeo为0。 * 3.如果没有使用SO_SNDTIMEO选项,timeo为无穷大,即MAX_SCHEDULE_TIMEOUT, * 那么返回值也是这个,而超时时间不定。为了无限阻塞,需要上面的while循环。 */ timeo = schedule_timeout(timeo); //该方法会让需要延迟的任务睡眠到指定的延时时间耗尽后在重新运行 lock_sock(sk); //被唤醒后重新上锁 if (signal_pending(current) || !timeo) //如果进程有待处理的信号,或者睡眠超时了,退出循环,之后会返回错误码 break; prepare_to_wait(sk_sleep(sk), &wait, TASK_INTERRUPTIBLE); } finish_wait(sk_sleep(sk), &wait); //等待结束时,把等待进程从等待队列中删除,把当前进程的状态设为TASK_RUNNING sk->sk_write_pending -= writebias; return timeo; }
至此,客户端发送SYN报文,等待server端确认。
五、总结
客户端在connect的时候,把本地sock状态设置成了TCP_SYN_SENT,选了一个可用的端口,接着发出SYN握手请求并启动重传定时器。