TCP协议
文章目录
- TCP协议
- TCP的特点
- TCP的基本特性
- 确认应答机制
- 超时重传机制
- 丢包
- 连接管理机制
- TCP建立连接---三次握手
- TCP断开连接---四次挥手
- 滑动窗口机制
- 丢包问题
- 流量控制机制
- 拥塞控制机制
- 延迟应答机制
- 捎带应答机制
- 面向字节流问题
- TCP中的异常处理
- 程序崩溃了
- 正常关机
- 突然断电关机
- 掉电的是接收方
- 掉电的是发送方
- 网线断开
- TCP与UDP的比较
在传输层中,UDP和TCP协议都是很重要的,其中的TCP协议是重点,更是难点。
TCP的特点
有连接
可靠传输
面向字节流
全双工(信号能进行双向传输)
首部长度(4bit): 描述TCP报头有多长,表示范围是0 - 15(0000 - 1111)
6个标志位,后面再说
上面报文格式中很多的属性并没有解释,此时不好解释,会在后面进行一一解释
TCP的基本特性
面向字节流 有连接 全双工 都是能在代码中体现的,可靠传输是TCP中最核心的特性,但是TCP是如何保证可靠传输的?
所谓的可靠传输是指当发送方发送之后,发送方知道自己是否发送成功
确认应答机制
收到消息的一方给出应答,给出的应答就叫确认应答, 也叫ACK应答(acknowledge)
对于普通报文,ACK是0
对于应答报文,ACK是1
确认应答机制,是TCP保证可靠性的最核心的机制
发送多条请求的时候,可能会出现后发的请求先到达的情况,主要是因为网络上通信传输的路径是很复杂的,两点之间,报文走不同的路线,所以就可能会出现后发先至的情况
后发先至是网络传输中很难避免的事情
但是我们可以将 请求和应答报文进行编号,这样就不会搞混了
所以要在确认应答机制中引入序号来避免歧义
在TCP报文中,就有这样的设计:
上面的序列号是请求数据的编号
确认号是针对ACK报文有效,也就是只针对对应的请求数据有效
TCP是面向字节流的,所以编号的时候,也是以字节为单位,进行编号的
一开始报文的序号是1,报文的长度是1000,所以最后一个字节的数据编号是1000,在TCP报头中只能存一个序号,最后一个字节的序号 是根据报文长度推算出来的
确认应答报文的确认序号是1001,表示的是: 之前的1000个的数据都已经收到了,现在B向A索要序号从1001开始的数据
之后A又向B发送序号为1001开始,长度为1000的数据,之后B给出确认应答,并索要2001开始的数据
主要是想让 请求与应答 对应上
以上就是确认应答机制中的加入编号,这样就能有效的避免混淆
TCP是32位序号和确认序号,表示的数据范围也就是2^32,相当于4GB,就算是数据量很大,超出了范围,可以重头再来,只要能对应上就行了
超时重传机制
确认应答机制描述的是: 数据报顺利到达到对方,对方给出ACK响应
丢包
但是要是在传输过程中,出现了丢包,也就是请求根本没传过去,此时该怎么办呢?
首先要知道为什么会丢包
网络环境很复杂,之所以能上网,是因为接入了运营商的网络,运营商有很多路由器/交换机,同一时刻会有很多的数据报经过一个交换机,交换机的转发能力是有上限的,达到转发上限,有可能导致一部分数据超时,从而导致丢包
现在假设我要给别人发消息,要是我的报文丢了,或者他给我的响应报文(ACK)丢了,都会触发超时重传
超时重传机制通过设置一个超时时间,来检测数据包是否发送成功。如果在超时时间内没有收到确认信息,则认为数据包发送失败,系统会重新发送这个数据包。这样,就可以有效地避免数据包丢失,提高网络传输效率。
要是是我的报文丢了,只要重发一遍就行了
要是是ACK丢了,对方就会受到 两个一样的报文,此时就要考虑去重了
在TCP协议中,接收方因为丢失ACK导致受到相同的信息,TCP会根据序号来去重
超时重传也能保证可靠传输
超时时间如何确定?
一般系统中会有一个配置项,描述超时的时间阈值
第一次出现丢包,发送方会在达到时间阈值之后,进行重传,要是还是丢包了,还会继续重传,但是这次的时间阈值就会变得更长
所以,时间阈值不是均等的,是一次一次变长的
主要是因为,连续多次丢包的概率比较小,极端一点就是断网了,不管怎么重传都收不到信号,所以不如将重传的频率降低一点,节省点主机的开销
要是连续多次重传都不行,就尝试重置TCP连接(断开重连),要是还是连不上,就直接释放连接(彻底放弃)
注意: 这里并没有说明 第一次重传的时间 重传失败的间隔 重传的次数,主要是因为不同的系统中是不一样的,而且也是可以配置的,所以没有必要去记住
类似的案例:
HashMap解决哈希冲突,通过链表的方式,要是链表太长了,就要把链表转换成红黑树,所以链表元素达到多少才会转换成红黑树?
这也是没必要知道的,因为不同版本的哈希表的实现方式不同,数字也就不同
连接管理机制
ACK 确认报文段
SYN 同步报文段
FIN 结束报文段
注意在TCP中,主要强调的是发送方 和 接收方
连接管理描述的是TCP建立连接和断开连接的过程
TCP的连接是让通信双方建立逻辑上的 虚拟的连接
TCP建立连接—三次握手
建立连接的过程就是三次握手
要是syn为1,说明这是一个同步报文段,尝试与对方进行连接
为什么要进行三次握手?建立连接的意义是什么?
-
三次握手并不会传输任何的业务数据,只是先检查一下网络情况是否通畅
-
三次握手也是在检查通信双方的发送和接收能力是否正常
举一个简单的例子
假设AB两个人准备一起打游戏,在打之前,要先试一下声音
当B收到A的“我来了",B就知道自己的接收能力没问题,A的发送能力没问题
当A收到B的“我也来了”,A就知道自己的接收能力没问题,B的发送能力没问题,进一步知道了,自己之前发的消息B也知道了,所以A就知道自己的发送能力没问题,B的接收能力没问题,此时A已经全都清楚了
当B收到A的“准备开始”时 , B就知道自己的接收能力没问题,A的发送能力没问题,进一步知道了,A的发送能力没问题,自己的接收能力也没问题,此时B也全都清楚了
3.三次握手的过程中,也能协商一些重要的参数
两个重点的TCP状态:
- LISTEN : 服务器启动之后,绑定端口之后(new ServerSocket)完成之后,可以接收连接
- ESTABLISHED: 连接建立好之后的稳定状态(stable --> stablished,前面加个E)
为什么ACK 和 syn能合并在一起发送?
是因为在三次握手中,发送方在收到客户端的syn之后,ACK立即被触发(由内核完成的)
接收方给客户端的syn也是立即触发的(也是由内核触发的)
ACK和syn都是立即触发的,所以两者可以合并在一起
TCP断开连接—四次挥手
断开连接就是双方取消相互认同的关系,通信双方,各自向对方申请断开连接,再各自给对方回应
这里的ACK和FIN不一定能合并
ACK是操作系统内核在收到FIN后,立即触发的
FIN是应用程序显式调用socket的close方法触发的,主要是看代码实现
所以ACK和FIN的时机不一样,所以不一定能合并
但是,TCP中还有捎带应答机制,还是有可能合并在一起的
综上,还是四次挥手比较好
TCP的两个中重要的状态
CLOSE_WAIT: 等待代码中调用close方法
TIME_WAIT: 主动发去关闭的一方在处理完最后一个ACK之后,不能立刻释放连接,而是要保持一段时间,之后再关闭
不能直接关闭的原因是: 要是最后一个ACK没传过去,服务器就会重发一个FIN,此时客户端就会再传一个ACK
应答(ACK)和响应(response)是不一样的,ACK只是告诉对方,已收到消息,ACK是系统内核负责的,会在收到请求之后,立刻返回
响应会携带业务上的数据,由应用程序负责,取决于代码
滑动窗口机制
TCP确实有效地保证了数据的可靠性,但是效率并不高
主要是因为发送一条请求之后,要等到ACK之后,才发下一条请求,这样就会很慢
为了能尽量提高点效率,就有了滑动窗口机制
要想提高效率,就可以每次批量发送一波消息,之后再等一波ACK,之后每收到一条ACK就再发一条数据
把不需要等待,一次能直接发送的数据量称为“窗口大小”
批量发送4条数据,批量等待4条ACK
这里的滑动窗口是每收到一条ACK就再发一条数据,而不是收齐了所有的ACK才再发数据
要是假设窗口是无限大的,那么一次性就把所有的数据都发出去了,这样子就不用等待ACK了 ,此时效率就变得像UDP一样高了,但是不等ACK,就不可靠了,TCP也就失去了它的价值
丢包问题
丢包主要是两种情况:
- 发送的数据包丢了
- 数据到了,但是应答报文(ACK)丢了
对于ACK丢了的情况:
要是ACK丢了,其实根本就没有任何影响
因为,大的序号是包含之前的小的序号的,所以要是前面的ACK丢了,但是只要后面的ACK还在,就说明之前的数据都到达了
举一个例子:拥有了大学毕业证,就意味着拥有了高中毕业证,这里有包含的关系
要是刚好是最后一个ACK丢了,还有超时重传,所以也没事
对于数据包丢了的情况:
一开始1001-2000数据报丢失时,A不知道的,所以A继续发数据包,B也一直向A索要1001-2000,后来A意识到丢包了,所以重传了1001 - 2000,由于A之前已经把7000的数据报都传给B了,所以此时B已经收到了7000之前的所有数据报,所以B 开始索要7001开始的数据报,后面就回归正常了
这样子,只是将丢的数据包重传了,并没有多余的操作,所以效率很高
这就叫做快速重传(搭配滑动窗口机制的超时重传)
滑动窗口的大小是不能无限大的,因为要是发的很快,很多,接收方是跟不上的,就会丢弃一部分数据,这样子TCP就不能保证可靠传输了
流量控制机制
要是发送的速度太快,可靠性就不能保证
要是发送的速度太慢,也不好
流量控制就是基于滑动窗口的基础上,对发送速率做出限制,限制发送方的窗口大小不要太大,也不要太小
至于如何把握好度,主要是看接收方
接收方根据自己的接收能力,来反向影响发送方的发送速率
接收方会根据自己的接收缓冲区剩余空间(其实就是剩下的内存),来决定发送方的速率
接收方收到发送方的数据后,就会在ACK报文中,将当前 缓冲区剩余空间的大小的值反馈给发送方,这里的值就是报文中的窗口大小,只有在应答报文中才有效,每一个ACK传回来的窗口大小都会实时改变
要是窗口的大小 变成0了,此时发送方就不发了 ,要是不发了,接收方就没办法返回ACK,后续接收方有空闲空间了,怎么告诉发送方可以发呢?
要是超过了超时的时间,还是没有窗口更新的通知,发送方就会发送一个窗口探测报文,这样子接收方就能返回ACK,带回来窗口大小
拥塞控制机制
但是不是只要考虑发送方和接收方,在数据传输的时候,中间还会有很多的转发节点,这些转发节点也都会影响发送的速率,所以也要考虑,但是这并不是好量化的
思路: 将中间的设备想象成一个整体,之后在不断试探 来验证 合理的发送速度, 也就是一开始发送的速率比较慢,要是一路通畅,就提速,一旦发生了丢包就降低速度,变得通畅了之后,再提高速度…动态调整
这就是在一步一步试探底线,试探出最合理的发送速度的过程
拥塞控制机制的具体实现:
首先“慢启动”,先规定一个很小的数据,开始的时候先指数增长,达到阈值之后就线性增长,一步一步寻找到最大的窗口大小
以指数增长,是为了快速摸清楚最佳的窗口大小,但是为了防止翻得太快超过上限,达到阈值之后变成线性增长
一开始的阈值是系统规定的,当发生丢包之后,新的阈值就是刚才丢包时窗口的大小的一半,所以阈值都是动态调整的
要是新的阈值比之前窗口的一半还大,指数翻倍之后就会超过阈值
流量控制只考虑发送方和接收方,接收方来反制发送方的发送速度,来确定窗口大小
拥塞控制还要考虑中间的转发节点,来确定窗口大小
要是两者的值不一样,就会选值小的那个,这样更加保险
延迟应答机制
滑动窗口的大小是根据接收缓冲区的空余空间的大小来确定发送的数据大小
假设接收端缓冲区为1M. 一次收到了500K的数据; 如果立刻应答, 返回的窗口就是500K;
如果接收端稍微等一会再应答, 比如等待200ms再应答, 那么这个时候返回的窗口大小就是1M;
窗口越大,网络吞吐量就越大,传输效率就越高
捎带应答机制
捎带应答是基于延迟应答的基础上引入的
TCP中,只要把数据传过去,对方收到之后,就会立刻由内核
返回一个ACK报文,另外,响应的数据时程序设计负责传输的
由于这两个是在不同的时机传输的,所以不能将这两者合并起来,但是有了延时应答机制,会稍微等一下响应数据,这样子就能将ACK和响应数据合并在一起了,提高了传输效率
但是捎带应答并不是总能做到,也是由一定的偶发性,要是两个动作相隔的时间比较长,此时就不能合并了
面向字节流问题
面向字节流中存在一个很经典的问题: 粘包问题
举一个例子: 东北有一种粘豆包,只要蒸完之后,就会变成一整块,分不清单独的格式,全都粘到一起了
在TCP中,A给B发消息,B给A会消息,A的应用程序需要从接收缓冲区中读取收到的数据,由于是面向字节流的,所以A无法确定哪里到哪里是一个完整的应用层数据报
再举一个例子: A给B发消息"你想不想和我一起去吃东西?",B的回答是“想得美”,由于是面向字节流的,所以A可能将“想”当做完整响应,也有可能将“想得”当做完整响应,这些都是错误的
对于UDP,面向数据报就不会存在这个问题,它是对应着一个应用层报文
想要解决粘包问题,可以在定义应用层协议的时候,明确包与包之间的边界就行了
具体的解决粘包问题的方法有两种:
- 通过分隔符,比如约定使用分号来作为包结束的标识
- 通过指定包的长度,比如在数据报的开头位置声明长度
TCP中的异常处理
程序崩溃了
程序崩溃,操作系统会回收进程的资源,包括释放文件描述符表,这样子就相当于是调用了socket中的close,也就会触发FIN,进一步开始四次挥手,这样子其实与正常的四次挥手没什么区别,所以没事
正常关机
关机的时候,系统会强制结束所有的用户进程,和上面的进程崩溃类似,系统内核会释放文件描述符表,进一步开始四次挥手,所以也是没事
突然断电关机
由于是突然断开,所以系统根本来不及做出反应
此时就要分为两种情况
掉电的是接收方
接收方突然掉电,此时发送方还不知道,所以还会继续发送数据
但是此时发送到数据,没有ACK传回来了,发送方会触发超时重传,重传几次之后,还是无应答,就会尝试重置连接(复位报文段),也会失败,此时就只能放弃连接
掉电的是发送方
掉电的是发送方,接收方等了一段时间之后,就会发送一个“心跳包”,心跳包是周期性触发的,只是一个不携带任何业务数据的包,存在的意义就是确认一下对方是否还在。
要是对方不返回心跳包,就说明心跳遗失了,对方挂了,此时就会放弃连接了
网线断开
网线断开,也是像上面的突然断电,所以要是那两种情况
TCP与UDP的比较
首先一定要明确它们的特性
TCP是有连接 可靠传输 面向字节流 全双工
UDP是无连接 不可靠传输 面向数据报 全双工
使用场景
TCP适用于有可靠性的要求的场景
UDP适用于对可靠性要求不高,但是对传输效率要求很高的场景