LoginSignup
5
6

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-05-13

この記事ではWIN10にインストールしたJava 10.0.1を使っています。

初めに

前回はBIO転送路を作りました、今日は引き続きNIOを実装してみます。

NIOについて

NIOとは

簡単に言うと、I/Oリクエストをブロッキングさせないものです。既存のI/O処理(BIO)を改善できます。

BIOの良くない点

まずクライアント側からリクエストに対して、接続リクエストとI/Oリクエスト二つ部分が含まれています。
接続リクエストが必ず行いますが、I/Oリクエストは時と場合によって行います。
ですので、BIOのようなI/O処理があるかどうかを問わずに1リクエストに対して関わらず1個処理スレッドを用意する方式がサーバーの性能を無駄に利用しています。

NIOの良い点

NIOはクライアント側からの全てリクエストに対して、1個の処理スレッドしか用意していません。I/Oリクエストがある場合に、別のスレッドにI/O処理を頼みます。

実装

Java言語からSocketChannelServerSocketChannelを提供していので、今回はこれらを使ってTCP/IP+NIOを実装してみます。
(公式APIはここ:SocketChannelServerSocketChannel
サーバ側のファイル

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

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 の内容

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


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を生成し、指定されたサーバとポートをアクセスします

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);
        }
    }
}

結果認証

サーバーを起動し、接続を待ち受け中です:

サーバー

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

クライアント1,2それぞれを起動し、何かを入力します:

クライアント1


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

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

サーバー


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の転送路を作ってみました。
1サーバーが同時に複数のリクエストを対応できることも認証出来ました。

5
6
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
6