- 1 - ネットワークプログラミング(教科書 p.247

ネットワークプログラミング(教科書 p.247-312)
これまでに作成したプログラムは 1 台のコンピュータ上で動作するものだった。本日はネットワ
ーク上の別のコンピュータで実行しているプログラムと通信をしながら動作するプログラムの作
成方法について学ぶ。
ネットワークプログラミングを高度に使いこなすためには、関連する知識は幅広く求められる。
本日からの学習では単純なチャット(会話)を行うプログラムを題材として、ネットワークプログ
ラミングについての出発点を学んでいくことになる。
チャットを行うプログラムには Berkley Socket と呼ばれるライブラリを用いて、通信を行うプ
ログラムを作成する。
IP アドレス
コンピュータ間の通信では、それぞれのコンピュータを特定するために IP アドレスを使う。
「IP
アドレス」はちょうど自分たちの世界での「住所」や「電話番号」と同じ役割を果たす。お互いが
その情報を把握していれば、手紙や電話で情報をやり取りすることができるようになる。
自分が使用しているパソコンの IP アドレスを linux 上で確認するためにはターミナルを立ち上
げ、
「/sbin/ifconfig」とコマンドを入力する。すると、以下のような表示が得られる。
eth0
Link encap:Ethernet HWaddr 00:20:ED:5B:3E:F2
inet addr:192.168.1.31 Bcast:192.168.1.255 Mask:255.255.255.0
inet6 addr: fe80::220:edff:fe5b:3ef2/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:6220 errors:0 dropped:0 overruns:0 frame:0
TX packets:5403 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:6446053 (6.1 MiB) TX bytes:627205 (612.5 KiB)
lo
Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
inet6 addr: ::1/128 Scope:Host
UP LOOPBACK RUNNING MTU:16436 Metric:1
RX packets:2507 errors:0 dropped:0 overruns:0 frame:0
TX packets:2507 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:3116664 (2.9 MiB) TX bytes:3116664 (2.9 MiB)
いろいろと表示されるが、IP アドレスは eth0 の項目中に下線で示した「inet addr:」の後に表
示される。この例の場合の IP アドレスは「192.168.1.31」となる。
システムコール
通信に関わる処理は OS にその処理を任せることで簡単に扱うことができる。ディスプレイへの
表示やコンピュータ間の通信はハードウェアを考慮したプログラムとなるため、その処理をプログ
ラムで記述することは大変である。そのため、システムコールというものを利用することで対応す
るのが普通である。システムコールとは「OS への関数呼び出し」という意味が込められており、
-1-
OS に用意されている機能を呼び出すために使用される。以下に通信に関するシステムコールにつ
いて、教科書から引用したものを示す。
表1.通信に関するシステムコール一覧
ソケットの作成
ソケットに名前を付ける
接続の受付準備
接続要求
接続受理
データ転送
socket()
bind()
listen()
connect()
accept()
read(), write()
recv(), send()
recvfrom(), sendto()
shutdown(), close()
ioctl(), socketpair()
setsockopt(), getsockopt()
getsockname(), getpeername()
ソケットのクローズ
その他
システムコールの関数は見た目上、引数や返却値も持った今まで利用してきた普通の関数と同じ
であるため、ほとんど区別がつかない。しかし、マニュアルを表示するコマンドである「man」を
用いて、ターミナルで「man socket」のように入力すると以下のように表示される。
SOCKET(2) Linux Programmer ’s Manual SOCKET(2)
名前
socket - 通信のための端点(endpoint) を作成する
書式
#include <sys/types.h>
#include <sys/socket.h>
以下は省略
最初の行の下線部はSOCKET(2) と表示されており、この2という数字からシステムコールである
ことが分かる。普通の関数の場合では、例えば、「man sin」と入力すると、以下のように表示さ
れる。
SIN(3) Linux Programmer ’s Manual SIN(3)
名前
sin, sinf, sinl - 正弦(サイン) 関数
書式
#include <math.h>
double sin(double x);
float sinf(float x);
long double sinl(long double x);
-lm でリンクする。
以下は省略
SIN(3)と表示されており、この 3 という数字から普通の関数であると分かる。
-2-
通信を行うプログラムの例
ネットワークを利用して、コンピュータ間で会話を行うチャットプログラムの例を示す。これは
コンピュータからメッセージを送るクライアントプログラムと、コンピュータでメッセージを受け
取り、文字列を表示するサーバープログラムの2つを使用する。ネットワーク上のコンピュータで
はサービスを提供するパソコンをサーバーといい、サービスの提供を求めるパソコンをクライアン
トと呼ぶ。
テキストを送信するクライアントプログラム client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 5320
int main(int argc, char *argv[])
{
int soc;
char message[80], ip_address[16];
struct sockaddr_in server;
if(argc == 1)
{
printf("引数に IP アドレスが必要です\n");
exit(0);
}
strcpy(ip_address, argv[1]);
// コマンドライン引数の IP アドレスをコピー
soc = socket(PF_INET, SOCK_STREAM, 0);
// ソケットの作成
memset((char *)&server, 0, sizeof(server));
// アドレス構造体の初期化
// アドレス構造体のメンバに値を設定
server.sin_family = AF_INET;
server.sin_port = htons(PORT);
server.sin_addr.s_addr = inet_addr(ip_address);
connect(soc, (struct sockaddr *)&server, sizeof(server));
// メッセージを送る
while(1)
{
printf("Input message:");
fgets(message, 80, stdin);
if(strncmp(message, "exit", 4) == 0) break;
send(soc, message, strlen(message), 0);
}
-3-
//接続要求
close(soc);
return 0;
}
テキストを受信するサーバープログラム server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 5320
int main(void)
{
int soc, acc, size;
char buffer[80];
struct sockaddr_in client, server;
soc = socket(PF_INET, SOCK_STREAM, 0);
memset((char *)&server, 0, sizeof(server));
// ソケットの作成
// アドレス構造体の初期化
// アドレス構造体のメンバに値を設定
server.sin_family = AF_INET;
server.sin_addr.s_addr = htonl(INADDR_ANY);
server.sin_port = htons(PORT);
bind(soc, (struct sockaddr *)&server, sizeof(server));
size = sizeof(client);
listen(soc, 1);
acc = accept(soc, (struct sockaddr *)&client, &size);
// メッセージを受ける
while(1)
{
recv(acc, buffer, 80, 0);
printf("%s", buffer);
memset(buffer, 0, sizeof(buffer));
}
close(soc);
close(acc);
return 0;
}
-4-
// 接続要求の受理
まずは自分のパソコン上でこれらのプログラムの動作を確認する。それぞれのプログラムを作成
し、コンパイルを行い、別々のターミナルで実行する。サーバープログラムは今まで通りの実行方
法で良いが、クライアントプログラムについては、例えば実行ファイル名を client とし、自分の
パソコンの IP アドレスが 192.168.1.31 であるならば、以下のように実行する必要がある。
./client 192.168.1.31
自分のパソコンの IP アドレスは「/sbin/ifconfig」コマンドで確認することはすでに示した。
このプログラムの例の通信の手順に着目すると以下のような流れとなっている。
クライアント(client)
サーバー(server)
socket()
ソケットの作成
socket()
ソケットの作成
bind()
ソケットにアドレスを設定
listen()
ソケット接続準備
SYN
connect()
接続要求待ち
SYN+ACK
ACK
accept()
接続要求待ち
3 way handshake
send()
接続要求待ち
recv()
データ受信
close()
ソケット記述子をクローズ
close()
ソケット切断
close()
ソケット切断
図1 通信の順序
教科書の p.259-260 に書いてある通り、通信とファイルの読み書きの処理を比較すると、非常に
良く似ている。ここでは述べないが、デバイス(キーボードなどの外部機器)との入出力もファイ
ルの読み書きに似ている。コンピュータから見るとデータを入出力するものはすべてファイルとし
て取り扱われ、初期化の手続きやオプションの処理を除くとほとんど同じ方法で読み書きすること
ができる。
通信を行うためには通信相手のコンピュータとそのプログラムを特定しなくてはならない。コン
-5-
ピュータを特定するために「IP アドレス」を用い、プログラムを特定するには「ポート番号」を
用いる。コンピュータは多くの通信処理を同時に行っているため、どのプログラムなのか特定する
必要があるため、ポート番号を利用する。
通信に関わるシステムコールの概要について
表1に通信に使うシステムコールの一覧を示した。教科書に詳しい説明があるので、ここでは簡
単に概要をまとめておく。
1.初期化処理
初期化処理はソケットを作成する socket()とソケットに名前をつける bind()をセットで行う。
socket()
ソケットと呼ばれる通信ポートを作成する。サーバーとクライアントの間で通信を行うための口
(ソケット)を作成するものと考えればよい。socket システムコールを使うために必要なヘッダ
ファイルとそのプロトタイプ宣言は以下である。
#include <sys/socket.h>
#inlcude <sys/types.h>
int socket(int domain, int type, int protocol);
戻り値はファイルディスクリプタと呼ばれる整数値である。ファイルディスクリプタとはプログ
ラムがアクセスするファイルや標準入出力などを OS が識別するために使う整数値である。ここで
はこの整数値でソケットを区別する。引数は以下のような値を設定する。
・domain は通信領域の指定を行う。通常のインターネット接続であれば、
「PF_INET」と設定する。
これは、いわゆる IP(Interne protocol)である。protocol はプロトコルや通信規約と訳される。
・type は TCP 接続を利用するならば、
「SOCK_STREAM」と設定する。
・protocol は、0 と設定すればよい。
bind()
ソケットに名前を付ける。具体的には受信ポート番号を指定する。
#include <sys/socket.h>
#inlcude <sys/types.h>
int bind(int sock, struct sockaddr *addr, socket_t *addrlen);
2.接続受理準備
listen()
ソケットが接続を持っていることを OS に知らせる。
#include <sys/socket.h>
int listen(int sock, int backlog);
・sock はソケットのファイルディスクリプタ。
・backlog は受け付けるコネクションの最大数。
3.接続要求
connect()
-6-
クライアント側がサーバーに対して接続の要求を出す。
#include <sys/socket.h>
#inlcude <sys/types.h>
int connect(int sock, const struct sockaddr *addr, socket_t addrlen);
4.接続要求受理
accept()
ソケットに他のプログラム(プロセス)が接続してくるのを待ち、接続が完了したらファイルデ
ィスクリプタを戻り値として返す。受信にはこのファイルディスクリプタを使う。
#include <sys/socket.h>
#inlcude <sys/types.h>
int accept(int sock, struct sockaddr *addr, socket_t *addrlen);
5.データの送信
send()
データの送信を行う。
#include <sys/types.h>
#include <sys/socket.h>
size_t send(int s, const void *buf, size_t len, int flags);
6.データの受信
recv()
ソケットからメッセージを受け取る。
#include <sys/types.h>
#include <sys/socket.h>
size_t recv(int s, void *buf, size_t len, int flags);
7.終了処理
close()
ファイルディスクリプタを閉じる。
#include <unistd.h>
int close(int fd);
引数はファイルディスクリプタ(整数)である。
演習1 自分のパソコンの IP アドレスを調べなさい。
演習2 クライアントとサーバーのプログラムを実行し、自分自身とチャットを行いなさい。二つ
のターミナルを開いて通信を行う。
演習3 隣の人と通信しなさい。
演習4 通信に使ったファイルディスクリプタの番号を調なさい。socket() とaccept() システム
コールの戻り値であり、整数である。
-7-
余力があるものは以下を行いなさい。
演習5 accept() システムコールの第2引数を調べることにより、接続要求を行ってきたクライア
ントのIP アドレス、ポート番号を調べよ。
演習6 send() とrecv() システムコールではなく、write() とread() システムコールを使った
プログラムに書き直しなさい。
参考文献 このテキストは以下の文献を参考としました。
[1] C 言語によるプログラミング応用編第2 版、内田智史監修、オーム社
[2] UNIX ネットワークプログラミング、金内典充・今安正和著、株式会社オーム社
[3] プログラミング技術、伊理政夫他、実教出版株式会社
-8-