深入理解TCP协议及其源代码之三次握手

本次实验主要分析TCP连接建立的过程,尤其是三次握手的过程。

TCP连接建立原理

正如我们所知道的,在客户端与服务端进行socket网络通信前,首先需要在两端之间建立TCP/UDP传输层连接。而在建立TCP连接的过程中有一个十分重要的过程,称之为三次握手。

所谓的三次握手就是在客户端和服务端正式确定连接建立成功前,所进行的三次特定格式的数据报文通信,只有当这三次报文传输通信成功时,才认为客户端和服务端之间的TCP连接建立成功。因为服务端启动后一直在监听端口,等待客户端连接。所以三次握手必然是由客户端发起的,下图展示了三次握手的示意过程以及传输的报文信息。

1.可以看到由客户端发送第一个报文,且SYN标志位被置位。

2.待服务端接收到由客户端发来的SYN报文,服务端向客户端发送一个SYN标志位以及ACK标志位被置位,且ack为接收报文seq+1的响应报文。

3.当客户端接收到服务端发来的响应报文后,紧接着向服务端发送一个ACK标志位置位且ack为服务端响应报文seq+1的报文。当服务端接收到此报文后,即认为三次握手完成,TCP连接建立成功。接下来就可以进行具体的通信了。

 

TCP连接建立底层分析

接下来我们通过源码来分析三次握手的底层实现。正如我们上述所说,服务端启动后开始监听端口,直到客户端试图连接,三次握手过程才开始进行。因此三次握手必然发生在客户端调用connect之后到服务端accept函数执行结束前,在之前的文章中我们已经知道了connect的核心逻辑为系统调用__sys_connect,accept的核心逻辑为系统调用__sys_accpet4。下面就从这两个函数入手来分析三次握手。 

 

__sys_connect:

首先查看__sys_connect源码,发现其最核心功能代码为第19行sock->ops->connect函数调用。该调用是通过函数指针来调用的,所以通过gdb来确定具体调用的是哪个方法。

 1 int __sys_connect(int fd, struct sockaddr __user *uservaddr, int addrlen)
 2 {
 3     struct socket *sock;
 4     struct sockaddr_storage address;
 5     int err, fput_needed;
 6 
 7     sock = sockfd_lookup_light(fd, &err, &fput_needed);
 8     if (!sock)
 9         goto out;
10     err = move_addr_to_kernel(uservaddr, addrlen, &address);
11     if (err < 0)
12         goto out_put;
13 
14     err =
15         security_socket_connect(sock, (struct sockaddr *)&address, addrlen);
16     if (err)
17         goto out_put;
18 
19     err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen,
20                  sock->file->f_flags);
21 out_put:
22     fput_light(sock->file, fput_needed);
23 out:
24     return err;
25 }

可以看到调用的是inet_stream_connect方法,进入该方法继续调试

 1 int inet_stream_connect(struct socket *sock, struct sockaddr *uaddr,
 2             int addr_len, int flags)
 3 {
 4     int err;
 5 
 6     lock_sock(sock->sk);
 7     err = __inet_stream_connect(sock, uaddr, addr_len, flags, 0);
 8     release_sock(sock->sk);
 9     return err;
10 }

可以看到该方法的核心功能函数为第7行__inet_stream_connect,前后只是做了并发控制。我们继续进入__inet_stream_connect方法调用,在该函数中通过函数指针进行如下调用   sk->sk_prot->connect(sk, uaddr, addr_len); 通过gdb调试我们发现其实际调用了tcp_v4_connect。因为tcp_v4_connect的代码较长,就不在此处全部贴出,我们只看其中重要的两个函数调用。

1)tcp_set_state(sk, TCP_SYN_SENT); 

TCP_SYN_SENT是定义在tcp_states.h文件中的一个enum,其值为2。该函数的功能即是对sock类型的变量sk的sk_state属性赋值为2。

2)tcp_connect(sk);

这个函数的功能是构建一个SYN报文并发送。

在该函数中调用tcp_init_nondata_skb(buff, tp->write_seq++, TCPHDR_SYN);用来构建报文common控制位,其具体的实现为TCP_SKB_CB(skb)->tcp_flags = flags;将TCPHDR_SYN赋值给tcp_skb_cb的tcp_flags属性。其中TCPHDR_SYN的值为0x02,即只有次低有效位置位,对比TCP报文结构的标志位,发现次低有效位即为SYN,所以SYN在此处实现置位。

TCPHDR_SYN及其他状态位定义如下,与TCP报文状态位一一对应:

1 #define TCPHDR_FIN 0x01
2 #define TCPHDR_SYN 0x02
3 #define TCPHDR_RST 0x04
4 #define TCPHDR_PSH 0x08
5 #define TCPHDR_ACK 0x10
6 #define TCPHDR_URG 0x20
7 #define TCPHDR_ECE 0x40
8 #define TCPHDR_CWR 0x80

 

__sys_accept4:

当客户端发送出第一个SYN报文后,服务端接收到,并进行后续处理,其代码逻辑位于__sys_accpet4中。__sys_accept4的核心函数为sock->ops->accept(sock, newsock, sock->file->f_flags, false);通过gdb我们可以得知此时的accpet函数指针实际指向inet_accept。

对inet_accept函数进一步调试可知,inet_accept中的核心功能通过sk1->sk_prot->accept(sk1, flags, &err, kern)函数指针调用inet_csk_accept来完成。

 

 下面给出inet_csk_accept的代码

 1 struct sock *inet_csk_accept(struct sock *sk, int flags, int *err, bool kern)
 2 {
 3     struct inet_connection_sock *icsk = inet_csk(sk);
 4     struct request_sock_queue *queue = &icsk->icsk_accept_queue;
 5     struct request_sock *req;
 6     struct sock *newsk;
 7     int error;
 8 
 9     lock_sock(sk);
10 
11     /* We need to make sure that this socket is listening,
12      * and that it has something pending.
13      */
14     error = -EINVAL;
15     if (sk->sk_state != TCP_LISTEN)
16         goto out_err;
17 
18     /* Find already established connection */
19     if (reqsk_queue_empty(queue)) {
20         long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);
21 
22         /* If this is a non blocking socket don't sleep */
23         error = -EAGAIN;
24         if (!timeo)
25             goto out_err;
26 
27         error = inet_csk_wait_for_connect(sk, timeo);
28         if (error)
29             goto out_err;
30     }
31     req = reqsk_queue_remove(queue, sk);
32     newsk = req->sk;
33 
34     if (sk->sk_protocol == IPPROTO_TCP &&
35         tcp_rsk(req)->tfo_listener) {
36         spin_lock_bh(&queue->fastopenq.lock);
37         if (tcp_rsk(req)->tfo_listener) {
38             /* We are still waiting for the final ACK from 3WHS
39              * so can't free req now. Instead, we set req->sk to
40              * NULL to signify that the child socket is taken
41              * so reqsk_fastopen_remove() will free the req
42              * when 3WHS finishes (or is aborted).
43              */
44             req->sk = NULL;
45             req = NULL;
46         }
47         spin_unlock_bh(&queue->fastopenq.lock);
48     }
49 out:
50     release_sock(sk);
51     if (req)
52         reqsk_put(req);
53     return newsk;
54 out_err:
55     newsk = NULL;
56     req = NULL;
57     *err = error;
58     goto out;
59 }

可以看到当queue为空即还没有请求连接且该socket为阻塞socket时,该函数会调用inet_csk_wait_for_connect函数阻塞等待连接。

如果队列中存在连接请求连接,则将其从请求连接队中移除,并返回其连接控制块。  

posted @ 2019-12-25 15:56  fiveFish  阅读(377)  评论(0编辑  收藏  举报