Perl入門ゼミ

  1. Perl
  2. ソケット

ソケットによる通信を行う

Perlの「ソケット」に関する解説です。

ソケットとは

ソケットはネットワークを通じてデータを交換したい場合に使用されます。たとえば、Webサーバはネットワークごしにブラウザとデータを交換します。このとき内部の実装でソケットが使用されています。

Perlのモジュールには、LWP::UserAgentというWebサイトのページを取得するためのモジュールがあります。この実装のもっとも低レベルの部分を見ればソケットが使用されています。

ネットワークを通じてデータを交換したいアプリケーションを作りたい場合は、ソケットを使うか、ソケットを簡単に使用できるようにしたラッパークラスを使用します。

ソケットのイメージ

ソケットは一種のファイルハンドルだと考えるのが一番わかりやすいと思います。ファイルを書き込みモードでオープンするには

my $fh;
my $file = 'somefile';
open($fh, ">", $file)

のようにします。このようにオープンすると

print $fh 'aaa';

print 関数を使用してファイルに書き込むことができます。ソケットの場合もよく似ています。ソケットを作成してそこに書き込めば、ネットワークごしのコンピュータはそれを読み取ることができます。

簡単に解説するため詳細は省略しますがソケットを開いて書き込むには、以下のように記述します。

my $sock;
socket($sock, 引数省略);

print $sock 'aaa';

socket関数が、open関数に対応しています。書き込むときはprint関数で書き込むことができます。

ソケットの勉強は難しい

ソケットの勉強は難しいです。難しい原因がいくつかあります。

ソケットを作成する手続きが煩雑

ソケットを作成する手続きが煩雑です。ソケットを暗記して使えるようになるには、何回も何回もソースコードを書く必要があります。

通信するにはクライアントとサーバの2つのプログラムが必要

ソケットを勉強するのが難しいのは、クライアントとサーバの2つのプログラムを書く必要があるということです。クライアントだけを作成しても、通信相手がいなければそのプログラムを試すことができないからです。このような難しさがあるので、最初はクライアント側のプログラムの作成に専念できるように最初はサーバ側のプログラムはこちらで準備します。

通信相手の状態が不確定

ネットワークごしに通信するので、相手の状態が不確定です。ローカルにファイルのようにほぼ確実に書き込めるという保障はどこにもありません。突然の切断があったり、遅延があったりします。そのようなエラーに対処できるようにプログラムを書く必要があるということです。

ソケットの一番簡単なサンプル。エコーサーバ

まず最初に実行できるソケットのプログラムのサンプルを示します。クライアントがHelloという文字を送ると、サーバがその文字列にecho:とう文字列を追加してクライアントに返すプログラムです。

このサンプルは完全なものではありませんが、クライアントとサーバの通信を学習するための基礎になります。クライアント・サーバプログラムの中ではもっとも簡単な部類に入るものですが、普通のプログラムから見ると相当難しいです。最初はサンプルが動くところを見てください。

クライアント側のプログラム echo_client.pl

サーバに文字列を送信して、受け取った文字列を出力するプログラムです。

use strict;
use warnings;
use Socket;

# クライアント
# 1. ソケットの作成
my $sock;
socket($sock, PF_INET, SOCK_STREAM, getprotobyname('tcp' ))
  or die "Cannot create socket: $!";

# 2. ソケット情報の作成

# 接続先のホスト名
my $remote_host = 'localhost';
my $packed_remote_host = inet_aton($remote_host)
  or die "Cannot pack $remote_host: $!";

# 接続先のポート番号
my $remote_port = 9000;

# ホスト名とポート番号をパック
my $sock_addr = sockaddr_in($remote_port, $packed_remote_host)
  or die "Cannot pack $remote_host:$remote_port: $!";

# 3. ソケットを使って接続
connect($sock, $sock_addr)
  or die "Cannot connect $remote_host:$remote_port: $!";

# 4. データの書き込み
# 書き込みバッファリングをしない。
my $old_handle = select $sock;
$| = 1; 
select $old_handle;

print $sock "Hello";

# 書き込みを終了する
shutdown $sock, 1;

# 5. データの読み込み
while (my $line = <$sock>) {
  print $line;
}

# 6. ソケットを閉じる
close $sock;

2. サーバ側のプログラム echo_server.pl

サーバ側のプログラムです。クライアントから受け取った文字列にecho:という文字列を加えてクライアントに返します。

use strict;
use warnings;
use Socket;

# サーバ

# 1. 受付用ソケットの作成

my $sock_receive;
socket($sock_receive, PF_INET, SOCK_STREAM, getprotobyname( 'tcp' ))
  or die "Cannot create socket: $!";

# 2. 受付用ソケット情報の作成
my $local_port = 9000;

my $pack_addr = sockaddr_in($local_port, INADDR_ANY);

# 3. 受付用ソケットと受付用ソケット情報を結びつける
bind($sock_receive, $pack_addr)
  or die "Cannot bind: $!";

# 4. 接続を受け付ける準備をする。
listen($sock_receive, SOMAXCONN)
  or die "Cannot listen: $!";

# 5. 接続を受け付けて応答する。
my $sock_client; # クライアントとの通信用のソケット

while (accept( $sock_client, $sock_receive )) {
  my $content;
  
  # クライアントからのデータの読み込み
  while (my $line = <$sock_client>) {
    $content .= $line;
  }
  
  # クライアントへのデータの書き込み
  print $sock_client "echo: $content";
  close $sock_client;
}

3. サンプルの実行

このサンプルを実行するには、まず

perl echo_server.pl

として、サーバを立ち上げます。サーバはクライアントからの要求を待機します。

次に、クライアント側のプログラムを実行するために、もうひとつ端末を立ち上げます。そして、

perl echo_client.pl

とします。すると、エコーサーバから応答が返ってきて、

echo: Hello

と表示されます。

ソケットを作成する

クライアント側のソケットの作成の部分の解説です。

use Socket;

# 1. ソケットの作成
my $sock;
socket($sock, PF_INET, SOCK_STREAM, getprotobyname('tcp'))
  or die "Cannot create socket: $!";

1. ソケットの作成

ソケット関連の関数や定数を使用するためには

use Socket;

とします。ソケットを作成するにはsocket関数を使用します。

my $sock;
socket($sock, PF_INET, SOCK_STREAM, getprotobyname('tcp' ))

socket関数は、4つの引数をとります。第1引数は、読み書きを行いたいソケットを指定します。my $sock; で宣言した $sockを第1引数に渡してsocket関数が成功した場合、$sockを通してネットワークごしにデータを読み書きできるようになります。

第2引数には、ドメインを指定します。第3引数には、タイプを指定します。第4引数には、プロトコル番号を指定します。

2. ドメインの指定

socket関数の第2引数はドメインを指定します。非常にややこしくいのですが、ホスト名とよく似た意味で利用されるドメイン名とは何の関係もありません。語感としてはドメインを日本語に訳した領域という言葉をイメージするのが良いかもしれません。よく使用されるドメインには、PF_UNIXドメインPF_INETドメインとがあります。

ドメイン 範囲
PF_UNIX ローカル
PF_INET インターネット

PF_UNIXドメインは、ひとつのローカルのコンピュータの領域を表現します。つまり通信の範囲は、ローカルなコンピュータの中でだけということを意味します。

PF_INETドメインは、インターネットで扱われる領域を表現します。つまり世界中のコンピュータが通信の範囲になるということを意味します。

今回のサンプルはインターネットを通じて通信しようとしているので、ドメインとしてPF_INETを指定しています。ちなみにPFという頭文字はプロトコルファミリーという言葉の省略系です。

3. タイプの指定

socket関数の第3引数にはタイプを指定します。TCPを使用して通信したい場合は、SOCK_STREAMを指定します。UDPを使用して通信したい場合は、SOCK_DGRAMを指定します。

タイプ 通信方式
SOCK_STREAM TCP
SOCK_DGRAM UDP

TPCは信頼性が高い通信方式です。相手との接続を確認してからデータの交換を始めます。またネットワーク上でデータが喪失したとしても、再送を行って確実にデータを送り届けてくれる通信方式です。多くのアプリケーションはTCPを使って通信を行います。

UPDは信頼性が低いですが、速度が早い通信方式です。相手との接続を確認しないでデータを送信します。またネットワーク上データが喪失しても再送してくれません。音声や動画など一部のデータが喪失しても十分データとして意味があるもので速度が優先される場合はUDPを使用します。

文字列を通信する場合は一部が喪失するとデータが意味を成さなくなるのでTCPを使って通信する必要があります。

4. プロトコル番号の指定

socket関数の第4引数にはプロトコル番号を指定します。

socket($sock, PF_INET, SOCK_STREAM, getprotobyname('tcp'))

プロトコルといってもHTTPやFTPなどのアプリケーション階層のプロトコルではなくて、TCP、UDPなどのトランスポート層のプロトコルを番号で指定します。

プロトコル名からプロトコル番号を取得するには、getprotobyname関数を使用します。OSに存在するプロトコル名とプロトコル番号の対応を示すファイルを参照して、プロトコル名からプロトコル番号を取得します。プロトコル名は小文字で指定します。

プロトコル名とプロトコル番号の対応を示すファイルはWindowsの場合は

C:\WINDOWS\system32\drivers\etc\protocol

にあります。Unix系OSの場合は、

/etc/protocols

にあると思います。(Fedora7で確認)

これらのファイルの中身は

# <protocol name>  <assigned number>  [aliases...]   [#<comment>]
ip       0     IP       # Internet protocol
icmp     1     ICMP     # Internet control message protocol
ggp      3     GGP      # Gateway-gateway protocol
tcp      6     TCP      # Transmission control protocol
egp      8     EGP      # Exterior gateway protocol
pup      12    PUP      # PARC universal packet protocol
udp      17    UDP      # User datagram protocol
hmp      20    HMP      # Host monitoring protocol
xns-idp  22    XNS-IDP  # Xerox NS IDP
rdp      27    RDP      # "reliable datagram" protocol
rvd      66    RVD      # MIT remote virtual disk

のようになっています。

5. エラー処理

エラー処理を行っておいたほうが無難です。

socket($sock, PF_INET, SOCK_STREAM, getprotobyname('tcp'))
  or die "Cannot create socket: $!";

socket関数は失敗するとundefを返して $! にエラー内容を設定します。

6. ソケットの学習のコツ

ソケットを使えるようになるには、かなりの努力が必要です。ソケットの作成だけを見ても理解しなければならないことが多いということがわかります。ソケットを覚えるこつは何回もタイピングすることです。見ているだけではどうやっても覚えられません。

ソケットの接続先の情報を作成する

ソケットの接続先の情報の作成について解説します。エコーサーバのサンプルのクライアント側のプログラムの以下の部分です。

# 2. ソケット情報の作成

# 接続先のホスト名
my $remote_host = 'localhost';
my $packed_remote_host = inet_aton($remote_host)
  or die "Cannot pack $remote_host: $!";

# 接続先のポート番号
my $remote_port = 9000; 

# ホスト名とポート番号をパック
my $sock_addr = sockaddr_in($remote_port, $packed_remote_host)
  or die "Cannot pack $remote_host:$remote_port: $!";

ソケットを使ってサーバに接続するのですが、そのときに接続先のサーバの情報が必要になります。

1. 接続先のホスト名あるいはIPアドレス

まず必要になるのは接続先のホスト名あるいはIPアドレスです。インターネットでは、コンピュータを一意に識別するためにIPアドレスが使用されます。またそれに対応するホスト名を持っているのが一般的です。ホスト名かIPアドレスのどちらかが利用できます。

# 接続先のホスト名
my $remote_host = 'localhost';

あるいは

my $remote_host = '127.0.0.1';

127.0.0.1はループバックアドレスと呼ばれるもので、localhostというのは、ループバックアドレスに対応するホスト名です。ループバックアドレスというのは自分自身指し示す仮想的なアドレスのことです。

ループバックアドレスを使用すると、まるでネットワークを通して自分自身と通信しているような効果が得られます。ローカルの環境でサーバとクライアントの試験を行いたいのでこのようなことをしています。

実際にインターネットを通して通信するときは、サーバのコンピュータのIPアドレスかホスト名を指定します。

2. 文字列のホスト名をバイナリ形式に変換する

ホスト名あるいはIPアドレスは文字列のままでは利用できませんinet_aton関数を使用してバイナリ形式に変換する必要があります。

inet_atonに与えられた文字列がホスト名だった場合は対応するIPアドレスに変換されてからさらに、IPアドレスを表現するバイナリ形式に変換されます。

my $packed_remote_host = inet_aton($remote_host)
  or die "Cannot pack $remote_host: $!";

atonのaはasciiという意味で、nはnetworkという意味です。バイナリ形式というのを正確にいうとネットワークバイトオーダで表現された4バイトの数値です。

3. ポート番号

IPアドレスやホスト名がコンピュータを一意に識別するものであるのに対し、ポート番号はそのコンピュータの中で動くアプリケーションを識別するのに使用されます。

# 接続先のポート番号
my $remote_port = 9000;

ポート番号は0~65535まで使用できます。ただし、0~1023は一般的なポート番号なので、1024~65535を使うようにします。クライアントとサーバで通信するには、同じポート番号を使用するようにします。

4. ポート番号とIPアドレスをひとまとめにする

次にポート番号とIPアドレスをsockaddr_in関数を使ってひとまとめにする必要があります。

# ホスト名とポート番号をパック
my $sock_addr = sockaddr_in($remote_port, $packed_remote_host)
  or die "Cannot pack $remote_host:$remote_port: $!";

第1引数にポート番号、第2引数にバイナリ形式に変換されたIPアドレスです。このようにして作成した $sock_addr が接続のためのconnect関数の引数に渡されることになります。

サーバに接続する - connect関数

サーバへ接続するにはconnect関数を使用します。

# 3. ソケットを使って接続
connect( $sock, $sock_addr )
  or die "Cannot connect $remote_host:$remote_port: $!";

サーバに接続する

サーバに接続するにはconnect関数を使用します。第1引数は、socket関数で作成したソケットです。

第2引数は「sockaddr_in」関数で作成した接続先のポート番号とIPアドレスの情報]です。

この2つを引数に与えてconnect関数を呼び出すと、$sockは読み書き可能なソケットになります。$sockを使ってサーバとデータのやり取りを行うことができます。