协议是存在于传输层,详细见我的文章传输层
传输层的协议中,最常听见的是UDP协议和TCP协议,其中TCP协议比较复杂,而TCP协议的复杂主要是为了保证可靠性和效率的问题,这篇文章主要是讲解TCP协议的一些内容
要说TCP协议,那么就要想到TCP的报头,TCP的可靠性实现、TCP的提高效率的方法以及著名的“粘包问题”
1 TCP协议报头
- 16位源/目的端口号:表示数据是从哪个进程来到哪个进程去
32位序号:表示发送数据的位置,每多发送一次数据,就多累加一次该数据字节数的大小
注:序列号不会从0或1开始,而是在建立连接时由计算机生成的一个随机数作为其初始值,通过SYN包发送给接收端主机。然后再将每转发过去的字节数累加到初始值上表示数据的位置。
32位确认序号:表示下一次应该收到的数据的序列号。发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。
32位序号和32位确认序号双向确认了数据的可靠性
- 4位部首长度(TCP报头长度):表示该TCP头部有多少个4字节,所以TCP头部最大长度位15*4 = 60
- 6位标志位
标志位字段 | 含义 |
---|---|
URG | 紧急指针是否有效;置为1表示要优先处理 |
ACK | 确认号是否有效;设为1表示为确认应答报文(通常情况下会设为1,但是第一次的TCP不会设为1) |
PSH | 提示接收端应用程序立刻从TCP缓冲区把数据读走(若接收端缓冲区数据长时间未处理,那么发送端将强制将数据交付) |
RST | 复位报文段,对方要求重新建立连接;置为1表示要复位 |
SYN | 同步报文段,请求建立连接;置为1表示连接请求报文,其他类型置为0 |
FIN | 结束报文,通知对方,本端要关闭了(是根据五元组(源IP,目的IP,源端口号,目的端口号,协议号)进行关闭) |
- 16位窗口大小:接收缓冲区剩余空间大小
- 16位校验和:发送端填充,CRC校验,接收校验不通过,则认为数据有问题。和UDP的区别是,UDP校验的是数据本身,TCP校验的不仅包含TCP首部,而且包含TCP数据部分
- 16位紧急指针:标识哪部分数据是紧急数据,只有在URG为1时有效
- 40字节头部选项:用于提高TCP的传输性能。需要根据首部长度进行控制,其最大长度为40字节。
在谈TCP协议的可靠性和效率之前必须要知道数据传输的过程——三次握手,四次挥手
2 TCP协议的可靠性
为保证TCP的可靠性,所以产生了一系列的可靠机制
(1)面向连接
(2)确认应答(ACK)——对应报头的确认序号
TCP将每个字节的数据都进行了编号,即为序列号,那么每一个ACK都到带有对应的确认序号,就是告诉发送者,我已经收到了哪些数据,下次需要从哪里开始发送数据
根据上面这张图片,
第一个往返:A向B发送数据1——100,假设全部接收到,那么B会给A返回一个ACK告诉下一个要传101了
第二个往返:A收到B之前发送的确认应答,这一次A向B发送101——200,但是在传输途中中间某个数162丢失了(也就是说101—161和163—200)都收到了,但是此时B给A的确认应答是下一个是162,就是说下一个要从162开始发送
(3)序号——按序到达
TCP报头中有一项32位序号字段,这样保持是保证数据发送有序,不会出现乱序的情况
(4)超时重传——少量丢包
根据上面这张图,A向B发送1—100的数据,但是在传输途中因为网络堵塞或者什么原因发生了丢包的情况,那么数据全部都没有传输到B,所以B也不能给A一个确认应答,所以在经过一段特定的时间间隔后A依旧没有收到B发来的ACK,那么A将重新将数据进行发送
但是A没有收到B的ACK还有一种情况,那就是B收到数据,也向A发送了ACK,但是ACK在传输途中发生了丢包的现象,见下图
A未收到B发送的ACK,在A等待了特定时间间隔后依旧没有收到ACK,那么A就认为B没有收到数据,因此A就按照超时重传的方式来处理,如果发生了大量的这样的问题,那么B就会收到大量的重复数据,
(5)去重
针对上面出现的问题,TCP需要能够识别出那些重复的包,并把重复的包去掉,这样就必须对包进行去重,可以根据TCP报头中的序号来进行去重
上面说的超时重传和去重两种保证可靠性的方法中,有一个很重要的决定因素,就是”特定时间间隔“,这个时间究竟该如何确定??
- 在最理想状态下,找到一个最小的时间,保证“确认应答一定能够在这个时间内返回”
- 但是这个时间是根据网络环境的不同而不同
- 如果超时时间设的太长,会影响整体的传输效率;但是若超时时间设的太短,又会出现频繁发送重复的包的现象
在TCP中为了保证高性能的通信,所以会动态地计算这个最大超时时间
- Linux,Windows,Unix中,超时以500ms为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍
- 如果重发一次后仍得不到应答,等待2*500ms后再继续重传
- 如果仍得不到应答,等待4*500ms进行重传,依次类推进行指数形式递增
- 累计到一定的重传次数后,TCP认为网络或对端主机出现异常,强制关闭连接
(6)流量控制
由于TCP的接收端有发送缓冲区和接收缓冲区(此时要联想到UDP没有发送缓冲区,有接收缓冲区),但是由于接收缓冲区的容量是有限额,若发送端发送的太快,那么就会导致就收缓冲区会很快装满,这时候发送端继续发送数据,就会造成丢包的现象,那么接下来又会发生重传,丢包等一系列现象,因此TCP具有了根据接收端的处理能力来决定发送端的发送速度,这个机制叫流量控制
- 接收端将自己可以接收的缓冲区大小放入TCP首部中的“窗口大小”字段,通过ACK端通知发送端
- 窗口大小字段越大,说明网络的吞吐量越高
- 接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置为一个更小的值通知给发送端
- 发送端接收到这个窗口之后,就会减慢自己的发送速度
- 如果接收端缓冲区满了,就会将窗口置为0;此时发送方将不再发送数据,但是需要定期发送一个窗口来探测数据段
接收端如何把窗口大小告诉发送端??
在TCP的报头中包含一个16位窗口字段,这里存放着窗口大小信息,但是16位数字最大表示65535,但是TCP的窗口大小不止这么大,所以在TCP的首部40字节选项中包含一个窗口扩大因子M,实际的窗口大小是窗口字段的值左移M位
(7)拥塞控制——大量丢包
通过上面的一些发送处理,数据若是在传输途中发生丢包的现象或者接收缓冲区满了的情况都有了很好的解决,并不会出现大量数据丢失的情况,但是如果在传输途中没有发生丢包或者接收端的问题,而是网络太“卡”了,导致大量数据“卡”在半路上过不来,这种现象称为拥塞,拥塞与双方主机无关,只是因为网络的关系
这时如果发送端并不清楚发生了什么状况而当作丢包等问题处理,就会进行重传处理,这时网络本来就因为大量数据拥塞了,但是又不断涌入大量数据,那么网络就会更加艰难
为了解决这个问题,TCP引入了慢启动的机制,慢启动是先发少量数据来“探探路”,根据网络的状况来判断下一步要按照多大的速度发送数据
根据上面这张图,需要引入“拥塞窗口”,在刚开始的时候拥塞窗口大小为1,然后程指数增长(1,2,4,8…),但是拥塞窗口的值却不能一直增长,所以又引入“阙值”,当拥塞窗口超过这个阙值就变为线性增长,当增长到窗口最大值的时候,拥塞窗口变为初始大小1,进行重发,再次进行指数增长,只不过此时的阙值变为上一次窗口最大值的一半,达到阙值再进行线性增长,如此往复。。。
- 当TCP开始通信后,网络吞吐量会逐渐上升,但是随着网络发生堵塞,网络吞吐量逐渐下降
- 拥塞控制,是TCP想要尽快将数据发送而给网络带来的压力而产生的折中方法
3 TCP协议的效率
(1)滑动窗口
像之前说过的发送数据的方式是发一个,接收一个ACK,然后再发送下一个,这样能保证数据的传输可靠性,但是这样传输的效率实在是有点低,现在引出一种批量传输的方式——滑动窗口
- 从上图看,A先批量发送四个数据包,然后等待B的应答,当A收到第一个ACK应答的时候就继续发送第五个数据包,以此类推
- 滑动窗口是存在于发送端的发送缓冲区中
- 而之前说过滑动窗口的大小是无需等待确认应答而可以继续发送数据最大值,而该窗口大小是由接收缓冲区剩余空间的大小和网络状况共同决定的
- 操作系统为了维护这个滑动窗口,需要开辟发送缓冲区来记录当前哪些数据还没有应答,只有确认过应答的才可以从缓冲区中删除
- 窗口越大,网络吞吐量越大
(2)快重传——批量发送数据包的数据包丢包
接着上面说到的滑动窗口批量发送数据包的发送方式,在数据传输的过程中可能发生两种丢包重传的情况
第一种:
A发送到B,B接收到数据,B给A发送一个ACK,但是,此时,ACK丢失,发生这种情况没关系,可以通过后序的ACK来进行确认,并不需要发生重传
第二种:——快重传
A发送到B,在传输途中,数据包丢失,B没有接收到数据,此时需要用到快重传(高速重发控制)的方式——在发送一整段数据,但是其中在一段报文段丢失后,接收端给的ACK将是相同的内容,若连续收到3段相同的ACK内容,那么将对这段数据进行重新发送,但是之后的数据已经收到了,所以此时再给出的ACK将是这一段全部接收完成的ACK信息,之前接收的数据被放在了接收缓冲区中
快重传的效率更高,那为什么还存在超时重传??
快重传只是针对于批量发送的情况好用,但是若只发送了一个数据,这样返回的ACK根本达不到3条,所以超时重传也要存在,超时重传是底线
(3)延迟应答
在之前的一些数据传输的过程中,都是发送端向接收端发送数据,接收端接收到数据之后会立即返回一个确认应答ACK,此时返回的窗口可能较小
假设接收端缓冲区为1M. 一次收到了500K的数据; 如果立刻应答, 返回的窗口就是500K,但实际上可能处理端处理的速度很快, 10ms之内就把500K数据从缓冲区消费掉了,在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来,如果接收端稍微等一会再应答, 比如等待200ms再应答, 那么这个时候返回的窗口大小就是1M
所以窗口越大, 网络吞吐量就越大, 传输效率就越高. 我们的⽬目标是在保证网络不拥塞的情况下尽量提高传输效率
是所有的包都可以延迟应答吗??
不是的,要想延迟应答是有时间和数量限制的(且根据操作系统的不同,时间和数量是不同的)
(1)数量:每隔N个包就需要应答一次,一般N为2
(2)时间:每经过最大的延迟时间就要应答一次,一般最大延迟时间为200ms
(4)捎带应达
在之前说的一些数据交互的过程都是A发送消息,然后B给出相应的ACK应答,但是在很多时候客户端在进行数据交换的时候是“有收有发”的,就好像是两个人进行说话,A说“你好呀!!”,B说“你也好呀!!”,B所说的话是对A说的,但是之所以这么说是因为收到了A发出的消息,这样相当于B在给A发出自己想要发送的消息后,顺带地将自己的ACK一起返回给A
4 粘包问题
粘包问题是TCP中很容易出现的问题,说到粘包问题就先要说TCP传输方式——面向字节流
创建一个TCP的socket,同时在内核中创建一个发送缓冲区和一个接收缓冲区
- 调用write时, 数据会先写入发送缓冲区中;
- 如果发送的字节数太长, 会被拆分成多个TCP的数据包发出;如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去;
- 接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区;然后应用程序可以调用read从接收缓冲区拿数据;
- TCP的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一个连接, 既可以读数据,也可以写数据. 这个概念叫做全双工(UDP的socket也是全双工)
由于缓冲区的存在,TCP的读写不需要一一匹配,即当读(写)100个字节时可以读(写)一次,也可以读(写)100次
而因为TCP是面向字节流的,所以在TCP传输途中,站在传输层的角度上TCP是一个个报文传输过来的,按照序号排放在缓冲区中,但是数据的传输是将TCP的报文传送到应用层,在应用层上进行数据的处理,那么站在应用层的角度来看,看到的是一串连续的字节数据,应用层并不知道该从什么地方开始到哪个结束是一个完整的数据包,所以就会产生读取数据时由于数据多读或者少读导致的应用层解析错误——这就是粘包问题
那么如何解决粘包问题呢??
其实要做的就是将数据保证数据读取正确——即明确包之间的边界
- 字描述——确定每个到底是多长
- 特殊字符——使用空行等特殊字符来分隔(HTTP)
TCP会出现粘包问题,那么UDP也会出现粘包问题吗??
不会
- 站在传输层的角度来说,UDP的报文长度存在,且UDP是一个一个把数据交付给应用层,有很明确的数据边界
- 站在应用层的角度来说,UDP是面向数据报,即UDP一定是一个完整的报文,要么不收,要么收到完整报文