Engineering Note

プログラミングなどの技術的なメモ

FTPクライアントプログラムの作成③(Javaによるネットワークプログラミング)

ftp

本記事は、Javaによるネットワークプログラミングについての学習メモとなります。

その中でNIOフレームワークであるNettyを使った実装方法について学んでいきます。

今回は、前回作成したFTPクライアントプログラムのデータ転送用コネクションの機能追加としてgetコマンドを作成します。

 

 

FTPについて

FTPはFile Transfer Protocol(ファイル・トランスファープロトコル )の略で、その名の通りファイル転送を行うためのプロトコルになります。

その歴史は古く、最初に登場したのは1971年4月でRFC114としてまとめられています。

 

 

FTPではTCPポート20番と21番を使いデータ転送を行っていきます。

TCPポート20番はデータ転送用コネクションとして使用し、TCPポート21番は制御用コネクションとして使用します。

前回ではデータ転送用コネクションの機能としてlsコマンドを実装しました。

 

 

今回は追加としてgetコマンドを作成し、リモートからファイルを取得してみます。

 

バイナリモードとアスキーモード

 

FTPのファイル転送にはバイナリモードとアスキーモードというものが存在します。

UNIXLinuxなどのOSでは改行文字で"LF(ラインフィード)"を使用しますが、Windowsでは改行文字で"CR(キャリッジリターン) + LF(ラインフィード)"を使用します。

そのため、バイナリモードではファイルを変換せずにそのまま転送しますが、アスキーモード(テキストモード)では、改行部分を変換してから転送します。

 

今回はgetコマンドが入力された場合は、TYPEコマンドにオプションI(Image)を付与して、強制的にバイナリモードにします。

 

Nettyについて

Javaには元々java.ioというパッケージが存在しますが、ブロッキングIOやストリーム指向であることから、Java 1.4からjava.nioというパッケージが導入されました。

このNIO(New IO)では、ノンブロッキングIOとバッファ指向で設計され、さらにセレクタを利用し単一スレッドで複数の入力チャネルをモニタ出来るようになり、様々な改良が加えられました。

今回はこのNIOのフレームワークであるNettyを利用していきます。

以下が公式ドキュメントになります。

 

 

FTPクライアントプログラムの作成

それではFTPクライアントプログラムにgetコマンドを追加していきます。

 

# SimpleTCPChannelHandler.java
package ftpclient;


import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

public class SimpleTCPChannelHandler extends SimpleChannelInboundHandler<String> { 
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        String[] remoteIp = ctx.channel().remoteAddress().toString().split("/");
        System.out.println("[*]connect to " + remoteIp[1] + " successfully!!");
    }
    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        System.out.println("[*]server connection closed.");
    }
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        int status_code = Integer.parseInt(msg.substring(0, 3));
        switch (status_code) {
            case 220:
                FtpUtil.doLoginUser(ctx.channel());
                break;
            case 221:
                ctx.disconnect();
                break;
            case 331:
                FtpUtil.doLoginPass(ctx.channel());
                break;
            case 530:
                System.out.println("login failed.");
                FtpUtil.command(ctx.channel());
                break;
            case 229:  
                int port = Integer.parseInt(FtpUtil.getPortNum(msg));
                FtpUtil.getDataConnection(FtpUtil.hostName, port);
                if (FtpUtil.currentCommand.equals("get")) {
                    ctx.channel().writeAndFlush("RETR " + FtpUtil.fileName + "\r\n");
                    FtpUtil.openFile();
                    FtpUtil.recvData();
                }
                 else {
                    FtpUtil.recvData();
                }
                break;
            case 226:
                break;
            case 200:  // switching binary mode
                if (FtpUtil.currentCommand.equals("get")) {
                    ctx.channel().writeAndFlush("EPSV 2\r\n");
                }
                break;
            case 250:
            case 230:
            case 150:
                FtpUtil.command(ctx.channel());
                break;
            default:
                System.out.print(msg);
                FtpUtil.command(ctx.channel());
        }
    }
}

 

 

# SimpleTCPClientBootstrap.java
package ftpclient;

import java.net.ConnectException;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;

public class SimpleTCPClientBootstrap {   
    public void start(String host, int port) throws InterruptedException, ConnectException {
        System.out.println("[*]connecting to " + host + ":" + port);
        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
        try {
            Bootstrap clientBootstrap = new Bootstrap();
            clientBootstrap.group(eventLoopGroup).channel(NioSocketChannel.class)
                    .handler(new SimpleTCPChannelInitializer());
            ChannelFuture cf = clientBootstrap.connect(host, port).sync();
            Channel channel = cf.sync().channel();
            while (channel.isActive()){

            }
            cf.channel().closeFuture().sync();                        
        } finally {
            eventLoopGroup.shutdownGracefully();   
        }        
    }
}

 

 

# SimpleTCPChannelInitializer.java
package ftpclient;

import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

public class SimpleTCPChannelInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        socketChannel.pipeline().addLast(new StringEncoder());
        socketChannel.pipeline().addLast(new StringDecoder());
        socketChannel.pipeline().addLast(new SimpleTCPChannelHandler());
    }
}

 

 

# FtpUtil.java
package ftpclient;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import io.netty.channel.Channel;


public class FtpUtil {
    public static String currentCommand;
    public static File fileName;
    public static String hostName;
    private static FileOutputStream writeFile;
    private static String user;
    private static String pass;
    private static Socket dataSocket;
    private static BufferedReader reader = new BufferedReader(
        new InputStreamReader(System.in));
    private FtpUtil() {
        throw new AssertionError();
    }
    public static void doLoginUser(Channel channel) {
        try {
            System.out.print("user: ");
            user = reader.readLine();
            channel.writeAndFlush("USER " + user + "\r\n");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public static void doLoginPass(Channel channel) {
        try {
            System.out.print("pass: ");
            pass = reader.readLine();
            channel.writeAndFlush("PASS " + pass + "\r\n");
            channel.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public static void showMenu() {
        System.out.println("2.command 9.quit");
        System.out.print("ftp> ");
    }
    public static int selectMenu() {
        int select = 9;
        try {
            select = Integer.parseInt(reader.readLine());
        } catch (IOException e) {
            e.printStackTrace();
        }
        return select;
    }
    public static void command(Channel channel) {
        String command;
        String[] commandList;
        try {
            System.out.print("ftp> ");
            command = reader.readLine();
            commandList = command.split(" ");
            FtpUtil.currentCommand = commandList[0];
            switch (FtpUtil.currentCommand) {
                case "pwd":
                    channel.writeAndFlush("PWD\r\n");
                    break;
                case "cd":
                    if (commandList.length == 1) {
                        try {
                            System.out.print("remote directory: ");
                            String path = reader.readLine();
                            channel.writeAndFlush("CWD " + path + "\r\n");                        
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    } else {
                        channel.writeAndFlush("CWD " + commandList[1] + "\r\n");
                    }
                    break;
                case "quit":
                case "exit":
                    channel.writeAndFlush("QUIT\r\n");
                    break;
                case "delete":
                    channel.writeAndFlush("DELE " + commandList[1] + "\r\n");
                    break;
                case "rmdir":
                    channel.writeAndFlush("RMD " + commandList[1] + "\r\n");
                    break;
                case "user":
                    channel.writeAndFlush("USER " + commandList[1] + "\r\n");
                    break;
                case "ls":
                    channel.writeAndFlush("EPSV 2\r\n");  
                    channel.writeAndFlush("LIST\r\n");
                    break;
                case "quote":
                    channel.writeAndFlush("EPSV 2\r\n");
                    break;
                case "get":
                    FtpUtil.fileName = new File(commandList[1]);
                    channel.writeAndFlush("TYPE I\r\n");
                    break;
                case "":
                    FtpUtil.command(channel);
                    break;
                default:
                    System.out.println("invalid command.");
                    FtpUtil.command(channel);
                    break;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public static void getDataConnection(String host, int port) throws IOException, UnknownHostException {
        FtpUtil.dataSocket = new Socket(host, port);
    }
    public static void recvData() throws IOException, UnknownHostException {        
        int n;
        byte[] buf = new byte[1024];
        BufferedInputStream bis = new BufferedInputStream(dataSocket.getInputStream());
        while ((n = bis.read(buf)) > 0) {
            if (FtpUtil.currentCommand.equals("get")) {
                writeFile.write(buf, 0, n);
            } else {
                for (byte b : buf) {
                    if (b == 0) {
                        break;
                    } else {
                        System.out.print((char) b);
                    }
                }
            }
        }
        if (currentCommand.equals("get")) {
            closeFile();
        }
        FtpUtil.dataSocket.close();
        bis.close();
    }
    public static String getPortNum(String msg) {
        Pattern pat = Pattern.compile("\\|(\\d+)\\|");
        Matcher mat = pat.matcher(msg);
        if (mat.find()) {
            return mat.group(1);
        } else {
            return null;
        }
    }
    public static void openFile() throws IOException {
        switch (currentCommand) {
            case "get":
                writeFile = new FileOutputStream(FtpUtil.fileName);
                break;
            case "put":
                // pass
                break;
        }
    }
    public static void closeFile() throws IOException {
        if (currentCommand.equals("get")) {
            writeFile.close();
        } else if (currentCommand.equals("put")) {
            // pass
        }
    }
}

 

 

# Main.java
package ftpclient;

import java.net.ConnectException;

public class Main {
    public static void main(String[] args) {
        try {
            SimpleTCPClientBootstrap client = new SimpleTCPClientBootstrap();
            client.start("localhost", 21);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ConnectException e) {
            System.out.println("[*]failed to connect server.");
        }
    }
}

 

動作確認

それでは、上記プログラムを実行し、ローカル環境で起動しているvsftpdに対して接続してみます。

 

 [*]connecting to localhost:21
 [*]connect to 127.0.0.1:21 successfully!!
 user: test
 pass: test
 ftp> ls
 -rw-rw-r--    1 1001     1001           48 Aug 01 10:23 test
 -rw-rw-r--    1 1001     1001           21 Aug 01 09:17 test2
 -rw-rw-r--    1 1001     1001            0 Jul 29 12:27 test3
 ftp> get test
 ftp> quit
 [*]server connection closed.
 $ cat test2
 this is a test.
 this is a test.
 this is a test.

 

最後に

今回はデータ転送用コネクションの機能としてgetコマンドについて学びました。

次回はputコマンドを作成していきます。

 

参考書籍

基礎からわかるTCP/IP Javaネットワークプログラミング