1.上期问题的答案
如果客户端connect操作时,服务端对应的端口号不接受连接,在这种情况下不会设置SYN的值,而是会把RST比特设为1
2.本期主题
上一期讲解了在TCP下协议栈的socket操作和connect操作,那么本期我们会讲解TCP协议栈的write操作,read操作和close操作。
3.网络包的大小
3.1 包太小怎么办
3.1.1 MTU和MSS
在控制流程从connect回到应用程序后,应用程序会调用write操作把要发送的数据交给协议栈,并且应用程序还会把要发送数据的长度也一并告诉协议栈。协议栈在收到数据后并不会马上发送出去,而是会把数据放在内部的发送缓冲区中,并等待应用程序的下一段数据。因为如果应用程序发送的是逐行的数据,甚至发送一个个字节,那么如果协议栈一接收到数据就发送出去,那么会发送大量的小包,这会降低网络的效率。因此协议栈要判断是否可以发送数据。
第一个判断依据就是MTU,即一个网络包的最大长度,它包含了头部的总长度。如果要得到网络包中所能容纳的最大数据长度,即MSS,也就是要减去头部的总长度。那么协议栈在发送数据时,如果数据长度一直很接近MSS的值,那么就可以避免发送小包的问题了。
3.1.2 时间
还有一个依据是时间。如果应用程序发送的数据本来就很小,那么如果一直没有达到接近MSS的长度,协议栈就会一直陷入等待,这会造成发送延迟。因此,协议栈内部有一个计时器,在一定的时间里,就算网络包的数据没有达到MSS的大小,它还是会把数据发送出去。
这两个条件是相互矛盾的,因此协议栈的开发者会自己来定一个值来保持平衡,不同种类和版本的操作系统也就会有些差异。
3.2 包太大怎么办
如果HTTP请求的消息太大,长度已经超过了一个网络包可以容纳的最大长度,那么发送缓冲区的数据会被以MSS的长度分成若干个网络包,然后根据套接字中记录的控制信息所对应的IP地址和端口号,然后交给IP模块来发送数据。
4.确认网络包收到
在接收方收到发送方发送的数据后,需要进行确认操作,来告诉发送方已经成功接收到数据。在发送方发送数据时,发送方会把发送数据的长度,和所对应的序号一起告诉接收方。就比如发送方和接收方说:我现在要发送从xxx开始的数据,一共有xxx字节。如果接收方上次接收到第1000字节,如果发送方发送了从1001开始的包,那么说明没有遗漏,那么发送方会把序号加上数据的总长度再加上1,这个值就储存在ACK号中。就相当于对发送方说:xxx前的数据我已经收到了。
然后在收发数据阶段,序号的起始值不是1,而是一个随机的数。这样做的目的是为了防止被有心之人抓到机会发动进攻。而随机出来的序号会在执行连接操作的时候一并发送给通信对象。
上面讲到的操作是单向操作的时候要做的事,那如果是双向操作呢?其实很简单,在连接阶段客户端会先发起连接并把序号初始值告诉服务端,服务端在接收到数据后会返回ACK号,并把自己的序号初始值告诉客户端。客户端在收到服务端发来的数据后也会生成ACK号并返回给服务端。之后收发数据,客户端和服务端各发各的,在收到数据后也会根据对方的序号和长度发送对应的ACK号。
5.TCP通信的补救措施
在收到接收方发回的ACK号之前,发送方发送的数据会保存在发送缓存区中。这是为了防止如果接收方没有收到对应的数据,发送方可以重新发送这种包。这样一来,无论什么错误,如果数据丢失,发送方会重新发送包。如果数据出现错误,接收方会丢弃这些包,并等待发送方重新发送包。但是如果发送方在发送几次数据后仍然无效,那么发送方会强制结束通信,并向应用程序报错。
5.1 ACK号的等待时间
那么发送方等待ACK号的等待时间要设置成多久呢?如果ACK号的等待时间太短,在接收方的ACK号到达之前再一次发送相同的包,那势必会造成网络的拥堵。如果ACK号的等待时间太长,那么如果真的出现了错误,网络包的重传就要经过很久。
因此ACK号的等待时间会被动态调整,如果ACK号的返回很快会缩短等待时间,反之,ACK号的返回时间很慢会延长等待时间。虽然说缩短等待时间,但是这个值是有最小值的,一般在0.5秒到1秒之间。
5.2 滑动窗口
如果每次发送一次数据,就等待对应的ACK号返回,这样的效率其实是很低的。因此协议栈在发送数据的时候会使用滑动窗口来管理发送数据和ACK号。在发送一个网络包后,不会等待ACK号返回,而是继续发送之后的包。
但是这样又引出了别的问题,如果数据一股脑全部发送给接收方,接收方来不及处理,那就只能丢弃新发送过来的包了,这种浪费肯定也是不允许的。
接收方收到包后,会把数据存到接收缓存区中。然后接收方要计算ACK号,然后把数据还原再交给应用程序。因此如果发送方一直发送数据的话,接收方的缓存区是有可能溢出的。因此在连接阶段,接收方其实也会把自己接受缓存区的大小告诉发送方,发送方在每次发送完数据后都会计算接收方还有多少缓存区,如果要溢出了,那么就会更改发送包的速率。如果缓存区越来越多,说明接收方处理数据的速度很快,那么发送方也会提升发送包的速率。
接收方也会一直处理数据,所以在返回ACK值的时候,接收方也会把现在缓存区的大小告诉发送方,好让发送方调整发送数据的策略。这就是TCP调优参数中非常有名的一个。
5.3 ACK与滑动窗口
那么ACK和滑动窗口不可能一直保持同步出现,如果先计算完ACK准备发送,或者先处理好数据并把准备把剩余的缓存区的大小告诉发送方。那么一方一定会等待另一方,比如在发送缓存区还有3000字节的消息的时候,其实最新的消息是缓存区还有5000字节,那不是会影响滑动窗口吗?
TCP做了这样一个优化,如果在计算滑动窗口的时候,ACK号更新了两次,如果ACK号是连续的,那么就可以直接发送那个最新的ACK号。滑动窗口也是一样的,发送最新的缓存区的大小给发送方就可以了。
5.4 接收响应消息
浏览器在发送请求消息之后,会调用read程序来获取响应的消息。协议栈会从接受数据的缓冲区中取出数据,如果里面有数据的话就会直接给浏览器。如果没有数据,协议栈会把这个read操作挂起等到之后再执行。
6.断开连接
在发送方发送完数据后,应用程序会调用close程序,我们假设现在是服务端发送完数据调用了close程序。服务端的协议栈会生成控制位中FIN比特为1的TCP头部,服务端的套接字会记录下断开操作的相关消息。客户端接受到服务端的断开操作消息后,会将自己的套接字标记为断开操作状态,然后客户端会返回ACK号给服务端,之后协议栈就可以等待应用程序来取数据。
应用程序调用read来读取数据,如果有已经被应用程序接受部分的数据,这些数据会被传递给应用程序。否则会告知应用程序数据已经全部结束了。在收到了服务端的所有数据后,客户端也会调用close并发送一个FIN比特为1的包,收到服务端的ACK号后,服务端和客户端的通信就结束了。
7.删除套接字
在和服务端发送FIN比特为1的包后,客户端不会立刻删除套接字。这是因为如果这个FIN比特为1的包如果没有发送到服务端,那么客户端还要进行重发。如果这个时候已经没有套接字了,而这个时候又创建了一个新的套接字,端口号正好就是刚刚删除的那个端口号,那么重发的包就会发送给这个刚刚创建的那个套接字。因此套接字一般会等待几分钟再删除,如果重传了几次依然没有响应,就会停止重传。
8.下期预告
本章就不出思考题了,因为协议栈的内部结构的部分比较多,我分成了上下两部分,因此我在下一期会带大家把创建套接字,连接,收发数据,断开连接这部分内容,从头到尾的串起来讲一遍。