PHP - 获取数据包长度

时间:2014-12-24 03:21:18

标签: php sockets udp

我目前正在处理PHP中的套接字和数据包,并且不知道从哪里开始获取数据包的长度。我从GitHub回购中试过这个(不记得是哪一个):

    private function get_packet_length($socket) {
        $a = 0;
        $b = 0;
        while(true) {
            $c = socket_read($socket, 1);
            if (!$c) {
                return 0;
            }
            $c = ord($c);
            $a |= ($c & 0x7F) << $b++ * 7;
            if ($b > 5) {
                return false;
            }
            if (($c & 0x80) != 128) {
                break;
            }
        }
        return $a;
    }

有谁知道为什么这不起作用?我得到int(1)

修改

private function get_packet_length($socket) {
        $a = 0;
        $b = 0;
        while(true) {
            $c = socket_read($socket, 10240);
            if (!$c) {
                return 0;
            }
            $c = ord($c);
            $a .= strlen($c);
            if ($b > 5) {
                return false;
            }
            if (($c & 0x80) != 128) {
                break;
            }
        }
        return $a;
    }

输出:string(2) "01"

3 个答案:

答案 0 :(得分:6)

数据包没有固有长度,即使从C获取此信息实际上也相对困难。这是因为在用户层,套接字数据不以分组表示。 (UDP以完整的有效载荷表示,但是在用户空间中接收的单个UDP有效载荷仍然可能代表多个数据包,这绝对是可能的。)因此,问题并不恰当,你真正应该问的是如何确定套接字上可读取的字节数。

那么为什么你粘贴的代码告诉你呢?你应该真正理解这段代码的作用,因为它很有趣。但是,因为它与您可能真正感兴趣的信息相关,所以我稍后会留下这些信息。

不幸的是,PHP没有为您提供使用原始套接字FD调用ioctl(2)的方法。这将允许您在PHP-land中执行类似ioctl(sock, FIONREAD, &n)的操作来确定可读取的字节数。 (实际上,如果您使用fopenfsockopen,显然可以这样做,但我猜您没有。)唉,这不会奏效。

幸运的是,您有两种选择:

  1. 使用非阻塞套接字。您可以在套接字流上调用socket_set_nonblock。执行此操作后,对socket_readsocket_write等的任何调用都将以非阻止模式运行。这意味着,如果您这样做,例如$data = socket_read($socket, 1024)少于的1024字节可用,返回可用字节。 (N.B.如果没有可用数据,则返回的字节数可以为0.)

  2. 使用socket_select对一系列套接字执行相同操作。此函数会通知您哪些套接字具有可读数据/处于可写状态/有错误需要处理。

  3. 无论您使用哪种版本,处理套接字无法接收足够数据的情况的一般方法是实现超时。 socket_select为此提供了一个本机接口;使用非阻塞套接字手动执行此操作需要您记住在socket_read的调用之间等待并实现休眠的时间。如果在一段时间内(例如10秒)没有收到足够的数据,请关闭套接字并忘记它。

    如果您收到的数据超出了您的预期,那就是错误,您关闭了套接字并忘了它。

    您处理的协议也很重要。因为您还没有说出您正在处理的协议,所以我无法帮助指出您需要多少数据。但也许你正在实施自己的协议以获得乐趣。

    在这种情况下,您需要确定编码线路上数据量的方法。因为您在问题中使用的方法会使用一些二进制技巧,所以我也会这样做。您可能希望将pack 32位值放入字符串的开头。当您收到连接时,等待前4个字节进入。一旦您读取了这4个字节,您可以unpack该数据来确定您需要读取多少。

    <?php
    $payload = "Have a nice day!\n";
    $len = strlen($payload) + 1; // + 1 for terminating NUL byte
    $packet = pack("Na", $len, $payload);
    socket_send($sock, $packet, $len + 4); // + 4 is length 
    ...
    

    然后,在服务器

    <?php
    $r = socket_read($sock, 4);
    $la = unpack("N", $r);
    
    // Because we don't know how much to read until we get the first 4 bytes.
    // Obviously this is a DoS vector for someone to hold the connection open,
    // so you will likely want to use socket_select to get that first bit of
    // data. That's an exercise for you.
    socket_set_nonblock($sock);
    $len = $la[1];
    $time = 0;
    $payload = "";
    while ($len > 0 && $time < 10) { 
        $data = socket_read($sock, $la[1]);
        $tlen = strlen($data);
        $payload .= $data;
        $len -= $tlen;
        if ($len == 0) { 
            break;
        }
        sleep(1); // Feel free to usleep.
        $time++;
     }
    

    N.B。我没有测试过这段代码,确保我正确编码打包/解包后的数据,所以我不确定你可以逐字使用它。将其视为建筑伪代码。

    其他协议具有其他长度编码方式。例如,HTTP在常见情况下使用Content-Length。

    您的示例代码

    在之前的编辑中,我只是将其视为第一个获取长度的字节。我回过头来看看这个问题,因为我看到了一个upvote,因为我的一些关于数据包的措辞让我困扰。我也很清楚地看到那段代码后,我发现自己的结论非常错误。

    代码读取套接字的各个字节以尝试获取可变长度的剩余有效负载。 (我想知道这是来自ZeroMQ的回购代码还是Apple推送通知的代码。)无论如何,看起来代码做了一些奇怪的事情,但是实际上发生了什么?

    private function get_packet_length($socket) {
        $a = 0;
        $b = 0;
        while(true) {
            /* Read next single byte off of the socket */
            $c = socket_read($socket, 1);
            if (!$c) {
                return 0;
            }
    
            /* Use integer value of the byte instead of the character value */
            $c = ord($c);
    
            /*
             * Get the first 7 bits of $c. Since $c represents an integer value
             * of a single byte, its maximum range is [0, 2^8). When we use only
             * 7 bits, the range is constrained to [0, 2^7), or 0 - 127. This
             * means we are using the 8th bit as a flag of some kind -- more on
             * this momentarily.
             *
             * The next bit executed is ($b++ * 7), since multiplication has
             * higher precedence than a left shift. On the first iteration, we
             * shift by 0 bits, the second we shift 7 bits, the third we shift
             * 14 bits, etc. This means that we're incrementally building an
             * integer value byte by byte. We'll take a look at how this works
             * out on real byte sequences in a sec.
             */
            $a |= ($c & 0x7F) << $b++ * 7;
    
            /*
             * If we've tried to handle more than 5 bytes, this encoding doesn't
             * make sense.
             */
            if ($b > 5) {
                return false;
            }
    
            /*
             * If the top bit was 1, then we still have more bytes to read to
             * represent this number. Otherwise, we are done reading this
             * number.
             */
            if (($c & 0x80) != 128) {
                break;
            }
        }
        return $a;
    }
    

    因此,让我们考虑一下这对几个不同的字节流意味着什么:

    $stream = "\x01"; /* This is pretty obviously 1 */
    $stream = "\x81\x80\x80\x00";
        /* This is also 1, can you figure out why? */
    
    $stream = "\xff\x01"; /* You might think it 256, but this is 255 */
    $stream = "\x80\x82"; /* This is 256. */
    $stream = "\xff\xff\x01"; /* 32767 */
    $stream = "\x80\x80\x02"; /* 32768 */
    
    $stream = "\x0c\x00\x48\x65\x6c\x6c\x6f\x2c\x20\x77\x6f\x72\x6c\x64\x21"
        /*
         * A static value 13, a length terminator, and 13 bytes that happen 
         * to represent "Hello, world!".
         * This shows how such an encoding would be used in practice
         */
    

    如果您熟悉字节顺序,您可能会注意到这些值是以little-endian编码传输的。这通常很奇怪(网络字节顺序是大端),但我们实际上并没有发送整数值:我们正在发送可变长度的字节流。每个字节的编码有助于我们确定长度是多少。但是,如果不知道此代码实现了什么协议,这实际上可能是一个错误,导致此代码无法在具有不同字节顺序的计算机上进行移植。

    值得注意的是,这是字节流协议的一部分,并不是获取任何长度的标准方法。实际上,许多二进制协议未定义为字节流协议。这是因为字节流协议通常根本不定义字节序(因为它不是流所必需的)。因此,如果您在PPC或某些ARM处理器上运行此代码,它将无法工作。因此,我们说这段代码不可移植。

    在处理字节流或通过网络发送原始二进制数据时,请务必确定数据的字节顺序。如果不这样做,您的协议将创建不可移植的实现。另请参阅Rob Pike的this great post,了解有关字节顺序主题的更多信息,以及为什么任何时候出现问题,您都会感到困惑或有人做错了(比如没有定义字节流协议)完全定义数字的编码。)

答案 1 :(得分:0)

socket_read返回一个简单的字符串

$packet_length=strlen($c);

应该有效

另外,你获得int(1)的原因是你在socket_read中设置了length参数。 删除它可能会省去一些麻烦

:此

        $c = socket_read($socket, 1);

        $c = socket_read($socket);

答案 2 :(得分:0)

使用

$c = socket_read($socket, 1024); //1kb read.
if ($c === "") //from documentation - no data is empty string.
  break;

我不知道你想要收到什么,但是如果字符串可以使用

$str = '';
while(true) {
        $tmp = socket_read($socket, 1024);
        if ($tmp === '')
            break;
        $str .= $tmp;
    }
    return $str;
}