目录
前文
前几天在php文档的笔记中搞了段php ping的代码php ping ipv4 by socket,顺便学习了ipv4 ICMP 报文和ipv4包结构,对ping的过程和php socket的使用有了初步的认识。本文记录下自己遇到的一些情况和代码分享。
copy的代码遇到的问题
- ping 域名时,会把域名dns查找时间也计算进去
- 在本地测试ok,扔服务器上测试,失败率很高
而直接用bash ping的话,丢包率很低,用这段代码的话,丢包率很高,且时高时低。实在是搞不明白原因是啥,只能去看看ip报文看看是代码哪写得不对了。
在后续测试中发现:
可以看到;
- 这里的包类型是8,是请求报文,还有169 254 128 12,是发包的ip,正常应该是我们ping的ip地址,也就是说我们收到了一个ping的请求包。
- 因为数据与会包的对不上,直接被算成失败,如果计算成丢包的话,丢包率会特别高。
- 而且下次ping的时候会收到上次ping的回包,得到了一次响应时间很低,但标识码和序列号错误的会包,此时由被视为丢包。
- 结果就是,一个错误的会包会导致后续的ping的雪崩式错误。
优化版本
改进的地方
- 域名解析时间单独计算
- 检测回包地址,丢弃非预期的回包
- 非预期的回包丢弃,在有效时间内继续等待回包
- 支持多次ping和统计
demo
require_once 'xping.php';
$back = xping::init()
->setDebug(false)
->setRet($ret)
->setError($error)
->doit('baidu.com',5,2000,1000,32);
var_dump(__FILE__.' line:'.__LINE__,$back,$ret,$error);exit;
out
//debug info
68.974018096924
63.398838043213
62.793016433716
62.896966934204
62.525987625122
string(64) "\xping.php line:*"
// $back
bool(true)
// $ret
array(4) {
["host"]=>
string(9) "baidu.com"
["dns_ms"]=>
float(0.0179290771484375)
["ip"]=>
string(14) "220.181.38.251"
["ping_ret"]=>
array(7) {
["avg"]=>
float(64.12)
["min"]=>
float(62.52598762512207)
["max"]=>
float(68.97401809692383)
["loss"]=>
float(0)
["sc"]=>
int(5)
["rc"]=>
int(5)
["str"]=>
string(65) "send=5 recive=5 loss=0.00 % min=62.53ms max=68.97ms avg=64.12ms "
}
}
//$error
string(8) "No Error"
$ret 部分说明
代码
Xxx-Bin/php-scoket-ping-Ipv4
测试代码
<?php
$g_icmp_error = "No Error";
function ping($host, $timeout)
{
$port = 0;
$datasize = 64;
global $g_icmp_error;
$g_icmp_error = "No Error";
$ident = [ord('J'), ord('C')];
$seq = [rand(0, 255), rand(0, 255)];
$packet = '';
$packet .= chr(8);
$packet .= chr(0);
$packet .= chr(0);
$packet .= chr(0);
$packet .= chr($ident[0]);
$packet .= chr($ident[1]);
$packet .= chr($seq[0]);
$packet .= chr($seq[1]);
for ($i = 0; $i < $datasize; $i++) {
$packet .= chr(0);
}
$chk = icmpChecksum($packet);
$packet[2] = $chk[0];
$packet[3] = $chk[1];
$sock = socket_create(AF_INET, SOCK_RAW, getprotobyname('icmp'));
$time_start = microtime(1);
socket_sendto($sock, $packet, strlen($packet), 0, $host, $port);
$read = [$sock];
$write = null;
$except = null;
$select = socket_select($read, $write, $except, 0, $timeout * 1000);
if ($select === null) {
$g_icmp_error = "Select Error";
socket_close($sock);
return -1;
} elseif ($select === 0) {
$g_icmp_error = "Timeout";
socket_close($sock);
return -1;
}
$recv = '';
$time_stop = microtime(1);
socket_recvfrom($sock, $recv, 65535, 0, $host, $port);
$recv = unpack('C*', $recv);
if ($recv[10] !== 1)
{
$g_icmp_error = "Not ICMP packet";
socket_close($sock);
return -1;
}
if ($recv[21] !== 0)
{
$g_icmp_error = "Not ICMP response ".implode(' ', $recv);
socket_close($sock);
return -1;
}
if ($ident[0] !== $recv[25] || $ident[1] !== $recv[26]) {
$g_icmp_error = "Bad identification number ".implode(' ', $recv);
socket_close($sock);
return -1;
}
if ($seq[0] !== $recv[27] || $seq[1] !== $recv[28]) {
$g_icmp_error = "Bad sequence number ".implode(' ', $recv);
socket_close($sock);
return -1;
}
$ms = ($time_stop - $time_start) * 1000;
if ($ms < 0) {
$g_icmp_error = "Response too long";
$ms = -1;
}
socket_close($sock);
return $ms;
}
function icmpChecksum($data)
{
$bit = unpack('n*', $data);
$sum = array_sum($bit);
if (strlen($data) % 2) {
$temp = unpack('C*', $data[strlen($data) - 1]);
$sum += $temp[1];
}
$sum = ($sum >> 16) + ($sum & 0xffff);
$sum += ($sum >> 16);
return pack('n*', ~$sum);
}
function getLastIcmpError()
{
global $g_icmp_error;
return $g_icmp_error;
}
echo ping('1.0.0.3', 2000);
echo $g_icmp_error.PHP_EOL;
echo ping('1.0.0.3', 2000);
echo $g_icmp_error.PHP_EOL;
echo ping('1.0.0.4', 2000);
echo $g_icmp_error.PHP_EOL;
后续
geerlingguy/Ping测试了这个项目种的socket ping,发现用socket->content后再socket_send和socket_read,不会出现收到请求包。难道是socket_recvfrom和socket_sendto有问题?还是socket_select呢?一脸懵逼。
再后续:
socket_create 确实也可以作为服务端接受数据,通过阅读文档得知,socket_recvfrom接收数据时,可以获取来源ip,此时可以直接进行判断,非预期的会包直接丢弃。check socket_recfrom host
相关
bjun.tech about ping