深入理解TCP协议及其源代码
本文参考了《TCP/IP协议族》第四版
进程到进程的通信
与UDP一样,TCP也是使用端口号提供进程到进程之间的通信。下表是我们常见的TCP使用的熟知端口号。
端口 | 协议 | 说明 |
---|---|---|
7 | Echo | 把收到的数据报回送到发送方 |
9 | Discard | 丢弃收到的任何数据报 |
11 | Users | 活跃的用户 |
20和21 | FTP | 文件传输协议 |
23 | TELNET | 终端网络 |
25 | SMTP | 简单邮件传送协议 |
53 | DNS | 域名服务器 |
80 | HTTP | 超文本传输协议 |
面向字节流
TCP创造了一个环境使得两个进程之间好像有一个管道连接,而中间流动着的,就是字节流。在发送进程写入字节流,而在另一端的接收进程则读取字节流。
因为在发送进程和接收进程的读取和写入的速度可能不一样,那么可能存在发送进程发送的太快,而接收进程来不及接收就使得一些数据丢失。那么在这里就设置了TCP缓存。这种协调发送者和接收者之间速度的控制方式也叫做流量控制。在发送方有发送缓存,接收方有接收缓存。
在发送方,缓存有三种类型的槽,白色区域是空槽,也就是可以让发送进程填入数据的地方,深灰色区域保存的是已经发送出去但是没有接收到ACK的字节,发送TCP的缓存中还需要保存这些字节,在必要的时候进行重传。灰色区域表示发送进程即将发送的字节。在深灰色的槽中的字节被确认后,这些位置就可以被回收并且被发送进程再次利用。在接收方呢,这里的缓存就被划分成了两种颜色,白色区域也是空槽,灰色区域是已经接收到的字节,还未被读取的,而这些字节即将被接收进程读取,在一个槽的数据被读取后就可以加入到白色区域,也就是槽被回收。这就是环形缓冲区域的好处了。
TCP的连接建立阶段三向握手
TCP的连接建立阶段也可以称为是三向握手,是一个客户的应用程序希望使用TCP作为传输层协议来和服务器的应用程序建立连接。客户端发送请求,所以客户端的行为也可以称为是主动打开,服务器程序告诉它的TCP自己已经准备好接受连接,这个过程也叫做被动打开。
-
step1:首先客户发送第一个报文段,也就是一个请求连接报文段,这个报文段的SYN标志位置为1,SYN也就是同步序号。仿佛就是客户端向服务器说:“嘿,我想给你发信息啦。”这里选择了随机的序号8000.SYN报文段不携带任何数据,但是要消耗一个序号。
-
step2:服务器如果同意连接,就会返回一个SYN+ACK报文段,这里的SYN也指的是同步,ACK表示对刚才接收到的SYN报文段的确认。这个报文还有一个很重要的功能就是定义了接受窗口的大小rwnd。SYN+ACK报文段不懈怠数据,但是要消耗一个序号。这里的序列号也是随机序列号,ack是期待对方下一个发过来的报文段的序列号。这也就是服务器在跟客户端说:”好了,我直到你要发信息了,我期待你下一次发ack这个报文段过来,对了,我的接收窗口大小是rwnd。“
-
step3:此时发送方返回一个ACK报文段,是对第二个报文段的确认,这里使用了ACK标志和确认号。值得注意的是,这里的序号和刚开始还发过去的SYN报文段的序号一样,也就是如果这个ACK不携带任何数据就不消耗序列号。这里也定义了窗口大小。
状态转换图
为了理解这个状态转换图,我们要分为以下几种情况:
连接建立和半关闭终止
-
客户端
客户进程发送一个连接请求,主动打开。这时TCP发送一个SYN报文段,进入到SYN-SENT状态,在接收到SYN+ACK报文段后,TCP发送一个ACK报文段,进入到ESTABLISHED状态。进入数据传送阶段。
当客户端数据传送结束后,就发出主动关闭的请求,于是TCP发送FIN报文段,进入到FIN-WAIT-1状态。一直等待接收到对刚才的FIN报文段的ACK后,就进入到FIN-WAIT-2状态,直到服务器也结束数据发送,发送过来一个FIN报文段后,客户端就发送对这个FIN报文段的ACK报文段,此时进入到TIME-WAIT状态,启动2MSL计时器。设置这个计时器的目的是为了防止在最后一个ACK报文段丢失的情况下,此时如果客户端已经关闭,那么服务器就会陷入到盲等的状态。
-
服务器
服务器是在客户端主动打开后被动打开的,这是服务器TCP进入到LISTEN状态,被动接收客户端发来的SYN报文段。当服务器TCP接收到SYN报文段后,就发送SYN+ACK报文段,进入到SYN+RCVD状态,等待客户端发送ACK报文段。在接收到ACK报文段后,进入到ESTABLISHED状态,进入传送数据阶段。
收到客户的TCP的FIN报文段后,服务器发送ACK报文段,进入CLOSE-WAIT状态。如果此时发送队列中还有未发送数据,就继续发送。因为TCP提供的是全双工服务,此时仅仅关闭了客户端到服务器的发送数据方向,但是在服务器到客户端的方向还未关闭,所以这就是半关闭终止。在发送数据接收后,服务器TCP发送一个FIN报文段,进入到LAST-ACK状态。并且等待最后从客户发来的ACK报文段,接下来进入CLOSED状态。
此处的终止阶段称为四向握手。
常见情况
-
在数据传送阶段完成后,客户端发出关闭命令。命令TCP发送FIN报文段,进入到FIN-WAIT-1状态。服务器在收到这个FIN报文段后,继续向客户端发送剩余数据,最后加上EOF标记,表示这个连接要关闭了。此时服务器TCP进入到CLOSE-WAIT状态,此处推迟对客户端发来的FIN报文段的确认,直到自己收到关闭命令时,服务器TCP就向客户端发送FIN+ACK报文段,进入到LAST-ACK状态,等待最后的ACK。客户取消了FIN-WAIT-2状态直接进入了TIME-WAIT状态。
此处的终止阶段采用的是三向握手。
同时打开
这种情况下双方都主动发出打开命令。此时通信的双方是对等的,双方的TCP同时发出SYN报文段,此后进入SYN-SENT状态,在收到SYN+ACK后双方同时进入SYN-RCVD状态,接下来进入ESTABLISHED状态。
同时关闭
这种情况下,双方都主动发出主动关闭,双方的TCP都发送FIN报文段,进入FIN-WAIT-1状态。在收到FIN报文段后,双方进入CLOSING状态,并且发送ACK报文段。此处的CLOSING状态取代了FIN-WAIT-2和CLOSE-WAIT。在收到ACK后双方进入TIME-WAIT状态。
拒绝连接
当服务器TCP拒绝连接时,服务器在收到SYN报文段后会发送一个RST报文段,拒绝这条连接。客户在收到这个报文段后进入CLOSED状态。
异常终止连接
进程可以异常终止连接,在进程出现故障的时候,TCP发送RST+ACK报文段异常终止,把队列中的所有数据都丢弃,双方的TCP立即进入CLOSED状态。
理解TCP源代码
-
在TCP建立连接的过程中主要调用的函数是connect和accept,在前面我们已经分析过了,客户端主动发起connect,而服务器是被动接收accept的。accept和connect最终调用了__sys_accept4,sys_connect两个内核处理函数。这两个函数对应着sock->opt->connet和sock->opt->accept两个函数指针,而这两个函数指针对应着tcp_v4_connect和inet_csk_accept函数。
tcp_v4_connect
/* This will initiate an outgoing connection. */ int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len) { ... 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); ... /* 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);//调用tcp_connect(sk)构造SYN ... rt = ip_route_newports(fl4, rt, orig_sport, orig_dport, inet->inet_sport, inet->inet_dport, sk); ... err = tcp_connect(sk); ... } EXPORT_SYMBOL(tcp_v4_connect);
tcp_connect
/* 构造SYN并且发出去 */ int tcp_connect(struct sock *sk) { ... tcp_init_nondata_skb(buff, tp->write_seq++, TCPHDR_SYN); tp->retrans_stamp = 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; 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);//超时计时器 return 0; }
inet_csk_accept
/* * This will accept the next outstanding connection. */ struct sock *inet_csk_accept(struct sock *sk, int flags, int *err) { struct inet_connection_sock *icsk = inet_csk(sk); struct request_sock_queue *queue = &icsk->icsk_accept_queue; struct sock *newsk; struct request_sock *req; int error; lock_sock(sk); /* We need to make sure that this socket is listening, * and that it has something pending. */ error = -EINVAL; if (sk->sk_state != TCP_LISTEN) goto out_err; /* Find already established connection */ if (reqsk_queue_empty(queue)) { long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK); ... error = inet_csk_wait_for_connect(sk, timeo); if (error) goto out_err; } req = reqsk_queue_remove(queue); newsk = req->sk; sk_acceptq_removed(sk); if (sk->sk_protocol == IPPROTO_TCP && queue->fastopenq != NULL) { spin_lock_bh(&queue->fastopenq->lock); if (tcp_rsk(req)->listener) { /* We are still waiting for the final ACK from 3WHS * so can't free req now. Instead, we set req->sk to * NULL to signify that the child socket is taken * so reqsk_fastopen_remove() will free the req * when 3WHS finishes (or is aborted). */ req->sk = NULL; req = NULL; } ... return newsk; ... }
-
客户端通过tcp_v4_connect函数调用到tcp_connect函数,将请求发送数据包出去,服务器端则通过inet_csk_accept函数调用inet_csk_wait_for_connect函数中的for循环进入阻塞,直到监听到请求才跳出循环。connect启动到返回和accept返回之间就是所谓三次握手的时间。
实验验证
首先我们用抓包工具来验证三次握手过程中的报文段相应字段的值
这是在访问百度的时候抓包过程,这里记录了运用http协议中的传输层TCP建立连接的过程。
我们清晰的可以看到在前面三个报文段中,第一个有SYN标记,是客户端发来的第一次握手,第二次是服务器返回的SYN+ACK报文段,也就是第二次握手过程。第三次是一个客户端返回的ACK。