前几天,我厂剑英和晓培同学在定位一个TCP通信失败问题时,发现原因是客户端发送的TCP数据过长(1460字节),导致数据包无法成功发送到服务端。但通过抓包发现,在三次握手时,双方协商的MSS就是1460。那么,应该是在这个连接的传输过程中,数据包的传输路径发生了变化,走了不同的中间设备,从而导致协商时的MSS大小已经超过了实际的传输路径限制。因为客户端已经发布,我们通过修改服务端的对外接口的MTU,暂时解决了这样的问题。
对于MSS选项,我以前曾经做个简单的研究,这次借着本厂遇到的这个问题,对MSS进行一个比较详细的技术总结。
MSS,全称为Max Segment Size。根据RFC的定义,MSS是一个TCP选项,并且只出现在TCP三次握手的SYN包中(包括SYN+ACK),用于通知对端本地最大可以接收的TCP报文数据大小(不包含TCP和IP报文首部) —— 注意这里是本地可以接收的大小。同一个TCP连接,两个方向上的MSS大小可以不同,并且发送方的TCP报文的最大数据长度不能超过对端声明的MSS大小。
明确了MSS的含义之后,就要问MSS的大小由什么决定?再次感谢开源的Linux内核,可以帮助我们解开这个秘密。
在函数tcp_syn_options中,
对TCP选项mss进行了赋值。接下来进入tcp_advertise_mss。
上图箭头所指的变量metric,个人认为起名不太明确,其值为dst_metric_advmss,实际上是得到的事dst的advmss大小。
其中dst_metric_raw取得对应dst的advmss属性 —— 通过ip route配置路由时,可以指定advmss选项。如果没有配置,则调用default_advmss。对于IPv4来说,就是ipv4_default_advmss。
也就是说,对于IPv4的dst advmss,其值一般为该接口的MTU减去TCP和IP的固定首部大小。
不要忘了前文中的tp->advmss,其值在函数tcp_connect_init中,由tcp_mss_clamp决定,即tp->advmss = tcp_mss_clamp(tp, dst_metric_advmss(dst))。
而user_mss由用户通过套接字选项TCP_MAXSEG配置。
综上所述,可以总结出影响TCP的MSS的因素:
通常我们都不会设置路由的advmss选项,因此,TCP的MSS的值一般就为出口路由的MTU减去40。
TCP握手阶段的MSS,在内核代码中被称为advmss,即通告MSS。而在TCP的传输过程中,就像开头提到的那样,中间路由设备发生了变化,从而导致协商时的MSS大小不再适用于当前传输路径。那么这时就要引出PMTU了,留待后文分解。
专注Linux网络领域开发,坚持每周一更。