Task

Write a server for a minimal text based chat.

People should be able to connect via ‘telnet’, sign on with a nickname, and type messages which will then be seen by all other connected users. Arrivals and departures of chat members should generate appropriate notification messages.




import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader
import java.io.OutputStreamWriter
import java.io.Writer
import java.net.ServerSocket
import java.net.Socket
import java.util.ArrayList
import java.util.Collections
 
class ChatServer private constructor(private val port: Int) : Runnable {
    private val clients = ArrayList<Client>()
 
    private val onlineListCSV: String
        @Synchronized get() {
            val sb = StringBuilder()
            sb.append(clients.size).append(" user(s) online: ")
            for (i in clients.indices) {
                sb.append(if (i > 0) ", " else "").append(clients[i].clientName)
            }
            return sb.toString()
        }
 
    override fun run() {
        try {
            val ss = ServerSocket(port)
            while (true) {
                val s = ss.accept()
                Thread(Client(s)).start()
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
 
    @Synchronized
    private fun registerClient(client: Client): Boolean {
        for (otherClient in clients) {
            if (otherClient.clientName!!.equals(client.clientName!!, ignoreCase = true)) {
                return false
            }
        }
        clients.add(client)
        return true
    }
 
    private fun deRegisterClient(client: Client) {
        var wasRegistered = false
        synchronized(this) {
            wasRegistered = clients.remove(client)
        }
        if (wasRegistered) {
            broadcast(client, "--- " + client.clientName + " left ---")
        }
    }
 
    private fun broadcast(fromClient: Client, msg: String) {
        // Copy client list (don't want to hold lock while doing IO)
        var clients: List<Client> = Collections.emptyList()
        synchronized(this) {
            clients = ArrayList(this.clients)
        }
        for (client in clients) {
            if (client.equals(fromClient)) {
                continue
            }
            try {
                client.write(msg + "\r\n")
            } catch (e: Exception) {
                e.printStackTrace()
            }
 
        }
    }
 
    inner class Client internal constructor(private var socket: Socket?) : Runnable {
        private var output: Writer? = null
        var clientName: String? = null
 
        override fun run() {
            try {
                socket!!.sendBufferSize = 16384
                socket!!.tcpNoDelay = true
                val input = BufferedReader(InputStreamReader(socket!!.getInputStream()))
                output = OutputStreamWriter(socket!!.getOutputStream())
                write("Please enter your name: ")
                var line: String
                while (true) {
                    line = input.readLine()
                    if (null == line) {
                        break
                    }
                    if (clientName == null) {
                        line = line.trim { it <= ' ' }
                        if (line.isEmpty()) {
                            write("A name is required. Please enter your name: ")
                            continue
                        }
                        clientName = line
                        if (!registerClient(this)) {
                            clientName = null
                            write("Name already registered. Please enter your name: ")
                            continue
                        }
                        write(onlineListCSV + "\r\n")
                        broadcast(this, "+++ $clientName arrived +++")
                        continue
                    }
                    if (line.equals("/quit", ignoreCase = true)) {
                        return
                    }
                    broadcast(this, "$clientName> $line")
                }
            } catch (e: Exception) {
                e.printStackTrace()
            } finally {
                deRegisterClient(this)
                output = null
                try {
                    socket!!.close()
                } catch (e: Exception) {
                    e.printStackTrace()
                }
 
                socket = null
            }
        }
 
        @Throws(IOException::class)
        internal fun write(msg: String) {
            output!!.write(msg)
            output!!.flush()
        }
 
        internal fun equals(client: Client?): Boolean {
            return (client != null
                && clientName != null
                && client.clientName != null
                && clientName == client.clientName)
        }
    }
 
    companion object {
        @JvmStatic
        fun main(args: Array<String>) {
            var port = 4004
            if (args.isNotEmpty()) {
                port = Integer.parseInt(args[0])
            }
            ChatServer(port).run()
        }
    }
}