Chat server
You are encouraged to solve this task according to the task description, using any language you may know.
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.
Erlang
<lang erlang> -module(chat).
-export([start/0, start/1]).
-record(client, {name=none, socket=none}).
start() -> start(8080). start(Port) ->
register(server, spawn(fun() -> server() end)), {ok, LSocket} = gen_tcp:listen(Port, [binary, {packet, 0}, {active, false}, {reuseaddr, true}]), accept(LSocket).
% main loop for message dispatcher server() -> server([]). server(Clients) ->
receive {join, Client=#client{name = Name, socket = Socket}} -> self() ! {say, Socket, "has joined." ++ [10, 13]}, server(Clients ++ [Client]); {leave, Socket} -> {value, #client{name = Name}, List} = lists:keytake(Socket, 3, Clients), self() ! {say, none, Message = "has left."}, server(List); {say, Socket, Data} -> {value, #client{name = From}, List} = lists:keytake(Socket, 3, Clients), Message = From ++ " : " ++ Data, lists:map(fun(#client{socket = S}) -> gen_tcp:send(S, Message) end, List) end, server(Clients).
% accepts connections then spawns the client handler accept(LSocket) ->
{ok, Socket} = gen_tcp:accept(LSocket), spawn(fun() -> connecting(Socket) end), accept(LSocket).
% when client is first connect send prompt for user name connecting(Socket) ->
gen_tcp:send(Socket, "What is your name? "), case listen(Socket) of {ok, N} -> Name = binary_to_list(N), server ! {join, #client{name = lists:sublist(Name, 1, length(Name) - 2), socket = Socket} }, client(Socket); _ -> ok end.
% main client loop that listens for data client(Socket) ->
case listen(Socket) of {ok, Data} -> server ! {say, Socket, binary_to_list(Data)}, client(Socket); _ -> server ! {leave, Socket} end.
% utility function that listens for data on a socket listen(Socket) ->
case gen_tcp:recv(Socket, 0) of Response -> Response end.
</lang>
Java
Broadcasting of messages is done by the thread that received the message, so a bad client could potentially disrupt the server. The output buffer is set to 16K in an attempt to alleviate possible symptoms, but I'm not sure if it's effective. Server does not allow duplicate client names, and lists all users online after a successful connection. Client can type "/quit" to quit.
I think ideally, NIO would be used to select() sockets available/ready for I/O, to eliminate the possibility of a bad connection disrupting the server, but this increases the complexity.
<lang java>import java.io.*; import java.net.*; import java.util.*;
public class ChatServer implements Runnable {
private int port = 0; private List<Client> clients = new ArrayList<Client>(); public ChatServer(int port) { this.port = port; } public void run() { try { ServerSocket ss = new ServerSocket(port); while (true) { Socket s = ss.accept(); new Thread(new Client(s)).start(); } } catch (Exception e) { e.printStackTrace(); } }
private synchronized boolean registerClient(Client client) { for (Client otherClient : clients) if (otherClient.clientName.equalsIgnoreCase(client.clientName)) return false; clients.add(client); return true; }
private void deregisterClient(Client client) { boolean wasRegistered = false; synchronized (this) { wasRegistered = clients.remove(client); } if (wasRegistered) broadcast(client, "--- " + client.clientName + " left ---"); } private synchronized String getOnlineListCSV() { StringBuilder sb = new StringBuilder(); sb.append(clients.size()).append(" user(s) online: "); for (int i = 0; i < clients.size(); i++) sb.append((i > 0) ? ", " : "").append(clients.get(i).clientName); return sb.toString(); } private void broadcast(Client fromClient, String msg) { // Copy client list (don't want to hold lock while doing IO) List<Client> clients = null; synchronized (this) { clients = new ArrayList<Client>(this.clients); } for (Client client : clients) { if (client.equals(fromClient)) continue; try { client.write(msg + "\r\n"); } catch (Exception e) { } } }
public class Client implements Runnable { private Socket socket = null; private Writer output = null; private String clientName = null; public Client(Socket socket) { this.socket = socket; } public void run() { try { socket.setSendBufferSize(16384); socket.setTcpNoDelay(true); BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); output = new OutputStreamWriter(socket.getOutputStream()); write("Please enter your name: "); String line = null; while ((line = input.readLine()) != null) { if (clientName == null) { line = line.trim(); 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(getOnlineListCSV() + "\r\n"); broadcast(this, "+++ " + clientName + " arrived +++"); continue; } if (line.equalsIgnoreCase("/quit")) return; broadcast(this, clientName + "> " + line); } } catch (Exception e) { } finally { deregisterClient(this); output = null; try { socket.close(); } catch (Exception e) { } socket = null; } } public void write(String msg) throws IOException { output.write(msg); output.flush(); } public boolean equals(Client client) { return (client != null) && (client instanceof Client) && (clientName != null) && (client.clientName != null) && clientName.equals(client.clientName); } } public static void main(String[] args) { int port = 4004; if (args.length > 0) port = Integer.parseInt(args[0]); new ChatServer(port).run(); }
} </lang>
PicoLisp
<lang PicoLisp>#!/usr/bin/picolisp /usr/lib/picolisp/lib.l
(de chat Lst
(out *Sock (mapc prin Lst) (prinl) ) )
(setq *Port (port 4004))
(loop
(setq *Sock (listen *Port)) (NIL (fork) (close *Port)) (close *Sock) )
(out *Sock
(prin "Please enter your name: ") (flush) )
(in *Sock (setq *Name (line T)))
(tell 'chat "+++ " *Name " arrived +++")
(task *Sock
(in @ (ifn (eof) (tell 'chat *Name "> " (line T)) (tell 'chat "--- " *Name " left ---") (bye) ) ) )
(wait)</lang> After starting the above script, connect to the chat server from two terminals:
Terminal 1 | Terminal 2 ---------------------------------+--------------------------------- $ telnet localhost 4004 | Trying ::1... | Trying 127.0.0.1... | Connected to localhost. | Escape character is '^]'. | Please enter your name: Ben | | $ telnet localhost 4004 | Trying ::1... | Trying 127.0.0.1... | Connected to localhost. | Escape character is '^]'. | Please enter your name: Tom +++ Tom arrived +++ | Hi Tom | | Ben> Hi Tom | Hi Ben Tom> Hi Ben | | How are you? Tom> How are you? | Thanks, fine! | | Ben> Thanks, fine! | See you! Tom> See you! | | ^] | telnet> quit --- Tom left --- | | Connection closed. | $
Python
<lang python>#!/usr/bin/env python
import socket import thread import time
HOST = "" PORT = 4004
def accept(conn):
""" Call the inner func in a thread so as not to block. Wait for a name to be entered from the given connection. Once a name is entered, set the connection to non-blocking and add the user to the users dict. """ def threaded(): while True: conn.send("Please enter your name: ") try: name = conn.recv(1024).strip() except socket.error: continue if name in users: conn.send("Name entered is already in use.\n") elif name: conn.setblocking(False) users[name] = conn broadcast(name, "+++ %s arrived +++" % name) break thread.start_new_thread(threaded, ())
def broadcast(name, message):
""" Send a message to all users from the given name. """ print message for to_name, conn in users.items(): if to_name != name: try: conn.send(message + "\n") except socket.error: pass
- Set up the server socket.
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.setblocking(False) server.bind((HOST, PORT)) server.listen(1) print "Listening on %s" % ("%s:%s" % server.getsockname())
- Main event loop.
users = {} while True:
try: # Accept new connections. while True: try: conn, addr = server.accept() except socket.error: break accept(conn) # Read from connections. for name, conn in users.items(): try: message = conn.recv(1024) except socket.error: continue if not message: # Empty string is given on disconnect. del users[name] broadcast(name, "--- %s leaves ---" % name) else: broadcast(name, "%s> %s" % (name, message.strip())) time.sleep(.1) except (SystemExit, KeyboardInterrupt): break</lang>
Ruby
<lang Ruby>require 'gserver'
class ChatServer < GServer
def initialize *args super
#Keep a list for broadcasting messages @chatters = []
#We'll need this for thread safety @mutex = Mutex.new end
#Send message out to everyone but sender def broadcast message, sender = nil #Need to use \r\n for our Windows friends message = message.strip << "\r\n"
#Mutex for safety - GServer uses threads @mutex.synchronize do @chatters.each do |chatter| begin chatter.print message unless chatter == sender rescue @chatters.delete chatter end end end end
#Handle each connection def serve io io.print 'Name: ' name = io.gets
#They might disconnect return if name.nil?
name.strip!
broadcast "--+ #{name} has joined +--"
#Add to our list of connections @mutex.synchronize do @chatters << io end
#Get and broadcast input until connection returns nil loop do message = io.gets
if message broadcast "#{name}> #{message}", io else break end end
broadcast "--+ #{name} has left +--" end
end
- Start up the server on port 7000
- Accept connections for any IP address
- Allow up to 100 connections
- Send information to stderr
- Turn on informational messages
ChatServer.new(7000, '0.0.0.0', 100, $stderr, true).start.join </lang>
Tcl
<lang tcl>package require Tcl 8.6
- Write a message to everyone except the sender of the message
proc writeEveryoneElse {sender message} {
dict for {who ch} $::cmap {
if {$who ne $sender} { puts $ch $message }
}
}
- How to read a line (up to 256 chars long) in a coroutine
proc cgets {ch var} {
upvar 1 $var v while {[gets $ch v] < 0} {
if {[eof $ch] || [chan pending input $ch] > 256} { return false } yield
} return true
}
- The chatting, as seen by one user
proc chat {ch addr port} {
### CONNECTION CODE ### #Log "connection from ${addr}:${port} on channel $ch" fconfigure $ch -buffering none -blocking 0 -encoding utf-8 fileevent $ch readable [info coroutine] global cmap try {
### GET THE NICKNAME OF THE USER ### puts -nonewline $ch "Please enter your name: " if {![cgets $ch name]} { return } #Log "Mapping ${addr}:${port} to ${name} on channel $ch" dict set cmap $name $ch writeEveryoneElse $name "+++ $name arrived +++"
### MAIN CHAT LOOP ### while {[cgets $ch line]} { writeEveryoneElse $name "$name> $line" }
} finally {
### DISCONNECTION CODE ### if {[info exists name]} { writeEveryoneElse $name "--- $name left ---" dict unset cmap $name } close $ch #Log "disconnection from ${addr}:${port} on channel $ch"
}
}
- Service the socket by making corouines running [chat]
socket -server {coroutine c[incr count] chat} 4004 set ::cmap {}; # Dictionary mapping nicks to channels vwait forever; # Run event loop</lang>