ping
昔 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 でも実装してみようと思う。