JAVAでTCP/IP+NIOについて実装してみ

この記事ではWIN10にインストールしたJava 10.0.1を使っています。
#初めに
前回は[BIO転送路を作りました](https://qiita.com/haoyu_ma/items/2bfbcb411e9279f9cabd)、今日は引き続きNIOを実装してみます。

#NIOについて
##NIOとは
簡単に言うと、I/Oリクエストをブロッキングさせないものです。既存のI/O処理(BIO)を改善できます。
##BIOの良くない点
まずクライアント側からリクエストに対して、接続リクエストとI/Oリクエスト二つ部分が含まれています。
接続リクエストが必ず行いますが、I/Oリクエストは時と場合によって行います。
ですので、BIOのようなI/O処理があるかどうかを問わずに１リクエストに対して関わらず１個処理スレッドを用意する方式がサーバーの性能を無駄に利用しています。
##NIOの良い点
NIOはクライアント側からの全てリクエストに対して、1個の処理スレッドしか用意していません。I/Oリクエストがある場合に、別のスレッドにI/O処理を頼みます。

#実装
Java言語から***SocketChannel***、***ServerSocketChannel***を提供していので、今回はこれらを使ってTCP/IP+NIOを実装してみます。
(公式APIはここ：[SocketChannel](https://docs.oracle.com/javase/jp/8/docs/api/java/nio/channels/SocketChannel.html)、[ServerSocketChannel](https://docs.oracle.com/javase/jp/8/docs/api/java/nio/channels/ServerSocketChannel.html)
サーバ側のファイル

**ChannelServer.java の内容**
*ServerSocketChannel**を生成し、クライアント側のアクセスを待ち受けます*

```java
public class ChannelServer {

    public static void main(String[] args) throws IOException {
        // I/Oリクエストを処理するスレッドプールを用意
        ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 10, 1000, TimeUnit.MILLISECONDS, 
                new ArrayBlockingQueue<Runnable>(100));

        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(1234));

        while (true) {
            SocketChannel socketChannel = serverSocketChannel.accept();

            if (socketChannel != null) {
                // リクエストをスレッドプールにコミット
                executor.submit(new ChannelServerThread(socketChannel));
            }
        }
    }
}
```

**ChannelServer.java の内容**

***リクエストを処理するスレッド***

```java

public class ChannelServerThread implements Runnable {

    private SocketChannel socketChannel;
    private String remoteName;

    public ChannelServerThread(SocketChannel socketChannel) throws IOException {
        this.socketChannel = socketChannel;
        this.remoteName = socketChannel.getRemoteAddress().toString();
        System.out.println("client:" + remoteName + " access successfully!");
    }

    // I/Oリクエストを処理
    @Override
    public void run() {
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        ByteBuffer sizeBuffer = ByteBuffer.allocate(4);
        StringBuilder sb = new StringBuilder();
        byte b[];
        // socketChannelからデータと長さを読み込んで、標準出力に出力する
        while(true) {
            try {
                sizeBuffer.clear();
                int read = socketChannel.read(sizeBuffer);
                if (read != -1) {
                    sb.setLength(0);
                    sizeBuffer.flip();
                    int size = sizeBuffer.getInt();
                    int readCount = 0;
                    b = new byte[1024];
                    while (readCount < size) {
                        buffer.clear();
                        read = socketChannel.read(buffer);
                        if (read != -1) {
                            readCount += read;
                            buffer.flip();
                            int index = 0 ;
                            while(buffer.hasRemaining()) {
                                b[index++] = buffer.get();
                                if (index >= b.length) {
                                    index = 0;
                                    sb.append(new String(b,"UTF-8"));
                                }
                            }
                            if (index > 0) {
                                sb.append(new String(b,"UTF-8"));
                            }
                        }
                    }
                    System.out.println(remoteName +  ":" + sb.toString());
                }
            } catch (Exception e) {
                System.out.println(remoteName + "access colsed");
                try {
                    socketChannel.close();
                } catch (IOException ex) {
                }
                break;
            }
        }
    }
}

```

**クライアント側のファイル**

***socketChannelを生成し、指定されたサーバとポートをアクセスします***

```java
public class ChannelClient {

    public static void main(String[] args) throws IOException {

        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress(1234));
        while (true) {
            Scanner sc = new Scanner(System.in);
            String next = sc.next();
            sendMessage(socketChannel, next);
        }
    }

    // ChannleがIOするための容器を用意
    public static void sendMessage(SocketChannel socketChannel, String mes) throws IOException {
        if (mes == null || mes.isEmpty()) {
            return;
        }
        byte[] bytes = mes.getBytes("UTF-8");
        int size = bytes.length;
        ByteBuffer buffer = ByteBuffer.allocate(size);
        ByteBuffer sizeBuffer = ByteBuffer.allocate(4);

        sizeBuffer.putInt(size);
        buffer.put(bytes);

        buffer.flip();
        sizeBuffer.flip();
        ByteBuffer dest[] = {sizeBuffer,buffer};
        while (sizeBuffer.hasRemaining() || buffer.hasRemaining()) {
            socketChannel.write(dest);
        }
    }
}
```

#結果認証
***サーバーを起動し、接続を待ち受け中です：***

**サーバー**

```bash
PS C:\Users\ma\Documents\work\socket\nio_tcp> java ChannelServer

```

***クライアント１，２それぞれを起動し、何かを入力します:***

**クライアント１**

```bash

PS C:\Users\ma\Documents\work\socket\nio_tcp> java ChannelClient
iamfirstuser!
byebyefirstone!
Exception in thread "main"
PS C:\Users\ma\Documents\work\socket\nio_tcp>

```

**クライアント2**

```bash
java ChannelClient
iamseconduser
byebyesecondone!
Exception in thread "main"
PS C:\Users\ma\Documents\work\socket\nio_tcp>
```

**サーバー**

```bash

PS C:\Users\ma\Documents\work\socket\nio_tcp> java ChannelServer
client:/192.168.56.1:50138 access successfully!
client:/192.168.56.1:50139 access successfully!
/192.168.56.1:50138:iamfirstuser!
/192.168.56.1:50139:iamseconduser
/192.168.56.1:50138:byebyefirstone!
/192.168.56.1:50138access colsed
/192.168.56.1:50139:byebyesecondone!
/192.168.56.1:50139access colsed
```

#まとめ
今回はJavaのSocketChannel、ServerSocketChannelライブラリを使ってTCP/IP+NIOの転送路を作ってみました。
１サーバーが同時に複数のリクエストを対応できることも認証出来ました。