目录
1.TCP协议的特性
TCP是传输层协议.
关于TCP的特性:
- 有连接
- 可靠传输
- 面向字节流
- 全双工
这里的特性我们可以对照着传输层的另外一个重要的协议 UDP协议 来理解.(关于UDP协议的具体内容可以参考这篇博客---UDP协议)
有连接: 发送端需要先与接收端建立连接之后才能进行数据的传输. 类比打电话的场景,打电话的人需要等对方接通电话之后才会进行通话.
可靠传输: 发送方能够知道对方是不是收到了数据. 这里注意:可靠 != 安全.所谓的可靠传输,不是说发送的数据100%就能被对方收到! 而是接收方是否收到数据发送方这边是心知肚明的.
面向字节流: 想发送100个字节数据,可以一次发1个字节,也可以一次发10个字节重复10次. 面向字节流是可以非常灵活的完成发送数据与接收的. 相比之下 UDP协议中面向数据报的形式就没有那么灵活.
全双工: 双向通信,A和B可以同时向对方发送接收数据. 这里UDP协议与TCP协议都是全双工.
2.TCP协议的报文格式
- 源/目的端口号:表示数据是从哪个进程来,到哪个进程去.
- 32位序号: 一次TCP通信(从TCP连接建立到断开)过程中某一个传输方向上的字节流的每个字节的编号.
- 32位确认号 : 用作对另一方发送的tcp报文段的响应. 其值是收到的TCP报文段的序号值加1.
- 4位TCP报头长度: 表示该TCP头部有多少个32位bit(有多少个4字节),单位不是字节,而是4个字节.例如,4位首部长度值为1111=>15,实际的首部长度就是15*4=>60字节.
- 保留位: 先占个位置,虽然暂时不用,但保不齐以后可能会用.
- 6位标志位: URG:紧急指针是否有效
ACK:确认号是否有效
PSH:提示接收端应用程序立刻从TCP缓冲区把数据读走
RST:对方要求重新建立连接;我们把携带RST标识的称为复位报文段
SYN:请求建立连接;我们把携带SYN标识的称为同步报文段
FIN:通知对方,本端要关闭了,我们称携带FIN标识的为结束报文段- 16位窗口大小: 下文中会详细介绍.
- 16位校验和: 发送端填充,CRC校验.接收端校验不通过,则认为数据有问题.此处的检验和不光包含TCP首部,也包含TCP数据部分.
- 16位紧急指针: 标识哪部分数据是紧急数据.
- 选项: 此部分占 0-40 字节. TCP协议报头(首部)最长是60个字节,除选项这40字节之外的20个字节是一定要有的.
3.TCP原理
TCP对数据传输提供的管控机制,主要体现在两个方面 : 可靠性和效率。
以下要介绍的这些机制和多线程的设计原则类似:保证数据传输可靠的前提下,尽可能的提高传输效率。
<1>确认应答
确认应答是保证可靠性的核心机制.
发送方发数据给接收方了,接收方就会回应一个应答报文.如果发送方收到了这个应答报文,那么就认为是对方已经收到了数据.
由于网络上数据的传输,顺序是不能确定的,因此不能就单纯通过收到数据的顺序来确定逻辑,所以就需要对数据进行编号. 所以TCP将每个字节的数据都进行了编号,即序列号.
如图, 第一个请求,A给B发送了1000个字节的数据,序号就是1~1000(假设从1开始编号(实际上TCP的序号的起始不一定是从1开始的)). 确认应答数据报里面的确认序号就是1001,意思就是,1001之前的数据B都已经收到了, 另外B同时也在向A索要1001开始的数据.
发送方可以根据确认应答报文来确定接收方是否收到了数据,只要发送方收到了应答,就认为接收方已经收到,可靠传输就完成了~
<2>超时重传
上面确认应答机制中举的是比较顺利的情况, 但是传输数据过程中还是可能会出现丢包的!
此时就存在两种可能:
1.主机A发送数据给B之后,可能因为网络拥堵等原因,数据无法到达主机B.
2.主机B接收到了A发送的数据,但是应答ACK丢失了.
此时发送发就无法区分当前是发的数据丢了还是应答数据丢了.发送方能做的就是,在一段时间之后,重新发一条数据.
而这里的一段时间具体是多长时间呢?
Linux(BSD Unix和Windows也是如此,会以500ms为一个单位进行控制. 方法方把数据发送出去之后,等待500ms,如果没有收到应答就认为是丢包了. 而这里的等待时间是会动态变化的(如果重发一次之后,仍然得不到应答,等待 2*500ms 后再进行重传. 如果仍然得不到应答, 等待4*500ms 进行重传.依次类推,以指数形式递增),等待时间会逐渐延长,延长就意味着让重试的频率尽量降低,累计到一定的重传次数,TCP会认为网络或者对端主机出现异常,强制关闭连接.
<3>连接管理
TCP是有连接的. 这里连接管理说的就是,如何建立连接(三次握手)和如何断开连接(四次挥手).
三次握手:
如图,A向B请求连接(发送同步报文段SYN),B给与回应(ACK),B也向A请求连接(SYN),A也会给与回应(ACK).
这里的SYN ACK对应TCP报头中的6位标志位.
本来应该是"四次握手",但是中间的两次操作是可以合在一起的,这两个操作在时间上是同时发生的,没必要分成两次传输,直接一次到位.
那为什么要三次握手,为什么要建立连接呢?
1.投石问路.
通过三次握手的过程,来确认A和B之间的传输时通畅的.尤其是要确认,A和B各自的发送能力以及接收能力是否正常.
2.协商参数.
通过三次握手,让A和B之间通通气,选择一些传输中合适的参数.例如TCP的序号从几开始......
那么四次握手行不行,两次握手行不行?
1.四次握手完全可以,但是没必要. 因为如果中间的ACK和SYN分成两次发送的话,会牵扯到两次封装和分用,传输的开销就要比一个更大.
2.两次握手不可以!
四次挥手:
A向B发送断开连接请求FIN(结束报文段),B会先给A回复一个ACK,然后再向A发送断开连接请求,A收到B发来的FIN之后就会回复给B一个ACK.
四次挥手中,客户端和服务器都可以主动断开连接.
上面B向A发送的ACK和FIN为什么不合并?
对于B来说,ACK 和 FIN的触发实际上是不一样的.
B只要收到FIN就会立即触发ACK,这个事是内核完成的. 但是B发送FIN 实际上是用户代码来控制的(代码中出现 socket.close() 这样的操作时才会出发FIN).
甚至说,如果B的代码出现问题了就有可能一直不调用close().
四次挥手一定是四次吗?是否可能是三次呢?
答案是有可能的! 虽然ACK 和 FIN是不同时机,但是再延时应答和捎带应答(下文会介绍)的情况下是有可能合并在一起的.
四次挥手一定会执行吗?
不一定! 四次挥手是一个TCP正常断开的流程,但是实际上,有的时候TCP的连接也会异常断开(网线断了).
<4>滑动窗口
TCP不仅是为了保证可靠性,还要尽可能的提高传输效率! TCP努力的在可靠性的前提之下,又做出了一些性能优化的手段,滑动窗口就是其中一种.
上面我们讨论了确认应答策略,对每一个发送的数据段,都要给一个ACK确认应答.收到ACK后再发送下一个数据段.这样做有一个比较大的缺点,就是性能较差.尤其是数据往返的时间较长的时候.
既然这样一发一收的方式性能较低,那么我们一次发送多条数据,就可以大大的提高性能(其实是将多个段的等待时间重叠在一起了).
- 窗口大小指的是无需等待确认应答而可以继续发送数据的最大值.上图的窗口大小就是4000个字节(四个段).
- 发送前四个段的时候,不需要等待任何ACK,直接发送.
- 收到第一个ACK后,滑动窗口向后移动,继续发送第五个段的数据,依次类推.
- 操作系统内核为了维护这个滑动窗口,需要开辟 发送缓冲区 来记录当前还有哪些数据没有应答,只有确认应答过的数据才能从缓冲区删掉.
- 窗口越大,则网络的吞吐率就越高.
什么是滑动呢?
当前的窗口范围是1001~5000.也就意味着,发送方现在同时发送了 1001~2000 2001~3000 3001~4000 4001~5000四组数据, 然后同时再等待着四组数据的ACK.
假设,2001这个ACK先到, 发送方就知道了1001~2000这个数据已经被对方接收到了,发送方也就不用再继续等这个数据了,接下来就会立即再发一个5001~6000 ,任然保证窗口的大小是四份数据.
那如果出现了后发先至的情况, 比如3001的ACK比2001的ACK先到, 那么此时确认序号表示从该序号之前,前面的数据都已经收到了,收到3001就代表 1001~2000和2001~3000都被对方收到了,此时2001这个ACK 收不收到就已经不关键了.
如果在滑动窗口的场景中出现丢包了怎么办?
情况一:数据报已经抵达,ACK丢了.
这种情况没关系,只要不是全部都丢了就好. 因为可以通过后面到达的ACK进行确认(确认序号的设定,后一个能包含前一个). 比如:2001这个ACK丢了,但3001这个ACK到了,此时发送方也就知道3001前面的数据都被正确收到了,此时1001~2000这个数据报也得到了一个确认应答.
情况二:数据报直接丢了.
当某一段报文段丢失之后,发送端会一直收到 1001 这样的ACK,就像是在提醒发送端 "我想要的是 1001" 一样; 如果发送端主机连续三次收到了同样一个 "1001" 这样的应答,就会将对应的数据 1001~2000 重新发送;这个时候接收端收到了 1001 之后,再次返回的ACK就是7001了(因为2001 ~ 7000)接收端其实之前就已经收到了,被放到了接收端操作系统内核的接收缓冲区中.
这种机制被称为 "高速重发控制"(也叫 "快重传").
<5>流量控制
对滑动窗口的进一步补充,本质上就是控制滑动窗口的大小. 基于接收方的处理能力(缓冲区的剩余空间)来限制窗口大小的.
接收端处理数据的速度是有限的, 如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送, 就会造成丢包, 继而引起丢包重传等等一系列连锁反应. 因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制(Flow Control)
- 接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 "窗口大小" 字段, 通过ACK端通知发送端.
- 窗口大小字段越大, 说明网络的吞吐量越高.
- 接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端
- 发送端接受到这个窗口之后, 就会减慢自己的发送速度.
- 如果接收端缓冲区满了, 就会将窗口置为0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端
接收端如何把窗口大小告诉发送端呢?回忆我们的TCP首部中,有一个16位窗口字段, 就是存放了窗口大小信息;
那么问题来了, 16位数字最大表示65535, 那么TCP窗口最大就是65535字节么?
实际上, TCP首部40字节选项中还包含了一个窗口扩大因子M, 实际窗口大小是 窗口字段的值左移 M位;
<6>拥塞控制
站在另外一个角度来限制发送方的窗口大小.
虽然TCP有了滑动窗口这个大杀器, 能够高效可靠的发送大量的数据. 但是如果在刚开始阶段就发送大量的数据, 仍然可能引发问题. 因为网络上有很多的计算机, 可能当前的网络状态就已经比较拥堵. 在不清楚当前网络状态下, 贸然发送大量的数据, 是很有可能引起雪上加霜的.
TCP引入 慢启动 机制, 先发少量的数据, 探探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据.
此处引入一个概念程为拥塞窗口. 发送开始的时候, 定义拥塞窗口大小为1; 每次收到一个ACK应答, 拥塞窗口加1; 每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际发送的窗口.
像上面这样的拥塞窗口增长速度, 是指数级别的. "慢启动" 只是指初使时慢, 但是增长速度非常快.为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍.此处引入一个叫做慢启动的阈值,
当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长.
当TCP开始启动的时候, 慢启动阈值等于窗口最大值; 在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回1.
少量的丢包, 我们仅仅是触发超时重传.大量的丢包, 我们就认为网络拥塞.
拥塞控制, 归根结底是TCP协议想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案.
<7>延时应答
效率机制 提高效率==> 琢磨窗口大小
如果接收数据的主机立刻返回ACK应答, 这时候返回的窗口可能比较小.
假设接收端缓冲区为1M. 一次收到了500K的数据; 如果立刻应答, 返回的窗口就是500K; 但实际上可能处理端处理的速度很快, 10ms之内就把500K数据从缓冲区消费掉了; 在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来; 如果接收端稍微等一会再应答, 比如等待200ms再应答, 那么这个时候返回的窗口大小就是1M.
窗口越大, 网络吞吐量就越大, 传输效率就越高. 我们的目标是在保证网络不拥塞的情况下尽量提高传输效率.
那么所有的包都可以延迟应答吗?
肯定不是.
- 数量限制: 每隔N个包就应答一次.
- 时间限制: 超过最大延迟时间就应答一次.
具体的数量和超时时间,依操作系统不同也有差异; 一般N取2, 超时时间取200ms.
<8>捎带应答
也是效率机制 提高效率(在延时应答的基础上).
如上图,客户端发送请求给服务器,服务器会立即给客户端回复一个ACK,然后才返回响应. 但是有了延时应答,返回的ACK并不是立即就返回了,而是会等一会,正好在等的这一会,服务器要返回业务上的响应了,此时就可以把ACK 和 响应 合二为一打包成一个包传输过去.
针对四次握手来说,确实是有可能变成三次的. 捎带应答是可能把中间的ACK和FIN 合并成一个包发送.那四次挥手就变成了三次挥手了.
但不是肯定的. 捎带应答本身就是一个"概率性的机制". 当前ACK延时的时间正好是要比接下来发业务数据的时间要长一些.例如: 服务器收到请求到返回响应,这个过程消耗时间50ms; 但是延时应答假设最多等20ms; 此时就无法触发捎带应答了. 但要是延时应答假设是最多等60ms, 第50ms的时候,此时要返回响应给客户端,ACK就可以和这个响应打包一起传输过去了.
4.粘包问题
在面向字节流传输的情况下,需要注意一个重要的问题: 粘包问题.
- 首先要明确, 粘包问题中的 "包" , 是指的应用层的数据包.
- 在TCP的协议头中, 没有如同UDP一样的 "报文长度" 这样的字段, 但是有一个序号这样的字段.
- 站在传输层的角度, TCP是一个一个报文过来的.按照序号排好序放在缓冲区中.
- 站在应用层的角度, 看到的只是一串连续的字节数据.
- 那么应用程序看到了这么一连串的字节数据, 就不知道从哪个部分开始到哪个部分, 是一个完整的应用层数据包.
那么如何避免粘包问题呢?
归根结底就是一句话,明确两个包之间的边界.
通过设计一个合理的应用层协议来解决.
- 给应用层数据设定"结束符"/"分隔符".
- 给应用层数据设定"长度".
关于这两种解决粘包问题的方案,在HTTP协议中都有所体现.
5.TCP中的一些异常情况
- 进程终止:进程终止会释放文件描述符,仍然可以发送FIN. 和正常关闭没有什么区别.
- 机器重启:和进程终止的情况相同.
- 机器掉电/网线断开: 1.掉电的是接收方. 此时另外一边还在发送数据,但是接收方不会再返回 ACK, 于是就会超时重传,重传几次之后就会尝试重置连接(复位报文段). 然后发送发就会放弃连接并回收对应的资源.
2.掉电的是发送方. 接收方接收不到任何数据,那么接收方是如何知道发送发挂了呢?还是接收方只是暂时还没发送呢? 此时接收方采取的策略就是"心跳包"机制(也叫做"保活"). 每隔一段时间接收方向对方发送一个PING包,期待对方返回一个PONG包,如果PING包发过去,过了很久还没有收到PONG,并且重试几次也不行,此时就认为对方已经挂了.
心跳包是一个应用非常广泛的机制,不仅仅是在TCP协议中.
6.基于TCP应用层协议
- HTTP
- HTTPS
- SSH
- Telnet
- FTP
- SMTP
- 当然也包括自己写TCP程序时自定义的应用层协议
7.TCP小结
为什么TCP这么复杂? 因为要保证可靠性, 同时又尽可能的提高性能.
可靠性:
- 校验和
- 序列号
- 确认应答机制
- 超时重传机制
- 连接管理机制
- 流量控制
- 拥塞控制
提高性能,效率:
- 滑动窗口
- 延时应答
- 捎带应答