ping

iTunesPing という機能が付いた。

ping コマンドをネットワークの勉強がてら実装したことがあったのを思い出した。そのときは C 言語しか知らなかったので C で実装した。今は C 以外にも幾つか使える言語ができたので、それらで実装してみようと思った。

PHP

用意されているソケット関連の関数が C 言語とほぼ同じなので、以前 C で実装したものと似た感じになっていると思う(昔の記憶をたよりに作ったので似るのは当然なんだろうけど)。
C と大きく違うのは pack(), unpack() を使っているところか。C の場合はメモリの内容をそのまま送信できたけど、PHP では pack() でバイナリ化してやらないといけない。
バイナリ値を扱う場合は C みたいなメモリを直接操作できる言語の方が楽だと思う。
ちなみに、この実装だと Ctrl+C で止めようとしても即時に終了せず少し待たされる。どうも recvfrom() が SIGINT では復帰しないのが原因っぽい。SIGALRM だと復帰するんだけど。ノンブロッキングモードにすれば解決かな。

<?php
declare(ticks = 1);

$icmp_sock = socket_create(AF_INET, SOCK_RAW, getprotobyname('icmp'));
$icmp_addr = gethostbyname(@$argv[1]);
$icmp_id   = mt_rand(0x00, 0x7fff);
$icmp_seq  = 1;
$icmp_stat = array();

pcntl_signal(SIGALRM, 'sig_handler');
pcntl_signal(SIGINT,  'sig_handler');

sig_handler(SIGALRM);

while(true) {
  $res = @socket_recvfrom($icmp_sock, $buf, 1024, 0, $from, $port); 
  if ($res === false) {
    // error
  } else if ($res > 0) {
    parse_echo_reply($buf);
  }
}

function parse_echo_reply($buf)
{
  $revc_time = floatval(microtime(true));

  $header = unpack('Ctop/Ctos/nlen/nid/nflag/Cttl/Cproto/nchecksum/Nsrc/Ndst', $buf);
  $ihl    = $header['top'] & 0x0f;
  $ttl    = $header['ttl'];
  $src    = long2ip($header['src']);
  $header = unpack("N{$ihl}/Ctype/Ccode/nchecksum/nid/nseq/a64data", $buf);
  $type   = $header['type'];
  if ($type == 0) {
    $seq    = $header['seq'];
    $send_time = floatval($header['data']);

    $time = ($revc_time - $send_time) * 1000.0;
    $time = round($time, 3);

    echo "64 bytes from {$src}: icmp_seq={$seq} ttl={$ttl} time={$time} ms\n";
  } else if ($type === 3) {
    echo "Destination Unreachable\n";
  }
}

function sig_handler($signo)
{
  if ($signo === SIGALRM) {
    global $icmp_sock, $icmp_id, $icmp_seq, $icmp_addr;

    $msg = make_echo_message($icmp_id, $icmp_seq++);
    $len = strlen($msg);
    if (@socket_sendto($icmp_sock, $msg, $len, 0, $icmp_addr, 0) === false) {
      exit(socket_strerror(socket_last_error()) . "\n");
    }
    pcntl_alarm(1);

  } else if ($signo === SIGINT) {
    exit;
  }
}

function make_echo_message($id, $seq)
{
  $message = pack('C2n3a64', 0x08, 0x00, 0x0000, $id, $seq, microtime(true));
  list($message[2], $message[3]) = checksum($message);
  return $message;
}

function checksum($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);
}
Ruby

Ruby版 は PHP 版と比べて少しスッキリした感じになった。Socket.recvfrom や Socket.unpack_sockaddr_in など結果を内部で解析してくれるメソッドのおかげかな。

require 'socket'

ICMP_ECHO = 8

def checksum(data)
  data += "\0" if data.size % 2 == 1
  data.unpack('n*').each {|v| sum += v}
  sum  = (sum >> 16) + (sum & 0xffff)
  sum += (sum >> 16)
  ~sum
end

icmp_sock = Socket.new(Socket::AF_INET, Socket::SOCK_RAW, Socket::IPPROTO_ICMP)

Thread.start do
  id       = $$ & 0xffff
  seq      = 1 
  addr     = IPSocket.getaddress(ARGV[0])
  sockaddr = Socket.sockaddr_in(0, addr)
  icmp_msg = [
    ICMP_ECHO, # type
    0,         # code
    0,         # check sum
    id,        # identifier
    0,         # sequence
    0          # data
  ]

  loop do
    icmp_msg[4] = seq
    icmp_msg[5] = Time.new.to_f.to_s 
    msg   = icmp_msg.pack("C2n3a64")
    cksum = checksum(msg)
    msg[2], msg[3] = cksum >> 8, cksum & 0xff
    icmp_sock.send msg, 0, sockaddr
    seq += 1
    sleep 1 
  end

end

loop do
  msg, sockaddr = icmp_sock.recvfrom(1024)
  recv_f    = Time.now.to_f
  head      = msg.unpack('C2n3C2nN2')
  ihl       = head[0] & 0x0f
  ttl       = head[5]
  head      = msg.unpack("N#{ihl}C2n3a64")
  seq       = head[ihl + 4]
  send_f    = head[ihl + 5].to_f 
  port, src = Socket.unpack_sockaddr_in(sockaddr) 

  time = sprintf('%.2f', (recv_f - send_f) * 1000.0)
  puts "64 bytes from #{src}: icmp_seq=#{seq} ttl=#{ttl} time=#{time} ms"
end
その他

今の時点では厳しいけど、そのうち Haskell でも実装してみようと思う。