Javaプログラミング TCP通信 ライブラリ使用イメージ¶
各ライブラリの使用イメージ(コンパイルは通らない主要APIの呼び出しイメージ)を次に載せます。
java.netパッケージ¶
TCPサーバー側¶
ServerSocket server = new ServerSocket(12345); // ポート12345番で待ちうけるサーバーソケット生成
Socket socket = server.accept(); // TCPクライアントからのコネクションを待つ(コネクションが来るまでブロック)
InputStream in = socket.getInputStream(); // ソケットの入力ストリーム
OutputStream out = socket.getOutputStream(); // ソケットの出力ストリーム
// 以降、inとoutを読み書きすることでクライアントとデータを送受する
TCPクライアント側¶
Socket socket = new Socket("node.example.com", 12345); // コネクションを確立した上で復帰する
OutputStream out = socket.getOutputStream(); // ソケットの出力ストリーム
InputStream in = socket.getInputStream(); // ソケットの入力ストリーム
// 以降、inとoutを読み書きすることでサーバーとデータを送受する
java.nioパッケージ(ブロッキング)¶
TCPサーバー側¶
ServerSocketChannel server = ServerSocketChannel.open(); // サーバーソケットチャネル取得(未バインド)
server.bind(new InetSocketAddress(12345)); // サーバーソケットチャネルをローカルアドレスにバインド
SocketChannel channel = server.accept(); // TCPクライアントからのコネクションを待つ(コネクションが来るまでブロック)
ByteBuffer buffer = ByteBuffer.allocate(1024); // 読み書き用のバッファを作成(バイト数指定で)
channel.read(buffer); // データの受信(データが来るまでブロック)
channel.write(buffer); // データの送信
TCPクライアント側¶
SocketChannel channel = SocketChannel.open(); // ソケットチャネル取得(クライアント用)
channel.connect(new InetSocketAddress("node.example.com", 12345)); // サーバーへ接続を確立(接続確立するまでブロック)
ByteBuffer buffer = ByteBuffer.allocate(1024); // 読み書き用のバッファを作成
channel.write(buffer); // データの送信
channel.read(buffer); // データの受信(データが来るまでブロック)
- チャネルをTCPサーバーと非接続で生成する場合は、引数がからのopenメソッドを呼びます。
- writeメソッドは戻り値 int型を返し、その値は書き込まれたバイト数を示します。
- readメソッドは戻り値 int型を返し、その値は書き込まれたバイト数を示します。
読み込み(read)処理¶
- SockeChanelのreadメソッドに渡したByteBufferを、read完了後に読み出すときの注意点
ByteBufferのpositionが読み込んだデータの直後を指しています。そこで、ByteBufferのflip()を呼びpositionを読み込んだデータの最初を指し、limitが読み込んだデータの最後を指すようにします。
flipを忘れると、読み込んだはずなのにデータが空、とか、flipではなくrewindを読んで、データは一見あっているようだが読み込みデータのあとのごみまで含んでしまって、といった問題が生じます。
java.nioパッケージ(ノンブロッキング)¶
ノンブロッキングモードでは、accept、connect、read、writeなどのメソッドが操作完了を待たずにリターンします。
そのため、操作完了を待つコードを書く必要があります。ビジーループで待つとCPUを喰い、CPUの温度も上がり、エコではないので、通常はI/Oの多重化(Selector)を使って待ちます。これはUNIX系のselectシステムコールとほぼ同様です。
TCPサーバー側¶
ServerSocketChannel server = ServerSocketChannel.open(); // サーバーソケットチャネル取得(未バインド)
serverChannel.configureBlocking(false); // ノンブロッキングモード指定
server.bind(new InetSocketAddress(12345)); // サーバーソケットチャネルをローカルアドレスにバインド
Selector selector = Selector.open(); // セレクタを取得
server.register(selector, server.validOpts()); // ServerSocketChannelで選択できるI/O操作(OP_ACCEPT)をセレクタに登録
while (selector.select() > 0) { // セレクタに関するイベントが発生するまでブロック
for (Iterator keyIter = selector.selectedKeys().iterator(); keyIter.hasNext();) { // 発生したイベント(複数かも)の枚挙
SelectionKey key = (SelectionKey) ke1yIter.next();
keyIter.remove();
if (!key.isValid()) {
continue;
} else if (key.isAcceptable()) { // 発生したイベントがaccept
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel channel = server.accept();
channel.configureBlocking(false); // 受け付けたソケットをノンブロッキングに指定
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.register(key.selector(), SelectionKey.OP_READ, buffer);
} else if (key.isReadable()) {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
channel.read(buffer);
channel.write(buffer);
channel.register(key.selector(), SelectionKey.OP_READ, buffer);
}
}
}
コードイメージですが、未確認、ByteBufferの読み書きははしょり過ぎ、など雑過ぎますね。
ですが、Selectorを使うと制御ループが深くなり、複雑になるのは見受けられると思います。
ここに、例外処理のネストがさらに入るともうコードがスパゲッティになりそうです。
TCPクライアント側¶
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false);
channel.connect(new InetSocketAddress("node.example.com", 12345));
Selector selector = Selector.open();
channel.register(selector, SelectionKey.OP_CONNECT);
while (selector.select() > 0) {
for (Iterator keyIter = selector.selectedKeys().iterator(); keyIter.hasNext();) {
SelectionKey key = (SelectionKey) keyIter.next();
keyIter.remove();
if (!key.isValid()) {
continue;
} else if (key.isConnectable()) {
SocketChannel channel = (SocketChannel) key.channel();
if (channel.isConnectionPending()) {
channel.finishConnect();
}
// データ送信等
channel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
// 省略
}
}
}
コードはこんなイメージになるのかなと想像で書いてますが、未検証です。
TCPのクライアント側でSelect使ってコードを書く必要があるのかかなり疑問ですし、Select使って書くのは面倒でバグりそうでちょっといやな感じです。
NIO.2非同期チャンネル¶
NIO.2の非同期チャネルには、非同期処理を開始後に非同期処理をモニター、制御する仕組みが2つあります。
- Future
- CompletionHandler
NIO.2非同期チャンネル(Future使用)¶
TCPサーバー側¶
AsynchronousServerSocketChannel server
= AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(12345));
Future<AsynchronousSocketChannel> acceptFuture = server.accept(); // ノンブロックでリターンする
AsynchronousSocketChannel worker = acceptFuture.get(); // ブロック待ち
worker.read(buffer).get();
TCPクライアント側¶
AsynchronousSocketChannel client = AsynchronousSocketChannel.open();
client.connect(new InetSocketAddress("node.example.com", 12345)).get();
ByteBuffer buffer = ByteBuffer.allocate(16);
client.write(buffer).get();
NIO.2非同期チャネル(CompletionHandler使用)¶
TCPサーバー側¶