首页 > 代码库 > TCP校验和计算原理与实现

TCP校验和计算原理与实现

1. 概述
TCP首部校验和计算三部分:TCP首部+TCP数据+TCP伪首部。

TCP校验和覆盖TCP首部和TCP数据,而IP首部中的校验和只覆盖IP的首部,不覆盖IP数据报中的任何数据。

伪首部是为了增加TCP校验和的检错能力:如检查TCP报文是否收错了(目的IP地址)、传输层协议是否选对了(传输层协议号)等。伪首部来自IP首部。

RFC 793的TCP校验和定义
The checksum field is the 16 bit one‘s complement of the one‘s complement sum of all 16-bit words in the header and text.
If a segment contains an odd number of header and text octets to be checksummed, the last octet is padded on the right
with zeros to form a 16-bit word for checksum purposes. The pad is not transmitted as part of the segment. While computing
the checksum, the checksum field itself is replaced with zeros.
 
上述的定义说得很明确:
首先,把伪首部、TCP报头、TCP数据分为16位的字,如果总长度为奇数个字节,则在最后增添一个位都为0的字节。
把TCP报头中的校验和字段置为0。
其次,用反码相加法累加所有的16位字(进位也要累加)。
最后,将上述结果作为TCP的校验和。

验证示例:
校验和  反码求和过程
以4bit 为例
发送端计算:
数据:   1000  0100   校验和  0000
则反码:0111  1011               1111
叠加:   0111+1011+1111 = 0010 0001   高于4bit的, 叠加到低4位      0001 + 0010 = 0011 即为校验和
 
接收端计算:
数据:  1000   0100   检验和  0011
反码:  0111   1011                1100
叠加:  0111 + 1011 +1100 = 0001 1110  叠加为4bit为1111.   全为1,则正确

2.  校验和反码求和  的实现
发送方:原码相加 ,并将高位叠加到低位,取反 ,得到反码求和结果,放入校验和
接收方:将所有原码 相加,高位叠加, 如全为1,则正确  
代码如下:
    USHORT checksum (USHORT *buffer,int size)
    {
        Unsigned long cksum=0;
        While (size>1)
        {
            Cksum +=*buffer++;
            size -=sizeof(USHORT);
        }
        If (size)
        {
            Cksum +=*(UCHAR *) buffer;
        }
        //将32位转换为16位
        While (cksum>>16)
            Cksum = (cksum>>16) + (cksum & 0xffff);
        return (USHORT) (~cksum);
    }
    buffer是指向需要校验数据缓冲区的指针,size是需要检验数据的总长度(字节为单位)。
    4-8行代码是对数据按16bit累加求和,由于最高位的进位需要加在最低位上,所以cksum必须是32位的unsigned long型,高16bit用于保存累加过程中的进位;
    9-11行是对size为奇数情况的处理。
    14~15行代码的作用是将cksum高16bit的值加到低16bit上,即把累加中最高位的进位加到最低位上。这里使用了 while循环,判断cksum高16bit是否非零,因为第16行代码执行的时候,还是可能向cksum的高16bit进位。
    有些地方是通过下面两条代码实现的:
    Cksum = (cksum >> 16) + (cksum & 0xffff);
    Cksum = (cksum >> 16);
    这里只进行了两次相加,即可保证相加后cksum的高16位为0,两种方式的效果是一样,事实上,上面的循环也最多执行两次!
    16行代码即对16bit数据累加的结果取反,得到二进制反码求和的结果,然后函数返回该值。

3. Linux kernel中实现:
 
csum为32bit ,存储原码相加的结果, 将原码高16bit叠加到低16bit,然后求反,即可得到 反码求和的结果
static inline __sum16 csum_fold(__wsum csum)
{
    u32 sum = (__force u32)csum;
    sum = (sum & 0xffff) + (sum >> 16);      //将高16 叠加到低16
    sum = (sum & 0xffff) + (sum >> 16);      //将产生的进位   叠加到 低16
    return (__force __sum16)~sum;               //求反,   得到二进制反码求和的结果
}

其中上述csum的计算如下:
skb_add_data -> csum_partial     计算  添加数据段的校验和 ,将其放置在 skb->csum 中,完成tcp数据部分计算。
tcp_transmit_skb(传输层统一出口函数) 为 skb添加 tcp首部,并在其中调用  tcp_v4_send_check 来完成 tcp首部的最终校验和计算( tcp首部+tcp数据+tcp伪首部 )。
 
源码分析:
tcp_transmit_skb
{
    icsk->icsk_af_ops->send_check(sk, skb->len, skb); //完成 tcp首部校验和的计算
    ...
    ...
    err = icsk->icsk_af_ops->queue_xmit(skb, 0);  //将其发往 IP层     ip_queue_xmit
}
其中 tcp_transmit_skb-->icsk->icsk_af_ops->send_checktcp_v4_send_check
tcp_v4_send_check
{
    .....    
    th->check = tcp_v4_check(len, inet->inet_saddr, inet->inet_daddr,
                     csum_partial(th, th->doff << 2, skb->csum));  //将 th首部 校验和 和 tcp数据段校验和相加
    ....
}
//NETIF_F_ALL_CSUM
static inline __sum16 tcp_v4_check(int len, __be32 saddr,
                   __be32 daddr, __wsum base)
{
    return csum_tcpudp_magic(saddr,daddr,len,IPPROTO_TCP,base);// 将tcp首部+tcp数据+tcp伪首部 三部分校验和相加,并将 32bit的 csum (skb->csum),折叠计算为16bit的 check,存放在 th->check (tcp首部校验和字段)中
即产生最终校验和
}
//对伪头 计算校验和
static inline __sum16 csum_tcpudp_magic(__be32 saddr, __be32 daddr, unsigned short len,
                           unsigned short proto, __wsum sum)
{
    return csum_fold(csum_tcpudp_nofold(saddr,daddr,len,proto,sum));
}

//将32位值的高16位折叠到低16位中,然后取反输出值,即为发送端应填入的校验和
static inline __sum16 csum_fold(__wsum csum)
{
u32 sum = (__force u32)csum;
sum = (sum & 0xffff) + (sum >> 16);      //将高16 叠加到低16
sum = (sum & 0xffff) + (sum >> 16);      //将产生的进位   叠加到 低16
return (__force __sum16)~sum;               //求反,   得到二进制反码求和的结果
}

TCP校验和计算原理与实现