我目前正在处理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;
}
Run Code Online (Sandbox Code Playgroud)
有谁知道为什么这不起作用?我得到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;
}
Run Code Online (Sandbox Code Playgroud)
输出: string(2) "01"
数据包没有固有长度,即使从C获取此信息实际上也相对困难.这是因为在用户层,套接字数据不以分组表示.(UDP以完整的有效载荷表示,但在用户空间中接收的单个UDP有效载荷绝对可能仍然代表多个数据包.)因此,问题并不恰当,您应该问的是如何确定如何可以在套接字上读取许多字节.
那么为什么你粘贴的代码不能告诉你呢?你应该真正理解这段代码的作用,因为它很有趣.但是,因为它与你可能真正感兴趣的信息有些相似,所以我稍后会留下.
不幸的是,PHP没有为您ioctl(2)
提供使用原始套接字FD 调用的方法.这将允许您执行类似ioctl(sock, FIONREAD, &n)
PHP-land的操作来确定可读取的字节数.(实际上,显然你可以这样做,如果你使用fopen
或fsockopen
,但我猜你没有.)唉,这是行不通的.
幸运的是,有两种选择:
使用非阻塞套接字.您可以在套接字流上调用socket_set_nonblock.一旦你这样做,任何调用socket_read
,socket_write
等将在非阻塞模式操作.这意味着如果您执行eg $data = socket_read($socket, 1024)
,并且可用的字节少于 1024个,则返回可用字节.(如果没有数据可用,则返回的字节数可以为0.)
使用socket_select对一系列套接字执行相同操作.此函数会通知您哪些套接字具有可读数据/处于可写状态/有错误需要处理.
无论您使用哪种版本,处理套接字无法接收足够数据的情况的一般方法是实现超时.socket_select
为此提供本机接口; 使用非阻塞套接字手动执行此操作需要您记住在调用之间等待和实现休眠的时间socket_read
.如果在一段时间内(例如10秒)没有收到足够的数据,请关闭套接字并忘记它.
如果您收到的数据超出了预期,那就是错误,并关闭套接字并忘记它.
您正在处理的协议也很重要.因为你还没有说过你正在处理什么协议,所以我无法帮助指出你需要多少数据.但也许你正在实现自己的协议以获得乐趣.
在这种情况下,您需要确定编码线路上数据量的方法.因为你说你在你的问题中使用的方法做了一些二进制技巧,我也会这样做.您可能希望将32位值打包到字符串的开头.当您收到连接时,等待前4个字节进入.一旦读取了这4个字节,就可以解压缩该数据以确定需要读取多少.
<?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
...
Run Code Online (Sandbox Code Playgroud)
然后,在服务器中
<?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++;
}
Run Code Online (Sandbox Code Playgroud)
NB我没有测试这段代码,确保我正确编码打包/解包数据,所以我不确定你可以逐字使用它.将其视为建筑伪代码.
其他协议具有其他长度编码方式.例如,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;
}
Run Code Online (Sandbox Code Playgroud)
那么让我们考虑一下这对几个不同的字节流意味着什么:
$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
*/
Run Code Online (Sandbox Code Playgroud)
如果您熟悉字节顺序,您可能会注意到这些值是以little-endian编码传输的.这通常是奇怪的(网络字节顺序是big-endian),但我们实际上并没有发送整数值:我们发送可变长度的字节流.每个字节的编码有助于我们确定长度是多少.但是,如果不知道此代码实现了什么协议,这实际上可能是一个错误,阻止此代码在具有不同字节顺序的计算机上进行移植.
重要的是要注意,这是字节流协议的一部分,并不是获取任何长度的标准方法.实际上,许多二进制协议未定义为字节流协议.这是因为字节流协议通常根本不定义字节序(因为它不是流所必需的).因此,如果您在PPC或某些ARM处理器上运行此代码,它将无法正常工作.因此,我们说这段代码不可移植.
处理字节流或通过网络发送原始二进制数据时,请务必确定数据的字节顺序.如果不这样做,您的协议将创建不可移植的实现.另请参阅Rob Pike的这篇精彩文章,了解有关字节顺序主题的更多信息,以及为什么任何时候出现问题,您都会感到困惑或有人做错了(比如定义字节流协议而不完全定义数字编码) .