Android C++系列:Linux网络TCP详解
Posted 轻口味丿
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android C++系列:Linux网络TCP详解相关的知识,希望对你有一定的参考价值。
1. tcp状态转换图
这个图N多人都知道,它排除和定位网络或系统故障时大有帮助,但是怎样牢牢地将这 张图刻在脑中呢?那么你就一定要对这张图的每一个状态,及转换的过程有深刻 的认识, 不能只停留在一知半解之中。下面对这张图的11种状态详细解析一下,以便加强记忆!不过在这之前,先回顾一下TCP建立连接的三次握手过程,以及关闭连接的四次握手过程。
1.1建立连接协议(三次握手)
- 客户端发送一个带SYN标志的TCP报文到服务器。这是三次握手过程中的报文1;
- 服务器端回应客户端的,这是三次握手中的第2个报文,这个报文同时带ACK标志和SYN标志。因此它表示对刚才客户端SYN报文的回应;同时又标志SYN给客户端,询问客户 端是否准备好进行数据通讯;
- 客户必须再次回应服务段一个ACK报文,这是报文段3。
1. 2连接终止协议(四次握手)
由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这 一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方 将执行主动关闭,而另一方执行被动关闭。
- TCP客户端发送一个FIN,用来关闭客户到服务器的数据传送(报文段4);
- 服务器收到这个FIN,它发回一个ACK,确认序号为收到的序号加1(报文段5)。 和SYN一样,一个FIN将占用一个序号;
- 服务器关闭客户端的连接,发送一个FIN给客户端(报文段6);
- 客户段发回ACK报文确认,并将确认序号设置为收到序号加1(报文段7)。
1.3 11种状态
- CLOSED: 这个没什么好说的了,表示初始状态。
- LISTEN: 这个也是非常容易理解的一个状态,表示服务器端的某个SOCKET处于监听状态,可以接受连接了。
- SYN_RCVD: 这个状态表示接受到了SYN报文,在正常情况下,这个状态是服务器端的SOCKET在建立TCP连接时的三次 握手会话过程中的一个中间状态,很短暂,基本 上用netstat你是很难看到这种状态的,除非你特意写了一个客户 端测试程序,故意将三次TCP握手过程中最后一个ACK报文不予发送。因此这种状态 时,当收到客户端的ACK报文 后,它会进入到ESTABLISHED状态。
- SYN_SENT: 这个状态与SYN_RCVD遥想呼应,当客户端SOCKET执行CONNECT连接时,它首先发送SYN报文,因此也随即 它会进入到了SYN_SENT状 态,并等待服务端的发送三次握手中的第2个报文。SYN_SENT状态表示客户端已发送SYN 报文。
- ESTABLISHED:这个容易理解了,表示连接已经建立了。
- FIN_WAIT_1: 这个状态要好好解释一下,其实FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报 文。而这两种状态的区别 是:FIN_WAIT_1状态实际上是当SOCKET在ESTABLISHED状态时,它想主动关闭连接,向 对方发送了FIN报文,此时该SOCKET即 进入到FIN_WAIT_1状态。而当对方回应ACK报文后,则进入到FIN_WAIT_2状 态,当然在实际的正常情况下,无论对方何种情况下,都应该马 上回应ACK报文,所以FIN_WAIT_1状态一般是比较 难见到的,而FIN_WAIT_2状态还有时常常可以用netstat看到。
- FIN_WAIT_2:上面已经详细解释了这种状态,实际上FIN_WAIT_2状态下的SOCKET,表示半连接,也即有一方要求 close连接,但另外还告诉对方,我暂时还有点数据需要传送给你,稍后再关闭连接。
- TIME_WAIT: 表示收到了对方的FIN报文,并发送出了ACK报文,就等2MSL后即可回到CLOSED可用状态了。如果 FIN_WAIT_1状态下,收到了对方同时带 FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过 FIN_WAIT_2状态。
- CLOSING: 这种状态比较特殊,实际情况中应该是很少见,属于一种比较罕见的例外状态。正常情况下,当你发送 FIN报文后,按理来说是应该先收到(或同时收到)对方的 ACK报文,再收到对方的FIN报文。但是CLOSING状态表 示你发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。什 么情况下会出现此种情况 呢?其实细想一下,也不难得出结论:那就是如果双方几乎在同时close一个SOCKET的话,那么就出现了双方同时 发送FIN报 文的情况,也即会出现CLOSING状态,表示双方都正在关闭SOCKET连接。
- CLOSE_WAIT: 这种状态的含义其实是表示在等待关闭。怎么理解呢?当对方close一个SOCKET后发送FIN报文给自 己,你系统毫无疑问地会回应一个ACK报文给对 方,此时则进入到CLOSE_WAIT状态。接下来呢,实际上你真正需要 考虑的事情是察看你是否还有数据发送给对方,如果没有的话,那么你也就可以 close这个SOCKET,发送FIN报文 给对方,也即关闭连接。所以你在CLOSE_WAIT状态下,需要完成的事情是等待你去关闭连接。
- LAST_ACK: 这个状态还是比较容易好理解的,它是被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收 到ACK报文后,也即可以进入到CLOSED可用状态了。
2. TCP流量控制(滑动窗口)
介绍UDP时我们描述了这样的问题:如果发送端发送的速度较快,接收端接收到数据后处理的速度较慢,而接收缓冲区的大小是固定的,就会丢失数据。TCP协议通过’滑动窗口 (Sliding Window)’机制解决这一问题。看下图的通讯过程。
- 发送端发起连接,声明最大段尺寸是1460,初始序号是0,窗口大小是4K,表示“我 的接收缓冲区还有4K字节空闲,你发的数据不要超过4K”。接收端应答连接请求,声明最大 段尺寸是1024,初始序号是8000,窗口大小是6K。发送端应答,三方握手结束。
- 发送端发出段4-9,每个段带1K的数据,发送端根据窗口大小知道接收端的缓冲区满了,因此停止发送数据。
- 接收端的应用程序提走2K数据,接收缓冲区又有了2K空闲,接收端发出段10,在应答已收到6K数据的同时声明窗口大小为2K。
- 接收端的应用程序又提走2K数据,接收缓冲区有4K空闲,接收端发出段11,重新声明窗口大小为4K。
- 发送端发出段12-13,每个段带2K数据,段13同时还包含FIN位。
- 接收端应答接收到的2K数据(6145-8192),再加上FIN位占一个序号8193,因此应答 序号是8194,连接处于半关闭状态,接收端同时声明窗口大小为2K。
- 接收端的应用程序提走2K数据,接收端重新声明窗口大小为4K。
- 接收端的应用程序提走剩下的2K数据,接收缓冲区全空,接收端重新声明窗口大小为 6K。
- 接收端的应用程序在提走全部数据后,决定关闭连接,发出段17包含FIN位,发送端应答,连接完全关闭。
上图在接收端用小方块表示1K数据,实心的小方块表示已接收到的数据,虚线框表示接收缓冲区,因此套在虚线框中的空心小方块表示窗口大小,从图中可以看出,随着应用程序 提走数据,虚线框是向右滑动的,因此称为滑动窗口。
从这个例子还可以看出,发送端是一K一K地发送数据,而接收端的应用程序可以两K两 K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就 是说,应用程序所看到的数据是一个整体,或说是一个流(stream),在底层通讯中这些数 据可能被拆成很多数据包来发送,但是一个数据包有多少字节对应用程序是不可见的,因此 TCP协议是面向流的协议。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必 须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。
3. TCP半链接状态
当TCP链接中A发送FIN请求关闭,另一段B回应ACK后,B没有立即发送FIN给A时,A方处在半链接状态,此时A可以接收B发送的数据,但是A已不能再向B发送数据。
#include <sys/socket.h>
int shutdown(int sockfd, int how);
- sockfd: 需要关闭的socket的描述符
- how:允许为shutdown操作选择以下几种方式:
- SHUT_RD:关闭连接的读端。也就是该套接字不再接受数据,任何当前在套接字接受缓冲区的数据将被丢弃。 进程将不能对该套接字发出任何读操作。对 TCP套接字该调用之后接受到的任何数据将被确认然后无声的丢弃掉。
- SHUT_WR:关闭连接的写端,进程不能在对此套接字发出写操作
- SHUT_RDWR:相当于调用shutdown两次:首先是以SHUT_RD,然后以SHUT_WR
使用close中止一个连接,但它只是减少描述符的参考数,并不直接关闭连接,只有当描述符的参考数为0时才关闭连接。 shutdown可直接关闭描述符,不考虑描述 符的参考 数,可选择中止一个方向的连接。
注意:
- 如果有多个进程共享一个套接字,close每被调用一次,计数减1,直到计数为0时,也就是所用进程都调用了close,套接字将被释放;
- 在多进程中如果一个进 程中shutdown(sfd, SHUT_RDWR)后其它的进程将无法进行通信. 如果一个进程close(sfd) 将不会影响到其它进程. 得自己理解引用计数的用法了。
4. 2MSL
MSL是*Maximum Segment Lifetime*英文的缩写,中文可以译为“报文最大生存时间”,*2MSL*即两倍的*MSL*,*TCP*的*TIME_WAIT*状态也称为*2MSL*等待状态。
2MSL TIME_WAIT状态存在的理由:
TIME_WAIT状态的存在有两个理由:
- 让4次握手关闭流程更加可靠;4次握手的最后一个ACK是是由主动关闭方发送出去的,若这个ACK丢失,被动关闭方会再次发一个FIN过来。若主动关闭方能够保持一个2MSL的TIME_WAIT状态,则有更大的机会让丢失的ACK 被再次发送出去;
- 防止lost duplicate对后续新建正常链接的传输造成破坏。lost duplicate在实际的网络中非常常见,经常是由于路由器产生故障,路径无法收敛,导致一个packet在路由器A,B,C之间做类似死循环的跳转。IP头部有个TTL,限制了一个包在网络中的最大跳数,因此这个包有两种命运,要么最后TTL变为0,在网络中消失;要么TTL在 变为0之前路由器路径收敛,它凭借剩余的TTL跳数终于到达目的地。但非常可惜的是TCP通过超时重传机制在早些时候发送了一个跟它一模一样的包,并先于它达到了目的地,因此它的命运也就注定被TCP协议栈抛弃。另外一个概念叫做incarnation connection,指跟上次的socket pair一摸一样的新连接,叫做incarnation of previous connection。lost duplicate加上incarnation connection,则会对我们的传输造成致命的错误。大家都知道 TCP是流式的,所有包到达的顺序是不一致的,依靠序列号由TCP协议栈做顺序的拼接;假设一个incarnation connection这时收到的seq=1000, 来了一个lost duplicate为seq=1000, len=1000, 则tcp认为这个lost duplicate合法,并存放入了receive buffer,导致传输出 现错误。通过一个2MSL TIME_WAIT状态,确保所有的lost duplicate都会消失掉,避免对新连接造成错误。
该状态为什么设计在主动关闭这一方:
- 发最后ack的是主动关闭一方;
- 只要有一方保持TIME_WAIT状态,就能起到避免incarnation connection在2MSL内的重新建立,不需要两方都有如何正确对待2MSL TIME_WAIT?
RFC要求socket pair在处于TIME_WAIT时,不能再起一个incarnation connection。但绝大部分TCP实现,强加了更为严格的限制。在2MSL等待期间,socket中使用的本地端口在默认情况下不能再被使用。若A 10.234.5.5:1234 和B 10.55.55.60:6666建立了连接,A主动关闭,那么在A端只要port为1234,无论对方的port和ip是什么,都不允许再起服务。显而易见这是比RFC更为严格的限制,RFC仅仅是要求socket pair不一致,而实现当中只要这个 port处于TIME_WAIT,就不允许起连接。这个限制对主动打开方来说是无所谓的,因为一般用的是临时端口;但对于被动打开方,一般是server,就悲剧了,因为server一般是熟知端口。比如http,一般端口是80,不可能允许这个服务在2MSL内不能起来。解决方案是给服务器的socket设置SO_REUSEADDR选项,这样的话就算熟知端口处于 TIME_WAIT状态,在这个端口上依旧可以将服务启动。当然,虽然有了SO_REUSEADDR选项,但sockt pair这个限制依旧存在。比如上面的例子,A通过SO_REUSEADDR选项依旧在1234端口上起了监听,但这时我们若是从B通过6666端 口去连它,TCP协议会告诉我们连接失败,原因为Address already in use.
5. 总结
本文介绍了TCP的三次握手、四次挥手、11种状态、TCP滑动窗口流量控制、TCP半连接状态以及TCP TIME_WAIT两倍报文最大生存时长。
以上是关于Android C++系列:Linux网络TCP详解的主要内容,如果未能解决你的问题,请参考以下文章
RK3399平台开发系列讲解(网络调试)7.27如何分析TCP重传问题?
RK3399平台开发系列讲解(网络调试)7.28如何分析常见的TCP问题?
RK3399平台开发系列讲解(网络调试)7.31TCP连接的建立和断开过程分析
C++笔记--Linux网络编程(15-0)-socket(供自查,文档说明)