java.net.Socket

目的
FTP経由でファイルダウンロードを行う。

関連クラス

今回のソース
今回は、二つのファイルに分けました。
////////////////////  jftp.java  ////////////////////

class jftp
{
	public static void main(String args[])
	{
		if(args.length < 4)
		{
			Usage();
		}
		new FTPClient(args[0],args[1],args[2],args[3]);
	}

	// 引数が足りない時に、使い方を表示する。
	public static void Usage()
	{
		System.err.println("Usage:java jftp URL UserName Password type");
		System.err.println("URL:(ex)http://www.yahoo.co.jp/index.html");
		System.err.println("type:Ascii or Binary");
		System.exit(-1);
	}
}

////////////////////////////////////////////////////////////

////////////////////  FTPClient.java  ////////////////////

import java.net.*;
import java.util.StringTokenizer;
import java.io.*;

class FTPClient
{
	// 受信コード
	public static final String READY = "220";
	public static final String USER = "331";
	public static final String PASS = "230";
	public static final String COMMAND = "200";
	public static final String OPEN = "150";
	public static final String TRANSFER = "226";

	// 定数
	public static final char return_code = 0x0a;
	public static final int client_port = 1094;

	// メッセージ送受信用I/Oストリーム
	BufferedReader input;
	DataOutputStream output;

	// Publicメンバ変数
	URL url;
	String user_name;
	String password;
	String type;
	String file_name;
	int ip[];

	// コンストラクタ
	public FTPClient(String param_url,String user_name,String password,String type)
	{
		this.user_name = user_name;
		this.password  = password;
		this.type      = type;

		connection(param_url);
	}

	// FTPコネクション
	public void connection(String param_url)
	{
		try
		{
			url = new URL(param_url);

			// IPアドレスの取得
			int index = 0;
			ip = new int[4];
			StringTokenizer string;

			Socket socket = new Socket(url.getHost(),21);
			InetAddress ipaddress = InetAddress.getLocalHost();
			string = new StringTokenizer(ipaddress.getHostAddress(),".");
			while(string.hasMoreTokens())
			{
				ip[index] = Integer.parseInt(string.nextToken());
				index++;
			}

			// ファイル名の解析
			string = new StringTokenizer(url.getFile(),"/");
			String file_name = new String();
			while(string.hasMoreTokens())
			{
				file_name = string.nextToken();
			}

			// 送受信用リスナ
			ServerSocket ssocket = new ServerSocket(client_port);

			// メッセージ送受信用I/Oストリームの取得
			input = new BufferedReader(new InputStreamReader(socket.getInputStream()));
			output  = new DataOutputStream(socket.getOutputStream());

			// FTP 通信の開始
			ReceiveMessage(READY);

			UserAuthentication(user_name,password);
			SetType(type);
			OpenClientPort(ip,client_port);
			int file_size = ReceiveFile(url.getFile().substring(1));

			// 受信用リスナの構築
			Socket receive = ssocket.accept();
			DataInputStream data_in = new DataInputStream(receive.getInputStream());
			DataOutputStream file_out = new DataOutputStream(new FileOutputStream(file_name));

			// 受信&書き出し
			String str;
			for(int i=0;i<file_size;i++)
			{
				file_out.writeByte(data_in.readByte());
			}

			ReceiveMessage(TRANSFER);

			// 終了処理
			socket.close();
			ssocket.close();
		}
		catch(Exception e)
		{
			System.err.println(e.toString());
		}
	}

	// リターンコードの確認
	protected boolean isReady(String string,String type)
	{
		if(string.substring(0,3).compareTo(type) == 0)
		{
			return(true);
		}
		return(false);
	}

	// エラーメッセージルーチン
	protected void ErrorMessage(String message)
	{
		System.err.println(message);
		System.exit(-1);
	}

	// メッセージの送信
	protected void SendMessage(String message)
	{
		try
		{
			output.writeBytes(message + return_code);
		}
		catch(IOException e)
		{
			System.err.println(e.toString());
		}
	}

	// 受信メッセージの判断
	protected String ReceiveMessage(String type)
	{
		try
		{
			String res = input.readLine();
			System.out.println(">" + res);
			if(!isReady(res,type))
			{
				ErrorMessage("Error: code = " + type);
			}
			return(res);
		}
		catch(IOException e)
		{
			System.out.println(e.toString());
			return("Error_Code");
		}
	}

	// ユーザー認証
	protected void UserAuthentication(String user_name,String password)
	{
		SendMessage("USER " + user_name);
		ReceiveMessage(USER);

		SendMessage("PASS " + password);
		ReceiveMessage(PASS);
	}

	// Ascii or Binaryモードセット
	protected void SetType(String type)
	{
		if(type.toLowerCase().compareTo("ascii") == 0)
		{
			SendMessage("TYPE A");
			ReceiveMessage(COMMAND);
		}
		else
		{
			SendMessage("TYPE I");
			ReceiveMessage(COMMAND);
		}
	}

	// データ受信用ポートの通知
	protected void OpenClientPort(int ip[],int port)
	{
		int high_port_number,low_port_number;

		high_port_number = port / 256;
		low_port_number  = port % 256;
		SendMessage("PORT " + ip[0] + "," + ip[1] + "," + ip[2] + "," + ip[3] + "," 
						+ high_port_number + "," + low_port_number);
		ReceiveMessage(COMMAND);
	}

	// ファイル受信コマンド
	protected int ReceiveFile(String file_name)
	{
		String receive;

		SendMessage("RETR " + file_name);
		receive = ReceiveMessage(OPEN);
		return(GetFileSize(receive));
	}

	// ファイル送信コマンド
	protected int StoreFile(String file_name)
	{
		String receive;

		SendMessage("STOR " + file_name);
		receive = ReceiveMessage(OPEN);
		return(GetFileSize(receive));
	}

	// 受信/送信メッセージからファイルサイズを求める
	protected int GetFileSize(String message)
	{
		int size,begin_index,end_index;

		begin_index = message.lastIndexOf('(') + 1;
		end_index   = message.lastIndexOf("bytes") - 1;
		size = Integer.parseInt(message.substring(begin_index,end_index));

		return(size);
	}
}

////////////////////////////////////////////////////////////
Source is here. (ZIP Format,2250Byte,Shift-JIS)

コンパイル&実行
javac jftp.java
java jftp <URL> <ユーザー名> <パスワード> <転送モード>
転送モード : (AsciiかBinaryのどちらかを指定)

説明
(概略)

SocketクラスとServerSocketクラスを用いた、
独自設計のSocket通信プログラムは、サンプルがあちこちに
あるはずなので、止めました。
なので今回は、Socketクラスを用いて、
FTP Serverに接続するようなプログラムを書きました。
ダウンロードのみ、GUIは無しと、あまり使えるプログラムではありませんが、
Socket通信とFTPのプロトコルについて、何かの参考になれば幸いです。

Socket通信の概略は、
  1. サーバーのアドレスとポート番号を指定して、サーバーに接続する。
  2. 入出力ストリームを得て、サーバーとの送受信を可能にする。
  3. プロトコルにしたがって、データを送受信する。

FTPの概略は、
  1. まず、21番Portにつなぐ。
  2. ユーザー認証をする。
  3. データ送受信用のリスナを作成する。
  4. データタイプを決定する。(テキスト or バイナリ)
  5. クライアントが受け付け可能なポートをサーバーに通知する。(3で指定したもの)
  6. RETRもしくはSTORを送り、送信もしくは受信を指定する。
  7. リスナにaccept()で接続を受け付ける。
  8. 送受信をする。

(サンプルプログラムの説明)

受信コード
これは、サーバーから送られてくる受信メッセージの
先頭の3桁の数字をあらわします。
この定数と照合して、正しいメッセージが送られているかどうかを
ReceiveMessageメソッドで確認します。
コンストラクタ
プログラムの起動オプションを受け取って、メンバ変数に設定します。
URLだけは、URLクラスのインスタンスの生成の時に例外を発生するので、
今回は、connectionメソッドの方に渡してしまいました。
Socket socket = new Socket(url.getHost(),21);
クライアントのソケットの構築。
URLからホスト名を得て、FTPの21番ポートにつないでいます。
ip[index] = Integer.parseInt(string.nextToken());
ローカルホストのIPアドレスを、'.'で分解し、
それぞれをint型に直して、配列に格納しています。
このIPアドレスは、サーバーからのデータを受け付けるための、
ポートを知らせる、PORTコマンドに使います。
file_name = string.nextToken();
/usr/home/taka/index.htmlのようなパス付きのファイルを、
'/'で区切り、前から順番に代入して行くので、
(この場合だと、usr、home、taka、index.htmlの順に代入される。)
パスの最後にくるindex.htmlがfile_nameに代入される。
メモリのムダ使いですが、Javaならガーベージコレクタが処理してくれるので、
問題は無いでしょう。
リターンコードの確認
このメソッドは、FTPサーバーから送られてくる、
メッセージを確認しています。
例えば、FTPサーバーにログインした後に送られてくる、
230 User taka logged in.
のようなメッセージには、必ず3桁の数字がついてくるので、
その最初の3桁の数字を用いて、正しい応答かどうかを判断しています。
output.writeBytes(message + return_code);
メッセージの送信には、送るメッセージに改行コードを付加する必要があります。
改行コードに定数を使っているので、このプログラムは既に
100% Pure Javaでは無くなっていますね。
データ受信用ポートの通知
これは、私が一番悩んだところなので、詳しく説明しようと思います。

FTPクライアントは、サーバーとのメッセージのやりとりは、
クライアントとして行いますが、データのやりとりは、
サーバーとして行います。(FTPサーバーがクライアントになる。)
つまり、FTPクライアントには、サーバーを実装しなければならないのです。
その(クライアントで実装した)サーバーが、何番のポートで接続待ちを
しているかを知らせるのがこのPORTコマンドです。

送信の形式は、
PORT ip,ip,ip,ip,port,port
で、各数値は8bitの整数で、カンマで区切られています。
port番号は、0〜65535(だったと思う)までなので、
16bitの整数で表わせますよね。
つまり、PORTコマンドの下位2バイト(=16bit)を用いて、
接続可能なポートをサーバーに教えてやることになります。
受信/送信メッセージからファイルサイズを求める
このファイルサイズは、RETRコマンドを送信した後の応答で、
150 OPEN ... (xxx bytes ...)
というようなメッセージが送られてくるので、
そのメッセージを分解して、バイト単位でサイズを取り出しています。
このとき気をつけなければいけないのは、substringメソッドの挙動です。
substring(a,b)という風にやると、aを含み、bの一つ前までの文字列を切り出す
ということです。
要は、bの位置の文字を含まないことに注意して下さい。