コンピューター:C言語講座:TCP/IPプログラム(その1:クライアント)
概要
TCP/IPはイーサーネットなどで使用される標準的なプロトコルで、以前はワークステーション系でのみ使用されていた感じでしたが、最近ではインターネットやイントラネットさらにWindows95の普及によって、非常に広い分野で使われてきています。プロトコル自体は文献がたくさんでておりますし、それほど細かく知らなくても問題は無いのですが、いざ、自分でTCP/IPを使用したプログラムを作成しようとすると以外と文献が少ないものです。
ここでは、実際に通信することを目的として(プロトコルの研究ではなく)、まずはクライアントプログラムを作成してみます。詳細な内容まではとてもホームページに簡単にまとめられる量ではないので、それは文献を参照してください。
通信
通信といっても非常に幅が広いのですが、プログラミングで言うところの通信としては、やはりプロセス間の通信が主体となります。プロセス間通信が行なえるとそれまで単独で機能していたアプリケーションを連携させて非常に応用の効く使い方が出来るようになります。
通信にもいろいろな方法があり、最も身近な物ではパイプのように1対1で標準入出力を利用してプロセス間で通信する方法もあります。これについては「fork,exec,pipeについて」にも記述してあります。また、作業用ファイルを利用して通信したり、共有メモリーを用いても通信できます。
これらさまざまな通信手段の中でTCP/IPとはいったいどのような特徴があるかを知ることがこのプロトコルを使用する第一歩となると思うのですが、前述した方法に比べ、TCP/IPは汎用性が非常に高い点が最大の特徴ではないでしょうか?TCP/IPを使用すれば異なるハードウエアー間でも同じ手段で通信が出来ますし、前に述べたようにインターネットのプロトコルであることから世界中を相手に通信が行なえます。
当然、便利な点ばかりではなく、パイプなどのどちらかと言うとアプリケーション寄りの通信手段に比べるとあらかじめ設定しておかねばならない事が非常にたくさんあり、環境を整えるまでが大変ですが、PCではWindows95になって非常に簡単にTCP/IPも使用できるようになりましたし、ワークステーションではもともと広く普及していた技術なので問題無く使用できるでしょう。
クライアントプログラム
はじめからサーバを作成してもかまわないのですが、やはり簡単な方からが良いと思いますので、クライアントプログラムを作成してみましょう。とりあえず、TCP/IPで何かにつなぎ、標準入力から入力した文字列を送信し、相手からの文字列を標準出力に表示する物を作ってみます。何につなぐかが問題ですが、とりあえず文字ベースでやりとりが出来るftpやsmtpにつないでみることにします。
TCP/IPでは接続先の指定方法として、相手のホストアドレスとポート番号が必要です。ホストアドレスとしては192.1.1.96のようにイーサーネットアドレスを直接指定してもかまいませんが、一般的にはUNIXでは/etc/hosts、Windows95では\windows\hostsに記述したホスト名を使用します。DNSやNISを使用している場合はhostsに記述されていない名前も使用できるかも知れません。また、ポート番号も直接数字を指定してもかまわないのですが、一般的にはUNIXでは/etc/services、Windows95では\windows\servicesに記述した名前を使用するようになっています。
ここでは起動時の引数があれば1つめの引数をホスト名とし、2つめの引数をポート名とするようにします。
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <sys/stat.h>
#include <sys/param.h>
#include <sys/time.h>
int Soc=NULL;
void CloseSocket();
void main(argc,argv)
int argc;
char *argv[];
{
struct servent *se;
struct hostent *servhost;
struct sockaddr_in server;
struct timeval timeout;
fd_set Mask,readOk;
int width;
char buf[512];
int ret,error;
char hostname[MAXHOSTNAMELEN];
char port[80];
if(argc<2){
gethostname(hostname,MAXHOSTNAMELEN);
}
else{
strcpy(hostname,argv[1]);
}
if(argc<3){
strcpy(port,"ftp");
}
else{
strcpy(port,argv[2]);
}
se=getservbyname(port,"tcp");
if(se==NULL){
perror("getservbyname");
exit(0);
}
servhost=gethostbyname(hostname);
if(servhost==NULL){
u_long addr;
addr=inet_addr(hostname);
servhost=gethostbyaddr((char *)&addr,sizeof(addr),AF_INET);
if(servhost==NULL){
perror("gethostbyaddr");
exit(0);
}
}
if((Soc=socket(AF_INET,SOCK_STREAM,0))<0){
perror("socket");
exit(0);
}
memset((char *)&server,0,sizeof(server));
server.sin_family=AF_INET;
server.sin_port=se->s_port;
memcpy((char *)&server.sin_addr,servhost->h_addr,servhost->h_length);
if(connect(Soc,&server,sizeof(server))== -1){
perror("connect");
exit(0);
}
signal(SIGINT,CloseSocket);
signal(SIGTERM,CloseSocket);
FD_ZERO(&Mask);
FD_SET(Soc,&Mask);
FD_SET(0,&Mask);
width=Soc+1;
error=0;
while(1){
readOk=Mask;
timeout.tv_sec=0;
timeout.tv_usec=100000;
switch(select(width,(fd_set *)&readOk,NULL,NULL,&timeout)){
case -1:
perror("select");
break;
case 0:
break;
default:
if(FD_ISSET(0,&readOk)){
fgets(buf,sizeof(buf),stdin);
if(strlen(buf)>0){
ret=send(Soc,buf,strlen(buf),0);
if(ret== -1){
perror("send");
error=1;
break;
}
}
}
if(FD_ISSET(Soc,&readOk)){
ret=recv(Soc,buf,sizeof(buf),0);
if(ret<=0){
perror("recv");
error=1;
break;
}
buf[ret]='\0';
fprintf(stderr,"%s",buf);
}
break;
}
if(error){
break;
}
}
CloseSocket();
exit(0);
}
void CloseSocket()
{
if(Soc!=NULL){
close(Soc);
Soc=NULL;
}
exit(0);
}
少々長いのですが、これでも出来るだけmainだけに収めて短くしてみました。本来はもうすこし構造的に作成したほうが良いのですが。インクルードファイルが非常に多いのですが、私も毎回調べていないのでいらないのもあるかも知れませんが、netinet/in.h,netdb.hなどはイーサーネットを使用する場合には非常に重要です。
mainのはじめのほうから順に説明しますと、まず、引数からホスト名・ポート名を得ています。無い場合にはローカルホスト・ftpとなります。ローカルホストはgethostname()で得られます。また、ポート名には/etc/servicesに記述されているtcpのポートを指定してください。ただ、ftp,smtpなどの単純なもの以外ではうまくテストできないかも知れません。
つぎに、ポート名からstruct servent型のデータを取り出します。getservbyname()を使用します。また、ホスト名からstruct
hostent型のデータを取り出しますが、gethostbyname()でホスト名から得られない場合に192.1.1.96のようなアドレスでも使える方が便利なので、inet_addr(),gethostbyaddr()でアドレス名から得るようにしました。
続いてsocket()を使用してソケットを作成します。これは通信用の終点と定義されるようですが、電気のソケットのように差し込み口みたいな物と考えておけば良いでしょう。ソケットにはいろいろなタイプがあるのですが、ここではイーサーネット型でストリーム型を使用します。
つぎに、ソケットを用いて、connect()でサーバに接続します。struct
sockaddr_in型にパラメータをセットして渡します。渡すものはホストとポートの情報です。
接続が成功すれば使用できるのですが、死ぬ時を考え、まずはシグナルをキャッチするようにしておき、割り込みが起きた時にソケットを閉じてから死ぬようにします。
ここから先は実際にやりとりするのですが、ここでは入力と出力を多重化する為にselect()を使用してみました。これは普通に読み込みと書き込みのループをまわしてしまうと読み込み時に何も入力されないとブロッキングされてしまい、通信でメッセージが送られてきてもそれを受け取る事が出来ない状態になるのを防ぐ為で、読み込み命令前にselect()で読み込み・書き込みどちらがデータがある状態かを調べてから実際に読み込みするように出来ます。select()を使用しなくてもioctl()などでも同様のことが出来ますし、入力をノンブロッキングにしても目的は達成できますが、select()以外ではループが回り続けるのでCPUを非常に忙しくします。select()ではタイムアウトを設定できますので、入力が無くてもタイムアウトするまでは待っていてくれるのでCPUは楽になります。
select()を使用する為にソケットと標準入力(0)をマスクとしてセットしておきます。それから無限ループに入りますが、select()のタイムアウトはここでは100000msecにしてあります。このプログラムの場合は入出力の待ちデータが無い場合は何もする必要が無いのでタイムアウトは長くて問題ありません。
標準入力から入力があった場合はわかりやすくfgets()で一行読み込み、send()でソケットに書き込みます。UNIXの場合はwrite()でも問題無く書き込めますが、Windows95ではソケットとファイルディスクリプタは全く別物なので必ずsend()を使わなければなりません。
ソケットからデータがきていればrecv()で受け取り、標準エラー出力にfprintf()で表示します。標準出力でもかまわないのですが、簡単にバッファリングを無くせるのでとりあえずここではエラー出力に出しています。
エラーがあった場合にはたいていはサーバが死んだとかですが、無限ループを抜けるようにしてあります。
プログラムを終えるときは割り込み時も含めてCloseSocket()という関数でソケットをclose()してから死ぬようにしました。
このソースはUNIX上で作成したのでWindows95などの場合はかなり手直しが必要ですが、私はどちらでも動く状態には出来ましたので基本的にはこの流れでソケットを使用できます。
使用方法としては、「実行ファイル名
ホスト名 ftp」とすると、ホスト名のftpサーバに接続できます。すると、
220 sunr1 FTP server (SunOS 4.1) ready.
という感じにftpサーバからメッセージが送られてきます。help[リターン]で説明が見れますが、何もここでftpすることはないので、quit[リターン]で抜けましょう。smtpに接続すると、
220 sunr1.PLASMA Sendmail 4.1/SMI-4.1 ready at Wed, 18 Jun 97
17:06:16 JST
という感じのメッセージがでます。ここもhelp[リターン]で説明が見れますが、通信が確認できれば良いのでquit[リターン]で抜けます。
まとめ
少し長いサンプルになってしまいましたが、いかがでしたか?ただサーバにつないでデータをやりとりするだけなら以外と簡単に出来ると思いませんか?
ただ、実は通信プログラムには大きなワナ?があって、今回のサンプルのような短いメッセージのやりとりなら問題無いのですが、大きなデータ、一般的に4096バイトを超えるメッセージの双方向のやりとりでは注意が必要です。これはソケットに限ったことではなく、通信プログラム全般の問題なのですが、通信はファイルに書き込むのに似ていますが、ファイルの場合はディスクがいっぱいにならなければ書き続けて閉じれば何とかなりますが、通信の場合は相手が受け取ってくれないとバッファーのサイズ(たいてい4096バイト)送った時点でストップしてしまいます。大きなデータを送るには何回もsend()し、相手が処理してくれるまでトライしなければなりません。
受け取りも同様にファイルの場合は何メガバイト読み込む時でも一度の命令で読み込んでくれますが、通信では最大バッファー単位でしか受け取れません。大きなデータを受け取るには何回かrecv()しなければならないのです。
今回接続したftp,smtpのような立派な人が作った?サーバではそういった問題は起きたとしてもクライアントが悪いくらいでしょうが、サーバも自分で作ると、こういったことを考えずに作って大きなデータをやりとりするとたちどころに両方とも固まります。お互いにデータを送りたがって受け取り処理をしないでいるといつまでたってもどちらにもデータが流れません。このような状態をデットロックと呼びますが、通信プログラムでは最も注意が必要な課題です。
実際にはプログラム中に送受信するたびに上記のような事を気にするのは大変なので、送受信それぞれにバッファーを持たせ、一度にやりとりできなくても問題無いように関数を準備しておくのが良いでしょう。と無責任に言っても申し訳ないので、C言語講座で近々取り上げてみたいと思います。
なお、Windows95ではこういった方法の他にMFCにソケット通信用のクラスが用意されていてもうすこし簡単に扱えるようです。ただ、MFCは調べるのが面倒で、私は必要に迫られていないので使ったことはありません。どなたかMFCが得意な方、投稿をお待ちしています。
参考:Windows95 VC++で動いたソース
テスト用に作ったものなのであまりまじめに考えていませんが、一応上のサンプルと同様に動きます。
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/types.h>
#include <winsock.h>
#include <sys/stat.h>
#define MAXHOSTNAMELEN 256
void main(argc,argv)
int argc;
char *argv[];
{
struct servent *se;
struct hostent *servhost;
struct sockaddr_in server;
int s;
FILE *fp;
char buf[512],*ptr;
WORD wVersionRequested;
WSADATA wsaData;
int err,ret,bytes;
u_long ulCmdArg;
char hostname[MAXHOSTNAMELEN];
char port[80];
if(argc<2){
gethostname(hostname,MAXHOSTNAMELEN);
}
else{
strcpy(hostname,argv[1]);
}
if(argc<3){
strcpy(port,"ftp");
}
else{
strcpy(port,argv[2]);
}
wVersionRequested = MAKEWORD(1, 1);
err = WSAStartup(wVersionRequested, &wsaData);
if (err != 0){
perror("WSAStartup");
exit(0);
}
if ( LOBYTE( wsaData.wVersion ) != 1 ||
HIBYTE( wsaData.wVersion ) != 1 ){
WSACleanup();
fprintf(stderr,"Version Error\n");
exit(0);
}
se=getservbyname(port,"tcp");
if(se==NULL){
perror("getservbyname");
WSACleanup();
exit(0);
}
servhost=gethostbyname(hostname);
if(servhost==NULL){
u_long addr;
addr=inet_addr(hostname);
servhost=gethostbyaddr((char *)&addr,sizeof(addr),AF_INET);
if(servhost==NULL){
perror("gethostbyaddr");
WSACleanup();
exit(0);
}
}
if((s=socket(AF_INET,SOCK_STREAM,0))<0){
perror("socket");
WSACleanup();
exit(0);
}
memset((char *)&server,0,sizeof(server));
server.sin_family=AF_INET;
server.sin_port=se->s_port;
memcpy(&server.sin_addr,servhost->h_addr,servhost->h_length);
if(connect(s,&server,sizeof(server))== -1){
perror("connect");
WSACleanup();
exit(0);
}
ulCmdArg=1;
ioctlsocket(s,FIONBIO,&ulCmdArg);
while(1){
ret=recv(s,buf,sizeof(buf),0);
if(ret==0){
perror("recv");
break;
}
else if(ret!= -1){
buf[ret]='\0';
if(ret!=0){
fprintf(stderr,"%s",buf);
}
}
buf[0]='\0';
while(1){
if(!kbhit()){
break;
}
fgets(buf,sizeof(buf),stdin);
if(strlen(buf)>0){
break;
}
}
if(strlen(buf)>0){
ret=send(s,buf,strlen(buf),0);
if(ret== -1){
perror("send");
break;
}
}
}
WSACleanup();
}