目录
- 一、重新理解封装和解包
- 二、TCP协议段格式
- 三、确认应答(ACK)机制
- 四、超时重传机制
- 五、连接管理机制
- 六、理解TIME_WAIT状态和CLOSE_WAIT状态
- 七、流量控制
- 八、滑动窗口
- 九、拥塞控制
- 十、延迟应答
- 十一、面向字节流
- 十二、粘包问题
一、重新理解封装和解包
在网络协议栈中(数据链路层、网络层、传输层),操作系统可能会收到很多报文,有的报文还没有处理,可能还没到传输层,在网络层或者链路层。操作系统要对这些报文进行管理,先描述,再组织。
定义描述报头属性的结构体对象,里面的指针指向内存空间的区域代表报头和数据存储的位置。封装:指向报头的指针往前移动;解包:指向报头的指针往后移动。所以封装和解包本质是指针在内存空间的移动。
二、TCP协议段格式
TCP 全称为 “传输控制协议(Transmission Control Protocol”),下面是TCP 协议段格式
报头和有效载荷如何分离?
提取固定的20字节长度和首部长度,4位首部长度是60(【0,15】,基本单位是4字节),报头是20。如果首部长度就是20,报头读完就是数据;如果首部长度大于20,减去20,再把多出来的读取出来,剩下的就是数据正文。
6位标志位:
- URG:紧急指针是否有效
- ACK:确认号是否有效
- PSH:提示接收端应用程序立刻从TCP缓冲区把数据读走
- RST:对方要求重新建立连接;我们把携带RST标识的称为复位报文段
- SYN:请求建立连接;我们把携带SYN标识的称为同步报文段
- FIN:通知对方,本端要关闭了,我们称携带FIN标识的为结束报文段
三、确认应答(ACK)机制
TCP如何保证可靠性?
客户端给服务端发送数据,服务端应答;服务端发送给客户端,客户端应答。如果一方没有收到应答,说明可能这个过程中有丢包,否则既能发送数据收到应答,又能接收数据应答另一方,就能保证传输过程的可靠性。发送数据和发送应答,一般是双方的操作系统自动完成的。
客户端可以给服务端发送多次数据,服务端多次应答回去
有一个问题:如何确定发送和应答是对应的?序号和确认序号。根据序号,进行tcp的按序到达;确认序号是收到序号的+1,就能保证发送和应答是对应的关系。
注意:发送的数据是有带报头的
为什么要有序号和确认序号两个,一个序号不能解决问题?
因为在传输的过程中,有可能发送的消息既是数据又是应答,这种情况叫捎带应答。如果只有一个序号,就很难确定哪个是数据哪个是应答,因此才要有序号和确认序号共同解决。
序号的意义:按序到达,应答和确认对应
确认序号&&序号:向对方发送信号的同时,也在作应答
为什么tcp报头要有标志位?
在数据传输的时候有建立连接的报文、正常通信的报文、断开连接的报文,总之就是通信过程中会收到各种各样的报文,而TCP报文是需要类型的,所以要区分报文的类型,标志位存在的意义就是区分不同的TCP报文类型。
初步认识三次握手,四次挥手:
三次握手:
客户端发起连接,标志位SYN置为1,服务端调用监听方法listen,收到信息,也发起连接并确认应答,标志位ACK也置为1,然后客户端向服务端发送应答(可能有数据),服务端接收。
注意:每次发送都是一个完整的报头
四次挥手:
客户端取消连接,标志位FIN置为1,服务端接收,发送应答。同样,服务端也要取消连接,客户端接收,发送应答。
如果只是应答,就是ACK;如果是稍等应答,即发送应答的同时又发送数据,那么就是ACK+数据。
四、超时重传机制
如果客户端发送数据,客户端没有收到服务端的应答,有两种可能:一是丢包了,服务端没有收到数据;二是服务端收到了数据,但是还没有收到应答。在收到应答前,客户端是需要等的,等也是有时间范围,如果超过了一个时间段还没收到应答,就判定超时了,需要重传,这个过程也叫超时重传。
超时的时间如何确定?
- 最理想的情况下,找到一个最小的时间,保证"确认应答一定能在这个时间内返回
- 但是这个时间的长短,随着网络环境的不同,是有差异的
- 如果超时时间设的太长,会影响整体的重传效率
- 如果超时时间设的太短,有可能会频繁发送重复的包
TCP为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大超时时间
- Linux中,超时以500ms为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍
- 如果重发一次之后,仍然得不到应答,等待2*500ms后再进行重传
- 如果仍然得不到应答,等待4*500ms进行重传。依次类推,以指数形式递增
- 累计到一定的重传次数,TCP认为网络或者对端主机出现异常,强制关闭连接
五、连接管理机制
在正常情况下,TCP要经过三次握手建立连接,四次挥手断开连接。
三次握手中,connect是发起连接的,accept是接收数据,并返回新的文件描述符,不参与三次握手。三次握手不能百分之百建立连接,而是经历了三次握手,让客户端和服务端认为已经建立好了连接。
因为最后客户端发送应答,服务端不一定能够接收。为什么?怎么知道服务端可能没有接收?因为在前面两次的握手中,客户端发送信息,建立连接,服务端应答,客户端收到服务端的应答,才确定之前发送的信息服务端是收到。而最后一次是单向的,服务端没有应答,所以客户端发送的过程中有存在丢包的可能。既然存在这个风险,那如何解决?其实不用担心,因为建立成功概率比较高,一般前两次连接好了,最后一次大概率也是成功的。
假设最后一次的ACK不成功,即客户端发送ACK后服务端没有收到,但是客户端会认为服务端收到了(其实没有),那么客户端自己就确定已经建立好连接了,而服务端还没有建立好连接。服务端会进行等待,然后超时重传。但是客户端可能很着急,直接就给服务端发送数据了,可是服务端连接还没建立好,怎么办?告知客户端要重置,通过RST标志位,把它置为1,连接重置就是重新进行三次握手。总结:RST是用来处理建立连接出现异常的问题。
为什么建立连接是三次握手?
- 需要保证通信是健康的。让客户端和服务端双方,都会有确定的一次收发,确认全双工
- 确保双方操作系统是健康且愿意通信的
为什么要进行四次挥手?
理由同三次握手,因为它们的原理类似。这里要说明的是发送数据的情况。
客户端close取消连接时,不会发送用户数据了,服务端会应答;然后服务端也close,客户端应答。有一个问题,不是说客户端不能发数据了吗?为什么还能发送ACK(哪方close了,就不能发送数据)?因为ACK发送的只是报头,不包含数据,用来确实应答的。
既然客户端close后不能发送数据了,服务端应答,如果服务端没有很着急close,那么在服务端Close之前这段时间,服务端可以向客户端发送数据。发送完数据,服务端再close,然后客户端应答。又有一个问题了,客户端都已经取消连接,就算收到数据怎么交给上层呢?解决方法:shutdown系统调用
如果服务端应答与close是比较靠近发生的,那么可以既发生ACK又发送FIN,这叫捎带应答。也可以看作是三次挥手。
六、理解TIME_WAIT状态和CLOSE_WAIT状态
客户端断开连接,服务端应答,但是服务端还没close,此时服务端处于CLOSE_WAIT状态。客户端断开连接之后,在它确定成功于服务端断开连接之前,处于TIME_WAIT状态。
如果服务器有大量的CLOSE_WAIT状态时,大概率是因为服务器写的有bug,主要是没有close。
TIME_WAIT 状态引起的 bind 失败,因为一方主动断开连接,而另一方没有应答或者也断开连接,就会产生大量的TIME_WAIT状态,解决方法:setsockopt
七、流量控制
发送端给接收端发送数据,接收端处理数据的速度是有限的,如果发送端发的太快,接收端的接收缓冲区来不及处理,就会造成丢包;如果发的太慢,导致发送的效率低。因此 TCP 支持根据接收端的处理能力,来决定发送端的发送速度,这个机制就叫做流量控制(Flow Control)。
- 接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 “窗口大小” 字段,通过 ACK 端通知发送端
- 窗口大小字段越大,说明网络的吞吐量越高
- 接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置成一个更小的值通知给发送端
- 发送端接受到这个窗口之后,就会减慢自己的发送速度
- 如果接收端缓冲区满了,就会将窗口置为 0;这时发送方不再发送数据,但是需要定期发送一个窗口探测数据段,使接收端把窗口大小告诉发送端
八、滑动窗口
主机A给主机B发送数据,可以一条一条发,但是发送效率低
可以多条发送,提高了效率
发送多条消息,暂时不需要应答,可以直接发送数据,在主机A一个“窗口大小”中,所谓窗口大小的窗口,就是滑动窗口。滑动窗口指的是无需等待确认应答而可以继续发送数据的最大值;滑动窗口的大小是对方缓冲区中剩余空间的大小(注意:对方缓冲区的剩余空间大小是会变化的)
滑动窗口在发送缓冲区中如下图,分为3个部分(其实是4个)
滑动窗口支持超时重传。对发送的报文,并且没有收到应答的报文进行保存,方便我们进行重传。保存在哪?滑动窗口。
滑动窗口只能向右移动,可以变大变小,可以为0;发送端收到ACK报文,应答报头中的窗口大小,表明对方的接受能力,这时候要变更滑动窗口。
有三个问题:最左侧丢包、中间报文丢失、最右侧丢包
最左侧丢包:
接受端发送确认应答,是数据的确认序号,确认序号的意义:确认序号之前的报文全部都收到了。如果确认应答是2001,说明1到1000和1001到2000都收到;如果2001到5000中有一段没收到,比如2001到3000,那么确认序号还是2001发送给发送端,此时滑动窗口是不能右移的。
如果有三次重复的确认应答,那么发送端就会进行重发,这叫做快重传。假如中间还有其他的数据段丢包了怎么办?没有关系,因为发送的过程的连续的,快重传补好了1001到2000,后面的也会开始补。
有了快重传为什么还有超时重传?快重传的效率会高些,但是不能保证所有的数据都能发送成功,而超时重传可以对快重传进行补漏,相当于兜底。
中间报文丢失、最右侧丢包最终都会转换为最左侧丢包。 以中间丢包为例,滑动窗口右移,然后原来的中间丢包,变成了最左侧丢包。最右侧丢包也是如此。
数据包已经抵达,ACK被丢了怎么办?这种情况下,部分ACK丢了并不要紧,因为可以通过后续的ACK进行确认。
前面学的流量控制中,让发送端调节发送的速度,如何调节:具体的细节就是滑动窗口。
九、拥塞控制
虽然TCP有滑动窗口,能够高效可靠的发送大量数据,但是如果在刚开始的阶段就发送大量数据,可能会引发网络拥塞。
一旦判定为网络拥塞,是不能立即重传的。解决方法:拥塞控制。
TCP引入慢启动机制,先发少量的数据,探探路摸清当前的网络拥堵状态,再决定按照多大的速度传输数据。
拥塞窗口:每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口大小做比较,取较小的值作为实际发送的窗口(滑动窗口=min(拥塞窗口,接收方窗口-接受能力))
拥塞窗口增长速度是指数级别的:
少量的丢包, 我们仅仅是触发超时重传; 大量的丢包, 我们就认为网络拥塞
十、延迟应答
发送端发生一段报文,接收端可以先不用那么着急就应答,可以等一会,再应答,这叫做延迟应答。
为什么?在等待的过程中,接收端的接收缓冲区里面的数据更多交给上层处理,即接收缓冲区空间更大了,这时再应答既可以确认前面的收到的信息,又能通知发送端可以发送更大的信息,提高了发送效率。
窗口越大, 网络吞吐量就越大, 传输效率就越高。在保证网络
不拥塞的情况下尽量提高传输效率
所有的包都可以延迟应答吗?不能。
- 数量限制: 每隔 N 个包就应答一次
- 时间限制: 超过最大延迟时间就应答一次
具体的数量和超时时间, 依操作系统不同也有差异。
十一、面向字节流
创建一个 TCP 的 socket,同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区
- 调用 write 时,数据会先写入发送缓冲区中
- 如果发送的字节数太长,会被拆分成多个 TCP 的数据包发出
- 如果发送的字节数太短,就会先在缓冲区里等待,等到缓冲区长度差不多了,或者其他合适的时机发送出去
- 接收数据的时候,数据也是从网卡驱动程序到达内核的接收缓冲区
- 然后应用程序可以调用 read 从接收缓冲区拿数据
由于缓冲区的存在, TCP 程序的读和写不需要一一匹配
写 100 个字节数据时, 可以调用一次 write 写 100 个字节, 也可以调用 100 次write, 每次写一个字节
读 100 个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100 个字节, 也可以一次 read 一个字节, 重复 100 次
十二、粘包问题
什么是粘包?
- 粘包问题中的 “包” ,是指的应用层的数据包
- 在 TCP 的协议头中,没有如同 UDP 一样的 “报文长度” 这样的字段,但是有一个序号这样的字段
- 站在传输层的角度,TCP 是一个一个报文过来的,按照序号排好序放在缓冲区中
- 站在应用层的角度,看到的只是一串连续的字节数据
- 应用程序看到了这么一连串的字节数据,就不知道从哪个部分开始到哪个部分是一个完整的应用层数据包
如何解决粘包问题?明确两个包之间的边界
- 对于定长的包,保证每次都按固定大小读取即可
- 对于变长的包,可以在包头的位置,约定一个包总长度的字段,从而就知道了包的结束位置;还可以在包和包之间使用明确的分隔符
对于 UDP 协议来说,是否也存在 “粘包问题” 呢?
- UDP如果还没有上层交付数据,它的的报文长度仍然在,同时,UDP 是一个一个把数据交付给应用层,所以有很明确的数据边界
- 站在应用层的站在应用层的角度,使用 UDP 的时候,要么收到完整的 UDP 报文,要么不收,不会出现"半个"的情况