西海岸より

つらつらざつざつと

nioライブラリを使ったechoサーバ

nioライブラリを勉強中で、手始めにechoサーバを作ってみた。

  • NioEchoServer.java
package echoserver;

import java.io.*;
import java.net.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;
import java.text.*;
import java.util.*;

public class NioEchoServer {

	private final int DEFAULT_BUFFER_SIZE = 8192;
	private final int port;
	private final ServerSocketChannel serverSocketChannel;
	private final Selector selector;
	private final static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

	private long selectInterval = 5 * 1000; // 5 sec

	public NioEchoServer (int port) throws IOException {
		this.port = port;
		selector = Selector.open();
		serverSocketChannel = ServerSocketChannel.open();
		serverSocketChannel.configureBlocking(false);
		serverSocketChannel.socket().setReceiveBufferSize(DEFAULT_BUFFER_SIZE);
		serverSocketChannel.socket().bind(new InetSocketAddress(this.port));
	}
	
	public void startService () throws IOException {
		info("Start server ---- ");
		info("Server status: " + serverSocketChannel.socket().toString());
		int ops = serverSocketChannel.validOps();
		info("ServerSocketChannel valid ops: 0x" + Integer.toHexString(ops));

		serverSocketChannel.register(selector, ops);
		try {
			startSelect();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			closeAll();
			info("End server ---- ");
		}
	}
	
	private void startSelect() throws IOException {
		int receivedKey = 0;
		long cnt = 0;
		while ((receivedKey = selector.select(selectInterval)) >= 0) {
			info(String.format("Select count(%d) received key => %d", cnt++, receivedKey));
			Iterator<SelectionKey> it = selector.selectedKeys().iterator();
			while(it.hasNext()) {
				SelectionKey selectedKey = it.next();
				it.remove();
				if (!selectedKey.isValid()) {
					warn("Received invalid key: " + selectedKey);
					continue;
				}
				if (selectedKey.isValid() && selectedKey.isAcceptable()) {
					info("selectedKey: accept");
					handleAcceptable(selectedKey);
				}
				if (selectedKey.isValid() && selectedKey.isReadable()) {
					info("selectedKey: read");
					handleReadable(selectedKey);
				}
				if (selectedKey.isValid() && selectedKey.isWritable()) {
					info("selectedKey: write");
					handleWritable(selectedKey);
				}
				if (selectedKey.isValid() && selectedKey.isConnectable()) { 
					info("selectedKey: connect");					
				}
			}
		}
	}
	
	private void handleAcceptable(SelectionKey key) throws IOException {
		ServerSocketChannel serverSock = (ServerSocketChannel)key.channel();
		SocketChannel sock = null;
		try {
			sock = serverSock.accept();
			sock.configureBlocking(false);
			info("Open client. " + sock);
			ByteBuffer buffer = ByteBuffer.allocate(DEFAULT_BUFFER_SIZE);
			sock.register(key.selector(), SelectionKey.OP_READ, buffer);
		} catch (ClosedChannelException e) {
			throw e;
		} catch (IOException e) {
			e.printStackTrace();
			error("Failed accept. " + e.getMessage());
		}
	}
	
	private void handleReadable(SelectionKey key) throws IOException {
		SocketChannel channel = (SocketChannel)key.channel();
		ByteBuffer buffer = (ByteBuffer)key.attachment();
		//buffer.clear();
		int count = channel.read(buffer);
		info("Read count: " + count);
		if (count < 0) {
			key.interestOps(key.interestOps() & ~SelectionKey.OP_READ);
			info("Close client(read). " + channel);
			try {
				channel.close();
			} catch (Exception ignore) {} 
			return; 
		} else if (count == 0) {
			key.interestOps(key.interestOps() & ~SelectionKey.OP_READ);
		}
		key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);
		handleWritable(key);
	}
	
	private void handleWritable(SelectionKey key) throws IOException {
		SocketChannel channel = (SocketChannel)key.channel();
		ByteBuffer buffer = (ByteBuffer)key.attachment();
		buffer.flip();
		
		//decode
		String msg = "";
		try { 
			msg = decode(buffer.duplicate());
		} catch(CharacterCodingException e) {
			msg = "Couldn't receive message.";
		}
		info("RECV " + msg);
		
		int count = channel.write(buffer);
		info("Write count: " + count);
		buffer.compact();
		
		if (count < 0) {
			key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE);
			info("Close client(write). " + channel);
			try {
				channel.close();
			} catch (Exception ignore) {} 
			return;
		} else if (count == 0) {
			key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE);
		}
	}
	
	private void closeAll() {
		try {
			if (selector != null) selector.close();
		} catch (IOException ignore) {}

		try {
			if (serverSocketChannel != null) serverSocketChannel.close();
		} catch (IOException ignore) {}
	}
		
	private static void info(String msg) {
		log(msg, "INFO");
	}
	private static void warn(String msg) {
		log(msg, "WARN");
	}
	private static void error(String msg) {
		log(msg, "ERROR");
	}
	private static void log(String msg, String level) {
		if (msg != null) msg = msg.trim();
		System.out.printf("[%s] %s %s", level, sdf.format(new Date()), msg);
		System.out.println();
	}

	private static String decode(ByteBuffer buffer) throws CharacterCodingException {
		Charset charset = Charset.forName("UTF-8");
		CharsetDecoder decoder = charset.newDecoder();
		return decoder.decode(buffer).toString();
	}
	
	public static void main(String[] args) throws IOException{
		new NioEchoServer(4020).startService();
	}
}
  • NioEchoClient.java
package echoserver;

import java.io.*;
import java.net.*;

public class NioEchoClient {
	
	public static void main(String[] args) throws IOException{
		System.out.println("start client ---- ");
		BufferedReader br = null;
		BufferedWriter bw = null;
		Socket sock = null;
		try{ 
			sock = new Socket("localhost", 4020);
			br = new BufferedReader(
						new InputStreamReader(sock.getInputStream()));
			bw = new BufferedWriter(
						new OutputStreamWriter(sock.getOutputStream()));
			
			//500msec間隔で書き込み
			for (int i=0; i<5; i++) { 
				bw.write("Hello from Client. i=" + i + "\n");
				bw.flush();
				String line = br.readLine();
				System.out.println("Received: " + line);
				try {
					Thread.sleep(2000);
				} catch (InterruptedException ignore) {}
			}
		} finally {
			try {
				bw.close();
			} catch (IOException ignore) {}
			try {
				br.close();
			} catch (IOException ignore) {}
			try {
				sock.close();
			} catch (IOException ignore) {}
			System.out.println("end client --- ");
		}
	}
}

勉強に使ったのはJavaネットワークプログラミングの真髄という本で、TCP/IPレベルのネットワークの基礎から、io、nioを含めたJavaのネットワークライブラリに関する詳細な解説がされており、とても良い本だと思う。特に、著者の経験に基づく見解が注釈なども使ってちりばめられており、とりあえずJavaでネットワークプログラミングを始めるにはこの本一冊で十分だろう。

ただし、本書で書かれているサンプルソースには誤植があるようで、上記のechoサーバも本書のものを参考にしたけどそのままでは動かなかった。このようなところが玉に傷で、動かない場合は自分で解決しなければならない。

uJavaƒlƒbƒgƒ[ƒNƒvƒƒOƒ‰ƒ~ƒ“ƒO‚̐^‘vƒTƒ|[ƒgƒy[ƒWにもあるように、

本書中のソースコードのこのような誤記は、[ソースアーカイブ]にある完成プログラムのソースを、
本書の原稿用としてリライトしたときに発生しています。これらの書籍原稿としてのソースリストは、
ビルドやテストが行われていません。

ということらしいが、うーん。。。

まぁ、そこで悩んだ分勉強になったってことでよしとしようか。

Javaネットワークプログラミングの真髄

Javaネットワークプログラミングの真髄

なお、Java7ではnioはさらに強化されるので、そっちの動向も見据えて勉強していくとよさそう。