前言:TCP是传输层协议,实现了一种可靠的通信。它从不同角度提供了多种可靠性保障措施来为网络传输提供确定性。连接性就是其中之一,不像UDP的无连接状态,TCP在数据传输之前会进行连接,只有双方都协调完成后,才会进行数据传输;同样的,在结束时,又会断开连接,通告传输的完成;在数据传输过程中,又会对每个传输进行确认。更多的可靠性措施在后面的系列中会仔细说明,这一篇,重点从连接这个角度看看TCP协议。
一. TCP状态机的运转
二. TCP的连接与断开
2.1 TCP连接处理
2.1.1 listen()调用
listen()系统调用是服务器侧编程的一个必要动作,主要是把创建的主动socket变成被动socket,那么这里主动和被动有什么区别呢?通过代码一探里面的操作:当调用socket()时,会调用对应的协议无关层的接口
sock->ops->listen(sock, backlog);
这里的ops->listen
最终调用的就是inet_listen()
,在这个函数中,我们看到
old_state = sk->sk_state;
if (!((1 << old_state) & (TCPF_CLOSE | TCPF_LISTEN)))
goto out;
/* Really, if the socket is already in listen state
* we can only allow the backlog to be adjusted.
*/
if (old_state != TCP_LISTEN) {
err = inet_csk_listen_start(sk, backlog);
if (err)
goto out;
}
sk->sk_max_ack_backlog = backlog;
err = 0;
在开始就是检查套接字的状态,如果已经是listen状态,则只要更改backlog的值;如果不是listen状态,就启动设置listen。看这个inet_csk_listen_start(sk, backlog);
,里面把套接字状态设置为listen:
sk->sk_state = TCP_LISTEN;
所以,所说的把主动套接字变成被动套接字主要就是改变TCP的初始状态,从closed状态转为listen状态。从第一节的状态机上可以看出不同的起始状态后续的处理也不同。
2.1.2 TCP发起连接
TCP的连接过程主要是3次握手,如下图所示:
这是一张截取《TCP/IP 详解》中的握手图,其中的序列号注意一下。左端为客户端,右端为服务端。
一般来说是客户端主动发起连接,而服务端则接收并建立连接。服务端是在调用connect()时发起SYN分节,那接下来就来看看这个connect系统调用里面都做了哪些事:
err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen,
sock->file->f_flags);
一样的,这里的ops->connect
在INET域就是inet_stream_connect()
,在这个函数中,看到在检查了套接字还没有连接的前提下,就调用TCP的连接函数:
err = sk->sk_prot->connect(sk, uaddr, addr_len);
if (err < 0)
goto out;
这里的sk_prot->connect
就是tcp_v4_connect()
,在这个函数中,查找路由,填充各种信息等,最后调用tcp_connect()
,这个函数主要就是构建一个SYN报文发送出去。后面是tcp_transmit_skb()
,填充一下消息头等各种操作,最后发送到队列中:
err = icsk->icsk_af_ops->queue_xmit(skb, 0);
if (likely(err <= 0))
return err;
最后会调用icsk_af_ops->queue_xmit()
将数据包往IP层发送,那么这个queue_xmit
是什么呢?如果我们去搜这个icsk_af_ops
注册的地方,就会发现在TCP操作集的初始化tcp_v4_init_sock()
中,icsk_af_ops->queue_xmit()
被设置为:
icsk->icsk_af_ops = &ipv4_specific;
const struct inet_connection_sock_af_ops ipv4_specific = {
.queue_xmit = ip_queue_xmit,
.send_check = tcp_v4_send_check,
.rebuild_header = inet_sk_rebuild_header,
.conn_request = tcp_v4_conn_request,
.syn_recv_sock = tcp_v4_syn_recv_sock,
.remember_stamp = tcp_v4_remember_stamp,
.net_header_len = sizeof(struct iphdr),
.setsockopt = ip_setsockopt,
.getsockopt = ip_getsockopt,
.addr2sockaddr = inet_csk_addr2sockaddr,
.sockaddr_len = sizeof(struct sockaddr_in),
.bind_conflict = inet_csk_bind_conflict,
#ifdef CONFIG_COMPAT
.compat_setsockopt = compat_ip_setsockopt,
.compat_getsockopt = compat_ip_getsockopt,
#endif
};
可以看出来,最后的queue_xmit
就是ip_queue_xmit()
。然后就交给IP层处理。
那么这个tcp_v4_init_sock()
是什么时候初始化的呢?我们看到这个函数是作为TCP操作集init的回调函数。它是在创建socket的时候,初始化的。系统调用socket会调用inet_init()
,在这个函数中:
if (sk->sk_prot->init) {
err = sk->sk_prot->init(sk);
if (err)
sk_common_release(sk);
当创建的是流式套接字时,对应于INET族的就是TCP协议,就会调用tcp_v4_init_sock()
。
这就是客户端主动发起连接的过程,当然里面还有复杂的多种其他任务的处理,自行根据需要分析。
2.1.3 TCP接收连接
对于tcp接收,首先要确认它的处理函数—tcp_v4_rcv()
,这是根据接收的报文头层层传递上来的,具体的过程,在讲设备无关层是具体说明。忽略其他的处理,然后就到了tcp_v4_do_rcv()
,在这里进行报文的实际处理,主要分为三条线:
-
对于已经建立的tcp连接,此时接收的就是数据报文,进入到内层处理:
if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */ TCP_CHECK_TIMER(sk); if (tcp_rcv_established(sk, skb, tcp_hdr(skb), skb->len)) { rsk = sk; goto reset; } TCP_CHECK_TIMER(sk); return 0; }
对于内层的
tcp_rcv_established()
就不进行详细说明了,对于它的工作,很显然的是拷贝报文到用户态应用程序,是在里面的tcp_copy_to_iovec()
中做的,就是拷贝报文。 -
对于是tcp listen状态的,则进入建立连接的处理。最终进入到
tcp_rcv_state_process()
状态机处理。 -
对于不是以上两种状态的,则直接进入到状态机处理。
从以上可以看出,对于接收,最重要的处理过程就是tcp状态机。理解了状态机的转换过程,也就明白了代码处理的逻辑。
2.2 TCP断开处理
tcp的断开过程形象的说是4次挥手的过程,如下图所示:
这里一共有4次交互过程,关于为什么中间的ack M+1和FIN N不合并成一条呢?在《unix 网络编程》中作者曾提到过,其中一个原因就是另一方暂时不想断开,也就是说tcp是双工的,允许一个方向断开,而另个方向暂时不断开的情形,就是所说的半关闭状态。
其中主动关闭的一方可以在调用close()时发送FIN报文,开始关闭过程。如果调用shutdown()会触发单向关闭,可以去查看源代码:在用户调用close()时,最终根据套接字类型,会找到tcp_close()
函数。在其中最后调用了tcp_send_fin()
发送FIN报文。然后就是接收报文进入状态机进行处理。
对于断开连接有一个问题:TIME_WAIT状态会保持2MSL时间,其中的解释如下(取自网络)
主动发起关闭连接的操作的一方将达到TIME_WAIT状态,而且这个状态要保持Maximum Segment Lifetime的两倍时间。为什么要这样做而不是直接进入CLOSED状态?
原因有二:
一、保证TCP协议的全双工连接能够可靠关闭
二、保证这次连接的重复数据段从网络中消失
先说第一点,如果Client直接CLOSED了,那么由于IP协议的不可靠性或者是其它网络原因,导致Server没有收到Client最后回复的ACK。那么Server就会在超时之后继续发送FIN,此时由于Client已经CLOSED了,就找不到与重发的FIN对应的连接,最后Server就会收到RST而不是ACK,Server就会以为是连接错误把问题报告给高层。这样的情况虽然不会造成数据丢失,但是却导致TCP协议不符合可靠连接的要求。所以,Client不是直接进入CLOSED,而是要保持TIME_WAIT,当再次收到FIN的时候,能够保证对方收到ACK,最后正确的关闭连接。
再说第二点,如果Client直接CLOSED,然后又再向Server发起一个新连接,我们不能保证这个新连接与刚关闭的连接的端口号是不同的。也就是说有可能新连接和老连接的端口号是相同的。一般来说不会发生什么问题,但是还是有特殊情况出现:假设新连接和已经关闭的老连接端口号是一样的,如果前一次连接的某些数据仍然滞留在网络中,这些延迟数据在建立新连接之后才到达Server,由于新连接和老连接的端口号是一样的,又因为TCP协议判断不同连接的依据是socket pair,于是,TCP协议就认为那个延迟的数据是属于新连接的,这样就和真正的新连接的数据包发生混淆了。所以TCP连接还要在TIME_WAIT状态等待2倍MSL,这样可以保证本次连接的所有数据都从网络中消失。
2.3 异常处理
-
超时
由于tcp是可靠的协议,因此,在一方发送数据后,期望能够确认数据发送成功或者失败。在linux中提供了重传定时器,用于在数据超时的时候进行重传。比如在
tcp_connect()
中,发送SYN连接时,设置的超时定时器:inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS, inet_csk(sk)->icsk_rto, TCP_RTO_MAX);
三. TCP连接的特殊状态
3.1 半打开状态
半打开状态是指连接的一方由于异常关闭,而另一端对此并不知情的场景。常见的触发原因有:主机掉电等。在这个时候,另一端如果发送数据,必然是不会通的,因为此时掉电重启的主机已经没有了连接的信息,会回复一个RST报文复位连接。对于检测半打开状态,可以使用keepalive保活定时器,默认是120分钟。也可以自己实现心跳来保持。
3.2 半关闭状态
由于tcp是全双工的,因此双方都可以关闭自己的连接。而有一种特殊的情况就是:主动发起关闭的一方发送FIN后,被动一方对其进行了确认,然而并没有接着发送自己的FIN,此时,表示被动一方仍然想要传输数据,当然,主动的一方虽然关闭了发送通道,但是仍然可以接收被动方的数据。