プロジェクト

全般

プロフィール

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つあります。

  1. Future
  2. 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サーバー側


2年以上前に更新