深入理解TCP协议的三次握手,分析源码并跟踪握手过程

1.TCP三次握手建立连接

在TCP中,面向连接的传输需要经过三个阶段:连接建立、数据传输和连接终止。

三次握手建立连接

在我们的例子中,一个称为客户的应用程序希望使用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.TCP协议三次握手相关源代码

TCP的三次握手从用户程序的角度看就是客户端connect和服务端accept建立起连接时背后的完成的工作,在内核socket接口层这两个socket API函数对应着sys_connect和sys_accept函数,进一步对应着sock->opt->connect和sock->opt->accept两个函数指针,在TCP协议中这两个函数指针对应着tcp_v4_connect函数和inet_csk_accept函数。

 

tcp_v4_connect函数

客户端调用tcp_v4_connect函数发起一个TCP连接。tcp_v4_connect函数设置了 TCP_SYN_SENT,并调用了 tcp_connect(sk)函数来实际构造SYN并发送出去。

tcp_connect函数的作用为具体负责构造一个携带SYN标志位的TCP头并发送出去,同时还设置了计时器超时重发。

140/* This will initiate an outgoing connection. */
141int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
142{
...
171    rt = ip_route_connect(fl4, nexthop, inet->inet_saddr,
172                  RT_CONN_FLAGS(sk), sk->sk_bound_dev_if,
173                  IPPROTO_TCP,
174                  orig_sport, orig_dport, sk);
...
215    /* Socket identity is still unknown (sport may be zero).
216     * However we set state to SYN-SENT and not releasing socket
217     * lock select source port, enter ourselves into the hash tables and
218     * complete initialization after this.
219     */
220    tcp_set_state(sk, TCP_SYN_SENT);//设置TCP_SYN_SENT
...
227    rt = ip_route_newports(fl4, rt, orig_sport, orig_dport,
228                   inet->inet_sport, inet->inet_dport, sk);
...
246    err = tcp_connect(sk);//实际构造SYN报文段,并发送SYN报文段
...
264}
265EXPORT_SYMBOL(tcp_v4_connect);

 

inet_csk_accept函数

服务端调用inet_csk_accept函数,从队列中取出一个连接请求。

如果队列为空则通过inet_csk_wait_for_connect阻塞住等待客户端的连接,inet_csk_wait_for_connec无限for循环,一旦出现连接请求则跳出循环。

289/*
290 * This will accept the next outstanding connection.
291 */
292struct sock *inet_csk_accept(struct sock *sk, int flags, int *err)
293{
294    struct inet_connection_sock *icsk = inet_csk(sk);
295    struct request_sock_queue *queue = &icsk->icsk_accept_queue;
296    struct sock *newsk;
297    struct request_sock *req;
298    int error;
299
300    lock_sock(sk);
301
302    /* We need to make sure that this socket is listening,
303     * and that it has something pending.
304     */
305    error = -EINVAL;
306    if (sk->sk_state != TCP_LISTEN)
307        goto out_err;
308
309    /* Find already established connection */
310    if (reqsk_queue_empty(queue)) {                  
311        long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);
...
318        error = inet_csk_wait_for_connect(sk, timeo);//队列为空无限for循环等待
319        if (error)
320            goto out_err;
321    }
322    req = reqsk_queue_remove(queue);
323    newsk = req->sk;
324
325    sk_acceptq_removed(sk);
326    if (sk->sk_protocol == IPPROTO_TCP && queue->fastopenq != NULL) {
327        spin_lock_bh(&queue->fastopenq->lock);
328        if (tcp_rsk(req)->listener) {
329            /* We are still waiting for the final ACK from 3WHS
330             * so can't free req now. Instead, we set req->sk to
331             * NULL to signify that the child socket is taken
332             * so reqsk_fastopen_remove() will free the req
333             * when 3WHS finishes (or is aborted).
334             */
335            req->sk = NULL;
336            req = NULL;
337        }
...
344    return newsk;
...
350}

 

3.跟踪调试代码验证握手过程

给tcp_v4_connect函数和inet_csk_accept函数,还有__sys_socket,__sys_bind,__sys_listen,__sys_connect打上断点,追踪代码执行过程

 

追踪TCP通信的代码发现,服务器端先执行__sys_socket函数创建套接字,接着调用__sys_bind函数绑定套接字和服务器的地址,然后调用__sys_connect监听,接着会调用__sys_accept4,进入inet_csk_accept函数,此时还未有客户端请求连接,所以队列为空,进入inet_csk_wait_for_connect的for循环,直到客户端请求连接。

 

下面是对客户端的追踪,客户端先调用__sys_socket创建套接字,然后调用__sys_connect请求建立连接,其中tcp_v4_connect是真正实现连接的函数,此函数设置构造SYN,并发出去。

服务器端调用inet_csk_accept,结束for循环,从队列中取出连接请求,建立连接。

 

在设置断点追踪的同时,抓包验证三次握手的具体过程。可以发现,客户端调用__sys_connect将连接请求SYN报文段发出,是第一次握手;之后服务器调用__sys_accpt4响应连接请求,完成后两次握手过程。从调用__sys_connect到__sys_accept4函数响应连接请求并返回之间就是三次握手的时间。

posted @ 2019-12-26 19:57  zhouxinghui  阅读(685)  评论(0编辑  收藏  举报