本記事は、Javaによるネットワークプログラミングについての学習メモとなります。
その中でNIOフレームワークであるNettyを使った実装方法について学んでいきます。
今回は、前回作成したFTPクライアントプログラムのデータ転送用コネクションの機能追加としてputコマンドを作成します。
FTPについて
FTPはFile Transfer Protocol(ファイル・トランスファー・プロトコル )の略で、その名の通りファイル転送を行うためのプロトコルになります。
その歴史は古く、最初に登場したのは1971年4月でRFC114としてまとめられています。
FTPではTCPポート20番と21番を使いデータ転送を行っていきます。
TCPポート20番はデータ転送用コネクションとして使用し、TCPポート21番は制御用コネクションとして使用します。
前回ではデータ転送用コネクションの機能としてgetコマンドを実装しました。
今回は追加としてputコマンドを作成し、リモートへファイルをアップロードしてみます。
バイナリモードとアスキーモード
FTPのファイル転送にはバイナリモードとアスキーモードというものが存在します。
UNIXやLinuxなどのOSでは改行文字で"LF(ラインフィード)"を使用しますが、Windowsでは改行文字で"CR(キャリッジリターン) + LF(ラインフィード)"を使用します。
そのため、バイナリモードではファイルを変換せずにそのまま転送しますが、アスキーモード(テキストモード)では、改行部分を変換してから転送します。
今回はputコマンドが入力された場合は、TYPEコマンドにオプションI(Image)を付与して、強制的にバイナリモードにします。
Nettyについて
Javaには元々java.ioというパッケージが存在しますが、ブロッキングIOやストリーム指向であることから、Java 1.4からjava.nioというパッケージが導入されました。
このNIO(New IO)では、ノンブロッキングIOとバッファ指向で設計され、さらにセレクタを利用し単一スレッドで複数の入力チャネルをモニタ出来るようになり、様々な改良が加えられました。
今回はこのNIOのフレームワークであるNettyを利用していきます。
以下が公式ドキュメントになります。
FTPクライアントプログラムの作成
それではFTPクライアントプログラムにputコマンドを追加していきます。
# 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 if (FtpUtil.currentCommand.equals("put")) { ctx.channel().writeAndFlush("STOR " + FtpUtil.fileName + "\r\n"); FtpUtil.openFile(); FtpUtil.sendData(); } else { FtpUtil.recvData(); } break; case 226: break; case 200: // switching binary mode if (FtpUtil.currentCommand.equals("get") || FtpUtil.currentCommand.equals("put")) { 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.BufferedOutputStream; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; 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 FileInputStream readFile; 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": case "put": 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 sendData() throws IOException, UnknownHostException { int n; byte[] buf = new byte[1024]; BufferedOutputStream bos = new BufferedOutputStream(dataSocket.getOutputStream()); while ((n = readFile.read(buf)) > 0) { bos.write(buf, 0, n); } closeFile(); bos.close(); } 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": readFile = new FileInputStream(FtpUtil.fileName); break; } } public static void closeFile() throws IOException { if (currentCommand.equals("get")) { writeFile.close(); } else if (currentCommand.equals("put")) { readFile.close(); } } }
# 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> put test ftp> ls -rw-r--r-- 1 1001 1001 16 Aug 03 02:09 test ftp> quit [*]server connection closed.
ローカルホスト側のファイルと同じbyte数が転送されたことが確認できました。
最後に
今回はデータ転送用コネクションの機能としてputコマンドについて学びました。
これでひとまずFTPにおける基本的なコマンドの実装が完了しました。
FTPに関しては一見複雑そうですが、内部で行われているやり取りはとてもシンプルだったのが印象として残りました。