前几天发了一个朋友圈,发现暗恋已久的女生给我点了个赞,于是我当晚辗转反侧、彻夜未眠!想着妹子是不是对我有感觉呢?不然怎么会突然给我点赞呢?要不趁机表个白?
于是第二天我在心中模拟了多次表白的话语,连呼吸都反复练习。
到了晚上,我拨通了妹子的微信语音,还没等对方开口我就按捺不住内心的想法,开始自说自话,一阵狂乱的表达…
足足五分钟一气呵成,一切都是那么自然!可是在我说完之后却半天都没有等到妹子的回应…
过了好一会儿才听到对方的声音:“喂!喂!我这边信号不好,你刚刚在说啥我一句都没听到,我在跟我男朋友逛街呢…”。
我挂断了电话,我也对我这次失败的表白进行了深度的总结!原因就是因为我没有学好 TCP!
如果我懂 TCP,那我在表白之前至少要先问一句“在吗?”!先建立可靠的连接,确保连接正常才能开始表白!
如果我懂 TCP,那我在我说话的过程中需要对方不断的确认,这样才能保证我说的每一句话对方都能听到!这样我才能表白成功!
所以一切都是因为我没有学好 TCP,于是我走进了图书馆…
我们先来看下 TCP 的定义:
TCP 全称为 Transmission Control Protocol(传输控制协议),是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP 是为了在不可靠的互联网络上提供可靠的端到端字节流而专门设计的一个传输协议。
这里面每一个字我们都认识,但是连在一块就不是那么好理解了!那我们就提炼一些关键的词,也就是我上面高亮的那些:面向连接、可靠、基于字节流、传输层、协议、端到端!
理解了这些关键字也就理解了 TCP 的实现原理,那我们就来从这些关键字开始进行分析!
传输层
我们先讲传输层,因为可以从比较高的层面去看 TCP,我们先看下经典的 OSI 七层网络参考模型:
当我们需要在网络上进行数据交换的时候,就需要经过这么几层。每一层都有相关落地的实现,我们今天要讲的 TCP 就是传输层的一种落地实现。
可能我们平时在说到传输层的时候自然而然的就想到的 TCP,但是 TCP 只是传输层的一种实现,其他比较常见的传输层协议还有 UDP 等!
我知道干巴巴的文字对你来说太抽象,那我就抓个包来看看,让这几层更加具象!
本文中所有的包都是通过 postman 发送请求,然后用 wireShark 来抓的!如果对这两款软件还不了解的盆友可以先去了解下哈,这里不过多说明。
我们在 postman 中输入 www.17coding.info 的域名,然后发送请求,wireshark 就能抓到数据包了。
图上已经标明每一层与抓到的数据包对应的关系了!咦!我们上面不是说的 7 层网络参考模型么?为什么数据包只有 5 层呢?
注意参考二字,7 层模型是一个理论模型,实际的网络中往往都把应用层、会话层、表示层统为应用层!
什么是协议?
说到协议,就是双方共同遵守的一种约定!比如我写的这篇文章里,你能够看懂我写的每一个字并明白我的意思,那就是因为我们都遵循了汉语的语法,这本身也就是一种协议。
还有比如我们写代码就必须按照规定的语法进行编写,这样编译器才能进行正确编译。
在计算机网络中也有很多协议,比如常见的应用层协议 http、ftp、dns 协议等等。
常见的传输层协议有 TCP、UDP 等等,其实这些协议都是发送方和接收方都在遵循的一种规范。
如果我们遵循了其规范,也能成为协议的实现者,比如自己写一个 web 服务器处理用户请求。甚至我们还能自己规定一套协议,供别人使用!
TCP 头部格式
我们前面说了协议的定义,那 TCP 协议肯定也有一定的规范咯!这样通信双方才能识别对方的数据报文,进行数据交换。
我们先看下 TCP 的报文格式:
TCP 报文包含数据头和数据体,头部有 5 行的固定长度以及 1 行可变长度!图上前面 5 行就是固定长度!
固定长度的每一行占有 4 个字节(32 位)。因此头部固定长度就为 5*4=20 个字节!
到这里我们可以抓个包来看下加深印象,我们依然向 www.17coding.info 发送一个请求,然后看看其 TCP 部分的数据包:
接下来那我们就一行一行的来分析 TCP 的头部:
第一行:
1、源端口:发送方端口
2、目标端口:接收方端口
前面我们说到 TCP 是端到端的,这里就能很好的体现了!每个数据包中都有发送方和接收方的端口。这里每个端口占用 2 个字节(16 位)。
第二行、第三行:
1、序号:TCP 是面向字节流的,数据分块在缓存存放及发送,序号用来标记某个数据包最开始的字节是整个数据的第多少个字节。
2、确认号:每次收到请求后,接收方都会回复发送方,告诉对方自己已经接收了多少字节,下一个数据包需要从第多少字节开始发送。这里的值一般等于接收到的序号+接收到的数据包数据部分长度。
这里的序号和确认号是保证 TCP 可靠特性所不可或缺的,我们后面会通过抓包来详细分析!序号和确认号分别都占用了 4 个字节(32 位)!
第四行:
1、数据偏移:这里叫头部长度更为合适。前面说过 TCP 头部长度有部分是可变的,所以需要标识数据包数据部分从哪里开始。这个值占用了 4 位。
2、保留:未使用,供扩展使用。这个值占了 3 位。
3、标志:标志一共有 9 个,每个标识占 1 位,共占 9 位。上面的抓包截图就能看到这 9 个标识位!
3.1、NS:Nonce,与 ECN 显式拥塞通知相关。
3.2、CWR:CWR 标志与后面的 ECE 标志都用于 IP 首部的 ECN 字段,ECE 标志为 1 时,则通知对方已将拥塞窗口缩小。
3.3、ECE:ECN-Echo,若设置了该标识,则会通知对方,从对方到这边的网络有阻塞。
3.4、URG:Urgent,用于在发送方加塞。比如在下载文件的时候,下到一半了需要停止下载,就需要发送一个紧急的请求告诉对方停止发送数据。数据包不排队。
3.5、ACK:Acknowledgment,标记为一个确认。
3.6、PSH:Push,与 URG 对应的,用于接收方加塞。
3.7、RST:Reset,表示出现严重差错,可能需要重新创建 TCP 连接。如果我们打开某个网站一直没刷出来,我们F5进行刷新,那之前的数据包就要拒绝。
3.8、SYN:用于同步,建立请求的时候用。在握手时候会带这个标记!
3.9、FIN:通信结束,释放连接的时候用。在挥手时候会带这个标记!
4、窗口:不管是发送方还是接收方,都有对应的发送窗口和接收窗口。在通信之前,通信双方会协商窗口的大小。
发送方按照接收方的接收窗口设置自己的发送窗口,同时发送窗口还受拥塞窗口的限制,这个在拥塞控制部分会提到!
在发送过程中窗口会根据接收方的处理能力调整。这个值对 TCP 的可靠传输及流量控制起了很大的作用!这个值占了 16 位。
第五行:
1、校验和:用于校验数据包是否完整或者被修改。这个值占了 16 位。
2、紧急指针:用来标记本报文段中紧急数据的指针,也就是指明了从数据包数据部分的头部到指定位置的数据为紧急数据,只有在设置了标志位 URG 的时候才起作用。这个值占了 16 位。
第六行:
1、选项:选项里面也有些重要的数据,我们挑几个讲一下。
1.1、MSS:MSS 的全称为 Maximum segment size,双方协商的每一个报文段所能承载的最大数据长度(不包括文段头)。
1.2、WS:WS 的全称为 Window scale,也叫窗口因子!是用来调整窗口大小的。前面我们说到过窗口大小的字段,那这个窗口因子又是做什么用的呢?
早期的网络带宽、硬件配置都比较差,所以窗口大小最大只预留了 16 个 bit,也就是最大能设置的值为 65535。
随着硬件和网络的发展,65535 已经不能满足。所以就增加了一个 WS 的选项来扩展!如果设置了 WS,那实际的窗口大小就等于窗口大小乘以窗口因子。
1.3、SACK:SACK 的全称为 Selective ACK,选择性确认是建立在累计确认(后面讲) 的基础上的!
只有收到失序的分组时才会可能会发送 SACK,如果接收方接收到了后面的数据包,而发现前面的数据包丢失,则会通知发送方哪些报文段丢失,需要重发!
2、填充:这个字段是为了让整个头部为 4 个字节的倍数。Java 中也有很多类似的用法!
我们找到一个数据包,看看其详细的头部数据:
红色部分显示了 TCP 头部的长度为 32byte,以及选项部分为 12byte。前面我们说了 TCP 首部固定长度为 20byte,所以 20+12=32。
黄线部分的窗口大小为 259byte,窗口因子为 256。所以实际的窗口大小为 259*256=66304!
面向连接怎么理解
从我表白失败的例子就能看到,我还未确保连接的正常就开始表白,导致我说完了对方却因为信号不好没有听到。
如果我事先确保连接正常,就不会出现这样的情况了!我们前面说了 TCP 是面向连接的,那 TCP 是怎么面向连接的呢?
三次握手交代了什么?
没错,都是从握手开始!我们都知道,TCP 建立连接需要经过三次握手,那每次握手都交代了什么呢?如果只进行两次握手行不行?
我们先看一个电话接通的场景:
A:你好,你能听到吗? B:我能听到,你能听到吗? A:我也能听到。 …….
在正式通话之前,为了确保通话的可靠,往往都需要经过上面的三次对话进行确认。
那这三次对话是必须的吗?每一次对话的必要性又是什么呢?
A:你好,你能听到吗?(让B知道A能说话) B:我能听到,你能听到吗?(让A知道B能听到,且能说话) A:我也能听到。(让B知道A能听到) …….
只有经过三次的对话,才能确认自己的声音能被对方听到且能听到对方的声音。这也才能开展后续的对话。
这里我们就不得不祭出经典的三次握手图了:
我们分析三次握手过程及每次握手后的状态如下:
A 主机发送标识 SYN=1(SYN 表示 A 请求跟 B 建立连接,前面在讲 TCP 头部时候有说到过),序号 Seq=x,第一次握手请求发送后 A 的状态为 SYN_SENT,B 在接收到请求后状态由 LISTEN 变为 SYN_RCVD!
B 主机收到连接请求后向 A 主机发送标识 SYN=1,ACK=1(SYN 表示 B 请求跟 A 建立连接,ACK 表示对 A 的连接请求进行应答),序号 Seq=y,确认号 Ack=(x+1),A 接收到 B 的确认后,状态变为 ESTABLISHED,B 的状态依然为 SYN_RCVD!
主机 A 收到后检查 ACK 是否正确,若正确,则发送标识 ACK=1(表示对 B 的连接请求进行应答),序号 Seq=(x+1),确认号 Ack=(y+1)。B 接收到 A 的确认后,A 和 B 的状态都变为 ESTABLISHED!
这里我们要注意的几点是:
图中的发送请求中中括号里面的 SYN、ACK 就是前面说 TCP 头部中的那几个标志位!而 Seq 和 ACK 分别代表序号和确认号。
接收方在接收到发送方发送的 Seq 后,应答一个 ACK,ACK 的值等于 Seq+1,表示已要发送方开始发送 Seq+1 位置的数据。
B 在接收到了 A 的连接请求后回复中同时发送了 SYN、ACK 两个标识位,将建立连接的请求和对 A 的应答在同一个包中发送了,这也是为什么只需要三次握手,就能建立连接。
我们依然向 www.17coding.info 发送请求,下面为三次握手的包:
在 info 那一栏,我们很明显的能看到发送的数据包头部有我们上面说到的那些标志位,还有 Seq、ACK 等头部信息,还有 Win、MSS 等头部选项数据!因此三次握手不仅仅是单纯建立连接,还会协商一些参数!
当我鼠标选择某一行时,如果这个数据包包含了对某个数据包的确认(也就是有 ACK 的标记),就能在对应的数据包的 No 列上面看到一个小勾勾,比如上面图中我鼠标选择的是第三次握手的数据包,在第二次握手的数据包前面就有个小勾勾。
为什么握手只需要三次而挥手需要四次?
通过三次握手,双方就建立了一个可靠的连接,就能进行数据的传输了!当数据传输完成,就得将连接关闭,因为连接也是一种资源!连接的关闭需要经过四次挥手!
为什么握手可以三次完成,但是挥手却需要四次呢?我偏要三次行不行?其实也没啥不可以的!
比如下面的对话场景:
A:我说完了,你说完就挂电话吧! B:好嘞,我也说完了,可以挂电话了! A:好嘞,拜拜。 挂断……
这样三次对话就可以实现挥手了,但是在实际的网络中,当我发出一个请求的时候,可能服务器的响应体比较大,需要较长时间的传输!
所以当客户端主动发起断开请求的时候,服务器先回应一个确认,等所有数据传输完毕后再发送服务器断开的请求。
A:我说完了,你说完就挂电话吧! B:好嘞… B:…… B:我也说完了,可以挂电话了 A:好嘞,拜拜 挂断……
所以大部分情况下都需要进行四次挥手!但是,在我个人的抓包实践中,也会有三次挥手就能完成断开连接的情况。
这里我们又不得不祭出经典的四次挥手图了:
我们分析四次挥手过程及每次挥手后的状态如下:
主机 A 发送标识 FIN=1(FIN 表示 A 请求关闭连接)用来关闭 A 到 B 的数据传输。此时 A 的状态为 FIN_WAIT_1!
主机 B 收到关闭请求后向 A 发送 ACK(ACK 表示应答 A 的关闭连接请求),A 不再向 B 发送数据。此时 A 的状态为 FIN_WAIT_2,B 为 CLOSE_WAIT!
主机 B 发送标识 FIN=1 用来关闭 B 到 A 的数据传输。此时 A 的状态为 TIME_WAIT,B 为 LAST_ACK!
主机 A 收到关闭请求后向 B 发送 ACK,此时 B 不再向 A 发送数据。此时 A、B 都关闭了,状态变为 CLOSED。
在图中我们能看到,A 的 TIME_WAIT 状态会持续 2MSL 再变成 CLOSED。
MSL(Maximum Segment Lifetime)的中文可以译为“报文最大生存时间”!他是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。
那 TIME_WAIT 维持 2MSL 的作用是什么呢?
第 4 次挥手的时候主机 A 发送 ACK 到主机 B,如果发送完成后就直接就关闭连接,那如果由于网络原因 B 没有收到 ACK,那 B 就没法关闭连接了!因此 A 在回复确认后,还需要等待,万一 B 没有收到应答还会继续发送 FIN 的请求。
如果不等待 2MSL,那客户端的端口可能会被重用,如果再次用这个端口建立与服务器的连接,那前后两个使用相同四元组的的连接之间会形成干扰!
我们看上面向 www.17coding.info 发送请求的挥手数据包:
可能大家在抓包的时候不能立马看到四次挥手的数据包!那是因为在 HTTP 1.1 及之后,默认都开启了长连接!
也就是在一次请求之后,建立的连接并不会立马关闭,而是供后续的其他请求继续使用,以减少每次重新建立连接的资源消耗!
如果想发出请求后立马能抓到四次挥手的数据包,可以设置 HTTP 的头部 Connection:close。
这样每次发送请求都能看到完整的三次握手四次挥手的过程啦!
TCP 是怎么保证可靠传输的?
保证传输的可靠我们前面已经说到了面向连接,建立连接是保证数据传输的第一步。那在连接建立之后的数据传输怎么保证可靠呢?
我们再次回到我们打电话的场景,一般在对话的过程中,都是得双方都有互动,给与对方回应。而不是一个人一个劲的说而另一方没有任何回应!
比如下面场景:
A:跟你讲哦,我上周网上认识了一个妹子 B:嚯,牛逼啊! A:然后我昨天约出来见面了 B:666啊!然后呢? A:然后我们@#¥%……& B:卧槽,你刚刚说啥我没听清,你再说一遍?...
这样的确认和应答就确保了双方的通信能够完整可靠。TCP 也采用了这种 y 应答和确认重传的机制,保证在不可靠的网络上实现可靠的传输。只要我没有收到确认,我就认为没有发送成功,就会重发。
停止等待协议
停止等待协议就是每次给对方发送数据包后,需要等待对方的回应然后再发送下一个数据包!
停止等待协议会出现如下几种情况:
①无差错情况:A 发送 M1 包到 B,B 收到后会给 A 一个确认,当 A 收到 B 的确认后再发送包 M2。
②超时重传:A 发送 M1 包到 B,如果发送过程中包丢失,A 会重新发送。A 等待重发的时间是比一个报文的往返时间(RTT)稍微多一点。
③确认丢失:如果 B 在给 A 发送确认的时候丢失,A 会重新发送 M1 包给 B,由于 B 已经处理过 M1 的数据包所以 B 会丢弃报文,然后重传确认 M1 给 A。
④确认迟到:如果 A 发送数据包 M1 给 B,B 回复确认的时候延迟了。这时 A 又会重新发送包 M1 给 B,B 收到后丢弃数据包,然后重传确认 M1 给 A。这时 A 会收到多次确认,当第二次收到迟到的确认后 A 也会丢弃该确认。
我们从上面能看到,停止等待协议每次都是等到收到确认后再发下一个数据包。
只要我没收到你给我的确认,我就认为你没有收到我发的数据包,我就会进行重发!这样虽然可靠,但是会导致信道利用率较低!
流水线传输
流水线传输就是每次发送多组数据包,不必每次发完一组就停下来等待对方的确认。由于信道上一直有数据不间断的传输,因此可以获得较高的信道利用率!
流水线传输如何保证可靠的呢?需要发送方维持发送窗口,假如发送窗口是 5,那 5 个数据包会同时发送,然后等确认!
如果有收到接收方的确认,窗口就会滑动,进行第 6 个数据包的发送。如果都是单个确认,可能效率会比较低,所以有了累计确认!
也就是说假如发送方发送了数据包 1、2、3、4,接收方只需要回复对数据包 4 的确认,那表示 1234 数据包都已经收到了,就可以进行第五个数据包的发送了!
假如发送了数据包 1、2、3、4,其中第三个数据包丢失,那该怎么确认呢?
TCP 只会回复对数据包 2 的确认,并且对数据包 4 进行选择性确认(TCP 头部选项讲到过的 SACK),这样发送方就知道数据包 4 已经成功发送,只需要重发数据包 3。
继续前面抓包的例子,接收方并不是对每个数据包都进行确认,而是对多个数据包进行累计确认:
这里我们能看到服务器发送多个数据包后,客户端才进行了一次确认。
流量控制和拥塞控制
通过前面我们知道了,通过建立可靠的连接和确认机制,保证了 TCP 的连接的可靠!
但是每个人使用的计算机的处理能力都是不一样的,我发送太快了对方处理不过来怎么办呢?通信双方怎么去协调发送和接收数据的频率呢?
以字节为单位的滑动窗口技术
在介绍 TCP 头部的时候,我们已经提到过滑动窗口,并且介绍了相关的控制参数 Win!也说到了接收窗口和发送窗口!那他们的关系是怎么样的呢?
假设现在 A 需要传输数据给 B,B 就先要告诉 A 自己的接收窗口有多大。A 根据 B 的接收窗口设置自己的发送窗口!A 的发送窗口时不能大于 B 的接收窗口的!
在开始传输数据之前,初始的窗口设置如下图:
如上图我们能否看到,B 的接收窗口设置为 10 个字节,那 A 的发送窗口设置不能超过 10 个字节!
如果开始传送数据,A 会将数据封装成多个数据包进行传输,如下图:
在没有收到 B 的确认之前,A 的窗口不会滑动,也就是说最多能发 10 个字节的数据。
如果 B 接受到数据且回复确认给了 A,那 A 的窗口则进行滑动,如下图:
这样,A 又可以进行第 11、12 个字节的发送啦!如果 B 的处理能力变弱了,也可以通知 A 将发送窗口调小!
这样也也就很好的协调了双方的接收和发送能力!这也就很好的实现了 TCP 的可靠传输和流量控制!
上面的数据包继续发送,如果在发送过程中,3、4、5 这三个字节组成的数据包丢了,但是后面的数据却收到了,这时候 A 的发送窗口会移动么?
如果是这种情况,A 的发送窗口是不会移动的。B 在接收到后面数据包的时候回复给 A 的 ACK 会设置为 3,且在选项中设置一个 SACK(在 TCP 头部选项里面有描述),告诉 A 哪部分数据收到了,而哪部分数据需要进行重发!
拥塞控制
利用滑动窗口技术,可以很好的协调双方的收发能力。但是,网络状况是非常复杂的,且在同一个网络上可能有千千万万个发送方和接收方!
如果大家都需要传输数据都需要占用网络,不做好控制措施,就会导致整个网络会堵塞甚至瘫痪。
如果我要从深圳开车去广州,我就会走高速。如果只有我一个人开车,那肯定能畅通无阻!但是高速公路不是我家的,大家都能通行!
所以一到了节假日,大家都蜂拥而上,而高速的承运能力不会因为节假日而调整!
这时候往往就需要交通管制、限流等措施去舒缓交通!
绿线代表理想状况下,如果高速公路的吞吐量为 100!当需要通过的车辆不超过 100 时,所有车辆都能顺利通过!当需要通过的车辆超过 100,那每次通行的车辆为 100,能提供的负载比较稳定。
红色代表没有任何交通管制情况下,如果高速公路的吞吐量为 100!当需要通过的车辆不超过 100 时,会出现轻微的塞车现象!但是随着车辆的增多,就会出现严重的阻塞,甚至瘫痪!
蓝色代表在交通管制下,如果高速公路的吞吐量为 100!当需要通过的车辆不超过 100 时,会出现轻微的塞车现象!但是随着车辆的增多,交通一直保存较高的负载,不会出现瘫痪的情况!
网络就好比高速公路,传输的数据包就好比要通过的车辆,而 TCP 则就更像一个交警,维护着数据传输的秩序!那 TCP 是怎么做的呢?
慢开始与拥塞避免
发送方维持一个 cwnd(拥塞窗口,注意这里的拥塞窗口不能大于前面说到的发送窗口!),刚开始拥塞窗口设置为 1。
如果发现这个包没有丢失,则调整拥塞窗口为 2!如果又没有丢包,则调整拥塞窗口为 4!
这样每次以 2 倍的速度一直增长到 16!然后 17、18、19 这样一个一个的增加,直到大小与发送窗口一致。
这就是所谓的慢开始和拥塞避免,16 就是慢开始门限……
有没有得寸进尺的感觉!
我就蹭蹭不进去… … 我就进去不动… … 我就..
如果在发送的过程中发现有丢包现象,则会调整拥塞窗口大小为 1,并且设置新的慢开始门限为出现拥塞时的二分之一,也就是说当拥塞窗口为 24 的时候出现丢包现象,那新的慢开始门限就调整为 12!
如果理解了上面的文字描述,下面的图就不难理解了:
快重传
前面说过累计确认,还说到了选择性确认。这个就跟快重传有关!
接收方如果发现丢包,不会等到累计确认,就通知发送方三个重复的确认通知对方重新发送丢失的包。当接收方收到三个重复的确认,则意识到数据包丢失,进行重传!
通过下图能看到,当出现丢包的情况,接收方的 ACK 都是等于 50,而 SACK 分别对 60~89 之间的字节都进行了选择性的确认!
这时候发送方也就知道 50~59 这部分数据丢失而进行重传!
快恢复
如果一旦发生丢包,拥塞窗口就变成 1,这种方式也太傻了吧。如果能有个快速恢复的机制就好了!TCP 就使用了快恢复机制!
当出现丢包时,不会再次进行慢开始,而是直接转入拥塞避免!也就是从新的慢开始门限进行加法增加!
看完全文,我们再回到 TCP 的定义,你是不是又能有更多的理解了呢?
TCP 全称为 Transmission Control Protocol(传输控制协议),是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP 是为了在不可靠的互联网络上提供可靠的端到端字节流而专门设计的一个传输协议。
作者:sullivan06
编辑:陶家龙
出处:转载自公众号 17coding 技术博客,17coding.info 是一个秉持着勤记录、齐分享的理念的公众号,用于记录平时所学,分享编程心得。