IP/TCP/UDP报文解析(1)IP报文

前言

关于IP报文的解析涉及到很多位运算的使用,如果不熟位运算规则的小伙伴,可以查看我的这篇博文《Java中的位运算》

正文

IP报文格式一览

这是IP报在网络传输中的格式
一个完整的IP包通常被看做 首部 和 数据 两部分,首部中又包含 固定长度(20字节)和可变长度(最大60-20=40字节),IP报头包含以下数据:

  • 版本号:在IP数据报中第1个字节的前4位,现在有IPv4和IPv6 两个IP协议版本,所以这里可能的值有0010(IPv4 十进制 4)和 0100(IPv6 十进制6),端与端之间只有同协议版本才能正常通信。
  • 首部长:首部长的作用是标识IP报头的长度,在IP报文中第1个字节的后4位,其最大取值为15,其单位为4字节,即首部长最大为15 * 4 = 60字节,通常IP报头默认长度为5 * 4 = 20字节。
  • 服务类型:服务类型的作用是标识IP报文的处理方式,在IP报文中第2个字节。前三位代表包的处理优先级,取值范围0-7,0为最低 7为最高。后4位是TOS(注意TOS之前还有一个位无效位),表示本数据报在传输过程中需要得到的服务,依次表示为 D(Minimum delay)最小延迟、T(Maximum throughput)最大传输率、R(Maximum reliability)最高可靠性、C(Minimum cost)最小成本。这里的服务类型只是代表用户希望得到的服务,并不一定要强制执行。
  • 总长度:总长度的作用是标识IP报文首部分+数据部分的总长度,在IP报文中第3、4字节中,其最大取值为65535,其单位为1字节,所以一个IP报最大为65535 * 1 = 65535字节。
  • 序列号:序列号的作用是标识该IP报文请求,由发起请求方随机生成且本地唯一,每次分包在上一个包基础上该值+1填入,在IP报文中第5、6字节中。
  • 标志位:标志位的作用是标识该IP报是否发生分片行为DF(Don’t fragment)和是否还有分片MF(More Fragment) ,在IP报文中第7个字节的前3位,但只有后两位是有效的,依次为0 DF MF。有三个可能的值:未发生分片010、发生分片且该包后续没有分片000、发生分片且该包后续还有分片001。
  • 片偏移: 片偏移的作用是在IP报发生分包时标识该包中的数据相对于原始数据的起始位置,在IP报文中第7个字后5位和第8个字节。最大取值8191,单位是8字节,所以片偏移的最大值为8191 * 8 = 65528字节。
  • 生存时间:生存时间的作用是标识该请求能穿过路由的最大次数,以防止路由环路。该值每经过一次网络路由后减小1,信息送达或该值为0时停止传递。在IP报文中第9个字节中。
  • 上层协议:上层协议标识IP包中数据部分是属于那种协议的封装,在数据报中第10个字节。列举几个常用的值 :
协议二进制十进制
TCP000001106
UDP0001000117
ICMP000000011
IGMP000000102
OSPF0101100189
  • 首部校验和:首部校验和用来校验IP报头是否准确。在IP数据报的第11、12个字节中,关于校验和的计算和使用等下再说。
  • 源IP地址:源IP地址标识这个IP包的初始发送地址,在IP数据报的第13、14、15、16个字节中,所以IP地址通常被写为255.255.255.255这样的格式。
  • 目的IP地址:目的IP地址标识这个IP包最终送达的地址。
  • 选项和填充:作用是附加IP包处理信息,可变长度,最大为40字节。

IP校验和

校验和计算

计算首部校验和的过程如下

  1. 将首部校验和置零
  2. 如果首部的字节长度为奇数,则在计算时需要在首部末尾添加一个全为0的空字节
  3. 将首部中所有字节划分成16位长度的数进行相加求和
  4. 和的溢出位折叠求和。
  5. 将和做非运算得到最终校验和
  6. 将校验和填入首部校验和位。

具体实现请看文末的代码。

校验数据

校验数据相对简单,大体上和计算时差不多,只是校验计算的时候不需要重置校验和位,最后的结果为0时说明该IP报文正确,若不为0说明该报文错误将其丢弃。

片偏移的计算

由于数据链路层单次发送的数据单元最大为1500字节(1贞的大小 这个值貌似受源机硬件影响),所以当IP报文总长度大于这个值的时候,数据链路层会对IP包进行拆分发送,当产生分包后,每个包中的数据相对于原始IP包中的数据,就会有一个衔接位置,那么这个位置该如果计算呢?
这里举例说明,假如:一个原始IP包总长度4000,报头长20。

当发送第1个拆分包的时候,其数据起始位就是IP包起始位,所以其偏移为0,目前发送出去的数据就是1500 - 20 = 1480字节(下标0 - 1479),剩余数据2520,还有拆分包。

当发送第2个拆分包的时候,其数据起始位置应该在原始数据的1480位上,因为片偏移的单位是8字节,所以该包的片偏移值为1480/8 = 185,目前发送出去的数据是1480*2 = 2560,剩余1440,还有拆分包

当发送第3个拆分包的时候,其数据起始位置应该在原始数据的2560位上,所以该包的片偏移值为2560/8 = 370,剩余0,发送结束

这里具体的计算规则我也不是太熟

报文解析代码

public class IPPacket extends Packet{
    private final static String TAG = IPPacket.class.getSimpleName();

    private final static int VERSION_BIT = 0;
    private final static int HEADER_LENGTH_BIT = 0;
    private final static int TOS_BIT = 1;
    private final static int TOTAL_LENGTH_BIT = 2;
    private final static int SEQUENCE_BIT = 4;
    private final static int TAG_BIT = 6;
    private final static int OFFSET_BIT = 6;
    private final static int LIVE_BIT = 8;

    private final static int PROTOCL_BIT = 9;
    public final static int PROTOCL_ICMP = 1;
    public final static int PROTOCL_IGMP = 2;
    public final static int PROTOCL_OSPF = 89;
    public final static int PROTOCL_TCP = 6;
    public final static int PROTOCL_UDP = 17;

    private final static int HEADER_SUM_BIT = 10;
    private final static int LOCAL_IP_BIT = 12;
    private final static int REMOTE_IP_BIT = 16;


    public IPPacket(byte[] bytes, int... parameters){
        super(bytes,parameters);
    }

    /**
     * 获取IP报文协议版本
     * 协议版本
     *     在IP报文第0个字节中前4位 占1/2字节 共4位
     * @return
     */
    public int getVersion(){
        return (bytes[VERSION_BIT] & 0xFF) >> 4;
    }

    /**
     * 获取IP报文头长度
     * 报头长度
     *     在IP报文第0个字节中后4位 占1/2字节 共4位
     *     最小表示为 0000(十进制0) 最大表示为 1111(十进制15) 单位为4字节
     *     也就是报头 最小为0 * 4字节0 * 4 * 8 = 0位,最大为15 * 4字节15 * 4 * 8 = 480位
     *     一般报头长度是0101(十进制5),也就是5 * 4 = 20字节
     * @return
     */
    public int getHeaderLength(){
        return (bytes[HEADER_LENGTH_BIT] & 0xF) * 4;
    }

    /**
     * 获取TOS服务字段
     * 服务类型
     *     在IP报文中第1个字节 占1字节 共8位
     *     一般情况不使用 也没查到具体有什么用
     * @return
     */
    public int getTos(){
        return bytes[TOS_BIT] & 0xFF;
    }

    /**
     * 获取IP报文总长度
     * 数据报总长度
     *     在IP报文中第2、3字节 占2字节 共16位 单位为1字节
     *     标注没有分片前整个IP请求的总长度(包括 数据 和 报头)
     * @return
     */
    public int getTotalLength(){
        return byteToInt(bytes[TOTAL_LENGTH_BIT],bytes[TOTAL_LENGTH_BIT + 1]);
    }

    /**
     * 获取序列码
     * 序列码
     *     在IP报文中第4个和第5个字节 占2字节 共16位
     *     分片请求时用作同IP请求标识
     *     开始发送数据时由发送方生成。标识发送方发送的每一个数据报,如果发送的数据报未发生分片,则此值依次加1,如果发生了分片,分片后的各个数据报使用同一个序列号。
     * @return
     */
    public int getSequence(){
        return byteToInt(bytes[SEQUENCE_BIT],bytes[SEQUENCE_BIT + 1]);
    }

    /**
     * 获取标志位
     * 分片标志
     *     在IP报文中第6个字节的前3位 占3/8字节 共3位
     *     第一位保留,未使用。
     *     第二位是DF(Don’t Fragment),如果为1,表示未发生分片;如果为0,表示发生了分片。
     *     第三位是MF(More Fragment),如果为1,表示发生了分片,并且除了分片出的最后一个报文中此标志为0,其余报文中此标志均为1
     *     该标志位有以下几个可能的值
     *      001(十进制1):发生分片且后续还有分片
     *      000(十进制0):发生分片且这是最后一个分片
     *      010(十进制2):不能分片
     * @return
     */
    public int getTag(){
        return (bytes[TAG_BIT] & 0xFF) >> 5 ;
    }

    /**
     * 是否发生分片
     * 1 为未发生分片 返回false
     * 0 为发生了分片 返回true
     * DF Don't Fragment
     * @return
     */
    public boolean isDF(){
        if((getTag() >> 1) == 1){
            return false;
        }else{
            return true;
        }
    }

    /**
     * 是否还有分片
     * 如果未发生分片直接返回false
     * 如果发生分片且后面还有分片返回true
     * 如果发生分片且后面没有分片了返回false
     * MF  More Fragment
     * @return
     */
    public boolean isMF(){
        if(isDF() && (getTag() & 1) == 1){
            return true;
        }else{
            return false;
        }
    }

    /**
     * 获取片偏移
     * 片偏移量 单位 8 byte
     *     在IP报文中第6个字节的后5位和第7个字节 占13/8字节 共13位
     *     标识分片相对于原始IP数据报开始处的偏移。只在分片传输时起作用
     *     链路层MTU(Maximum Transmission Unit 最大传输单元)为1500
     *     如果IP报文总长度超过这个数值,就会产生IP分片
     *      下面是计算公式
     *      假设链路层传输时分片数量为 n
     *      IP报头长度为20字节(实际传输时默认为20)
     *      IP报文总长度为 m
     *      分片后的片偏移为y (不管有没有分片,首个IP分片的片偏移肯定是0 如果有分片n肯定是大于1的)
     *      n = m > 1500  ? (m - m % 1500) / 1500 + 1 :1
     *      y = (1500 - 20) * (n - 1) / 8
     *      可能会觉得懵逼 为什么还会除一个8 这是因为片偏移的单位是 8 byte 这里得到的值是最后写到报文中片偏移的值
     *      我看过一个文档上面写的有个这样的描述:从0位到1479位是IP报数据位 当时不明白这个1479是怎么来的
     *      后来仔细想想   这玩意是从下标读数来的 数组中读数是从0位开始到 数组长度 - 1 结束。
     *      也就是 MTU - 报头长度 - 1 => 1500 - 20 - 1 = 1479
     *      从0位数到1479位 正好是1500个位置。
     * @return
     */
    public int getOffsetByte(){
        return byteToInt(bytes[OFFSET_BIT],bytes[OFFSET_BIT + 1]) * 8;
    }

    /**
     * 获取生命周期
     * 生存时间
     *     在IP报文中第8个字节 占1字节 共8位
     *     表示生存周期,每经过一个路由值减一,防止路由环路。(windows系统下默认值为128 二进制 10000000)
     *     例如 路由A发数据给B B又转发给A 值依次衰减,值为0时或主动停止时停止转发
     * @return
     */
    public int getLive(){
        return bytes[LIVE_BIT] & 0xFF;
    }

    /**
     * 获取上层协议
     * 上层协议
     *     在IP报文中第9个字节 占1字节 共8位
     *     该值标识上层协议
     *     其中常见的值有
     *      ICMP协议 十进制 1 二进制 00000001
     *      IGMP协议 十进制 2 二进制 00000010
     *      OSPF协议 十进制 89 二进制 01011001
     *      TCP协议 十进制 6 二进制 00000110
     *      UDP协议 十进制 17 二进制 00010001
     * @return
     */
    public int getProtocl(){
        return bytes[PROTOCL_BIT] & 0xFF;
    }

    /**
     * 获取头部校验和
     * 头部校验和
     *     在IP报文中第10、11个字节 占2字节 共16位
     *     该值是对整个数据包的包头进行的校验
     * @return
     */
    public int getHeaderSum(){
        return byteToInt(bytes[HEADER_SUM_BIT],bytes[HEADER_SUM_BIT + 1]);
    }



    /**
     * 获取本地IP地址 String类型
     * x.x.x.x格式
     * 源IP地址
     *        在IP报文中第12、13、14、15个字节 占4个字节(32位)
     *        表示本地IP地址(发送方MAC地址 也就是发送方的物理硬件地址) 那么地址掩码又是怎么回事?
     *        因为一个字节最大表示数为255 所以我们看到的IP地址通常写为x.x.x.x的格式(x为0-255的十进制数 转换为二进制数为00000000~11111111)
     * @return
     */
    public String getLocalIpString(){
        return (bytes[LOCAL_IP_BIT] & 0xFF)
                + "." + (bytes[LOCAL_IP_BIT + 1] & 0xFF)
                + "." + (bytes[LOCAL_IP_BIT + 2] & 0xFF)
                + "." + (bytes[LOCAL_IP_BIT + 3] & 0xFF);
    }

    public int getLocalIpInt(){
        return byteToInt(bytes[LOCAL_IP_BIT],bytes[LOCAL_IP_BIT + 1],bytes[LOCAL_IP_BIT + 2],bytes[LOCAL_IP_BIT + 3]);
    }

    /**
     * 获取远程IP地址 String类型
     * x.x.x.x格式
     * 目的IP地址
     *           在IP报文中第16、17、18、19个字节 占4个字节(32位)
     *           远程IP地址(接收方MAC地址 也就是接收方的物理硬件地址)
     *           因为一个字节最大表示数为255 所以我们看到的IP地址通常写为x.x.x.x的格式(x为0-255的十进制数 转换为二进制数为00000000~11111111)
     * @return
     */
    public String getRemoteIpString(){
        return (bytes[REMOTE_IP_BIT] & 0xFF)
                + "." + (bytes[REMOTE_IP_BIT + 1] & 0xFF)
                + "." + (bytes[REMOTE_IP_BIT + 2] & 0xFF)
                + "." + (bytes[REMOTE_IP_BIT + 3] & 0xFF);
    }

    public int getRemoteIpInt(){
        return byteToInt(bytes[REMOTE_IP_BIT],bytes[REMOTE_IP_BIT + 1],bytes[REMOTE_IP_BIT + 2],bytes[REMOTE_IP_BIT + 3]);
    }



    /**
     * 检查头部校验和
     * 很无语这个校验和的算法模式为什么会这样设计
     * 直接取每个字节中表示的十进制数相加不就得了
     * 还非得用16位来表示肯定会溢出的校验和
     * 这个方法还没有搞明白
     */
    public boolean checkSum(){
        return (( ~getSum() ) & 0xFFFF) == 0;
    }

    /**
     * 校验求和
     * @return
     */
    private int getSum(){
        int headerLength = getHeaderLength();
        int sum = 0;

        // 两字节一组依次计算16位和得到累加值
        for(int i = offset; i < offset + headerLength; i += 2){
            sum +=  byteToInt(bytes[i],bytes[i + 1]);
        }

        // 如果报头为奇数 那么最后一个字节的和也需要加上
        if((offset + headerLength) % 2 > 0 ){
            sum += (bytes[offset + headerLength - 1] & 0xFF) << 8;
        }

        //这里计算校验和16位之上是否有进位 若有进位则循环折叠求和 直到16位之上没有进位
        while ((sum >> 16) > 0){
            sum = (sum >> 16) + (sum & 0xFFFF);
        }

        return sum;
    }

    /**
     * 设置完参数后重新计算校验和并设置
     */
    public void refreshCheckSum(){
        bytes[HEADER_SUM_BIT] = 0;
        bytes[HEADER_SUM_BIT + 1] = 0;
        int sum = ~getSum();
        bytes[HEADER_SUM_BIT] = (byte) (sum >> 8);
        bytes[HEADER_SUM_BIT + 1] = (byte)(sum);
    }


    public void setLocalIp(String ip){
        String[] strings = ip.split("\\.");

        if(strings.length != 4) return;

        int y = 255;

        for(int i = 0;i < 4; i++){
            try {
                y = Integer.valueOf(strings[0]);
            }catch (NumberFormatException e){}

            bytes[REMOTE_IP_BIT + i] = (byte) y;
        }
    }

    public void setLocalIp(int ip){
        for(int i = 0; i < 4;i ++){
            bytes[LOCAL_IP_BIT + i] = (byte) (ip >> ((3 - i) * 8));
        }
    }

    public void setRemoteIp(String ip){

        String[] strings = ip.split("\\.");

        if(strings.length != 4) return;

        int y = 255;

        for(int i = 0;i < 4; i++){
            try {
                y = Integer.valueOf(strings[0]);
            }catch (NumberFormatException e){}

            bytes[REMOTE_IP_BIT + i] = (byte) y;
        }

    }

    public void setRemoteIp(int ip){
        for(int i = 0; i < 4;i ++){
            bytes[REMOTE_IP_BIT + i] = (byte) (ip >> ((3 - i) * 8));
        }
    }

}

关于文中的Packet类的代码如下:

public class Packet {

    // 数据报文
    protected byte[] bytes;

    // 偏移值
    protected int offset;

    // 有效数据长度
    protected int validLength;

    // 下一层的包
    protected Packet packet;

    /**
     *
     * @param bytes 收到的byte数据集合
     * @param parameters 一些参数 如:数据偏移 有效长度
     */
    public Packet(byte[] bytes,int... parameters){
        this.bytes = bytes;
        if(parameters != null){
            if(parameters.length > 0){
                offset = parameters[0];
                if(parameters.length > 1){
                    validLength = parameters[1];
                }
            }
        }

        if(validLength == 0){
            validLength = bytes.length;
        }else if(validLength > bytes.length){
            validLength = bytes.length;
        }
    }

    /**
     * 获取下级协议
     * @return
     */
    public Packet getPacket() {
        return packet;
    }

    /**
     * 设置下级协议 从上至下依次为 HTTP/HTTPS -> TCP/UDP -> IP -> 网路路由
     * @param packet
     */
    public void setPacket(Packet packet) {
        this.packet = packet;
    }

    /**
     * 获取数据
     * @return
     */
    public byte[] getBytes(){
        return bytes;
    }

    /**
     * 获取数据偏移
     * @return
     */
    public int getOffset(){
        return offset;
    }

    /**
     * 获取数据有效长度
     * @return
     */
    public int getValidLength(){
        return validLength;
    }

    /**
     * 获取报头长度
     * @return
     */
    public int getHeaderLength(){
        return 0;
    }


    public int byteToInt(byte... bytes){

        int sum = 0;

        if(bytes == null) return 0;

        for (int i =  0; i < bytes.length && bytes.length <= 4; i ++){
            sum |= (bytes[i] & 0xFF) << ((bytes.length - 1 - i) * 8);
        }

        return sum;
    }

    public byte intToByte(int i){
        return (byte) i;
    }

    public byte[] intToBytes(int i){
        byte[] bytes = new byte[4];
        bytes[0] = (byte) (i >> 24);
        bytes[1] = (byte) (i >> 16);
        bytes[2] = (byte) (i >> 8);
        bytes[3] = (byte) i;
        return bytes;
    }

}

代码中的注释很简洁,全部看完应该能理解是什么意思

总结

以上就是我对IP报文的理解,有问题请多指教。