connect及bindlistenaccept背后的三次握手
Posted hambug
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了connect及bindlistenaccept背后的三次握手相关的知识,希望对你有一定的参考价值。
1.TCP建立连接过程简介:
TCP是面向连接的协议。面向连接的传输层协议在源点和终点之间建立了一条虛路径。同属于一个报文的所有报文段都沿着这条虛路径发送。为整个报文使用一条虚路径能够更容易地实施确认过程以及对损伤或丢失报文的重传。
在TCP中,面向连接的传输需要经过三个阶段:连接建立、数据传输和连接终止。
三次握手建立连接
在TCP中使用的连接建立过程称为三向握手(three way handshaking)。在我们的例子中,一个称为客户的应用程序希望使用TCP作为传输层协议来和另一个称为服务器的应用程序建立连接。
这个过程从服务器开始。服务器程序告诉它的TCP自己已准备好接受连接。这个请求称为被动打开请求。虽然服务器的TCP已准备好接受来自世界上任何一个机器的连接,但是它自己并不能完成这个连接。
客户程序发出的请求称为主动打开。打算与某个开放的服务器进行连接的客户告诉它的TCP,自己需要连接到某个特定的服务器上。TCP现在可以开始进行如下图所示的三向握手过程。
每个报文段的首部字段值都是完整的,并且可能还有一些可选字段也有相应的数值,不过,为了方便我们理解每个阶段,只画出了其中很少几个字段。图中显示了序号、确认号、控制标志(只有那些置1的)以及有关的窗口大小。这个阶段的三个步骤如下所示。.
1.客户发送第一个报文段(SYN报文段),在这个报文段中只有SYN标志置为1。这个报文段的作用是同步序号。在我们的例子中,客户选择了一个随机数作为第一个序号,并把这个序号发送给服务器。这个序号称为初始序号(ISN)。 请注意,这个报文段中不包括确认号,也没有定义窗口大小。只有当一个报文段中包含了确认时,定义窗口大小才有意义。这个报文段还可以包含一些选项。请注意,SYN报文段是一个控制报文段,它不携带任何数据。但是,它消耗了一个序号。当数据传送开始时,序号就应当加1。我们可以说,SYN报文段不包含真正的数据,但是我们可以想象它包含了一个虚字节。
SYN报文段不携带任何数据,但是它要消耗一个序号。
2.服务器发送第二个报文段,即SYN + ACK报文段,其中的两个标志(SYN和ACK)置为1。这个报文段有两个目的。首先,它是另一个方向上通信的SYN报文段。服务器使用这个报文段来同步它的初始序号,以便从服务器向客户发送字节。其次,服务器还通过ACK标志来确认已收到来自客户端的SYN报文段,同时给出期望从客户端收到的下一个序号。因为这个报文段包含了确认,所以它还需要定义接收窗口大小,即rwnd (由客户端使用)。
SYN+ACK报文段不携带数据,但要消耗一个序号。
3.客户发送第三个报文段。这仅仅是一一个ACK报文段。它使用ACK标志和确认号:字段来确认收到了第二个报文段。请注意,这个报文段的序号和SYN报文段使用的序号一样,也就是说,这个ACK报文段不消耗任何序号。客户还必须定义服务器的窗口大小。在某些实现中,连接阶段的第三个报文段可以携带客户的第一一个数据块。在这种情况下,第三个报文段必须有一个新的序号来表示数据中的第一个字节的编号。通常,第三个报文段不携带数据,因而不消耗序号。
ACK报文段如果不携带数据就不消耗序号。
2.connect及bind、listen、accept背后的三次握手
TCP建立连接的过程,也就是三次握手的过程已经介绍过了,下面就应该介绍,隐藏在socketAPI后面的关于TCP连接时序的问题了。
根据socket编程的流程图,我们直观的感觉到,TCP连接的建立应该与connect函数直接相关,而诸如bind、listen、accept,都应该是建立连接之前做的准备或是建立连接之后的数据的传送。
查阅相关资料可知:
1. bind 函数主要是服务器端使用,把一个本地协议地址赋予套接字;socket 函数并没有为套接字绑定本地地址和端口号,对于服务器端则必须显性绑定地址和端口号。
2. listen() 函数的主要作用就是将套接字( sockfd )变成被动的连接监听套接字(被动等待客户端的连接),至于参数 backlog 的作用是设置内核中连接队列的长度(这个长度有什么用,后面做详细的解释),TCP 三次握手也不是由这个函数完成,listen()的作用仅仅告诉内核一些信息。
3. accept()函数功能是,从处于 established 状态的连接队列头部取出一个已经完成的连接,如果这个队列没有已经完成的连接,accept()函数就会阻塞,直到取出队列中已完成的用户连接为止。与三次握手也没有直接的联系,
下面给出这几个函数的关系图:
显然这几个函数中只有connect函数与TCP三次握手直接相关,所以下面直接分析connect函数:
鉴于上次对系统调用的分析,connect函数功能的实现本质上是进行了相关系统调用,所以应该查询__sys_connect()函数,下面给出这个函数的源码:
int __sys_connect(int fd, struct sockaddr __user *uservaddr, int addrlen) { struct socket *sock; struct sockaddr_storage address; int err, fput_needed; sock = sockfd_lookup_light(fd, &err, &fput_needed); if (!sock) goto out; err = move_addr_to_kernel(uservaddr, addrlen, &address); if (err < 0) goto out_put; err = security_socket_connect(sock, (struct sockaddr *)&address, addrlen); if (err) goto out_put; err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen, sock->file->f_flags); out_put: fput_light(sock->file, fput_needed); out: return err; }
阅读源码,并查看相关资料可得,__sys_connect()函数的工作过程大致如下:
1.查找文件句柄对应的socket
2.从用户态复制地址参数到内核中
3.安全审计
4.调用传输层的connet方法inet_stream_connect或inet_dgram_connect
其中真的牵涉到TCP建立连接的是函数inet_stream_connect()
为了验证猜想的真实性,在函数inet_stream_connect()处打上断点,并进行跟踪可得:
给出断点列表(这里的列表已经囊括了后面提及的一些API函数)
给出追踪结果:
通过截图可看出,果然可以追踪到此函数,通过打断点的提示信息,找到此函数的文件所在地,翻出源码分析如下:
这里张贴出其代码和注释:
/* connect系统调用的套接口层实现 */ int inet_stream_connect(struct socket *sock, struct sockaddr *uaddr, int addr_len, int flags) { struct sock *sk = sock->sk; int err; long timeo; lock_sock(sk);/* 获取套接口的锁 */ 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) { 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:/* 只有此状态才能调用connect */ err = -EISCONN; if (sk->sk_state != TCP_CLOSE)/* 如果不是TCP_CLOSE状态,说明已经连接了 */ goto out; /* 调用传输层接口tcp_v4_connect建立与服务器连接,并发送SYN段 */ err = sk->sk_prot->connect(sk, uaddr, addr_len); if (err < 0) goto out; /* 发送SYN段后,设置状态为SS_CONNECTING */ sock->state = SS_CONNECTING; err = -EINPROGRESS;/* 如果是以非阻塞方式进行连接,则默认的返回值为 EINPROGRESS,表示正在连接 */ break; } /* 获取连接超时时间,如果指定非阻塞方式,则不等待直接返回 */ timeo = sock_sndtimeo(sk, flags & O_NONBLOCK); if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {/* 发送完SYN 后,连接状态一般为这两种状态,但是如果连接建立非常快,则可能越过这两种状态 */ if (!timeo || !inet_wait_for_connect(sk, timeo))/* 等待连接完成或超时 */ goto out; err = sock_intr_errno(timeo); if (signal_pending(current)) goto out; } if (sk->sk_state == TCP_CLOSE)/* 运行到这里说明连接建立失败 */ goto sock_error; sock->state = SS_CONNECTED;/* 连接建立成功,设置为已经连接状态 */ err = 0; out: release_sock(sk); 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; }
可以分析出
inet_stream_connect()函数主要功能如下:
(1)调用tcp_v4_connect函数建立与服务器联系并发送SYN段;
(2)获取连接超时时间timeo,如果timeo不为0,则会调用inet_wait_for_connect一直等待到连接成功或超时;
这个时候就很明显了,由tcp_v4_connect函数建立与服务器联系并发送SYN段,所以三次握手环节肯定体现在该函数中,查阅资料可得,该函数包含的内容很多,针对从此次的实验,主要是tcp_connect(struct sock *sk)函数起到了发送SYN段并通过三次握手建立连接的作用。
和上面的环节一样,为了验证猜想,还是为tcp_connect函数打上断点,并追踪它,查看程序运行时是否调用过它:
根据提示信息查看源码,分析可得:
/* 构造并发送SYN段 */ int tcp_connect(struct sock *sk) { struct tcp_sock *tp = tcp_sk(sk); struct sk_buff *buff; tcp_connect_init(sk);/* 初始化传输控制块中与连接相关的成员 */ /* 为SYN段分配报文并进行初始化 */ buff = alloc_skb(MAX_TCP_HEADER + 15, sk->sk_allocation); if (unlikely(buff == NULL)) return -ENOBUFS; /* Reserve space for headers. */ skb_reserve(buff, MAX_TCP_HEADER); TCP_SKB_CB(buff)->flags = TCPCB_FLAG_SYN; TCP_ECN_send_syn(sk, tp, buff); TCP_SKB_CB(buff)->sacked = 0; skb_shinfo(buff)->tso_segs = 1; skb_shinfo(buff)->tso_size = 0; buff->csum = 0; TCP_SKB_CB(buff)->seq = tp->write_seq++; TCP_SKB_CB(buff)->end_seq = tp->write_seq; tp->snd_nxt = tp->write_seq; tp->pushed_seq = tp->write_seq; tcp_ca_init(tp); /* Send it off. */ TCP_SKB_CB(buff)->when = tcp_time_stamp; tp->retrans_stamp = TCP_SKB_CB(buff)->when; /* 将报文添加到发送队列上 */ __skb_queue_tail(&sk->sk_write_queue, buff); sk_charge_skb(sk, buff); tp->packets_out += tcp_skb_pcount(buff); /* 发送SYN段 */ tcp_transmit_skb(sk, skb_clone(buff, GFP_KERNEL)); TCP_INC_STATS(TCP_MIB_ACTIVEOPENS); /* Timer for repeating the SYN until an answer. */ /* 启动重传定时器 */ tcp_reset_xmit_timer(sk, TCP_TIME_RETRANS, tp->rto); return 0; }
tcp_connect()中又调用了tcp_transmit_skb函数:
/* 发送一个TCP报文 */ static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb) { if (skb != NULL) { struct inet_sock *inet = inet_sk(sk); struct tcp_sock *tp = tcp_sk(sk); struct tcp_skb_cb *tcb = TCP_SKB_CB(skb); int tcp_header_size = tp->tcp_header_len; struct tcphdr *th; int sysctl_flags; int err; BUG_ON(!tcp_skb_pcount(skb)); #define SYSCTL_FLAG_TSTAMPS 0x1 #define SYSCTL_FLAG_WSCALE 0x2 #define SYSCTL_FLAG_SACK 0x4 sysctl_flags = 0;/* 标识TCP选项 */ /* 根据TCP选项调整TCP首部长度 */ if (tcb->flags & TCPCB_FLAG_SYN) {/* 如果当前段是SYN段,需要特殊处理一下 */ /* SYN段必须通告MSS,因此报头加上MSS通告选项的长度 */ tcp_header_size = sizeof(struct tcphdr) + TCPOLEN_MSS; if(sysctl_tcp_timestamps) {/* 启用了时间戳 */ /* 报头加上时间戳标志 */ tcp_header_size += TCPOLEN_TSTAMP_ALIGNED; sysctl_flags |= SYSCTL_FLAG_TSTAMPS; } if(sysctl_tcp_window_scaling) {/* 处理窗口扩大因子选项 */ tcp_header_size += TCPOLEN_WSCALE_ALIGNED; sysctl_flags |= SYSCTL_FLAG_WSCALE; } if(sysctl_tcp_sack) {/* 处理SACK选项 */ sysctl_flags |= SYSCTL_FLAG_SACK; if(!(sysctl_flags & SYSCTL_FLAG_TSTAMPS)) tcp_header_size += TCPOLEN_SACKPERM_ALIGNED; } } else if (tp->rx_opt.eff_sacks) {/* 非SYN段,但是有SACK块 */ /* 根据SACK块数调整TCP首部长度 */ tcp_header_size += (TCPOLEN_SACK_BASE_ALIGNED + (tp->rx_opt.eff_sacks * TCPOLEN_SACK_PERBLOCK)); } if (tcp_is_vegas(tp) && tcp_packets_in_flight(tp) == 0) tcp_vegas_enable(tp); /* 在报文首部中加入TCP首部 */ th = (struct tcphdr *) skb_push(skb, tcp_header_size); /* 更新TCP首部指针 */ skb->h.th = th; /* 设置报文的传输控制块 */ skb_set_owner_w(skb, sk); /* Build TCP header and checksum it. */ /* 填充TCP首部中的数据 */ th->source = inet->sport; th->dest = inet->dport; th->seq = htonl(tcb->seq); th->ack_seq = htonl(tp->rcv_nxt); *(((__u16 *)th) + 6) = htons(((tcp_header_size >> 2) << 12) | tcb->flags); /* 设置TCP首部的接收窗口 */ if (tcb->flags & TCPCB_FLAG_SYN) { th->window = htons(tp->rcv_wnd);/* 对SYN段来说,接收窗口初始值为rcv_wnd */ } else { /* 对其他段来说,调用tcp_select_window计算当前接收窗口的大小 */ th->window = htons(tcp_select_window(sk)); } /* 初始化校验码和带外数据指针 */ th->check = 0; th->urg_ptr = 0; if (tp->urg_mode &&/* 发送时设置了紧急方式 */ between(tp->snd_up, tcb->seq+1, tcb->seq+0xFFFF)) {/* 紧急指针在报文序号开始的65535范围内 */ /* 设置紧急指针和带外数据标志位 */ th->urg_ptr = htons(tp->snd_up-tcb->seq); th->urg = 1; } /* 开始构建TCP首部选项 */ if (tcb->flags & TCPCB_FLAG_SYN) { /* 调用tcp_syn_build_options构建SYN段的首部 */ tcp_syn_build_options((__u32 *)(th + 1), tcp_advertise_mss(sk), (sysctl_flags & SYSCTL_FLAG_TSTAMPS), (sysctl_flags & SYSCTL_FLAG_SACK), (sysctl_flags & SYSCTL_FLAG_WSCALE), tp->rx_opt.rcv_wscale, tcb->when, tp->rx_opt.ts_recent); } else { /* 构建普通段的首部 */ tcp_build_and_update_options((__u32 *)(th + 1), tp, tcb->when); TCP_ECN_send(sk, tp, skb, tcp_header_size); } /* 计算传输层的校验和 */ tp->af_specific->send_check(sk, th, skb->len, skb); /* 如果发送的段有ACK标志,则通知延时确认模块,递减快速发送ACK 段的数量,同时停止延时确认定时器 */ if (tcb->flags & TCPCB_FLAG_ACK) tcp_event_ack_sent(sk); if (skb->len != tcp_header_size)/* 发送的段有负载,则检测拥塞窗口闲置是否超时 */ tcp_event_data_sent(tp, skb, sk); TCP_INC_STATS(TCP_MIB_OUTSEGS); /* 调用IP层的发送函数发送报文 */ err = tp->af_specific->queue_xmit(skb, 0); if (err <= 0) return err; /* 如果发送失败,则类似于接收到显式拥塞通知的处理 */ tcp_enter_cwr(tp); return err == NET_XMIT_CN ? 0 : err; } return -ENOBUFS; #undef SYSCTL_FLAG_TSTAMPS #undef SYSCTL_FLAG_WSCALE #undef SYSCTL_FLAG_SACK }
至此TCP三次握手涉及到的代码已全部介绍完。
以上是关于connect及bindlistenaccept背后的三次握手的主要内容,如果未能解决你的问题,请参考以下文章
深入理解TCP协议及其源代码——connect及bindlistenaccept背后的“三次握手”