一、TCP协议段格式
- 源/目的端口号:表示数据从哪个进程来到那个进程去
- 4位TCP报头长度:表示TCP头部有多少个32位bit(多少个4字节)4位最大是15,15x4=60字节,也就是说TCP头部最大长度是60字节。
- 6个标志位
URG:紧急指针是否有效 ACK:确认号是否有效 PSH:提示接收端应用程序立刻从TCP缓冲区把数据读走 RST:对方要求重新建立连接,我们报携带RST表示的成为复位报文段 SYN:请求建立连接,我们报携带SYN的称为同步报文段 FIN:通知对方,本段要关闭了,我们成携带的FIN表示为结束报文段
二、TCP原理(可靠)
确认应答机制
TCP是面向字节流的,他将每个字节的数据都进行了编号,即序列号 每一个ACK都带有对应的序列号,意思是告诉发送者,我已经收到了哪些数据,下一次你从哪里开始发。
超时重传机制
当主机A发送数据给主机B之后,因为网络拥塞或某种原因导致数据无法到达B 如果主机A在一个特定的时间内没有收到B发来的ACK,就会重发
也有第二种可能,是B收到了A发送的数据,但是B发出的确认应答丢失了 如果是第二种情况就会导致B会收到很多重复的包,此时我们可以利用序列号进行去重操作
超时时间的确定
- 计算机会动态计算这个时间
- Linux中,超时以500ms为一个单位
- 如果重发一次还得不到应答就会等待2 * 500ms,第三次就会等待4 * 500ms
- 累计一定重传次数之后,TCP认为网络或者主机出现了问题,强制关闭连接
连接管理(三次握手/四次挥手)
三次握手 本质上就是A向B请求连接,B给予回应,同时也向A发送请求,A给与回应
为什么要三次握手?
- 保证连接双方都有数据收发的能力
- 协商参数,例如TCP的序号从几号开始
四次挥手 为什么这里的ACK和FIN不合并呢
- B只要收到FIN就会立刻回复ACK,这是由内核完成的
- B发送FIN实际上是用户代码操纵的socket.close()
CLOSE_WAIT:服务器收到FIN之后进入的状态,等待用户调用CLOSE发送FIN关闭连接
TIME_WAIT:客户端收到FIN之后进入的状态(再保持连接2MSL),它存在的意义就是保证最后一个ACK不丢包
- MSL是报文最大生存时间
- 这样就能保证两个传输方向上还没被接收到的报文都能到达(防止服务器立即重启接收到上个进程迟到的数据)
- 如果最后一个ACK丢了,虽然客户端进程没了,但是TCP连接还在,仍然可以重发ACK
流量控制
因为滑动窗口接收端处理数据的速度是有限的,如果缓冲区满了,那么之后的数据段就会丢包,所以我们要控制发送端的发送速度。这个机制就叫流量控制
- 接收端把自己可以接收的缓冲区大小放入TCP头部的16位窗口大小字段,通过ACK通知发送端。
- 窗口越大,说明吞吐量越大
- 当缓冲区快慢了的时候,就把窗口大小设置为一个更小的数值
- 当缓冲区满了的时候,窗口大小设置为0,此时发送端停止发送数据段,但是会定时发送一个窗口探测数据段,让接收端把窗口大小告诉发送端。
综上所述,可以发现流量控制和生物当中的负反馈调节很相似。
一个小知识点: TCP头部的窗口大小是16位的字段,那么他的大小就是65535字节(64kb)吗?
其实在TCP头部还有一个选项是窗口扩大因子M,实际的窗口大小是16位字段再左移M位(左移就是*乘2 )
拥塞控制
虽然我们可以使用滑动窗口进行传输效率的提升,但是一开始就传输大量的数据可能会引发一些问题,TCP的慢启动机制先发送少量数据尝试网络是否通畅,再决定按照多大的速度传输。
- 此处引入一个概念程为拥塞窗口,发送开始的时候,定义拥塞窗口大小为1;
- 拥塞窗口初始值小,但是增长是指数级别的,等增长到某一个阈值之后就是线性增长了
- 当TCP启动的时候拥塞窗口的阈值是窗口最大值(64kb),每次超时重发之后阈值减半,并且拥塞窗口大小置为1。
- 每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口大小(流量控制)做比较,取较小的值作为实际发送的窗口大小;
TCP通信开始之后,网络吞吐量逐渐上升,网络拥堵之后,吞吐量逐渐下降。
拥塞控制其实就是TCP想尽快把数据报传送过去,但又要避免给网络造成太大压力的折中方案(有点像高中的楞次定律 增缩减扩 嘛)
三、TCP原理(效率)
滑动窗口
由于确认应答机制对于每一个发送的数据段都需要一个ACK来应答,收到ACK之后才发送下一个数据段,这样有个缺点就是性能较差,所以我们引出了滑动窗口这个概念。
我们一次发送多个数据段,让他们等待ACK的时间重叠
- 窗口大小指的是无需等待确认应答而可以继续发送数据的最大值。上图的窗口大小就是4000
个字节(四个段)。 - 发送前四个段的时候,不需要等待任何ACK,直接发送;
- 收到第一个ACK后,滑动窗口向后移动,继续发送第五个段的数据;依次类推;
- 操作系统内核为了维护这个滑动窗口,需要开辟 发送缓冲区 来记录当前还有哪些数据没有
应答;只有确认应答过的数据,才能从缓冲区删掉; - 窗口越大,则网络的吞吐率就越高;
如果丢包了怎么重传呢? 分两种情况: 一:数据报到达了,但是ACK丢了 ACK丢失没有多大关系,例如这里2001,3001,4001的ACK都丢了,但是当返回5001的时候,发送端就知道了5001之前的数据都收到了。
二:数据报丢失了
- 当某一段报文段丢失之后,发送端会一直收到 1001 这样的ACK,就像是在提醒发送端 “我想
要的是 1001” 一样; - 如果发送端主机连续三次收到了同样一个 “1001” 这样的应答,就会将对应的数据 1001 -
2000 重新发送; - 这个时候接收端收到了 1001 之后,再次返回的ACK就是7001了(因为2001 - 7000)接收端
其实之前就已经收到了,被放到了接收端操作系统内核的接收缓冲区中;
延迟应答
原理:如果接收端收到数据之后立刻返回ACK这时返回的窗口大小可能比较小
举个例子:假设缓冲区大小为1MB,接收到了512k的数据,如果直接ACK则返回的窗口大小是512k,但是加入这512k数据100ms就被缓冲区消费了,那么我直接ACK返回窗口大小为1MB就好了。
我们的目的就是再保证可靠性的基础上增大传输效率,而窗口大小越大传输效率越高!
延迟应答的规则:每N个包应答一次,N一般为2;每M毫秒应答一次,M一般为200;
捎带应答
在延迟应答的基础上我们发现很多情况下我们的服务器和客户端在应用层也是一发一收的,类似于我们聊天,所以服务器返回的ACK就可以搭着服务器的数据段“ 顺风车 ” 一起被发送。
四、TCP粘包问题
应用层从接受缓冲区读取数据的时候,并不知道从哪到哪是一个完整的应用层数据包,他看到的是接收缓冲区当中一个一个的字节。
解决粘包问题的关键就是要明确包与包之间的边界
方式一: 在包与包之间设定明确的分隔符(应用层协议自定义),例如使用分号隔开。 方式二: 在包头的位置,约定一个包的数据长度,这样就可以得到包的结束位置了。
|