質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

89.98%

WinsockでFTPのプロトコルだけでFTPクライアント処理の実現

解決済

回答 3

投稿 編集

  • 評価
  • クリップ 1
  • VIEW 2,261

kanbye

score 14

前提・実現したいこと

FTPの処理手順を知るために、Winsockを利用してFTPを通してファイルをやり取りするソフトを作成しています。最終的にはWinsockを利用しないため(別のAPIを利用するため)、Winsockにある直接ファイルをやり取りするFTPGetFile/FTPPutFilといった関数は利用することはできません。send/recvを利用してデータを宗純したいと思います。
ネットからいろいろサンプルを使って作成していますが、PORTコマンドを実行した後、再度コネクト処理を実現するところで必ず失敗します。
手順が不足または間違っていると思いますが、よろしくご教授ください。

発生している問題・エラーメッセージ

「対象のコンピューターによって拒否されたため、接続できませんでした。」
となり、2度目のCONNECT処理にて接続エラーになります。
なお、手動では問題なく接続できています。
手動での確認方法はコマンドプロンプトから"ftp -d"とftpのプロトコル表示をしながら確認しています。

該当のソースコード

開発言語はVS2008上のC言語で行っています。
本ソースはPassiv接続をActive接続に書き直してテストしています。

void _tmain(void){
    char szStr[256];    //    送信バッファ
    char szStrRcv[1024];    //    受信バッファ

    _tsetlocale(LC_ALL, _TEXT(""));    //

    //    FTPサーバーの定義
    strcpy_s(::szFtpServer, sizeof(::szFtpServer), SERVER_NAME);
    strcpy_s(::szUserName, sizeof(::szUserName), USER_NAME);
    strcpy_s(::szPass, sizeof(::szPass), PASSWORD);

    WSADATA wsaData;
    LPHOSTENT lpHost;

    SOCKET s;
    SOCKADDR_IN sockadd;

    //    Winsockの初期化
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
        WSA_ERROR_MSG(_TEXT("関数WSAStartup:error"));
        return;
    }
    lpHost = gethostbyname(::szFtpServer);
    if (lpHost == NULL) {
        WSA_ERROR_MSG(_TEXT("関数gethostbyname:error"));
        return;
    }
    //    コントロールコネクション用のソケット作成
    s = socket(AF_INET, SOCK_STREAM, 0);
    if (s == INVALID_SOCKET) {
        WSA_ERROR_MSG(_TEXT("関数socket:error"));
        WSACleanup();
        return;
    }

    sockadd.sin_family = AF_INET;
    sockadd.sin_port = htons(PORT);    // ホストバイトオーダーをネットワークバイトオーダーに変換
    sockadd.sin_addr.S_un.S_addr = inet_addr(::szFtpServer);    //    サーバーのIPアドレス取得
    if (connect(s, (PSOCKADDR)&sockadd, sizeof(sockadd))) {    //    コントロールコネクションサーバーへ接続
        WSA_ERROR_MSG(_TEXT("関数connect:error"));
        closesocket(s);
        WSACleanup();
        return;
    }

    memset(szStrRcv, '\0', sizeof(szStrRcv));
    recv(s, szStrRcv, sizeof(szStrRcv)-1, 0);    //    サーバーからのメッセージを取得
    printf(szStrRcv) ; 

    if (strncmp(szStrRcv, "220", 3) != 0){
        WSA_ERROR_MSG(_TEXT("error"));
        closesocket(s);
        WSACleanup();
        return;
    }
    //    ユーザー名を送信
    sprintf_s(szStr, sizeof(szStr), "USER %s\r\n", ::szUserName);
    printf(szStr) ; 
    send(s, szStr, (int)strlen(szStr), 0);
    memset(szStrRcv, 0, sizeof(szStrRcv));
    recv(s, szStrRcv, sizeof(szStrRcv)-1, 0);
    printf(szStrRcv) ; 

    if (strncmp(szStrRcv, "331", 3) != 0){
        WSA_ERROR_MSG(_TEXT("error"));
        closesocket(s);
        WSACleanup();
        return;
    }

    //    パスワードを送信

    sprintf_s(szStr, sizeof(szStr), "PASS %s\r\n", ::szPass);
    printf(szStr) ; 
    send(s, szStr, (int)strlen(szStr), 0);
    memset(szStrRcv, 0, sizeof(szStrRcv));
    recv(s, szStrRcv, sizeof(szStrRcv)-1, 0);
    printf(szStrRcv) ; 

    if (strncmp(szStrRcv, "230", 3) != 0){
        WSA_ERROR_MSG(_TEXT("error"));
        closesocket(s);
        WSACleanup();
        return;
    }

    //    サーバーのカレントディレクトリを指定
    sprintf_s(szStr, sizeof(szStr), "CWD ./\r\n");
    printf(szStr) ; 
    send(s, szStr, (int)strlen(szStr), 0);
    memset(szStrRcv, 0, sizeof(szStrRcv));
    recv(s, szStrRcv, sizeof(szStrRcv)-1, 0);
    printf(szStrRcv) ; 

    //BIND設定
    SOCKADDR_IN sockmine;
    sockmine.sin_family = AF_INET;
    int len = sizeof(SOCKADDR_IN);
    //sockmine.sin_addr.s_addr = INADDR_ANY;
    //sockmine.sin_port = 0;
    if (getsockname(s, (struct sockaddr *)&sockmine, &len) < 0)
    {
        WSA_ERROR_MSG(_TEXT("getsockname failed.\n"));
        closesocket(s);
        WSACleanup();
        return;
    }

    unsigned long low, hi;
    hi  = (ntohs(sockmine.sin_port) >> 8) & 0xff;
    low = ntohs(sockmine.sin_port) & 0xff;

    sprintf_s(szStr, "PORT %d,%d,%d,%d,%d,%d\n",
                    sockmine.sin_addr.S_un.S_un_b.s_b1,
                    sockmine.sin_addr.S_un.S_un_b.s_b2, 
                    sockmine.sin_addr.S_un.S_un_b.s_b3, 
                    sockmine.sin_addr.S_un.S_un_b.s_b4, 
                    hi,low);
    send(s, szStr, (int)strlen(szStr), 0);
    memset(szStrRcv, '\0', sizeof(szStr));
    recv(s, szStrRcv, sizeof(szStrRcv) - 1, 0);
      printf(szStrRcv) ;

    sockmine.sin_addr.S_un.S_addr = inet_addr(::szFtpServer)  ;

    SOCKET s2 = FtpDataConnect(sockmine.sin_addr, hi * 256 + low);
    //    LISTコマンドを送信
    sprintf_s(szStr, sizeof(szStr), "LIST\r\n");
    printf(szStr) ; 
    send(s, szStr, (int)strlen(szStr), 0);
    memset(szStrRcv, '\0', sizeof(szStr));
    recv(s, szStrRcv, sizeof(szStrRcv)-1, 0);
    printf(szStrRcv) ; 

    //    データーコネクションサーバーからLISTコマンドの結果を受け取る
    FtpList(s2);
    closesocket(s2);

    closesocket(s);
    WSACleanup();
    return;
}

//    データーコネクションサーバーへ接続

SOCKET FtpDataConnect(in_addr ipa, unsigned int port){
    SOCKET s;

    s = socket(AF_INET, SOCK_STREAM, 0);
    if (s == INVALID_SOCKET) {
        WSA_ERROR_MSG(_TEXT("関数socket:error"));
        return 0;
    }

    struct sockaddr_in sockadd = { AF_INET, htons(port) };
    sockadd.sin_addr = ipa;

    if (connect(s, (PSOCKADDR)&sockadd, sizeof(sockadd)) == -1) {
        WSA_ERROR_MSG(_TEXT("関数connect:error"));
        closesocket(s);
        return 0;
    }
    return s;
}

//    LISTコマンドの結果をファイルftplist.txtに保存

void FtpList(SOCKET s){
    char buf[16];

    FILE* fp;
    if (_tfopen_s(&fp, _TEXT("D:\\ftplist.txt"), _TEXT("w") )){
        _tprintf(TEXT("ファイルが開けません\n"));
        return;
    }
    int len;
    do{
        len = recv(s, buf, sizeof(buf), 0);
        fwrite(buf, sizeof(char), len, fp);
    } while (len == sizeof(buf));
    fclose(fp);
}


//    Winsockのエラーコードに対応するメッセージを標準出力に表示する
void WSA_erro_msg(void){
    __WSA_ERROR_CODE__ = WSAGetLastError();
    LPVOID lpMsgBuf;

    FormatMessage(
        FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
        FORMAT_MESSAGE_IGNORE_INSERTS,
        NULL,
        __WSA_ERROR_CODE__,
        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // 既定の言語
        (TCHAR*)&lpMsgBuf,
        0,
        NULL
        );

    _tprintf(_TEXT("%s\n"), lpMsgBuf);

    // バッファを解放する。
    LocalFree(lpMsgBuf);
}

試したこと

ポート番号が手動でコマンド打ちした場合(コマンドラインから"ftp -d"で動作)と、ポート番号があっていないのが原因かと思い、ポート番号を固定してみましたが、変化ありませんでした。

補足情報(言語/FW/ツール等のバージョンなど)

VS2013/2015上でもコンパイル動作はしていますので、VSのバージョンの問題はありません。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • kanbye

    2016/10/19 21:57

    設定どおりには入っております。にもかかわらず、接続できません。現在は、対処のサーバーより拒否されましたとなります。コードの問題以前に必要なプロトコルが正しいのかどうかが不明です。

    キャンセル

  • ikedas

    2016/10/19 22:06

    いやいや(^^;)、まずは正しい書き方のコードにしないと、コードが正しい実装になってるのかも確かめられないでしょう。まあそれはともかく、ぱっと見おかしいところはなくなったようなんで、これからコードをちゃんと読ませてもらいますね。FTPのアクティブモードの実装とはなかなか意欲的なテーマですね。

    キャンセル

  • kanbye

    2016/10/19 22:34

    要は安定的にFTPクライアントしてファイルを送受信できればいいのです。アクティブモードを選択したのはこれが標準と思っているためです。

    キャンセル

回答 3

+1

元ねたはこちらでしょうかね。それはともかく、回答します。


//    コントロールコネクション用のソケット作成

最初のgethostbyname()からconnect()までは、これまではサーバに接続する際の定番だった処理ですが、今後はgetaddrinfo()を使うようにしてもいいと思います。特に、今後IPv6対応の可能性があるのならgetaddrinfo()は必須です。Windows XP以降で対応しています。

recv(s, szStrRcv, sizeof(szStrRcv)-1, 0);    //    サーバーからのメッセージを取得

recv()の一度の呼び出しでサーバからの応答をすべて読み込めるとは限りません。これにはふたつの理由があります。

  • 応答はバッファリングされます。recv()の返値が正の値である間は、読みとられて空になったバッファに残りのデータが入ってくるかもしれないので、再びrecv()を呼び出して読み取ります。最終的にrecv()が0を返せば、送られてくるデータは本当におしまいです。(回答後追記) これはソケットがノンブロッキングモードのときの動作です。ブロッキングモードだとrecv()はなにかデータが来るかエラーになるまで返ってきません。デフォルトはブロッキングモードで、ノンブロッキングモードにするにはWinSockではioctlsocket()をFIONBIOを渡して呼び出します。
  • RFC 959によれば、FTPサーバからの応答は複数行になりえます (4.2. FTP REPLIESを参照)。recv()が0を返しても、サーバがまだ複数行応答の途中までしか送ってきていないのであれば、さらに待って読み取らなければなりません (具体的にいうと、「数字3桁と空白で始まる行の改行まで」を読み取るまでは、応答は完了していません)。
    プロトコルの実装では、相互運用性の問題が生じないよう、プロトコルの定義をしている元の文書も確認されることをお勧めします。FTPでは、上述のRFC 959ですね。
//    ユーザー名を送信

send()も、一度の呼び出しでバッファの内容すべてを送れるとは限りません。send()の返値は実際に送れたバッファの先頭からのバイト数ですので、途中までなら残りを送りなおさなければなりません (ちなみに、実際に「途中までしか送れない」といったことが起きるかどうか、私は確かめたことはないです。ただ、ドキュメントにそう書いてあるので)。

//BIND設定

bind()の説明によれば、第2の引数に渡されるsockaddr構造体について「For TCP/IP, if the port is specified as zero, the service provider assigns a unique port to the application from the dynamic client port range.」とのことです。ですからsin_portを0にしてbind()したあとでgetsockname()を使えばバインドしたポート番号がわかりますね。
ご質問のコードでもそうしようとしているようです。ただ、sockaddr構造体の内容を初期化していません。そのため、bind()を呼んだときにsin_portに0が入っている保証がありません。自動変数も含め、新たに確保したメモリは必ず初期化しましょう。

sockmine.sin_addr.S_un.S_addr = inet_addr(::szFtpServer)  ;

これは何をしているのかちょっとわかりませんでした。lesten()、connect()が成功すれば相手先アドレスは自動的に得られるはずです。

(nagaettyさんの指摘により追記)

//    データーコネクションサーバーへ接続

これなんですが、前のPORTコマンドの送信も含め、正しく処理できていないと思います。次のようにしないといけないです。

  1. socket()で新しいソケットを作成。※コントロールコネクションのソケットとは別です。
  2. sockaddr構造体を初期化。ソケットをbind()。
  3. getsockname()を使って、バインドしたポート番号を取得。
  4. listen()でソケットを接続受付可能に。
  5. PORTコマンドをコントロールコネクションで送信。
  6. accept()で接続待ち (接続するまでブロック)。
  7. コントロールコネクションでサーバからの応答を受信してコマンドが成功したことを確認。

(追記おわり)


あと、全般的なこととして、Windows Socketsや標準ライブラリの関数を呼び出したときは、必ず返値をチェックし、エラーのときは対処するようにコードを書くべきだと思います。

とりあえず気づいた点は以上です。コードを修正してみてまた問題が起きるようなら、「質問2」とでも見出しをつけて追記していただければいいと思います。また、エラーメッセージについては表示されたまま (エラーコードなども含めて) を知らせてください。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

check解決した方法

0

いろいろその後、調べました結果、Active接続の時点で勘違いしておりました。
Passive接続が一般であり、実際その方法で接続はうまくいきました。

サンプルソースに対して、PORTコマンドを実行後、PASVコマンドを実行することで、その後、思い通りの処理ができました。

sockmine.sin_addr.S_un.S_addr = inet_addr(::szFtpServer)  ;

これはPORTコマンドに実行で、利用するポート番号を取得しますが、この後、データコネクトを行う場合、サーバーアドレスが必要となるため、サーバーアドレスに戻す必要があるため、設定しています。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

0

※削除しました。

データ通信経路の接続要求を行う前に(PORT要求送信前に)受信用の処理を起動させて、受信待ちにしておく必要があるのでないかと思います。

理由は:
サーバ側の処理が速い場合は、サーバから接続要求が送信された時点で、受信PORTが開いていない
可能性があります。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2016/10/22 15:09 編集

    ※前のコメントは削除しました。

    本当だ! bind()したあとで二度目のconnect()をする前にlisten()していませんね。後で回答に追記します。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 89.98%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

同じタグがついた質問を見る