Socketとかがよくわからずハマったメモ

Test::TCPを使って何故かハマった

http://d.hatena.ne.jp/tokuhirom/20080817/1218953905 を見つつ、試しに書いてみた

#!/usr/bin/perl
use strict;
use warnings;
use Test::TCP;
use IO::Socket::INET;

test_tcp(
    client => sub {
        my $port1 = shift;

        test_tcp(
            client => sub {
                my $port2 = shift;
                while (1) { sleep 1; }
            },
            server => sub {
                my $port2 = shift;
                server($port2);
            },
        );
    },
    server => sub {
        my $port1 = shift;
        server($port1);
    },
);

sub server {
    my $port = shift;
    warn $port;

    my $sock = IO::Socket::INET->new(
        LocalPort => $port,
        LocalAddr => '127.0.0.1',
        Proto     => 'tcp',
        Listen    => 5,
        Type      => SOCK_STREAM,
    ) or die "Cannot open server socket: $!";

    while (my $remote = $sock->accept) {
        while (my $line = <$remote>) {
            print {$remote} $line;
        }
    }
}
$ perl hoge.pl
10001 at hoge.pl line 30.
10002 at hoge.pl line 30.

test_tcpの中で2つサーバを動かしても、空いているポートを自動的に見つけて振り分けてくれるらしい。


ここで、サーバをAnyEvent::Socketを使ったものにしてみた。

sub server {
    my $port = shift;
    warn $port;

    my $cv = AnyEvent->condvar;
    tcp_server undef, $port, sub {};
    $cv->recv;
}

すると、

$ perl fuga.pl
10001 at fuga.pl line 30.
10001 at fuga.pl line 30.
bind: Address already in use at fuga.pl line 33

portがぶつかってしまった。
でも、これが起こるのは自分のMacBookの環境で、他のLinux環境ではちゃんと別々のportに振り分けられた。

何が起こってるの?

AnyEvent::Socket::tcp_server、Test::TCP::empty_portの実装を少し追ってみた。
tcp_server の中身はsocket作ってbindしてlistenしたり、とやっているらしい。簡略化すると

use Socket qw(AF_INET SOCK_STREAM inet_aton sockaddr_in);
use IO::Socket::INET;

my $fh;
socket $fh, AF_INET, SOCK_STREAM, 0 or die $!;
my $name = sockaddr_in($port, inet_aton($host));
bind $fh, $name or die $!;
listen $fh, 128 or die $!;

というカンジ?


一方、Test::TCP::empty_portでは、IO::Socket::INETを使ってsocketを作成できるかどうかでportの空きを見ているようだ。

my $sock = IO::Socket::INET->new(
    Listen    => 5,
    LocalAddr => '127.0.0.1',
    LocalPort => $port,
    Proto     => 'tcp',
    (($^O eq 'MSWin32') ? () : (ReuseAddr => 1)),
);
return $port if $sock;

まぁこの中でも同じようにsocket作ってbindしてlistenしてるのか…

tcp_serverの第1引数

AnyEvent::Socketのtcp_serverについてのPODをよく読んでみると、

For internet sockets, $host must be an IPv4 or IPv6 address (or "undef", in which case it binds either to 0 or to "::", depending on whether IPv4 or IPv6 is the preferred protocol, and maybe to both in future versions, as applicable).

って書いてある。後半の意味がよく分からないのだけど…
とりあえず$hostにはundefを指定できるけど、この場合 0というアドレスにbindされることになるらしい。'127.0.0.1'とは違うのか!


試しに最初にハマったやつを

sub server {
    my $port = shift;
    warn $port;

    my $cv = AnyEvent->condvar;
    tcp_server '127.0.0.1', $port, sub {};
    $cv->recv;
}

って書き直したらちゃんと動くようになった…!

環境による違い?

なんでMacではダメでLinuxではアレで動いていたんだろう?という疑問。

#!/usr/bin/perl
use strict;
use warnings;
use AnyEvent::Socket;

my $cv =  AnyEvent->condvar;
tcp_server undef, 10001, sub {};
tcp_server '127.0.0.1', 10002, sub {};
$cv->recv;

というコードを走らせて、裏で使用portを調べてみる。

$ netstat -an | grep 1000.
tcp4       0      0  127.0.0.1.10002        *.*                    LISTEN
tcp4       0      0  *.10001                *.*                    LISTEN
$ netstat -an | grep 1000.
tcp        0      0 0.0.0.0:10001               0.0.0.0:*                   LISTEN      
tcp        0      0 127.0.0.1:10002             0.0.0.0:*                   LISTEN

Local Addressの扱いが環境によって違うっぽい。これが$host = undefでの挙動の違いになる…のかな?


ちょっとマジメにTCPの勉強したほうが良さそうだ… orz