如何在Java中从byte[]计算Internet校验和

17

我正在尝试弄清楚如何在Java中计算Internet校验和,这让我痛苦不堪。(我对位操作很糟糕。)我在C#中找到了一个版本Calculate an Internet (aka IP, aka RFC791) checksum in C#。然而,我试图将其转换为Java时似乎没有产生正确的结果。有人能看出我做错了什么吗?我怀疑是数据类型问题。

public long getValue() {
    byte[] buf = { (byte) 0xed, 0x2A, 0x44, 0x10, 0x03, 0x30};
    int length = buf.length;
    int i = 0;

    long sum = 0;
    long data = 0;
    while (length > 1) {
        data = 0;
        data = (((buf[i]) << 8) | ((buf[i + 1]) & 0xFF));

        sum += data;
        if ((sum & 0xFFFF0000) > 0) {
            sum = sum & 0xFFFF;
            sum += 1;
        }

        i += 2;
        length -= 2;
    }

    if (length > 0) {
        sum += (buf[i] << 8);
        // sum += buffer[i];
        if ((sum & 0xFFFF0000) > 0) {
            sum = sum & 0xFFFF;
            sum += 1;
        }
    }
    sum = ~sum;
    sum = sum & 0xFFFF;
    return sum;
}
3个回答

18

根据@Andy、@EJP、@RD等人的评论进行了编辑,并添加了额外的测试用例以确保正确性。

我使用了@Andy的解答(正确地确定了问题的位置),并更新了代码以包含链接答案中提供的单元测试,同时还添加了一个经过验证的消息校验和的附加测试用例。

首先是实现部分。

package org.example.checksum;

public class InternetChecksum {

  /**
   * Calculate the Internet Checksum of a buffer (RFC 1071 - http://www.faqs.org/rfcs/rfc1071.html)
   * Algorithm is
   * 1) apply a 16-bit 1's complement sum over all octets (adjacent 8-bit pairs [A,B], final odd length is [A,0])
   * 2) apply 1's complement to this final sum
   *
   * Notes:
   * 1's complement is bitwise NOT of positive value.
   * Ensure that any carry bits are added back to avoid off-by-one errors
   *
   *
   * @param buf The message
   * @return The checksum
   */
  public long calculateChecksum(byte[] buf) {
    int length = buf.length;
    int i = 0;

    long sum = 0;
    long data;

    // Handle all pairs
    while (length > 1) {
      // Corrected to include @Andy's edits and various comments on Stack Overflow
      data = (((buf[i] << 8) & 0xFF00) | ((buf[i + 1]) & 0xFF));
      sum += data;
      // 1's complement carry bit correction in 16-bits (detecting sign extension)
      if ((sum & 0xFFFF0000) > 0) {
        sum = sum & 0xFFFF;
        sum += 1;
      }

      i += 2;
      length -= 2;
    }

    // Handle remaining byte in odd length buffers
    if (length > 0) {
      // Corrected to include @Andy's edits and various comments on Stack Overflow
      sum += (buf[i] << 8 & 0xFF00);
      // 1's complement carry bit correction in 16-bits (detecting sign extension)
      if ((sum & 0xFFFF0000) > 0) {
        sum = sum & 0xFFFF;
        sum += 1;
      }
    }

    // Final 1's complement value correction to 16-bits
    sum = ~sum;
    sum = sum & 0xFFFF;
    return sum;

  }

}

然后在JUnit4中进行单元测试

package org.example.checksum;

import org.junit.Test;

import static junit.framework.Assert.assertEquals;

public class InternetChecksumTest {
  @Test
  public void simplestValidValue() {
    InternetChecksum testObject = new InternetChecksum();

    byte[] buf = new byte[1]; // should work for any-length array of zeros
    long expected = 0xFFFF;

    long actual = testObject.calculateChecksum(buf);

    assertEquals(expected, actual);
  }

  @Test
  public void validSingleByteExtreme() {
    InternetChecksum testObject = new InternetChecksum();

    byte[] buf = new byte[]{(byte) 0xFF};
    long expected = 0xFF;

    long actual = testObject.calculateChecksum(buf);

    assertEquals(expected, actual);
  }

  @Test
  public void validMultiByteExtrema() {
    InternetChecksum testObject = new InternetChecksum();

    byte[] buf = new byte[]{0x00, (byte) 0xFF};
    long expected = 0xFF00;

    long actual = testObject.calculateChecksum(buf);

    assertEquals(expected, actual);
  }

  @Test
  public void validExampleMessage() {
    InternetChecksum testObject = new InternetChecksum();

    // Berkley example http://www.cs.berkeley.edu/~kfall/EE122/lec06/tsld023.htm
    // e3 4f 23 96 44 27 99 f3
    byte[] buf = {(byte) 0xe3, 0x4f, 0x23, (byte) 0x96, 0x44, 0x27, (byte) 0x99, (byte) 0xf3};

    long expected = 0x1aff;

    long actual = testObject.calculateChecksum(buf);

    assertEquals(expected, actual);
  }

  @Test
  public void validExampleEvenMessageWithCarryFromRFC1071() {
    InternetChecksum testObject = new InternetChecksum();

    // RFC1071 example http://www.ietf.org/rfc/rfc1071.txt
    // 00 01 f2 03 f4 f5 f6 f7
    byte[] buf = {(byte) 0x00, 0x01, (byte) 0xf2, (byte) 0x03, (byte) 0xf4, (byte) 0xf5, (byte) 0xf6, (byte) 0xf7};

    long expected = 0x220d;

    long actual = testObject.calculateChecksum(buf);

    assertEquals(expected, actual);

  }

}

感谢您提供额外的单元测试。由于一些错误的测试导致我的代码出现了问题,我得到了一些相互矛盾的答案。 - chotchki
1
@chotchki 我已经编辑了答案,包括 @Andy 等人的评论。 - Gary
为什么 calculateChecksum 返回 long 值?UDP、TCP 和 IPv4 校验和只需要两个字节。因此,int(最多 4 个字节)应该足够了。 - Maksim Dmitriev
1
该代码是从RFC1071第4.1节的示例修改而来,该示例使用long作为其总和。在撰写RFC时,long可能是32位。 - Gary
为什么需要对 buf[i] 应用 0xFF00,对 buf[i+1] 应用 0xFF?例如,两个字节 { 64, 17 } 是成对出现的。它们的二进制值如下:01000000 00010001。因此,如果我将左移运算符应用于 64 八次,我将得到 01000000 0000000。使用 0xFF00 进行 AND 运算后,结果不会改变。对于 17 和 0xFF,同样适用。为什么要使用 | 而不是 + - Maksim Dmitriev
1
很大程度上与Java中避免类型提升有关。请查看其他答案以获取更多详细信息(@Andy现在是@Andrey Balaguta)。 - Gary

12

以下是一个更短的版本:

long checksum(byte[] buf, int length) {
    int i = 0;
    long sum = 0;
    while (length > 0) {
        sum += (buf[i++]&0xff) << 8;
        if ((--length)==0) break;
        sum += (buf[i++]&0xff);
        --length;
    }

    return (~((sum & 0xFFFF)+(sum >> 16)))&0xFFFF;
}

我喜欢你的版本,但<< 8应该在“偶数”字节上。 - Paulo Costa

2
我认为造成问题的是类型提升。让我们看看 data = (((buf[i]) << 8) | ((buf[i + 1]) & 0xFF))

  1. ((buf[i]) << 8)buf[i]提升为int,导致符号扩展。
  2. (buf[i + 1]) & 0xFF也会将buf[i+1]提升为int,导致符号扩展。但是使用0xff掩码对这个参数进行屏蔽是正确的 - 在这种情况下,我们得到了正确的操作数。
  3. 整个表达式被提升为long(再次包括符号)。

问题在于第一个参数 - 它应该使用0xff00进行掩码处理,像这样: data = (((buf[i] << 8) & 0xFF00) | ((buf[i + 1]) & 0xFF))。但我怀疑Java中实现了更有效的算法,甚至标准库中可能有一个。您可以查看一下MessageDigest,也许它有一个。


2
#2 是不正确的。字面量0xFF被视为int而不是byte。在步骤#3中,您将拥有一个short | int,其中第一个参数得到提升,而不是第二个参数。因此,问题不在于您示例中的0x00,0xFF,而在于0xFF,0x00(或任何buf [i]> 0x7F)。当第一个参数从short扩展为int时,它会像您解释的那样进行符号扩展。在原始程序中,正是0xed,0x2A的情况导致校验和不正确。 - robert_x44
2
这篇文章是完全错误的。#1、#2和#3的结果都是int,除非其中任何一个参数是long,那么结果就是long。整个表达式的结果同样是int,并且在存储为long时会被扩展为long。 (int)转换是完全不必要的。 - user207421
@RD:老实说,我不知道如何解决这个问题。 - chotchki
1
@EJP,@RD,感谢您的纠正,对于我的误导我深感抱歉。我已经相应地更正了帖子。 - Andrey Balaguta
这是符号扩展,而不是“符号扩张”。 - user207421
+1 表示实际解决了问题(然后根据评论进行修订)。 - Gary

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接