システム奮闘記:その49

ftpクライアントソフトの作成



Tweet

(2006年5月21日に掲載)
はじめに

  2005年1月に、C言語の最初の壁だったポインタが理解できた。
  ポインタと出会って、10年がかりだった。

  ポインタが理解できた時は、感激に浸っていたのだが、すぐに我に返って
嫌でも、次の現実を直視しないといけない。

  どうやって「失われた10年」を取り戻すか?

  これが緊急の課題になった。

  そこでC言語の勉強を行う事にした。主に、次の本を使った。  
  「Linuxプログラミング」(葛西重夫訳:ソフトバンク出版)だ。

  そして、一通り、C言語の勉強を終えた。
  そのお陰で、ソケットを活用したネットワークプログラムの練習として
足し算サーバープログラムが書けるようになったりした。
 詳しくは「システム奮闘記:その45」(足し算サーバープログラム)をご覧ください

  基本的なプログラムを書く方法は覚えたと思った。

  そして、Linuxの醍醐味と言われるカーネルのソースにまで手をつけた。
 詳しくは「システム奮闘記:その46」(CPUの特権モード。OSが固まる原因)をご覧下さい。


  だが、これで満足した訳ではなかった。

  簡単な実用アプリが書けへんし

 Apacheなどのソースも読めないし・・・

 という悶々とした物があった。

  この悶々とした気持ちを持ち続けて、2006年4月を迎えた。
  ふと思った。悶々とした物を吹き飛ばすため、C言語の読解力をつけよう!

  C言語の読解力をつけて、ソースを読む事によって、
プログラミングができるようにしようと考えた。
  ネットワークプログラムが組めるようになりたいと夢見る私は
次の本を読む事にした。

  「C言語による TCP/IP ネットワークプログラミング」
  (小俣光之:ピアソン・エデュケーション)

  しかし・・・

  ソース読みは退屈だ・・・ (--;;

  だった。
  そのため途中でソース読みをやめてしまった。

  だが、この時、ftpクライアントなどのアプリは、機能を複雑にしなければ
そんなに難しくない事を知った。

  なぜなら、ftpなら、ftpコマンドを、write()やsend()関数で送信して、
その結果を、read()やresv()関数で受信すれば良いだけだからだ!

  上の事を知っただけでも大きな収穫だと思う。

  そこで、簡単にsmtpクライアントっぽいプログラムを作ってみた。
  まずは、smtpサーバーに接続した時のやりとりの復習をした。


メールサーバーへtelnetで接続する様子
[suga@server]# telnet XXX.YYY.ZZZ.RRR smtp
Trying XXX.YYY.ZZZ.RRR...
Connected to XXX.YYY.ZZZ.RRR.
Escape character is '^]'.
220 kkkkk.xxxxx.co.jp ESMTP Postfix
HELO qqqqq.xxxxx.co.jp
250 kkkkk.xxxxx.co.jp
MAIL FROM:aaaaa@xxxxx.co.jp 
250 Ok
RCPT TO:suga@xxxxx.co.jp
250 Ok
DATA
354 End data with <CR><LF>.<CR><LF>
I write mail using "telnet"   
.
250 Ok: queued as A3E17FA84
QUIT   
221 Bye
Connection closed by foreign host.
青い部分は、メールコマンド。
HELO (クライアントのホスト or ドメイン名)の挨拶を行う。
MAIL FROM:(送信元のメールアドレス)を伝える。
RCPT TO:(送信先のメールアドレス)を伝える。
そして、内容を送るという合図でDATAコマンドを送る。

さて、赤い部分はメールの内容だ。この内容を書き込んで、
最後にピリオドをつける。

送信作業が終了すれば、QUITコマンドで終了させる。

  上での一連の流れを図にしてみると、次のようになる。

メール送信の一連の流れ
メール送信の一連の流れ

  メール配信の仕組みについては「システム奮闘記:その32」をご覧ください。
 (Postfixでメールサーバーの設定)


  さて、main()関数の中に全ての処理を書き込めば、修正の時など面倒だ。
  各用途別に関数を定義していくと、エラーが出た場合や、プログラムの
変更の時には楽だ。

  そこで、サーバーへデータを送信する関数を定義する。

サーバーへデータを送信する関数を定義
void SendData(int des , int len , char *mesg)
{
  send(des, mesg , len , 0 );
  printf("%s\n",mesg);
}
仮引数の「des」はディスクリプタ。
「len」は送信する文字数。
「mesg」は文字列の入ったアドレスを指すポインタ。

  そして、サーバーからの応答を受信する関数を定義する。

サーバーからの応答を受信する関数
void GetRes(int des)
{
  int size ; 
  char mesg[1024] ;

  memset(mesg,'\0',1024);
  size = recv(des,mesg,1024,0);
  printf("%s\n",mesg);
}
仮引数の「des」はディスクリプタ。

  これでサーバーからの応答の関数ができた。

補足(2006/6/14に追加) 上のサーバーからの応答を受信する関数だが、これについて LILO等で、お付き合いしています秋葉さんから以下のご指摘を受けた。
秋葉さんからのご指摘
関数 GetRes(int des)の処理内容に関してです。
バッファとして1024バイト確保されていますね。
char mesg[1024] ;
これは、mesg[0]〜mesg[1023]が確保されます。
この場合、仕様として、データ長は1023バイト
までとしたほうがいいでしょう。
したがってrecvは以下になります。
size = recv(des,mesg,1023,0);
でないと、もしsizeが1024になると、mesg[1024]
にNullを書いてしまいますよ。
次のエリアが壊れてしまいます。

  つまり図で描くと下のようになる。

秋葉さんのご指摘内容を図にすると
1024バイトのデータを取り込んだ場合、バッファオーバーフローを起こす
私が作った関数の場合だと、サーバーからのデータを受信する際、
1024バイトを受信するようになっている。
問題は受信データーを格納する配列も、1024バイトしか確保されていない。
そのため1024バイトを受信していまうと、NULL文字を入れる場所が
配列の外にはみだしてしまう。つまり領域違反を起こしてしまうのだ。
そのため以下の図のようにする必要がある。
1バイト少ないデータを取り込む事で領域違反を防止する
これだと最大1023バイトしか受信しないので、1023バイトを受信しても
データの後ろにNULL文字を入れる場所が確保できる。

  以上の事を踏まえて、関数を書き換えると以下のようになる。

サーバーからの応答を受信する関数
void GetRes(int des)
{
  int size ; 
  char mesg[1024] ;

  memset(mesg,'\0',1024);
  size = recv(des,mesg,1023,0);
  printf("%s\n",mesg);
}
仮引数の「des」はディスクリプタ。

  C言語のプログラムに慣れていない私にとっては、文字列の事を
キチンと考慮したプログラムを書くのには、まだまだ修行がいると感じた (^^;;

 補足終わりです

文字列操作

さて、データのやりとりを行うのに文字列を入れる変数を使うのだが、 文字列を扱う時、ちょっと面倒な作業がいる。 まぁ、慣れれば全く問題ないのだけど、ついつい私の場合は、 忘れがちなので、書く事にします。
文字列操作について
配列を指定しただけだと確保したメモリの中は綺麗にならない
上図のように、配列を宣言しただけですと、
配列の中身はゴミだらけです。
そのため、strcpy()関数で文字列を代入しても、
残りの要素の部分には、ゴミが残ったままになります。

その上、困った事に、文字列の終端の印はヌル文字「¥0」です。
これがないと、配列の要素を越えたエリアまでもが
文字列の範囲と認識してしまいます。


なぜ、配列を宣言した時に、配列の中に、ゴミがあるかと書きます。
宣言した配列のメモリ領域は、カーネルから割り当てられますが、
このメモリ領域は、他のプログラムが一度使って、
その後、手放したメモリ領域の場合が多々あります。

もちろん、プログラムがメモリを使い終えた時に掃除はしませんし、
カーネルがプログラムにメモリ領域を割り当てる際も、
そのメモリ領域の掃除はしません。
つまり、他のプログラムのデータの残骸が置いたまま
放置されているためです。


補足2(2006/6/14に追加) 上の図の説明だが、ここでも秋葉さんから以下のご指摘を受けた。
秋葉さんからのご指摘
strcpy(name,"mayumi")のところです。
説明では、nameが mayumi+ゴミ2バイトとなっていますが、
実際は、mayumi+\0+ゴミ1バイトになります。
定数"mayumi"は、6バイトではなく、mayumi+\0の7バイトが
確保されます。
又、strcpyは最後のNullも含めてコピーされます。
したがってその下の説明 strcpy(name,"mayumi\0") とnameは
同じ結果になります。
(ちなみに定数"mayumi\0"は、mayumi+\0+\0の8バイト)

  このご指摘を頂いた時・・・

  strcpy()関数は、ヌル文字をコピーするの???

  と思った。
  実は、strcpy()関数を使った時、文字列の後ろにNULL文字が付かなかったため
strcpy(name,"mayumi")だと、"mayumi"の後ろにはゴミが付くと説明を書いた。

  さて論より証拠なので、プログラムを使って確かめる事にした。
  すると・・・

  秋葉さんのおっしゃる通りだ!

確認のためのプログラム
#include <stdio.h>
#include <string.h>

int main(void)
{
char    name[8] ;

 strcpy(name,"mayumi\n");

 printf("%s",name);

 return(0);
}
プログラムの実行結果
suga@jitaku[~]% ./aki1 
mayumi
suga@jitaku[~]% 

  つまり図で描くと次のようになる。

図にすると
メモリの状態を図にした場合
strcpy()は、キチンと文字列の最後のNULL文字もコピーします。
  うーん、最初に私が実験した時は、ゴミがくっついてきたのだけに、
一体、最初に行った実験が、どんな条件下で行ったのか疑問になってきたが
そんな物は忘れてしまっているため、今となっては謎になってしまった (--;;

補足終わり

メモリ上のごみを表示させない方法

ところで他にゴミを表示させないようにするには以下の方法があります。
ゴミを表示させない方法(その1)
文字列の後ろにヌル文字を挿入する方法
代入する文字列「mayumi」の後ろにヌル文字「¥0」を追加します。
このヌル文字は、文字列の終端を知らせる印の役目を果たします。
実際にprintf("%s",name); を使って表示させても
「¥0」までの文字列しか表示されません。
そのため後ろの残ったゴミを気にする必要はありません。

 もう一つの方法は、最初からゴミを消してしまう事です。

ゴミを表示させない方法(その2)
確保したメモリ上を一度、全てヌル文字で埋める方法
memset()関数を使って、配列の中身を綺麗に掃除します。
つまり全てヌル文字で埋めつくします。

そうすれば、後から文字列を代入しても、必要な文字列だけを
表示する事ができます。

  こんな事は現役のプログラマーや、プログラムの得意な人から見れば、
「当り前やん!」というような事でも、プログラマーでない私には

  なかなか身につかないのらー!!

  だった。

smtpクライアントっぽいプログラム (smtp.c)
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>

void GetRes(int des)
{
  int size ; 
  char mesg[1024] ;

  memset(mesg,'\0',1024);
  size = recv(des,mesg,1023,0);
  printf("%s\n",mesg);
}

void SendData(int des , int len , char *mesg)
{
  send(des, mesg , len , 0 );
  printf("%s\n",mesg);
}

int main(void)
{
  int sockfd ;
  int len , i ;
  struct sockaddr_in address ;
  int result ;
  char mesg[100] ;

  sockfd = socket(AF_INET,SOCK_STREAM,0);

  address.sin_family = AF_INET ;
  address.sin_addr.s_addr = inet_addr("メールサーバーのIP");
  address.sin_port = htons(25) ;
  len = sizeof(address);

   result = connect(sockfd , (struct sockaddr *)&address , len);

  if ( result == -1 ) {
    perror("Cannot access to mail server!! \n");
    exit(1);
  }

  /*  接続した時のサーバーからの合図を受信 */

  GetRes(sockfd);

  /* HELO を送る */
  
  strcpy(mesg,"HELO xxx.xxxxx.co.jp\n\0") ;
  i = strlen(mesg);
  SendData(sockfd , i , mesg);
  GetRes(sockfd);

  /* MAIL FROM:aaaaa@xxxxx.co.jp を送る */

  strcpy(mesg,"MAIL FROM:suga@xxx.xxxxx.co.jp\n\0") ;
  i = strlen(mesg);
  SendData(sockfd , i , mesg);
  GetRes(sockfd);

  /* RCPT TO:suga@xxxxx.co.jp を送る */

  strcpy(mesg,"RCPT TO:suga@xxxxx.co.jp\n\0") ;
  i = strlen(mesg);
  SendData(sockfd , i , mesg);
  GetRes(sockfd);

  /* DATA を送る */

  strcpy(mesg,"DATA\n\0") ;
  i = strlen(mesg);
  SendData(sockfd , i , mesg);
  GetRes(sockfd);

  /* 内容を送る */

  strcpy(mesg,"test message\n\0") ;
  i = strlen(mesg);
  SendData(sockfd , i , mesg);

  /* 内容の終了を意味するピリオドを送る */

  strcpy(mesg,".\n\0") ;
  i = strlen(mesg);
  SendData(sockfd , i , mesg);
  GetRes(sockfd);

  /* quit を送る */
  strcpy(mesg,"QUIT\n\0") ;
  i = strlen(mesg);
  SendData(sockfd , i , mesg);
  GetRes(sockfd);

  close(sockfd);

  return(0);
}

  さて、このプログラムを実行させてみる。

プログラムの実行結果
[suga@xxx net]# ./smtp
220 mail.xxxxx.co.jp ESMTP Postfix

HELO xxx.xxxxx.co.jp

250 mail.xxxxx.co.jp

MAIL FROM:suga@xxx.xxxxx.co.jp

250 Ok

RCPT TO:suga@xxxxx.co.jp

250 Ok

DATA

354 End data with <CR><LF>.<CR><LF>

test message

.

250 Ok: queued as CCA832B4197

QUIT

221 Bye

[suga@xxx net]# 
赤い部分はサーバーからの応答の部分

  上手にsmtpサーバーと会話が出来ている。
  実際にメールの内容が届いているか確認してみる。

メールが届いているか確認を行う
[suga@yyy suga]$ mail -f suga
Mail version 8.1 6/6/93.  Type ? for help.
"suga": 1 messages 1 new
 N  1 suga@xxx.xxxxx.co.jp  Wed May  3 18:51  21/944  
& 1
Message 1:
From suga@xxx.xxxxx.co.jp  Wed May  3 18:51:21 2006
X-Original-To: suga@xxxxx.co.jp
Delivered-To: suga@xxxxx.co.jp
X-Virus-Status: clean(xxx.xxxxx.co.jp)
Date: Wed,  3 May 2006 18:51:21 +0900 (JST)
From: suga@xxx.xxxxx.co.jp
To: undisclosed-recipients:;

test message

&

  メッセージが正しく送られているのがわかる。

  実際にメーラーの作成になると相当厄介だ。
  日本語をメールで場合、文字コードの問題がある。
  JISコード(ISO-2022jp)で送信しないといけないため、文字コードの変換が必要だ。
  詳しくは「システム奮闘記:その14」をご覧下さい。
 (メールと日本語文字コードについて)

  だが、smtpクライアントっぽいプログラムは、面白くないのら・・・ (--;;

ftpクライアントのプログラム

ところで、一体、何をして良いのか全くわからない私。 そんな迷いを吹き飛ばしてくれる事を書いた本があった。 既存のアプリの真似でも良いから自作してみる事! これを見て私は「なるほど」と思った。 今までは、既にあるソフトを、私が作った所で仕方がないと思った。 その上、勤務先にとって役に立つ物という固定観念もあった。 だが、C言語の実践的な練習という事を考えれば、既存のソフトの 真似でも良いから、自作のソフトを作るのは大事な事だと気づいた。 そこで考えたのが、ftpクライアントソフトを作る事だった。 ftpクライアントのソフトなら以下の本にソースが書かれている。 「C言語による TCP/IP ネットワークプログラミング」 (小俣光之:ピアソン・エデュケーション) だが、今回はこの本を見ないで、どこまで自力で作れるかを 試してみる事にした。 まずはftpの時、クライアントとサーバーとのやりとりを見ていく必要がある。 図にしたら以下の通りだ。
クライアントとサーバーとのやりとり(Passiveモード)
クライアントとサーバーとのやりとり(Passiveモード)

  それをtelnetを使って、ftpコマンドのやりとりにしたら
以下のような、やりとりが行われる。

Passiveモードでの接続の様子
[suga@xxx suga]# telnet 192.168.X.Y ftp
Trying 192.168.X.Y...
Connected to 192.168.X.Y.
Escape character is '^]'.
220 ProFTPD 1.2.10 Server (ProFTPD Default Installation) [192.168.X.Y]
USER suga
331 Password required for suga.
PASS *****
230 User suga logged in.
PASV
227 Entering Passive Mode (192,168,X,Y,95,177)
青い部分はユーザーIDを入力。
赤い部分はパスワードの入力。

ピンクの部分は、PASVコマンドの入力。
すると、サーバーから返答が返ってくる。緑の部分だ。
サーバー側のIPと、ポート番号だ。

ここのポート番号の計算もPortモードの時と同様に
「ポート番号 = a × 256 + b」の方程式になる。
そのため上の場合だと、95×256+177=24497となる。

  以上の事を踏まえると、ftpサーバーへ認証を行う際、
次のようなデータのやりとりが行われる。

ftpサーバーへ認証を行う際の、データのやりとり
ftpサーバーへ認証を行う際の、データのやりとり
サーバーへ接続した時、サーバーから「接続ができた」の合図を受け取ります。
その次に、ユーザー名を送り、「OK」が出れば、パスワードを送ります。
そしてサーバーから「OK」が出れば、認証が完了です。

  そこで、認証部分だけをプログラムしてみる事にした。

認証部分だけをプログラム
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>

void GetRes(int des)
{
  int size ; 
  char mesg[1024] ;
  
  size = recv(des,mesg,1023,0);
  mesg[size] = '\0' ;
  printf("%s\n",mesg);
}

void SendData(int des , int len , char *mesg)
{
  send(des, mesg , len , 0 );
  printf("%s\n",mesg);
}

int main(void)
{
  int sockfd ;
  int len , i , cport ;
  struct sockaddr_in address ;
  int result ;
  char mesg[100] ;

  sockfd = socket(AF_INET,SOCK_STREAM,0);

  address.sin_family = AF_INET ;
  address.sin_addr.s_addr = inet_addr("ftpサーバーのIP");
  address.sin_port = htons(21) ;
  len = sizeof(address);

  result = connect(sockfd , (struct sockaddr *)&address , len);

  if ( result == -1 ) {
    perror("Cannot access to ftp server!! \n");
    exit(1);
  }

  /* 接続ができたというサーバーからの応答を受け取る  */

  GetRes(sockfd);

  /* ID を送る */
  
  strcpy(mesg,"USER suga\n\0") ;
  i = strlen(mesg);
  SendData(sockfd , i , mesg);
  GetRes(sockfd);

   /* パスワードを送る */

  strcpy(mesg,"PASS mayumi\n\0") ;
  i = strlen(mesg);
  SendData(sockfd , i , mesg);
  GetRes(sockfd);

  /* 接続を終了させるための処理として quit を送る */

  strcpy(mesg,"quit\n\0") ;
  i = strlen(mesg);
  SendData(sockfd , i , mesg);
  GetRes(sockfd);

  close(sockfd);

  return(0);
}
緑色の部分は、たった1行の何気ない部分ですが重要です。
なぜ、重要なのかは後述しています。

ピンクはユーザーID。赤い部分はパスワード。
実際には「mayumi」を使うと、小野真弓にラブラブの私なので、
即バレのパスワードなので使えませんが、ここでは例題なので、
「mayumi」にしています (^^;;

青い部分は改行文字とヌル文字です。
データを送る際は、telnetでの実演と同様に、改行を行わないと
サーバー側は、まだまだ文字列が続くと認識します。
そこで、改行文字を付け足して送ります。

最後に、改行文字の次にヌル文字を入れます。
このプログラムでは、文字列を入れる配列の中身を掃除していないため
配列の中はゴミだらけのため、文字列の終端がどこなのか判断できません。
そこで改行文字の後ろに文字列の終端を示すヌル文字を入れています。

慣れている人にとっては、なんともない処理ですが、
慣れていない私にとっては「面倒な処理だなぁ」と思ったりします (^^)

  早速、プログラムを実行させる。

プログラムの実行結果
[suga@xxx ftp]$ ./ftp1
220 ProFTPD 1.2.10 Server (ProFTPD Default Installation) [192.168.X.Y]

USER suga

331 Password required for suga.

PASS mayumi

230 User suga logged in.

quit

221 Goodbye.

[suga@xxx ftp]$
赤い部分はサーバーからのメッセージです。

  問題なくプログラムが動いた。

  さて、さきほどのプログラムの部分で、茶色の文字にした部分があります。
  接続時にサーバーからの合図を受け取る部分でした。
  なぜ、重要かと書きますと、これがないと・・・

  メッセージのやりとりがズレてしまうのらー!!

  つまり図にしますと、こういう事になります。

こんな事が起こってしまう!
クライアントへの応答のタイミングがズレて届いている現象
メッセージのやりとりで、サーバーからの応答が
ズレた応答が返ってくる。

  なぜ、こういう事を私が「重要だ」と書きますと・・・

  私がハマったからなのらー!!

  実は、最初、プログラムを書いてみた時、接続した時にサーバーからの応答を
考慮していませんでした。
  そのため、クライアントから送ったメッセージに対する反応ではなく
1つズレた反応が返ってくるため、「なんでやねん・・・」と思いました。

  実際に、どんな事が起こるのか、再現してみます。

接続時の際に、サーバーからの応答を受け取らないプログラム
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>

void GetRes(int des)
{
  int size ; 
  char mesg[1024] ;
  
  size = recv(des,mesg,1023,0);
  mesg[size] = '\0' ;
  printf("%s\n",mesg);
}

void SendData(int des , int len , char *mesg)
{
  send(des, mesg , len , 0 );
  printf("%s\n",mesg);
}

int main(void)
{
  int sockfd ;
  int len , i , cport ;
  struct sockaddr_in address ;
  int result ;
  char mesg[100] ;

  sockfd = socket(AF_INET,SOCK_STREAM,0);

  address.sin_family = AF_INET ;
  address.sin_addr.s_addr = inet_addr("ftpサーバーのIP");
  address.sin_port = htons(21) ;
  len = sizeof(address);

  result = connect(sockfd , (struct sockaddr *)&address , len);

  if ( result == -1 ) {
    perror("Cannot access to ftp server!! \n");
    exit(1);
  }

  /* 接続ができたというサーバーからの応答を受け取る  */

  /*
  GetRes(sockfd);
  */

  /* ID を送る */
  
  strcpy(mesg,"USER suga\n\0") ;
  i = strlen(mesg);
  SendData(sockfd , i , mesg);
  GetRes(sockfd);

   /* パスワードを送る */

  strcpy(mesg,"PASS mayumi\n\0") ;
  i = strlen(mesg);
  SendData(sockfd , i , mesg);
  GetRes(sockfd);

  /* 接続を終了させるための処理として quit を送る */

  strcpy(mesg,"quit\n\0") ;
  i = strlen(mesg);
  SendData(sockfd , i , mesg);
  GetRes(sockfd);

  close(sockfd);

  return(0);
}
接続時の時のサーバーからの応答を受信する部分を
コメントアウトしました。緑色の部分です。

  実際に、実行してみると以下のようになります。

実行結果
[suga@xxx ftp]$ ./ftp1-1
USER suga

220 ProFTPD 1.2.10 Server (ProFTPD Default Installation) [192.168.X.Y]

PASS mayumi

331 Password required for suga.

quit

230 User suga logged in.

[suga@xxx ftp]$ 
赤い部分はサーバーからのメッセージです。

  見事に、クライアントが送ったメッセージに対して、サーバーからは
一歩ズレたメッセージが返っているのが、よくわかります。

  つまり、クライアントとサーバーの間でメッセージのキャッチボールの
順番を理解した上で行わないと、うまく作動しません!!


ftpのデータやりとりはportモードを採用

さて、次にデータの転送を行う事を考えた。 まずは、データを受信、すなわち、サーバーにあるファイルのデータを クライアントに転送する部分を作る事にした。 この時、Portモードの方が簡単だと思った。 なぜ、そう思ったのかを書きますと・・・ 私の直感なのらー!! とても理系出身(一応、物理学科卒)とは思えない発想で前に進む私。 論理的に推論して結論を導き出すような頭脳なんぞ持っていないため、 常に嗅覚が頼りなのだが、システム奮闘記での失敗談の連続を見ている限りは 私の嗅覚も全くアテにはならないのだが、それには訳がある。 アレルギー鼻炎を持っているので、年中、鼻づまりしているからだ (^^) どーでも良い話は、これぐらいにします。 さて、Portモードの仕様でプログラムをしていく事を書く事にしました。 Portモードの場合、認証後、以下のような、やりとりをを行う。
認証後のやりとり (Portモードの場合)
認証後のやりとり (Portモードの場合)

  この時、ふと思った。
  クライアントが「RETR」のコマンドを送ると、サーバーから
データ転送用のポートへの接続を要求する。

  もしかして、「RETR」のコマンドを送る前に、クライアント側で
データ転送用のポートを開けて置いて、サーバーからの要求を
待ち受けできる状態にしておく必要があるのではないか。

  そこで、fork()関数を使い、親子プロセスに分離させて、
子プロセスにサーバーからの接続要求の受けを行う事にした。

私の考えた方法
親子プロセスを分離して子プロセスはデータ受信だけさせる
サーバーからのデータ転送のための接続要求処理を
子プロセスに任せる。そして、データ受信が終われば
子プロセスを終了させる。

  という事で、以下のソースを作成した。

私が作ったプログラムソース
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>

/*  サーバーからのデータを受信する関数 */

void RecvData(char *filename , int cport)
{
  int r_sockfd , g_sockfd ;
  int r_len , g_len ;
  char buf[1] ;
  struct sockaddr_in r_address ;

  FILE *fp ;
  fp = fopen(filename,"w");

  r_sockfd = socket(AF_INET,SOCK_STREAM,0);

  r_address.sin_family = AF_INET ;
  r_address.sin_addr.s_addr = INADDR_ANY ;
  r_address.sin_port = htons(cport) ;

  r_len = sizeof(r_address);
  bind(r_sockfd , (struct sockaddr *)&r_address , r_len);

  listen(g_sockfd , 5);

  g_sockfd = accept(r_sockfd ,
                    (struct sockaddr *)&r_address , &g_len);

  while ( read(g_sockfd , buf , 1 ) > 0 )
    {
      fwrite(buf,1,1,fp);
    }


  close(r_sockfd);
  close(g_sockfd);
}

void GetRes(int des)
{
  int size ; 
  char mesg[1024] ;
  
  size = recv(des,mesg,1023,0);
  mesg[size] = '\0' ;
  printf("%s\n",mesg);
}

void SendData(int des , int len , char *mesg)
{
  send(des, mesg , len , 0 );
  printf("%s\n",mesg);
}

int main(void)
{
  int sockfd ;
  int len , i , cport ;
  struct sockaddr_in address ;
  int result ;
  char mesg[100] ;

  sockfd = socket(AF_INET,SOCK_STREAM,0);

  address.sin_family = AF_INET ;
  address.sin_addr.s_addr = inet_addr("ftpサーバーのIP");
  address.sin_port = htons(21) ;
  len = sizeof(address);

  result = connect(sockfd , (struct sockaddr *)&address , len);

  if ( result == -1 ) {
    perror("Cannot access to ftp server!! \n");
    exit(1);
  }
 
  GetRes(sockfd);

  /* ID を送る */
  
  strcpy(mesg,"USER suga\n\0") ;
  i = strlen(mesg);
  SendData(sockfd , i , mesg);
  GetRes(sockfd);

  /* パスワードを送る */

  strcpy(mesg,"PASS mayumi\n\0") ;
  i = strlen(mesg);
  SendData(sockfd , i , mesg);
  GetRes(sockfd);

  /* PORTモードを送る */

  strcpy(mesg,"PORT 192,168,X,Z,200,200\n\0") ;
  cport = 200 * 256 + 200 ;
  i = strlen(mesg);
  SendData(sockfd , i , mesg);
  GetRes(sockfd);

  /* データ転送の部分  親子のプロセスで分ける */

  if ( fork() > 0 )
     {
       /* 親プロセス */
       
       /* データ転送の命令を送る */

       strcpy(mesg,"RETR server.dat\n\0") ;
       i = strlen(mesg);
       SendData(sockfd , i , mesg);
       GetRes(sockfd);  
     }
   else
     {
     /* 子プロセス */

     /* データ転送の待ち受けと
        送られたデータをファイル(server.dat)に記録する */

     strcpy(mesg,"server.dat") ;
     i = strlen(mesg);
     RecvData(mesg,cport);
     exit(0);
     }

  /* quit を送る */
  strcpy(mesg,"quit\n\0") ;
  i = strlen(mesg);
  SendData(sockfd , i , mesg);
  GetRes(sockfd);

  close(sockfd);

  return(0);
}

  さて、動かしてみる。
  こういう時、どんな結果が出るのかワクワクする。

プログラムの実行結果
[suga@xxx ftp]# ./ftp1-3  
220 ProFTPD 1.2.10 Server (ProFTPD Default Installation) [192.168.X.Y]

USER suga

331 Password required for suga.

PASS mayumi

230 User suga logged in.

PORT 192,168,X,Z,200,200

200 PORT command successful

RETR server.dat

425 Unable to build data connection: Connection refused

quit

221 Goodbye.

[suga@xxx ftp]$

  なんでデータ転送ができへんねん (TT)

  だった。

  そこで「ない知恵」を絞る。
  fork()関数を使う場所を変更しようと考えた。

  次のソースを書いた。

私が書いたソース
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>

/*  サーバーからのデータを受信する関数 */

void RecvData(char *filename , int cport)
{
  int r_sockfd , g_sockfd ;
  int r_len , g_len , result ;
  char buf[1] ;

  FILE *fp ;

  struct sockaddr_in r_address , g_address ;


  r_sockfd = socket(AF_INET,SOCK_STREAM,0);
  r_address.sin_family = AF_INET ;
  r_address.sin_addr.s_addr = INADDR_ANY ;
  r_address.sin_port = htons(cport) ;

  r_len = sizeof(r_address);

  bind(r_sockfd , (struct sockaddr *)&r_address , r_len);

   listen(r_sockfd , 5);

   /*  親子にわけて、親プロセスは、この関数から抜け出させる */

  if ( fork() > 0 )
     {
     /* 親プロセス */

     close(r_sockfd) ;
     }
  else 
    {
     /* 子プロセス */

    fp = fopen(filename,"w");

     g_sockfd = accept(r_sockfd ,
                   (struct sockaddr *)&g_address , &g_len);


      while ( read(g_sockfd , buf , 1 ) > 0 )
            {
            fwrite(buf,1,1,fp);
            }
      close(r_sockfd);
      close(g_sockfd);
      fclose(fp);
      }
}


void GetRes(int des)
{
  int size ; 
  char mesg[1024] ;
  
  size = recv(des,mesg,1023,0);
  mesg[size] = '\0' ;
  printf("%s\n",mesg);
}


void SendData(int des , int len , char *mesg)
{
  send(des, mesg , len , 0 );
  printf("%s\n",mesg);
}

int main(void)
{
  int sockfd ;
  int len , i , cport ;
  struct sockaddr_in address ;
  int result ;
  char mesg[100] ;

  sockfd = socket(AF_INET,SOCK_STREAM,0);

  address.sin_family = AF_INET ;
  address.sin_addr.s_addr = inet_addr("ftpサーバーのIP");
  address.sin_port = htons(21) ;
  len = sizeof(address);

  result = connect(sockfd , (struct sockaddr *)&address , len);

  if ( result == -1 ) {
    perror("Cannot access to ftp server!! \n");
    exit(1);
  }

  /* ID を送る */
  
  strcpy(mesg,"USER suga\n\0") ;
  i = strlen(mesg);
  SendData(sockfd , i , mesg);
  GetRes(sockfd);

  /* パスワードを送る */

  strcpy(mesg,"PASS mayumi\n\0") ;
  i = strlen(mesg);
  SendData(sockfd , i , mesg);
  GetRes(sockfd);

  /* PORTモードを送る */

  strcpy(mesg,"PORT 192,168,X,Z,200,200\n\0") ;
  cport = 200 * 256 + 200 ;
  i = strlen(mesg);
  SendData(sockfd , i , mesg);
  GetRes(sockfd);


  /* サーバーからのデータ送信の準備 */

     strcpy(mesg,"server.dat\0") ;
     i = strlen(mesg);
     RecvData(mesg,cport);


     /* データ転送の命令を送る */

     strcpy(mesg,"RETR server.dat\n\0") ;
     i = strlen(mesg);
     SendData(sockfd , i , mesg);
     GetRes(sockfd);  

  /* quit を送る */
  strcpy(mesg,"quit\n\0") ;
  i = strlen(mesg);
  SendData(sockfd , i , mesg);
  GetRes(sockfd);

  close(sockfd);

  return(0);
}

  さて、プログラムを実行させてみる。

プログラムの実行結果
[suga@xxx ftp]$ ./ftp3-1
USER suga

220 ProFTPD 1.2.10 Server (ProFTPD Default Installation) [192.168.X.Y]

PASS mayumi

331 Password required for suga.

PORT 192,168,X,Z,200,200

230 User suga logged in.

RETR server.dat

200 PORT command successful

quit

150 Opening ASCII mode data connection for server.dat (22 bytes)

[suga@xxx ftp]$ RETR server.dat

226 Transfer complete.

quit

221 Goodbye.


[suga@xxx ftp]$

  親子プロセスにわけて動作させているので、画面の表示が
ぎこちない感じがする。

  だが、エラーが出ていない事から、成功したようだ。
  実際に、データが転送されているのか確認を行ってみた。

データ転送が成功しているか確認してみた
[suga@xxx ftp]$ more server.dat 
From Server to Client
[suga@xxx ftp]$ 

  見事、成功  (^^)V

  だが、このfork()を使って、データ専用のポートを待ち受けするのは
実用的ではない。
  この事に気づいたのは、以下の事を考えた時だった。

  データ転送の前に、サーバーに置いてあるファイルのリストも
出力させようと考えた。

データ転送以外にも、ファイルリストの出力も考えた
データ転送以外にも、ファイルリストの出力の機能の追加を考えた
リスト出力のために、子プロセスを生成して、
その後、ファイル転送のため、新たに子プロセスを生成する。

リストの出力が終えるのを待つために、親プロセス側で
2秒ほど間を置くため、sleep(2); を使った。

  そこで、以下のプログラムを書いた。

私が書いたソース
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>

/*  サーバーからファイルのデータを受信する関数 */

void RecvData(char *filename , int cport)
{
  int r_sockfd , g_sockfd ;
  int r_len , g_len , result ;
  char buf[1] ;

  FILE *fp ;

  struct sockaddr_in r_address , g_address ;


  r_sockfd = socket(AF_INET,SOCK_STREAM,0);
  r_address.sin_family = AF_INET ;
  r_address.sin_addr.s_addr = INADDR_ANY ;
  r_address.sin_port = htons(cport) ;

  r_len = sizeof(r_address);

  bind(r_sockfd , (struct sockaddr *)&r_address , r_len);

  listen(r_sockfd , 5);

  if ( fork() > 0 )
     {
     close(r_sockfd) ;
     }
  else 
    {
    fp = fopen(filename,"w");
 
     g_sockfd = accept(r_sockfd ,
                   (struct sockaddr *)&g_address , &g_len);


      while ( read(g_sockfd , buf , 1 ) > 0 )
          {
            fwrite(buf,1,1,fp);
          }
      close(r_sockfd);
      close(g_sockfd);
      fclose(fp);
      }
}

/*  サーバーからLISTコマンドのデータを受信する関数 */

void RecvData2(int cport)
{
  int r_sockfd , g_sockfd ;
  int r_len , g_len , result ;
  char buf[1] ;

  struct sockaddr_in r_address , g_address ;

  r_sockfd = socket(AF_INET,SOCK_STREAM,0);
  r_address.sin_family = AF_INET ;
  r_address.sin_addr.s_addr = INADDR_ANY ;
  r_address.sin_port = htons(cport) ;

  r_len = sizeof(r_address);

  bind(r_sockfd , (struct sockaddr *)&r_address , r_len);

  listen(r_sockfd , 5);

  if ( fork() > 0 )
     {
     close(r_sockfd) ;
     }
  else
     {
     g_sockfd = accept(r_sockfd ,
                    (struct sockaddr *)&g_address , &g_len);


     while ( read(g_sockfd , buf , 1 ) > 0 )
           {
           write(1,buf,1);
           }
     close(r_sockfd);
     close(g_sockfd);
     }
}


void GetRes(int des)
{
  int size ; 
  char mesg[1024] ;
  
  size = recv(des,mesg,1023,0);
  mesg[size] = '\0' ;
  printf("%s\n",mesg);
}

void SendData(int des , int len , char *mesg)
{
  send(des, mesg , len , 0 );
  printf("%s\n",mesg);
}

int main(void)
{
  int sockfd ;
  int len , i , cport ;
  struct sockaddr_in address ;
  int result ;
  char mesg[100] ;

  sockfd = socket(AF_INET,SOCK_STREAM,0);

  address.sin_family = AF_INET ;
  address.sin_addr.s_addr = inet_addr("ftpサーバーのIP");
  address.sin_port = htons(21) ;
  len = sizeof(address);

  result = connect(sockfd , (struct sockaddr *)&address , len);

  if ( result == -1 ) {
    perror("Cannot access to ftp server!! \n");
    exit(1);
  }

  /* ID を送る */
  
  strcpy(mesg,"USER suga\n\0") ;
  i = strlen(mesg);
  SendData(sockfd , i , mesg);
  GetRes(sockfd);

  /* パスワードを送る */

  strcpy(mesg,"PASS mayumi\n\0") ;
  i = strlen(mesg);
  SendData(sockfd , i , mesg);
  GetRes(sockfd);

  /* サーバーにあるファイルのリストを出力させる処理 */

  /* PORTモードを送る */

  strcpy(mesg,"PORT 192,168,X,Z,200,201\n\0") ;
  cport = 200 * 256 + 201 ; /*  51401番を指定 */
  i = strlen(mesg);
  SendData(sockfd , i , mesg);
  GetRes(sockfd);

  /* サーバーから来るリストのデータの待ち受け処理を行う */

  RecvData2(cport);


  /* LISTコマンドを送る */

  strcpy(mesg,"LIST\n\0") ;
  i = strlen(mesg);
  SendData(sockfd , i , mesg);
  GetRes(sockfd);

  /* 間を置く2秒ほど */

  sleep(2);


  /* データをクライアントに転送する処理 */

  /* PORTコマンドを送る。 
     ポートの指定はLISTの場合とは別の番号を指示する */

  strcpy(mesg,"PORT 192,168,X,Z,200,200\n\0") ;
  cport = 200 * 256 + 200 ;  /*  51400番を指定 */
  i = strlen(mesg);
  SendData(sockfd , i , mesg);
  GetRes(sockfd);

  /* サーバーからのデータ送信の準備 */

     strcpy(mesg,"server.dat\0") ;
     i = strlen(mesg);
     RecvData(mesg,cport);


     /* データ転送の命令を送る */

     strcpy(mesg,"RETR server.dat\n\0") ;
     i = strlen(mesg);
     SendData(sockfd , i , mesg);
     GetRes(sockfd);  

  /* quit を送る */
  strcpy(mesg,"quit\n\0") ;
  i = strlen(mesg);
  SendData(sockfd , i , mesg);
  GetRes(sockfd);

  close(sockfd);

  return(0);
}

  さて、プログラムを実行させてみる。

プログラムの実行結果
[suga@xxx ftp]$ ./ftp3-2
USER suga

220 ProFTPD 1.2.10 Server (ProFTPD Default Installation) [192.168.X.Y]

PASS mayumi

331 Password required for suga.

PORT 192,168,X,Z,200,201

230 User suga logged in.

LIST

200 PORT command successful

-rw-r--r--   1 suga     suga        61440 Feb 10  2005 aaa.tar
drwxr-xr-x   2 suga     suga         4096 Feb 10  2005 back
(途中、省略)
-rw-r--r--   1 suga     suga           24 Apr  8 02:42 test.dat
-rw-rw-r--   1 suga     suga           28 Apr  8 03:42 transfer.dat

LIST

150 Opening ASCII mode data connection for file list

PORT 192,168,X,Z,200,200

226 Transfer complete.
425 Unable to build data connection: Connection refused

RETR server.dat

200 PORT command successful

quit

150 Opening ASCII mode data connection for server.dat (22 bytes)

[suga@xxx ftp]# PORT 192,168,X,Z,200,200

226 Transfer complete.
221 Goodbye.


[suga@xxx ftp]$

  ファイル一覧の閲覧はできたが、データ転送の処理の際に、クライアントが
サーバー側の接続を拒否している・・・。
  もちろん、データファイルの転送はできていない。

  なんでやねん (TT)

  一体、データ転送を行ったりする場合、ftpクライアントソフトでは
どんな風に行っているのか。考えても思いつかない。

  技術者ではなく事務員の私は、ここで白旗!
  カンニングとして以下の本のソースを見てみる事にした。

 「C言語による TCP/IP ネットワークプログラミング」
  (小俣光之:ピアソン・エデュケーション)

  この本に書かれているftpクライアントのソースを見て、
私が余計な事を考えていた事がわかった。

  つまり、親子プロセスにわける必要などなかったのだ。
  単純に以下のような流れになる。

実際の方法
実際の処理手順
サーバーにファイルの転送のコマンドを送った後で
データ転送のための待ち受け処理を行えば良いのだ。

  この流れを知った時は

  なんだ、難しく考えすぎた (^^;;

  と思った。

  そこで、リストの出力と、データの転送を行うプログラムの流れを
以下のような単純な方法にしてみた。

プログラムの流れ
ネットワーク処理手順とプログラムの流れ

  そして、単純な方法で動くかどうかを確認するためにプログラムを書いた。

私の書いたプログラム
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>

/*  サーバーからファイルのデータを受信する関数 */

void RecvData(char *filename , int cport)
{
  int r_sockfd , g_sockfd ;
  int r_len , g_len , result ;
  char buf[1] ;

  FILE *fp ;

  struct sockaddr_in r_address , g_address ;


  r_sockfd = socket(AF_INET,SOCK_STREAM,0);
  r_address.sin_family = AF_INET ;
  r_address.sin_addr.s_addr = INADDR_ANY ;
  r_address.sin_port = htons(cport) ;

  r_len = sizeof(r_address);

  bind(r_sockfd , (struct sockaddr *)&r_address , r_len);

  listen(r_sockfd , 5);

  fp = fopen(filename,"w");
 
  g_sockfd = accept(r_sockfd ,
                   (struct sockaddr *)&g_address , &g_len);


  while ( read(g_sockfd , buf , 1 ) > 0 )
        {
        fwrite(buf,1,1,fp);
        }

  close(r_sockfd);
  close(g_sockfd);
  fclose(fp);
}

/*  サーバーからLISTコマンドのデータを受信する関数 */

void RecvData2(int cport)
{
  int r_sockfd , g_sockfd ;
  int r_len , g_len , result ;
  char buf[1] ;

  struct sockaddr_in r_address , g_address ;

  r_sockfd = socket(AF_INET,SOCK_STREAM,0);
  r_address.sin_family = AF_INET ;
  r_address.sin_addr.s_addr = INADDR_ANY ;
  r_address.sin_port = htons(cport) ;

  r_len = sizeof(r_address);

  bind(r_sockfd , (struct sockaddr *)&r_address , r_len);

  listen(r_sockfd , 5);

  g_sockfd = accept(r_sockfd ,
                   (struct sockaddr *)&g_address , &g_len);


  while ( read(g_sockfd , buf , 1 ) > 0 )
        {
        write(1,buf,1);
        }

  close(r_sockfd);
  close(g_sockfd);
}


void GetRes(int des)
{
  int size ; 
  char mesg[1024] ;
  
  size = recv(des,mesg,1023,0);
  mesg[size] = '\0' ;
  printf("%s\n",mesg);
}

void SendData(int des , int len , char *mesg)
{
  send(des, mesg , len , 0 );
  printf("%s\n",mesg);
}

int main(void)
{
  int sockfd ;
  int len , i , cport ;
  struct sockaddr_in address ;
  int result ;
  char mesg[100] ;

  sockfd = socket(AF_INET,SOCK_STREAM,0);

  address.sin_family = AF_INET ;
  address.sin_addr.s_addr = inet_addr("ftpサーバーのIP");
  address.sin_port = htons(21) ;
  len = sizeof(address);

  result = connect(sockfd , (struct sockaddr *)&address , len);

  if ( result == -1 ) {
    perror("Cannot access to ftp server!! \n");
    exit(1);
  }

  /* ID を送る */
  
  strcpy(mesg,"USER suga\n\0") ;
  i = strlen(mesg);
  SendData(sockfd , i , mesg);
  GetRes(sockfd);

  /* パスワードを送る */

  strcpy(mesg,"PASS mayumi\n\0") ;
  i = strlen(mesg);
  SendData(sockfd , i , mesg);
  GetRes(sockfd);

  /* サーバーにあるファイルのリストを出力させる処理 */

  /* PORTモードを送る */

  strcpy(mesg,"PORT 192,168,X,Z,200,201\n\0") ;
  cport = 200 * 256 + 201 ; /*  51401番を指定 */
  i = strlen(mesg);
  SendData(sockfd , i , mesg);
  GetRes(sockfd);

  /* LISTコマンドを送る */

  strcpy(mesg,"LIST\n\0") ;
  i = strlen(mesg);
  SendData(sockfd , i , mesg);
  GetRes(sockfd);

  /* サーバーから来るリストのデータの待ち受け処理を行う */

  RecvData2(cport);

  /* データをクライアントに転送する処理 */

  /* PORTコマンドを送る。 
     ポートの指定はLISTの場合とは別の番号を指示する */

  strcpy(mesg,"PORT 192,168,X,Z,200,200\n\0") ;
  cport = 200 * 256 + 200 ;  /*  51400番を指定 */
  i = strlen(mesg);
  SendData(sockfd , i , mesg);
  GetRes(sockfd);

  /* データ転送の命令を送る */

     strcpy(mesg,"RETR server.dat\n\0") ;
     i = strlen(mesg);
     SendData(sockfd , i , mesg);
     GetRes(sockfd); 

  /* サーバーからのデータ送信の準備 */

     strcpy(mesg,"server.dat\0") ;
     i = strlen(mesg);
     RecvData(mesg,cport);


  /* quit を送る */

  strcpy(mesg,"quit\n\0") ;
  i = strlen(mesg);
  SendData(sockfd , i , mesg);
  GetRes(sockfd);

  close(sockfd);

  return(0);
}

  プログラムを実行させると

プログラムの実行結果
[suga@xxx ftp]$ ./ftp3-2
USER suga

220 ProFTPD 1.2.10 Server (ProFTPD Default Installation) [192.168.X.Y]

PASS mayumi

331 Password required for suga.

PORT 192,168,X,Z,200,201

230 User suga logged in.

LIST

200 PORT command successful

-rw-r--r--   1 suga     suga        61440 Feb 10  2005 aaa.tar
drwxr-xr-x   2 suga     suga         4096 Feb 10  2005 back
(途中、省略)
-rw-r--r--   1 suga     suga           24 Apr  8 02:42 test.dat
-rw-rw-r--   1 suga     suga           28 Apr  8 03:42 transfer.dat
PORT 192,168,X,Z,200,200

150 Opening ASCII mode data connection for file list

RETR server.dat

226 Transfer complete.

quit

200 PORT command successful
150 Opening ASCII mode data connection for server.dat (22 bytes)

[suga@xxx ftp]$ 

  どうやら成功しているようだ。
  念のため、データが転送されたかどうか確かめてみる。

データが転送されたかどうか確かめてみる
[suga@xxx ftp]$ more server.dat 
From Server to Client
[suga@xxx ftp]$

  見事、成功!  (^^)V


bind()関数の使い方

さて、だいたいのftpクライアントを作成する上での概略がわかった所で、 実際に使えそうなftpクライアントの作成を考える。 Portモードで通信を行う場合、クライアント側が己の空きポートを サーバーに指定する必要がある。 今までは私が勝手に「51400」のポートを使っていたのだが、 必ずしも、このポート番号が空いているとは限らない。 空きポートを探す必要が出てくる。 一体、空きポートを見つけるのに、どうすれば良いのか? 色々、考えたが思いつかない。 そこで、またカンニングペーパーを取り出す。 プログラムソースを見ると、答えは実に呆気なかった。 bind()関数がコケたら使えないポート! さて、bind()関数が、どういう関数だったのか、おさらいしてみる。 bind()関数は、サーバー側のソケットに命名を行う関数だ。 クライアント(接続する側)に対して、接続するための目印になるようにと サーバーのソケットに名前をつけるのだ。
bind()の役目を図式化すると
bind()関数の働き
クライアントのプロセスからサーバー側のソケットに接続するために
目印になるような名前をつけます。
この時、クライアントには何も設定しません。

  bind()関数の使い方は以下の通りになる。

bind()関数について
bind()関数の簡単な仕様
引数に、ソケットディスクリプタ、アドレス構造体のポインタ、
アドレス構造体の長さがあります。

そして、ソケットに名前をつけるのに成功したら「0」を返します。
失敗の場合は「-1」を返します。

  アドレスの構造体のメンバーには、待ち受けするためのポートも含まれている。
  つまり空きポート以外の番号を指定した場合、bind()関数がエラーを出すため
使えないポートである事がわかる。

  より、bind()関数の使い方がわかった (^^)


Portモードを諦め、Passiveモードにする

さて、Portモードでftpクライアントを作成しようと進めてきた。 だが、これだとファイヤウォール越えのデータ転送ができない。 これでは実用的やない! と思ったので、あっさりとPassiveモードの仕様に切り替える。

ftpクライアントに必要な機能と仕様

さて、ftpクライアントの作成にあたり、どういう機能をつけるのか、 どういう仕様で動かすのか大筋で決める必要がある。 そこで、思いつくままに並べてみた。
ftpクライアントに必要な機能
(1) ファイルのリストの出力
(2) ディレクトリの移動
(3) ファイルの受信
(4) ファイルの送信
(5) 終了コマンド

 次に、どういう仕様にするのか考えたみた。

ftpクライアントプログラムの仕様
(1) 第一引数を接続先のIPを入れる
(2) Passiveモードで通信を行う
(3) Linux(UNIX)同士の通信なので、データ転送時に
テキストとバイナリーを分ける事は行わない

  さて、ftpクライアントのソフトの動作手順を、どういう風にするのか
流れ図にしてみた。

動作手順の流れ図
ftpクライアントのプログラムの動作手順
認証後はメニュー画面で操作を行う。
メニュー画面から、それぞれの機能を選択する形にする。

  ふと思った。これって・・・

  基本情報処理技術者の試験に出てくるやん!

  システム開発の部分で、基本計画、外部設計、内部設計が出てくる。


  今回は、基本計画はなしだ。
  外部設計は、ftpクライアントが、どの機能を持たせるのかを決める事になる。
  内部設計は、上に上げた仕様に当たる。
  そして、プログラミング設計は、動作の手順になる。

  うーん、かなり強引な書き方だが、情報処理の教科書的な事(?)をやっている。

  さて、これだけ決めれば、次はプログラミングだ。

改行文字対策について

さて、ftpクライアントのソフトだが、転送するファイル名を指定するのだが 文字列の入力処理は、慣れていないと厄介だ。
文字列操作の厄介な部分
改行文字が引き起こす問題
fgets()関数で、ファイル名を取得するのだが
この場合、改行文字まで「文字」として認識してしまう。

  もし、この処理を怠ると正しいファイル名でファイルが作成されなくなる。
  つまり以下のようなプログラムを動かした場合、

プログラム (test.c)
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    char name[8] ;

    FILE *fp;

    memset(name,'\0',8);
    printf("filename : \n");
    fgets(name,8,stdin);

    fp = fopen(name,"w");

    fprintf(fp,"test\n");

    fclose(fp);

    return(0);
}
プログラムの動作結果
[suga@xxx ftp]$ ./test
filename : mayumi
[suga@xxx ftp]$ ls -l
合計 16
-rw-r--r--    1 suga     suga           5  5月 10日  22:16 mayumi?
-rwxr-xr-x    1 suga     suga        4791  5月 10日  22:16 test*
-rw-r--r--    1 suga     suga         231  5月 10日  22:15 test.c
[suga@xxx ftp]$
ファイル名を「mayumi」にしたのだが、lsコマンドで見てみると
「mayumi?」という感じで「?」がついている。
改行文字が、くっついてきた証拠だ。

  この問題を解消するには、いくつか方法があるが、私がとった方法は、
以下の方法だ。

改行文字を省く方法
改行文字をヌル文字に置換する事で解決
改行文字の部分をヌル文字に入れ換えてしまう。

  こう考えると、文字列の操作って手間なのだ。


ftpクライアントの操作画面の設計

次に文字列の操作で悩んだのがメニュー画面だった。 以下の画面を考えたのだった。
考えたメニュー画面
-------------------------
1 : list
2 : change directory
3 : send file
4 : receive file
9 : quit

Select number : 

  一見、簡単そうなのだが、番号を選んだ後の処理に手間がかかった。

  scanf()関数を使えば、簡単に数値として取得できるのだが、
この関数はバッファオーバーフロー攻撃に遭い易いため、
使う事を避けるべき関数になっている。

  そこで、fgets()関数を使って、選んだ値を文字列として取得するのだ。

  単純にプログラム的には以下のように考えてしまう。

こんなソースを考えてしまう
#include <stdio.h>
#include <stdlib.h>

int main(void)
{

  char buf[10];

  while( buf[0] != '9' )
       {
         printf("-------------------------\n");
         printf("1 : list\n");
         printf("2 : change directory\n");
         printf("3 : send file\n");
         printf("4 : receive file\n");
         printf("9 : quit\n");
         printf("\n");

         printf("Select number : ");
         fgets(buf,1,stdin);
         printf("\n");
	 }
   return(0);
}

  上の赤い部分にあるように、1文字だけ取得すれば良いと考えてしまう。 
  だが、それではうまくいかない。

  なぜなら「2」を選択した時、文字列「2」だけを入力するわけではない。
  改行が必要なため「2」と改行文字の「2\n」の2文字を入力した事になる。

  もし、fgets(buf,1,stdin);の状態だと、次のような事が起こる。

こんな事が起こってしまう
-------------------------
1 : list
2 : change directory
3 : send file
4 : receive file
9 : quit

Select number : 
-------------------------
1 : list
2 : change directory
3 : send file
4 : receive file
9 : quit

Select number : 
-------------------------
1 : list
2 : change directory
3 : send file
4 : receive file
9 : quit

Select number : 
(永遠に止まらない)
fgets()関数の入力待ち受け機能が働かずに
無限地獄に陥ってしまう。

  2文字だけを取得しようとして fgets(buf,2,stdin);にしても、
以下の厄介な問題が起こる。

こんな問題が起こる
-------------------------
1 : list
2 : change directory
3 : send file
4 : receive file
9 : quit

Select number : 1
-------------------------
1 : list
2 : change directory
3 : send file
4 : receive file
9 : quit

Select number : 
-------------------------
1 : list
2 : change directory
3 : send file
4 : receive file
9 : quit

Select number : 

  最初に私が「1」を入力してENTERを押すのだが、その後に、
幽霊が見えない文字を入力して、ENTERキーを押した感じだ。

  ポルターガイスト現象?

  おかしいなぁ。ちゃんと今年は墓参りへ行ったのに、
それでも、ご先祖さんが化けて出たのかいな。


  調べてみると、fgets(buf,2,stdin); に起こる問題は、
幽霊が原因ではなく、fgets()関数の仕様の問題らしい。

  実は、fgets()関数で文字列を取得する場合、1文字の入力文字と
改行文字以外にも、ヌル文字も含まれるのだ!!

  つまり次の事なのだ。

fgets()関数で文字列を取得する場合の注意点
fgets()関数で文字列を取得する場合の注意点
fgets()関数で、文字数の指定の部分がある。
もし、文字数の指定を「n」とした場合、
取得される文字数は「n-1」文字になる。

これは、n文字目(文字列の最後)に、文字列の最後尾の印である
ヌル文字(\n)を入れるためなのだ。

  この説明だと、最初に出てきた問題でfgets(buf,2,stdin);とした場合に
起こる現象が説明できる。

  つまり次の事だという。

fgets(buf,2,stdin);とした場合
fgets(buf,2,stdin);とした場合
fgets()関数で、文字数の指定が2文字の場合
文字列bufの中の最後尾(2文字目)の部分にはヌル文字が入る。
そのため、bufには1文字しか入らない

  この事から次のような事が起こる。

fgets(buf,2,stdin);とした場合
fgets(buf,2,stdin);とした場合
「1」を入力した後、ENTERキーで改行を行わないと、
fgets()関数に入力をした事を知らせる事ができないのだが、
その改行が、改行文字として認識されるのに問題がある。

上図のように「1」を入力して、改行を行うと、
2文字分の入力があったと認識される。

だが、fgets()関数の仕様上、文字数を2文字としているため
ヌル文字が入る部分を考慮すると、1文字しか入らない。
つまり「1」を入力して処理される。
while()文にしているため、次のfgets()の待ち受けの部分で
既に改行文字が入力されていると認識されているため、
fgets()は、待ち受けせずに、改行文字をbufに入れて処理を行う。

  そこで、fgets()を使う時、以下の事に気をつけない事がわかった。

fgets()関数の使用上の注意
(1)
通常、fgets()関数で文字を取り込む時は、
最後尾にくっついてくるヌル文字があるため
取り込む文字数より1つ多い文字数を
指定をしなければならない
(2)
だが、標準入力でfgets()を使う場合には
最後尾にくっついてくるヌル文字以外にも、
改行文字も1文字として認識されてしまうため、
取り込む文字数より2つ多い文字数を
指定しなければならない

  これで解決だと思ったのだが、fgets(buf,1,stdin);の場合、
待ち受けしてくれない原因が説明できない。

fgets(buf,2,stdin);とした場合
fgets(buf,2,stdin);とした場合

  しかも、上で挙げたfgets()の使用上の注意には当てはまらない。

fgets(buf,1,stdin);とした場合
以下のようには、ならない。
fgets(buf,1,stdin);とした場合
fgets()関数は、文字列の最後尾にヌル文字を入れるため、
通常なら、1文字指定の場合、上図のように、1文字目にヌル文字を
入れると思われる。

だが、bufの中身は全く書き換わらないのだ。

  実際、次のソースで実験してみたら、よくわかる。

実験プログラム( fgets1.c )
#include <stdio.h>

int main(void)
{
  char buf[10] = "Hello" ;

  fgets(buf,1,stdin);

  printf("buf = %s\n",buf);

  return(0);
}
プログラムの実行結果
[suga@xxx ftp]$ ./fgets1
buf = Hello
[suga@xxx ftp]$ 

  入力待ち受け状態にならずに、しかもbufの中身も変わらないまま。

  これは何故なのか?  全く見当がつかない。

  こんな時、いつもなら

  タダの事務員なので、わかりませーん (^^)

  で逃げるのだが、今回は違った。

  そう、孫悟空やベジータがスーパーサイヤ人に変身するが如く
事務員もバージョンアップして

  二束三文の事務員になったのらー!!

  そうなのです。タダ(無料)ではなく、値札がついたのだ!
  うーん、これから100金ショップならぬ、「100金」事務員と名乗ろうか。

  とは言え、googleなどを使って調べても、わからなかった。
  タダの事務員に戻って、諦めようとした時、ふと思いついた!

  glibcのソースを読めば、ええやん!

  だんだん事務員のやる事とは思えなくなってきた (--;;

  さて、この実験を行った環境は、RedHat7.3で、glibcは 2.2.5を使っている。

  そこでglibc-2.2.5のソースをダウンロードして、
fgets()関数を実装している、stdio/fgets.cを見てみる事にした。

stdio/fgets.cのソースの中身(glibc-2.5.5)
/* Copyright (C) 1991, 92, 95, 96, 97, 98 Free Software Foundation, Inc.
   This file is part of the GNU C Library.

   The GNU C Library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public
   License as published by the Free Software Foundation; either
   version 2.1 of the License, or (at your option) any later version.

   The GNU C Library is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General Public
   License along with the GNU C Library; if not, write to the Free
   Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
   02111-1307 USA.  */

#include <errno.h>
#include <stdio.h>
#include <string.h>

/* Reads characters from STREAM into S, until either a newline character
   is read, N - 1 characters have been read, or EOF is seen.  Returns
   the newline, unlike gets.  Finishes by appending a null character and
   returning S.  If EOF is seen before any characters have been written
   to S, the function returns NULL without appending the null character.
   If there is a file error, always return NULL.  */
char *
fgets (s, n, stream)
     char *s;
     int n;
     FILE *stream;
{
  register char *p = s;

  if (!__validfp (stream) || s == NULL || n <= 0)
    {
      __set_errno (EINVAL);
      return NULL;
    }

  if (ferror (stream))
    return NULL;

  if (stream->__buffer == NULL && stream->__userbuf)
    {
      /* Unbuffered stream.  Not much optimization to do.  */
      register int c = 0;
      while (--n > 0 && (c = getc (stream)) != EOF)
        if ((*p++ = c) == '\n')
          break;
      if (c == EOF && (p == s || ferror (stream)))
        return NULL;
      *p = '\0';
      return s;
    }

  /* Leave space for the null.  */
  --n;

  if (n > 0 &&
      (!stream->__seen || stream->__buffer == NULL || stream->__pushed_back))
    {
      /* Do one with getc to allocate a buffer.  */
      int c = getc (stream);
      if (c == EOF)
        return NULL;
      *p++ = c;
      if (c == '\n')
        {
          *p = '\0';
          return s;
        }
      else
        --n;
    }

  while (n > 0)
    {
      size_t i;
      char *found;

      i = stream->__get_limit - stream->__bufp;
      if (i == 0)
        {
          /* Refill the buffer.  */
          int c = __fillbf (stream);
          if (c == EOF)
            break;
          *p++ = c;
          --n;
          if (c == '\n')
            {
              *p = '\0';
              return s;
             }
          i = stream->__get_limit - stream->__bufp;
        }

      if (i > (size_t) n)
        i = n;

      found = (char *) __memccpy ((void *) p, stream->__bufp, '\n', i);

      if (found != NULL)
        {
          stream->__bufp += found - p;
          p = found;
          break;
        }

      stream->__bufp += i;
      n -= i;
      p += i;
    }

  if (p == s)
    return NULL;

  *p = '\0';
  return ferror (stream) ? NULL : s;
}

weak_alias (fgets, fgets_unlocked)
  
  行数としては短いが、プログラマーでもない上、
C言語に強いわけでもない私だと、このソースでも読む気が失せる。

  だが、ふと気づいた。

  n = 1 の部分に該当する処理を読めばええやないか!

  もちろん、エラー処理は読み飛ばす。
  すると意外にスッキリした感じで読める。

n = 1 の部分に該当する処理だけを抜粋すると
#include <errno.h>
#include <stdio.h>
#include <string.h>

char *
fgets (s, n, stream)
     char *s;
     int n;
     FILE *stream;
{
  register char *p = s;

  /* Leave space for the null.  */
  --n;

  if (p == s)
    return NULL;
}

weak_alias (fgets, fgets_unlocked)

  メチャクチャ呆気ない。
  何せ、青い部分の処理のため、「n = 0」となってしまい、
その後の処理が、ことごとく飛ばされるのだ。

  これから言えるのは、fgets()関数に渡した文字列変数を
全く加工している形跡がない事だ。
  つまり、glibc-2.2.5を使っている場合においては
fgets(buf,1,stdin);をしても、bufの中身が変わる事はないのだ。


  あと、待ち受けしない理由も、わかった。
  もし、指定した文字数が2以上の場合なら、ソースの処理の部分で、
getc()関数で入力の待ち受け処理を行っているのだ。


  さて、fgets(buf,1,stdin);のケースは、glibcのバージョンによって
話が変わってくる事が十分に考えらる。

  そのため同じ実験内容でも、違いバージョンのglibcを使った場合、
「結果が違うぞ!」と抗議にこないでくださいね。
  

IPアドレスの個々の数字の抽出。文字列の区切りの判定

結構、文字列処理に手間取るのだが、他にも文字列処理がある。 それは、ftpコマンドのPASVをサーバーに送った時に、 サーバーから返って来る返答(IPとポート番号)の処理だ。
PASVコマンドを送った時のサーバーからの返事
[suga@xxx suga]# telnet 192.168.X.Y ftp
Trying 192.168.X.Y...
Connected to 192.168.X.Y.
Escape character is '^]'.
220 ProFTPD 1.2.10 Server (ProFTPD Default Installation) [192.168.X.Y]
USER suga
331 Password required for suga.
PASS *****
230 User suga logged in.
PASV
227 Entering Passive Mode (192,168,X,Y,95,177)
赤い部分は、クライアントから送ったPASVコマンド。
青い部分は、サーバーからの返事だ。

  サーバーからの返事は227 Entering Passive Mode (192,168,X,Y,95,177)だが、
このメッセージは1つの文字列のため、この文字列を処理して
IPやポート番号を抽出する必要があるのだ。

  さすがに、困った。
  いくら考えても良い方法が思いつかないので、カンニングペーパーを見る。
  「C言語による TCP/IP ネットワークプログラミング」

  なるほどと思う処理だった。

  isdigit()関数とstrtok()関数を組み合わせているのだった。

  isdigit()関数は、文字変数の中身が数字かどうかを判断する物だ。


  さて、strtok()関数だ。
  この関数は慣れるまで手間がかかる。

strtok()関数とは
strtok()関数は文字列抽出に使う関数
1つの文字列があり、それをカンマごとに区切りたいと思った場合に
この関数を使います。

  具体的に、カンマ区切りにした後の、個々の文字列を取り出すには
次のようなstrtok()関数の使い方を行う。

strtok()関数とは
strtok()関数の使い方
最初の1つ目の文字列を抽出する際は、bufのアドレスを送るが、
2つ目以降は、NULLを送る。

なぜかって?  glibcを読んでくださいね。
私は読む気が起こりません (^^;;

  実際に、上のような事で実験を行ってみる。

strtok()関数の使い方 (strtok1.c)
#include <stdio.h>
#include <string.h>

int main(void)
{
    char buf[] = "10,20,30,40" ;
    char *a , *b , *c , *d ;

    a = strtok(buf,",");
    b = strtok(NULL,",");
    c = strtok(NULL,",");
    d = strtok(NULL,",");

    printf("%s - %s - %s - %s \n",a,b,c,d);

    return(0);
}
プログラムの実行結果
[suga@xxx ftp]$ ./strtok1
10 - 20 - 30 - 40
[suga@xxx ftp]$ 

  さて、isdigit()関数とstrtok()関数を組み合わせて、PASVコマンドの応答
すなわちサーバーからのIPとポート番号の指定を抽出する事にする。

  疑似的なプログラムで話を進めて行きます。

PASVのコマンドの結果からIP、ポート番号を抽出する方法
char res[] = "227 Entering Passive Mode (192,168,1,2,95,177)" ;
char *buf , *ip1 , *ip2 , *ip3 , *ip4 , *port1 , *port2 ;

buf = res ;

/*  最初の3文字は数字なので、3文字分ズラす */

buf = buf + 3 ;

/* この時 buf の中身は " Entering Passive Mode (192,168,1,2,95,177)" */

  次に、isdigit()関数を使って、IPアドレスのある場所まで
文字列をズラしていきます。

PASVのコマンドの結果からIP、ポート番号を抽出する方法
 while ( isdigit(*buf) == 0 ) 
       {
       buf = buf + 1 ;
       }

/* この時 buf の中身は "192,168,1,2,95,177)" */

  これでIPの先頭まで行った所で、ようやくstrtok()関数のお目見えです。
  これでIPアドレスと、ポート番号の抽出を行います。

PASVのコマンドの結果からIP、ポート番号を抽出する方法
 ip1 = strtok(buf,",");
 ip2 = strtok(NULL,",");
 ip3 = strtok(NULL,",");
 ip4 = strtok(NULL,",");
 port1 = strtok(NULL,",");
 port2 = strtok(NULL,",");

/* この時 port2 の中身は "177)" なので
   これから数字だけを抽出する処理が必要 */

  さて、残りをどうするか。何せ数字の後ろに、文字がついている。
  再び、isdigit()の登場になる。

PASVのコマンドの結果からIP、ポート番号を抽出する方法
 i = 0 ;
 while ( isdigit(port2[i]) != 0 )
    {
    i = i + 1 ;
    }

 /* 数字でなくなった場所にヌル文字を代入*/   
 port2[i] = '\0' ;

  これで完全に抽出できた。
  ただ、この時点で抽出された物だと、数字を文字列として扱っているので
atoi()関数を使って、数値に直す必要がある。
  なぜなら、ポート番号の計算を行う必要があるからだ。


  今までの、一連の流れをプログラムにすると次のソースになる。

IPアドレスの個々の値を抽出するプログラム
#include <ctype.h>
#include <stdio.h>
#include <string.h>

int main(void)
{
    char res[] = "227 Entering Passive Mode (192,168,1,2,95,177)" ;
    char *buf , *ip1 , *ip2 , *ip3 , *ip4 , *port1 , *port2 ;
    int i ;
    int ipa1 , ipa2 , ipa3 , ipa4 , po1, po2 ;

    buf = res ;

 /*  最初の3文字は数字なので、3文字分ズラす */

    buf = buf + 3 ;

    /* 数字の場所までズラす */

    while ( isdigit(*buf) == 0 ) 
	  {
          buf = buf + 1 ;
	  }

    /*  IP と ポート番号の抽出 */

    ip1 = strtok(buf,",");
    ip2 = strtok(NULL,",");
    ip3 = strtok(NULL,",");
    ip4 = strtok(NULL,",");
    port1 = strtok(NULL,",");
    port2 = strtok(NULL,",");

    /*  port2 の後ろについている文字列の消去 */

    i = 0 ;
    while ( isdigit(*(port2 + i)) != 0 )
	  {
	  i = i + 1 ;
	  }
    port2[i] = '\0' ;

    /*  この時点では数字は文字列と認識されているため
        数値に変換する */

    ipa1 = atoi(ip1) ;
    ipa1 = atoi(ip2) ;
    ipa1 = atoi(ip3) ;
    ipa1 = atoi(ip4) ;
    po1 = atoi(port1) ;
    po2 = atoi(port2) ;

    return(0);
}

  文字列の操作は、慣れるまで、本当に面倒だ。


入力パスワードを表示させない方法

どうせなら実際のftpソフトみたいに、パスワード入力の時、 入力文字を見えなくした方が良い。 入力文字が見える状態だと、肩越しでパスワードが丸見えになるからだ。 入力文字を表示させない方法だが、どうしようかと思ったが、 次のカンニングペーパーがある事を思い出す。 「Linuxプログラミング」(葛西重夫訳:ソフトバンク出版) termiosを使って、端末の操作を行うのだ。
パスワード入力の表示を消すには
  struct termios initterm , passwdterm ;

  /*  現在の端末の状態を取り出す */
  tcgetattr(fileno(stdin),&initterm);

  /*  現在の端末の状態を、passwdterm にコピー */
  passwdterm = initterm ;

  /* エコーがでない(入力した文字が表示しない)ようにする */
  passwdterm.c_lflag &= ~ECHO ;

  /* 変更した設定を反映させる */
  tcsetattr(fileno(stdin),TCSAFLUSH,&passwdterm);

  memset(mesg,'\0',100);
  printf("password : ");
  fgets(passwd,20,stdin);

  /* 元の状態に戻す */
  tcsetattr(fileno(stdin),TCSANOW,&initterm);

  こうする事によって、パスワードを入力した時に、入力文字が
一切、表示されなくなる。


ftpクライアントのプログラム

これで厄介な部分はなくなった。 プログラムを作成しては、テストを行い、そして追加していく方式をとった。 そして、ついにできた。388行のソースだ。 それをズラっと並べてみます。 ここをクリックすれば、ソースのダウンロードもできます。
完成したftpクライアントのソフト (ftps.c)
#include <termios.h>
#include <ctype.h>
#include <netdb.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>


/* メッセージの送信関数 */

void SendData(int fd , char *mesg)
{
  int len ;

  len = strlen(mesg) ;
  send(fd, mesg , len , 0 );
}


/* サーバーからのメッセージの受信 */

void GetRes(int fd)
{
  int size ; 
  char mesg[1024] ;

  memset(mesg,'\0',1024);
  size = recv(fd,mesg,1023,0);
}

void PortReq(char *IPaddr , int *i1 , int *i2 , int *i3 , int *i4 , int *i5 , int *i6)
{
  int j ;
  char *ip ;
  IPaddr = IPaddr + 3 ;

  printf("aaa %s \n",IPaddr);

  while( isdigit(*IPaddr) == 0 ) { IPaddr++ ; }

  ip = strtok(IPaddr,",");
  *i1 = atoi(ip) ;

  ip = strtok(NULL,",");
  *i2 = atoi(ip) ;

  ip = strtok(NULL,",");
  *i3 = atoi(ip) ;

  ip = strtok(NULL,",");
  *i4 = atoi(ip) ;

  ip = strtok(NULL,",");
  *i5 = atoi(ip) ;

  ip = strtok(NULL,",");

  j = 0 ;
  while ( isdigit(*(ip +j)) != 0 ) { j += 1 ; }
  ip[j] = '\0' ;
  *i6 = atoi(ip) ;
}

/* ファイルの送信 */

void SendTrans(int sockfd , char *ip , int sport)
{
  int result , len , trans_sockfd , size ;
  int filelen ;
  char buf[1024] , file[100] , mesg[100] ;
  struct sockaddr_in trans_address ;

  FILE *fp ;

  /* ファイル名を指定 */

  memset(file,'\0',100);
  printf("filename : ");
  fgets(file,100,stdin);
  filelen = strlen(file);
  file[filelen-1] = '\0' ;

  memset(mesg,'\0',100);
  sprintf(mesg,"STOR %s\n",file);
  SendData(sockfd ,mesg);


  fp = fopen(file,"r");

  trans_sockfd = socket(AF_INET,SOCK_STREAM,0);

  trans_address.sin_family = AF_INET ;
  trans_address.sin_addr.s_addr = inet_addr(ip);
  trans_address.sin_port = htons(sport) ;
  len = sizeof(trans_address);

  result = connect(trans_sockfd , (struct sockaddr *)&trans_address , len);
  if ( result == -1 )
     {
     fprintf(stderr,"Failed Connect ftp-server\n");
     exit(1);
     }

  memset(buf,'\0',1024);

  while ( ( size = fread(buf,1,1024,fp) ) > 0 )
        {
        write(trans_sockfd,buf,size);
        }  

  fclose(fp);
  close(trans_sockfd);
}

/* ファイルの受信 */

void ResvTrans(int sockfd , char *ip , int sport)
{
  int result , len , trans_sockfd , size ;
  int filelen ;
  char buf[1024] , file[100] , mesg[100] ;
  struct sockaddr_in trans_address ;

  FILE *fp ;

  /* ファイル名を指定 */

  memset(file,'\0',100);
  printf("filename : ");
  fgets(file,100,stdin);
  filelen = strlen(file);
  file[filelen-1] = '\0' ;

  memset(mesg,'\0',100);
  sprintf(mesg,"RETR %s\n",file);
  SendData(sockfd ,mesg);

  fp = fopen(file,"w");

  trans_sockfd = socket(AF_INET,SOCK_STREAM,0);

  trans_address.sin_family = AF_INET ;
  trans_address.sin_addr.s_addr = inet_addr(ip);
  trans_address.sin_port = htons(sport) ;
  len = sizeof(trans_address);

  result = connect(trans_sockfd , (struct sockaddr *)&trans_address , len);
  if ( result == -1 )
     {
     fprintf(stderr,"Failed Connect ftp-server\n");
     exit(1);
     }

  memset(buf,'\0',1024);
  while ( ( size = read(trans_sockfd,buf,1024) ) > 0 )
        {
        fwrite(buf,1,size,fp);
        }  

  fclose(fp);
  close(trans_sockfd);
}


/* ファイルリストの出力 */

void ListTrans(char *ip , int sport)
{
  int result , len , trans_sockfd , size ;
  char buf[1024] ;
  struct sockaddr_in trans_address ;


  trans_sockfd = socket(AF_INET,SOCK_STREAM,0);

  trans_address.sin_family = AF_INET ;
  trans_address.sin_addr.s_addr = inet_addr(ip);
  trans_address.sin_port = htons(sport) ;
  len = sizeof(trans_address);

  result = connect(trans_sockfd , (struct sockaddr *)&trans_address , len);
  if ( result == -1 )
     {
     fprintf(stderr,"Failed Connect ftp-server\n");
     exit(1);
     }

  memset(buf,'\0',1024);
  while ( ( size = read(trans_sockfd,buf,1024) ) > 0 )
        {
          fwrite(buf,size,1,stdout);
        }  

  close(trans_sockfd);
}

/* 転送ポートの割り当て */

void TransData(int sockfd,int sele)
{
  int size , i1 ,i2 ,i3 ,i4 , i5 , i6 , sport ;
  char mesg[100] , res[1024] , ip[20] ;


  /* PASV コマンドを送る */

  memset(mesg,'\0',100);
  sprintf(mesg,"PASV\n");
  SendData(sockfd , mesg);

  /*  PASVコマンドの返答を得る */

  memset(res,'\0',1024);
  size = recv(sockfd,res,1024,0);

  /*  ポート番号の割り出し */

  PortReq(res,&i1,&i2,&i3,&i4,&i5,&i6);

  memset(ip,'\0',20);
  sprintf(ip,"%d.%d.%d.%d",i1,i2,i3,i4);
  sport = i5 * 256 + i6 ;


  switch (sele)
         {
         case 1 :
         SendData(sockfd , "LIST\n");
         ListTrans(ip,sport);
         break;

         case 3 :
         SendTrans(sockfd , ip,sport);
         break;
         
         case 4 :
         ResvTrans(sockfd , ip,sport);
         break;

         default :
         break;
         }


  /* LIST , RETR , STOR コマンドへの返事 */
  GetRes(sockfd);

  /*  サーバーから、データ転送が終了した事を知らせるメッセージ */
  GetRes(sockfd); 
}

int main(int argc , char *argv[])
{
  int sockfd ;
  int len ;
  struct sockaddr_in address ;
  struct hostent *myaddr ;
  struct termios initterm , passwdterm ;
  int result ;
  char user[20] , passwd[20] , mesg[100] ;
  char chdir[20] , myhost[50] , myip[20] ;
  char menu[10] ;

  /* 引数がIPアドレスかどうかのチェック */

  if ( argc != 2 )  /* 引数が1つ以外にある場合 */
     {
       fprintf(stderr,"Usagi : ftps IP-Address\n");
       exit(1);
     }

  /*  ソケットの作成 */
  sockfd = socket(AF_INET,SOCK_STREAM,0);

  address.sin_family = AF_INET ;
  address.sin_addr.s_addr = inet_addr(*(argv + 1 ));
  address.sin_port = htons(21) ;
  len = sizeof(address);

  result = connect(sockfd , (struct sockaddr *)&address , len);

  /*  ftpサーバーへの接続 */

  if ( result == -1 )
     {
     fprintf(stderr,"Failed Connect ftp-server\n");
     exit(1);
     }
  else  /* 接続成功時のメッセージを受け取る */
     {
     GetRes(sockfd);
     }

  /* ID を送る */

  memset(mesg,'\0',100);
  printf("login name : ");
  fgets(user,20,stdin);
  sprintf(mesg,"USER %s",user);
  SendData(sockfd , mesg);
  GetRes(sockfd);

  /* パスワードを送る  */
  /*  入力文字を隠すため、端末制御を行う */

  tcgetattr(fileno(stdin),&initterm);
  passwdterm = initterm ;
  passwdterm.c_lflag &= ~ECHO ;

  tcsetattr(fileno(stdin),TCSAFLUSH,&passwdterm);

  memset(mesg,'\0',100);
  printf("password : ");
  fgets(passwd,20,stdin);
  sprintf(mesg,"PASS %s",passwd);
  SendData(sockfd , mesg);
  GetRes(sockfd);

  tcsetattr(fileno(stdin),TCSANOW,&initterm);

  printf("\n");

  /* メニュー */

  while( menu[0] != '9' )
       {
         printf("-------------------------\n");
         printf("1 : list\n");
         printf("2 : change directory\n");
         printf("3 : send file\n");
         printf("4 : receive file\n");
         printf("9 : quit\n");
         printf("\n");

         printf("Select number : ");
         fgets(menu,3,stdin);
         printf("\n");

         switch (menu[0]) {

         case '1' : /* ファイルのリストの出力 */
           TransData(sockfd,1);
           break ;

         case '2' :   /* ディレクトリー変更の処理 */
           memset(chdir,'\0',20);
           memset(mesg,'\0',100);
           printf("Directory name : ");
           fgets(chdir,20,stdin);
           sprintf(mesg,"CWD %s",chdir);
           SendData(sockfd , mesg);
           GetRes(sockfd);
           break ;

         case '3' :
           TransData(sockfd,3);
           break ;

         case '4' :
           TransData(sockfd,4);
           break ;

         default :
           break ;
         }
       }         

  /* quit を送る */

  memset(mesg,'\0',100);
  strcpy(mesg,"quit\n") ;
  SendData(sockfd , mesg);
  GetRes(sockfd);


  return(0);
}

  一応、実用的に使える物ができた (^^)V

  まぁ、細かい部分で、サーバーとのやりとりが失敗した時の
エラー処理などは考慮していませんので、完成度としては
高いとは言いづらいですが、まぁ、最初だから、これで良いのらー!!  (^^)


まとめ C言語を勉強した後、「さて、実際に何を作れば良いのか」という壁に ぶつかる場合があると思います。 新人プログラマーの場合、仕事上、会社や上司から「これ作ってね」と 課題が与えられたりしますが、独学の場合だと、それがないだけに 「何を作ろう」で戸惑うかと思います。 それに対する答えとして「既存のアプリの真似でも良いから作ってみる」が 正解かなぁと思ったりしました。 実際に、ftpクライアントの作成で、文字列処理に往生したり、 キチンとクライアントとサーバーとのやりとりを把握しないと メッセージのキャッチボールが、うまくできなかったりという事があります。 今回の事で、次の事を思いました。 「Linuxプログラミング」(葛西重夫訳:ソフトバンク出版)の内容を理解すれば 組めるプログラムは結構ありそうな事です。 だが問題は、いかに勉強した知識を使いこなすかです! 巷では、「Linuxプログラミング」(葛西重夫訳:ソフトバンク出版)の本は C言語の中級者を目指す人の本と言われていますが、私は、この本にある事を 使いこなさないと、C言語の中級者にはなれないなぁと感じました。 そのための覚えた知識を使う訓練として、色々なソースを参考にしながら、 自分でソフトを作る事だと思いました。  ここをクリックすれば、ソースのダウンロードができます。

次章:「Microsoft製品を使ってLinuxの勉強」を読む
前章:「ftpサーバー構築」を読む
目次:システム奮闘記に戻る

Tweet