本記事は、Javaによるネットワークプログラミングについての学習メモとなります。
その中でNIOフレームワークであるNettyを使った実装方法について学んでいきます。
今回は、FTPクライアントプログラムを作成してみます。
FTPについて
FTPはFile Transfer Protocol(ファイル・トランスファー・プロトコル )の略で、その名の通りファイル転送を行うためのプロトコルになります。
その歴史は古く、最初に登場したのは1971年4月でRFC114としてまとめられています。
FTPではTCPポート20番と21番を使いデータ転送を行っていきます。
TCPポート20番はデータ転送用コネクションとして使用し、TCPポート21番は制御用コネクションとして使用します。
今回は制御用コネクション機能のみを実装し、pwdコマンドやcdコマンドなどを実装してみます。
Nettyについて
Javaには元々java.ioというパッケージが存在しますが、ブロッキングIOやストリーム指向であることから、Java 1.4からjava.nioというパッケージが導入されました。
このNIO(New IO)では、ノンブロッキングIOとバッファ指向で設計され、さらにセレクタを利用し単一スレッドで複数の入力チャネルをモニタ出来るようになり、様々な改良が加えられました。
今回はこのNIOのフレームワークであるNettyを利用していきます。
以下が公式ドキュメントになります。
FTPクライアントプログラムの作成
それではFTPクライアントプログラムを作成していきます。
プログラムとしては、前回作成したTCPクライアントプログラムを元に、FTPにかかる機能を追加していきます。
- Bootstrap:サーバへのコネクト処理などを行う
- ChannelHandler:サーバとの接続を処理する
- ChannelInitializer:チャネル初期化処理(バイト列変換など)を行う
- FtpUtil:FTP機能をまとめたクラス
# 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]); } @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 230: FtpUtil.command(ctx.channel()); break; case 331: FtpUtil.doLoginPass(ctx.channel()); break; case 530: System.out.println("login failed."); 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.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import io.netty.channel.Channel; public class FtpUtil { private static String user; private static String pass; 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(" "); switch (commandList[0]) { 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": 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; default: System.out.println("invalid command."); FtpUtil.command(channel); break; } } catch (IOException e) { e.printStackTrace(); } } }
# 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 user: test pass: test ftp> pwd 257 "/home/test" ftp> cd test ftp> pwd 257 "/home/test/test" ftp> quit [*]server connection closed.
最後に
今回は制御用コネクションのみを実装したFTPクライアントプログラムの作成方法について学びました。
次回はデータ転送用コネクションを実装したFTPクライアントプログラムを作成していきます。