1. TCP建立连接(三次握手)
下面两个图是从协议和接口两个角度来解释TCP的三次握手过程(分别摘自计算机网络-谢希仁和UNIX网络编程卷1):
1.1. tcpdump三次握手
下面通过tcpdump来查看tcp建立连接的过程:用nc命令来进行测试,
1 | server:nc –l 9000 |
tcpdump默认情况下,只会在SYN报文段中显示绝对的sequence number, 其他报文段只会显示相对的sequence number,所以需要加上-S才能在握手以外的阶段显示绝对sequence number。-X参数用于解析和显示每一个包的包头和包体。这里只显示IP header + TCP packet, 不显示链路层link level的头部。
图中第一个tcp建立连接的请求包中6个控制位字段:URG|ACK|PSH|RST|SYN|FIN,SYN置为1,表示是一个连接请求包;
图中第二个tcp包为服务器对连接请求的应答包,6个控制位字段:URG|ACK|PSH|RST|SYN|FIN,ACK和SYN均置为1,表示连接请求的应答包。TCP协议规定,链接建立后,所有的报文的ACK控制位必须置为1;
图中第三个tcp包为客户端对应答包的确认包,6个控制位字段:URG|ACK|PSH|RST|SYN|FIN,ACK置为1。
对于tcp建立链接的过程,控制位SYN只有在链接建立前两次握手中会置为1, 其他阶段的包该控制位不能设置。控制位ACK,在第二次握手开始及以后会一直置为1,直到最后一个报文包。
- 为什么要采用三次握手,而不是两次握手
三次握手每次都是接收到数据包的一方可以得知对方的情况,但发送方其实没有任何头绪。我虽然有发包的动作,但是我怎么知道我有没有发出去,而对方有没有接收到呢?所以经历了上面的三次握手过程,客户端和服务端都确认了自己的接收、发送能力是正常的。之后就可以正常通信了。
其次还是是为了防止已失效的连接请求报文段突然又传送到服务器,产生错误。
已失效的连接请求报文段的产生原因:当客户A发送连接请求,但因连接请求报文丢失而未收到确认。于是A会再次重传一次连接请求,此时服务器端B收到再次重传的连接请求,建立了连接,然后进行数据传输,数据传输完了后,就释放了此连接。假设A第一次发送的连接请求并没有丢失,而是在网络结点中滞留了太长时间,以致在AB通信完后,才到达B。此时这个连接请求其实已经是被A认为丢失的了。如果不进行第三次握手,那么服务器B可能在收到这个已失效的连接请求后,进行确认,然后单方面进入ESTABLISHED状态,而A此时并不会对B的确认进行理睬,这样就白白的浪费了服务器的资源。
1.2. tcp同时打开
TCP协议也是支持同时打开的,尽管这个概率是非常小的,因为这要求通信双方都知道对方的端口号,这需要双方都事先bind()各自的端口,并同时发出SYN包。TCP针对同时打开的情况,仅建立一条连接而不是两条连接。
对于同时打开连接的双方,在发送SYN包后,都进入SYN_SEND状态,在收到对端的SYN包后,进入SYN_RCVD状态,并对此SYN包再次回复SYN包进行确认,当双方都收到对端的SYN包ack后,就会进入ESTABLISHED状态。如下:
tcp同时打开需要4次握手,比正常的建立连接多了一个包。
2. TCP释放连接(四次挥手)
下面两个图是从协议和接口两个角度来解释TCP的释放过程(分别摘自计算机网络-谢希仁和UNIX网络编程卷1)
2.1. tcpdump四次挥手
和建立连接的三次握手一样,在链接建立后,然后关闭链接,tcpdump的结果如下:
tcpdump的结果如下:
图中第一个tcp的请求包中6个控制位字段:URG|ACK|PSH|RST|SYN|FIN,ACK和FIN控制位置为1, client发起关闭tcp连接的请求。
图中第二个tcp包,为服务器响应客户端的关闭包,其中6个控制位字段:URG|ACK|PSH|RST|SYN|FIN, ACK和FIN控制位置为1, 且回包的ack = 前一个包seq + 1,
图中第三个tcp包,为客户端对服务器关闭链接的应答,其中6个控制位字段:URG|ACK|PSH|RST|SYN|FIN,ACK控制位被置为1,这个包代表链接正式关闭;
TCP规定,FIN报文段即使不携带数据,它也消耗掉一个序号。
TCP关闭时的状态两点需要说明:
- CLOSE_WAIT状态
在被动关闭连接情况下,在已经接收到FIN,但是还没有发送自己的FIN的时刻,连接处于CLOSE_WAIT状态,此时该tcp连接就处于半关闭状态。 - TIME_WAIT状态
在客户端收到服务器端最后的连接释放报文段后,客户端不会立即进入CLOSED状态。必须经过2MSL(Maximum Segment Lifetime)后,才进入CLOSED状态。- 为了确保客户端发送的最后一个ACK报文能够到达服务器端。
- 防止TCP连接过程中所说的“已失效的连接请求报文段“出现在本连接中。客户端在发送完最后一个ACK报文后,再经过2MSL,就可以是本连接持续时间内的所有报文都在从网络中消失。这样就可以使下一个新的连接中不会出现旧的连接请求报文。
2.2. tcp同时关闭
TCP协议是允许通信的双方同时执行关闭操作,即同时发送FIN包,当双方发送FIN包后,都进入FIN_WAIT_1状态(双方互不知对方已发送FIN,所以和正常关闭一样),当双方都收到FIN包后,TCP协议层就会知道是双方是同时关闭,然后就会进入CLOSING状态,并对对方的SYN包进行ack确认,当双方收到对方的ack确认后,就会进入TIME_WAIT状态。过程如下图:
3. TCP的半关闭(half-close)
TCP之所以在释放链接的时候进行四次挥手,是因为TCP链接是全双工的,因为每个方向的链接都需要单独关闭。这个是TCP协议的最基本原则,这个原则要求:当一方完成数据的发送后,就发送FIN包来终止这个方向的链接。
所以,TCP的半关闭就是:在连接一端发送结束并关闭后,还拥有可以从另一端接收数据的能力。
不过这个功能只有很少数的应用会使用它,我们平时基本上都是通过close()直接全关闭一个链接。如果应用程序要使用这个功能,可以通过调用shutdown,且第二个参数传1
下面介绍一下:shutdown 与 close 函数 的区别
1 | int close(int sockfd); |
close一个TCP的socket,默认行为是把该套接字标记成已关闭,然后立即返回到调用进程,该套接字描述符不能再由调用进程使用,也就是说它不能被read或write,否则会报错,bad file descriptor,然后TCP将尝试发送已排队等待发送到对端的任何数据,发送完毕后发生的是正常的TCP终止序列。
server端调用close()函数后,该链接不再是半关闭,而是全关闭。调用方TCP协议层会把该链接的FD标识为全关闭。server发送完FIN包后,对于客户端并不知道所谓的全关闭, 因为client只会收到一个FIN包, 按照tcp协议规定: client只会知道server到client方向的数据传输已经关闭,所以client理论上是允许再次向server端发送数据。
如果,此时client再次向server发送数据会成功,send()只负责把数据交给内核TCP发送缓冲区,然后就成功返回了,所以不会出错。但是client的tcp内核协议层会收到server的一个RST回包,表示server已不能接收数据,连接被重置。client的tcp内核协议层收到这个RST回包,无法立刻通知应用层,只是保存这个状态,如果client再次进行:
- read()操作:返回0,表示链接已关闭。
- write()操作:tcp协议层已经处于RST状态了,会直接触发SIGPIPE信号,默认该信号会终止程序。shutdown()可以选择关闭TCP全双工的单向或双向的连接。第二参数how值如下:
1
int shutdown(int sockfd, int how);
how的值 | 描述 |
---|---|
0 | Further receives are disallowed |
1 | Further sends are disallowed |
2 | Further sends and receives are disallowed (like close()) |
当how为0时,shutdown()关闭连接的读通道,并丢弃上层还没有读走的所有数据以及调用shutdown之后到达的数据,并且此时不会发送FIN包。
当how为1时,shutdown()关闭连接的写通道,所有的剩余数据被发送,完成后发送FIN包,执行TCP的半关闭。
这里需要指出(基本不会发生):
close()不能保证一定会发送FIN包,只有当某个sockfd的引用计数为0,close 才会发送FIN段,否则只是将引用计数减1而已。也就是说只有当所有进程(可能fork多个子进程都打开了这个套接字)都关闭了这个套接字,close 才会发送FIN 段。
4. TCP的半打开(half-open)
TCP的半打开是一种非常常见的链接异常情况:一方已经关闭或异常终止连接,而另外一方去还不知道。
这种情况在:server调用close()关闭连接后,client在收到FIN包后,并没有关闭链接,此时这个TCP链接就是半打开的,此时client再次发送数据对端就会在TCP协议层触发RST回包,例如下面的tcpdump抓包:
当然还有经常会发生的就是,另一端网络断开和断电等导致的TCP处于半打开状态。
因为客户端的多样性已经网络的复杂,服务器端很容易就产生很多半打开的连接,半打开链接的检测,有很多方法,
- 通过应用层,对每一个连接添加定时器监听,超时无数据就进行关闭操作。
- 通过TCP协议层的keepalive选项来开始TCP的保活定时器,但保活不是TCP标准规范的(但很多tcp的实现中实现了此功能),TCP RFC中给出了3个不使用保活定时器的理由:
- 在出现一个短暂的差错情况下,可能会是一个非常好的连接被释放掉;
- 保活功能会耗费不必要的带宽;
- 在按流量计费的情况下,会花掉更多的money;
下面是通过SO_KEEPALIVE选项来设置TCP连接支持保活的代码:
1 | int open = 1; // 开启keepalive属性. 缺省值: 0(关闭) |
5. TCP的复位报文段
TCP的复位报文是一个很常见的数据,引用TCP/IP详解的话:无论何时,一个报文段发往基准的连接(referenced connection)出现错误,TCP都会发出一个复位报文段。
主要有三种情况:
- 前面提到的半打开状态:一个连接的另一端已经关闭,此时发送数据到对端,就会触发RST回包;
- 到不存在的端口的连接请求:请求连接一个并没有监听的端口,TCP则会返回RST(UDP将会产生一个ICMP端口不可达的信息)。
- 异常终止一个连接:在关闭连接的时候不是通过FIN报文进行正常关闭,而是通过直接发送RST进行连接关闭。两者的区别:
- 通过FIN包进行关闭,又称:有序释放(orderly release),因为close()/shutdown()都会将缓冲区中的数据全部发送出去之后,才会发送FIN。
- 通过RST复位包进行关闭,又称:异常释放(abortive release)。异常释放的特点:
- 丢弃掉发送缓冲区中的全部数据,立刻发送RST报文;
- RST接收方会区分另一端执行的是正常关闭,还是异常关闭。
直接通过发送RST报文进行连接的异常关闭的代码如下,具体细节详见下节的TCP的延迟关闭:
1 | struct linger so_linger; |
6. TCP的延迟关闭
TCP支持通过SO_LINGER选项来设置连接延迟关闭。前面说过close()一个socket的默认行为是把该套接字标记成已关闭,然后立即返回到调用进程,然后TCP协议层将尝试发送已排队等待发送到对端的任何数据,发送完毕后发生的是正常的TCP终止序列。
SO_LINGER选项可以改变close()的默认行为,选项要求传入内核的参数的结构体如下:
1 | struct linger{ |
l_onoff开启时,l_linger为非0,调用close()关闭连接时,
- fd阻塞情况下,如果socket的发送缓冲区残留为发送的数据,那么进程将会被投入睡眠,直到所有数据被发送完并收到确定或则l_linger时间超时。如果延迟时间超时发送缓冲区的数据还没有发送完毕,那么close()会返回EWOULDBLOCK错误,并丢弃发送缓冲区的全部数据。
- fd非阻塞情况下,close()会立即返回,接下来TCP协议层就是和fd阻塞情况一样的操作。
l_onoff开启时,l_linger为0,调用close()关闭连接时,就会触发前面说的异常释放的流程,即发送复位报文进行连接的关闭。具体TCP协议层的操作和这么做的好处请见前文。
下图是close()的行为在有无SO_LINGER选项下的行为对比,截自《UNIX网络编程劵1:套接字网络API》
7. TCP状态说明
TCP连接的双方在连接的不同阶段会处于不同的阶段,对每一个阶段的了解,是很好的使用TCP的一个基础,下表是TCP连接的各个状态:
TCP端口状态 | 描述 |
---|---|
LISTEN | 等待从任何远端TCP和端口的连接请求 |
SYN_SENT | 发送完一个连接请求后等待一个匹配的连接请求。 |
SYN_RECEIVED | 发送连接请求并且接收到匹配的连接请求以后等待连接请求确认。 |
ESTABLISHED | 表示一个打开的连接,接收到的数据可以被投递给用户。连接的数据传输阶段的正常状态。 |
FIN_WAIT_1 | 等待远端TCP的连接终止请求,或者等待之前发送的连接终止请求的确认。 |
FIN_WAIT_2 | 等待远端TCP的连接终止请求 |
CLOSE_WAIT | 等待本地用户的连接终止请求 |
CLOSING | (同时关闭时)等待远端TCP的连接终止请求确认 |
LAST_ACK | 等待先前发送给远端TCP的连接终止请求的确认(包括它字节的连接终止请求的确认) |
TIME_WAIT | 等待足够的时间过去以确保远端TCP接收到它的连接终止请求的确认 |
CLOSED | 不在连接状态(这是为方便描述假想的状态,实际不存在) |
下图是TCP状态转换图(摘自:UNIX网络编程劵1:套接字网络API)
8. 参考
http://zheming.wang/blog/2013/08/18/4417B74D-F037-414A-A291-CEF7B3CD511D/
http://xstarcd.github.io/wiki/shell/tcpdump_TCP_three-way_handshake.html
http://www.cnblogs.com/lshs/p/6038458.html
http://blog.csdn.net/jnu_simba/article/details/9068059
《UNIX网络编程劵1:套接字网络API》
《TCP/IP详解 劵1:协议》
《计算机网络(谢希仁)》