Download Reference Manual
The Developer's Library for D
About Wiki Forums Source Search Contact
/**
 * TCP echo server.
 *
 * When started it binds to free local port and listens
 * for incoming connections.
 *
 * It uses several Tango features:
 *  * threads;
 *  * logging;
 *  * containers;
 *  * networking I/O;
 *
 * Contact:
 * dawid.ciezarkiewicz@asn.pl
 */

import tango.io.Stdout;
import tango.io.Buffer;

import tango.core.Thread;

import tango.net.ServerSocket;
import tango.net.InternetAddress;
import tango.net.Socket;
import tango.net.SocketConduit;

import tango.text.convert.Format;

import tango.util.log.ConsoleAppender;
import tango.util.log.Log;
import tango.util.log.Logger;

import tango.util.collection.LinkSeq;

/**
 * Each connection will have one instance of this.
 */
class ClientHandler {
	private Logger logger;
	private SocketConduit socket;
	private void[] buf;
	private static uint GlobalId = 0;
	private uint id;


	public this(SocketConduit socket) {
		this.socket = socket;
		id = GlobalId++;

		logger = Log.getLogger("client.handler." ~ Formatter("{0}", id));
		logger.info("creating");

		socket.getSocket.blocking(false);
		socket.setTimeout(0);

		/* this should give funny effect */
		buf = new byte[2];
	}

	public ~this() {
		/* get new one, because members are not valid now */
		auto logger = Log.getLogger("client.handler." ~ Formatter("{0}", id));
		logger.info("resources disposed");
	}


	/**
	 * Will throw error on the end of connection.
	 */
	public void call() {
		/*
		 * XXX:
		 * SocketConduit.read() is broken for a moment
		 * http://www.dsource.org/projects/tango/ticket/279
		 * because of that - all this isn't quite as
		 * it should be, but it works well.
		 */
		auto ammount = socket.getSocket.receive(buf);
		if (ammount == -1) {
			/* nothing */
		} else if ((ammount == IConduit.Eof || ammount == 0)) {
			logger.info("connection ended");
			throw new Exception("connection ended");
		} else if (ammount > 0) {
			logger.trace("got " ~ Formatter("{0}", ammount) ~ " bytes");
			socket.write(buf[0..ammount]);
		}

	}

}

/**
 * Manages client connections.
 *
 * It's a thread looping over list of clients
 * calling ClientHandler.call() member
 * and removing closed ones.
 */
class ClientManager {
	private Thread thread;
	private Logger logger;
	LinkSeq!(ClientHandler) clients;

	public this() {
		clients = new LinkSeq!(ClientHandler);
		logger = Log.getLogger("client.manager");

		logger.info("creating thread");
		thread = new Thread(&this.clientLoop);

		logger.info("starting thread");
		thread.start();
	}

	private void clientLoop() {
		while (true) {
			synchronized (clients) {
				auto to_delete = new LinkSeq!(ClientHandler);

				foreach (client; clients) {
					try {
						client.call();
					} catch (Exception e) {
						to_delete.append(client);
					}
				}

				foreach (client; to_delete) {
					clients.remove(client);
				}
			}
			Thread.sleep(0.1);
		}
	}

	public void spawn(SocketConduit sc) {
		synchronized (clients) {
			clients.append(new ClientHandler(sc));
		}
	}
}

/**
 * Thread that loops accepting incoming connections
 * and pass them to ClientManager.
 */
class Server {
	private Thread thread;
	private Logger logger;
	ServerSocket socket;
	ClientManager clientManager;

	public this(ClientManager cm)
	in {
		assert(cm);
	} body {
		this.clientManager = cm;

		logger = Log.getLogger("server");

		logger.info("creating socket");
		socket = new ServerSocket(new InternetAddress("0.0.0.0", 0));
		logger.info("listening on " ~ socket.getSocket().localAddress().toUtf8());


		logger.info("creating thread");
		thread = new Thread(&this.serverLoop);

		logger.info("starting thread");
		thread.start();
	}

	private void serverLoop() {
		while (true) {
			auto client_socket = socket.accept();
			clientManager.spawn(client_socket);
		}
	}

	public void join() {
		thread.join();
	}

}

int main() {

	Logger logger = Log.getRootLogger();
	logger.setLevel();
	logger.addAppender(new ConsoleAppender);

	auto ch = new ClientManager();
	auto a = new Server(ch);
	a.join();
	return 0;
}