深入理解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函数阻塞等待连接。
如果队列中存在连接请求连接,则将其从请求连接队列中移除,并返回其连接控制块。