Chat server

From Rosetta Code
Task
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.

Ada

Library: AdaSockets

<lang Ada>with Ada.Containers.Vectors; with Ada.Command_Line; use Ada.Command_Line; with Ada.Exceptions; use Ada.Exceptions; with Ada.Text_IO; use Ada.Text_IO; with Ada.Strings.Unbounded; use Ada.Strings.Unbounded; with Sockets; use Sockets;

procedure Chat_Server is

  package Client_Vectors is new Ada.Containers.Vectors
    (Element_Type => Socket_FD, Index_Type => Positive);
  All_Clients : Client_Vectors.Vector;
  procedure Write (S : String) is
     procedure Output (Position : Client_Vectors.Cursor) is
        Sock : Socket_FD := Client_Vectors.Element (Position);
     begin
        Put_Line (Sock, S);
     end Output;
  begin
     All_Clients.Iterate (Output'Access);
  end Write;
  task type Client_Task is
     entry Start (FD : Socket_FD);
  end Client_Task;
  task body Client_Task is
     Sock    : Socket_FD;
     Sock_ID : Positive;
     Name    : Unbounded_String;
  begin
     select
        accept Start (FD : Socket_FD) do
           Sock := FD;
        end Start;
     or
        terminate;
     end select;
     while Name = Null_Unbounded_String loop
        Put (Sock, "Enter Name:");
        Name := To_Unbounded_String (Get_Line (Sock));
     end loop;
     Write (To_String (Name) & " joined.");
     All_Clients.Append (Sock);
     Sock_ID := All_Clients.Find_Index (Sock);
     loop
        declare
           Input : String := Get_Line (Sock);
        begin
           Write (To_String (Name) & ": " & Input);
        end;
     end loop;
  exception
     when Connection_Closed =>
        Put_Line ("Connection closed");
        Shutdown (Sock, Both);
        All_Clients.Delete (Sock_ID);
        Write (To_String (Name) & " left.");
  end Client_Task;
  Accepting_Socket : Socket_FD;
  Incoming_Socket  : Socket_FD;
  type Client_Access is access Client_Task;
  Dummy : Client_Access;

begin

  if Argument_Count /= 1 then
     Raise_Exception (Constraint_Error'Identity,
                      "Usage: " & Command_Name & " port");
  end if;
  Socket (Accepting_Socket, PF_INET, SOCK_STREAM);
  Setsockopt (Accepting_Socket, SOL_SOCKET, SO_REUSEADDR, 1);
  Bind (Accepting_Socket, Positive'Value (Argument (1)));
  Listen (Accepting_Socket);
  loop
     Put_Line ("Waiting for new connection");
     Accept_Socket (Accepting_Socket, Incoming_Socket);
     Put_Line ("New connection acknowledged");
     Dummy := new Client_Task;
     Dummy.Start (Incoming_Socket);
  end loop;

end Chat_Server;</lang>

C

C has no built-in networking functions, but the POSIX library does provide some low-level networking functions. The functions of interest relating to sockets include bind, listen, select, accept, read, write and close.

The example below was compiled on Cygwin, and accepts PuTTY connections under the RAW protocol.

A glitch occurs if a connection is made using the Telnet protocol - user names are preceded by garbled text.

<lang c>#include <stdio.h>

  1. include <stdlib.h>
  2. include <sys/socket.h>
  3. include <sys/select.h>
  4. include <netinet/in.h>
  5. include <netinet/ip.h>

int tsocket; struct sockaddr_in tsockinfo; fd_set status, current; void ClientText(int handle, char *buf, int buf_len);

struct client {

   char buffer[4096];
   int pos;
   char name[32];

} *connections[FD_SETSIZE];

void AddConnection(int handle) {

   connections[handle] = malloc(sizeof(struct client));
   connections[handle]->buffer[0] = '\0';
   connections[handle]->pos = 0;
   connections[handle]->name[0] = '\0';

}

void CloseConnection(int handle) {

   char buf[512];
   int j;
   FD_CLR(handle, &status);
   
   if (connections[handle]->name[0])
   {
       sprintf(buf, "* Disconnected: %s\r\n", connections[handle]->name);
       for (j = 0; j < FD_SETSIZE; j++)
       {
           if (handle != j && j != tsocket && FD_ISSET(j, &status))
           {
               if (write(j, buf, strlen(buf)) < 0)
               {
                   CloseConnection(j);
               }
           }
       }
   } else
   {
       printf ("-- Connection %d disconnected\n", handle);
   }
   if (connections[handle])
   {
       free(connections[handle]);
   }
   close(handle);

}

void strip(char *buf) {

   char *x;
   
   x = strchr(buf, '\n');
   if (x) { *x='\0'; }
   x = strchr(buf, '\r');
   if (x) { *x='\0'; }

}

int RelayText(int handle) {

   char *begin, *end;
   int ret = 0;
   begin = connections[handle]->buffer;
   if (connections[handle]->pos == 4000)
   {
       if (begin[3999] != '\n')
           begin[4000] = '\0';
       else {
           begin[4000] = '\n';
           begin[4001] = '\0';
       }
   } else {
       begin[connections[handle]->pos] = '\0';
   }
   
   end = strchr(begin, '\n');
   while (end != NULL)
   {
       char output[8000];
       output[0] = '\0';
       if (!connections[handle]->name[0])
       {
           strncpy(connections[handle]->name, begin, 31);
           connections[handle]->name[31] = '\0';
           
           strip(connections[handle]->name);
           sprintf(output, "* Connected: %s\r\n", connections[handle]->name);
           ret = 1;
       } else 
       {
           sprintf(output, "%s: %.*s\r\n", connections[handle]->name, 
                   end-begin, begin);
           ret = 1;
       }
       
       if (output[0])
       {
           int j;
           for (j = 0; j < FD_SETSIZE; j++)
           {
               if (handle != j && j != tsocket && FD_ISSET(j, &status))
               {
                   if (write(j, output, strlen(output)) < 0)
                   {
                       CloseConnection(j);
                   }
               }
           }
       }
       begin = end+1;
       end = strchr(begin, '\n');
   }
   
   strcpy(connections[handle]->buffer, begin);
   connections[handle]->pos -= begin - connections[handle]->buffer;
   return ret;

}

void ClientText(int handle, char *buf, int buf_len) {

   int i, j;
   if (!connections[handle])
       return;
   j = connections[handle]->pos; 
   
   for (i = 0; i < buf_len; ++i, ++j)
   {
       connections[handle]->buffer[j] = buf[i];
       
       if (j == 4000)
       {
           while (RelayText(handle));
           j = connections[handle]->pos;
       }
   }
   connections[handle]->pos = j;
   
   while (RelayText(handle));

}


int ChatLoop() {

   int i, j;
   FD_ZERO(&status);
   FD_SET(tsocket, &status);
   FD_SET(0, &status);
   while(1)
   {
       current = status;
       if (select(FD_SETSIZE, &current, NULL, NULL, NULL)==-1)
       {
           perror("Select");
           return 0;
       }
       for (i = 0; i < FD_SETSIZE; ++i)
       {
           if (FD_ISSET(i, &current))
           {
               if (i == tsocket)
               {
                   struct sockaddr_in cliinfo;
                   socklen_t addrlen = sizeof(cliinfo);
                   int handle;
                   handle = accept(tsocket, &cliinfo, &addrlen);
                   if (handle == -1)
                   {
                       perror ("Couldn't accept connection");
                   } else if (handle > FD_SETSIZE)
                   {
                       printf ("Unable to accept new connection.\n");
                       close(handle);
                   }
                   else
                   {
                       if (write(handle, "Enter name: ", 12) >= 0)
                       {
                           printf("-- New connection %d from %s:%hu\n",
                               handle, 
                               inet_ntoa (cliinfo.sin_addr),
                               ntohs(cliinfo.sin_port));
                           FD_SET(handle, &status);
                           AddConnection(handle);
                       }
                   }
               } /* Read data, relay to others. */
               else
               {
                   char buf[512];
                   int b;
                   b = read(i, buf, 500);
                   if (b <= 0)
                   {
                       CloseConnection(i);
                   }
                   else
                   {
                       ClientText(i, buf, b);
                   }
               }
           }
       }
   } /* While 1 */

}

int main (int argc, char*argv[]) {

   tsocket = socket(PF_INET, SOCK_STREAM, 0);
   tsockinfo.sin_family = AF_INET;
   tsockinfo.sin_port = htons(7070);
   if (argc > 1)
   {
       tsockinfo.sin_port = htons(atoi(argv[1]));
   }
   tsockinfo.sin_addr.s_addr = htonl(INADDR_ANY);
   printf ("Socket %d on port %hu\n", tsocket, ntohs(tsockinfo.sin_port));
   if (bind(tsocket, &tsockinfo, sizeof(tsockinfo)) == -1)
   {
       perror("Couldn't bind socket");
       return -1;
   }
   if (listen(tsocket, 10) == -1)
   {
       perror("Couldn't listen to port");
   }
   ChatLoop();
   return 0;

}</lang>

CoffeeScript

This is ported from the JavaScript version. The tool js2coffee got me a mostly working version, and then I manually converted JS-style classes to CS "classic-style class" syntax.

<lang coffeescript> net = require("net") sys = require("sys") EventEmitter = require("events").EventEmitter

isNicknameLegal = (nickname) ->

 return false unless nickname.replace(/[A-Za-z0-9]*/, "") is ""
 for used_nick of @chatters
   return false if used_nick is nickname
 true

class ChatServer

 constructor: ->
   @chatters = {}
   @server = net.createServer @handleConnection
   @server.listen 1212, "localhost"
 handleConnection: (connection) =>
   console.log "Incoming connection from " + connection.remoteAddress
   connection.setEncoding "utf8"
   chatter = new Chatter(connection, this)
   chatter.on "chat", @handleChat
   chatter.on "join", @handleJoin
   chatter.on "leave", @handleLeave
 handleChat: (chatter, message) =>
   @sendToEveryChatterExcept chatter, chatter.nickname + ": " + message
 handleJoin: (chatter) =>
   console.log chatter.nickname + " has joined the chat."
   @sendToEveryChatter chatter.nickname + " has joined the chat."
   @addChatter chatter
 handleLeave: (chatter) =>
   console.log chatter.nickname + " has left the chat."
   @removeChatter chatter
   @sendToEveryChatter chatter.nickname + " has left the chat."
 addChatter: (chatter) =>
   @chatters[chatter.nickname] = chatter
 removeChatter: (chatter) =>
   delete @chatters[chatter.nickname]
 sendToEveryChatter: (data) =>
   for nickname of @chatters
     @chatters[nickname].send data
 sendToEveryChatterExcept: (chatter, data) =>
   for nickname of @chatters
     @chatters[nickname].send data  unless nickname is chatter.nickname


class Chatter extends EventEmitter

 constructor: (socket, server) ->
   EventEmitter.call this
   @socket = socket
   @server = server
   @nickname = ""
   @lineBuffer = new SocketLineBuffer(socket)
   @lineBuffer.on "line", @handleNickname
   @socket.on "close", @handleDisconnect
   @send "Welcome! What is your nickname?"
 handleNickname: (nickname) =>
   if isNicknameLegal(nickname)
     @nickname = nickname
     @lineBuffer.removeAllListeners "line"
     @lineBuffer.on "line", @handleChat
     @send "Welcome to the chat, " + nickname + "!"
     @emit "join", this
   else
     @send "Sorry, but that nickname is not legal or is already in use!"
     @send "What is your nickname?"
 handleChat: (line) =>
   @emit "chat", this, line
 handleDisconnect: =>
   @emit "leave", this
 send: (data) =>
   @socket.write data + "\r\n"


class SocketLineBuffer extends EventEmitter

 constructor: (socket) ->
   EventEmitter.call this
   @socket = socket
   @buffer = ""
   @socket.on "data", @handleData
 handleData: (data) =>
   console.log "Handling data", data
   i = 0
   while i < data.length
     char = data.charAt(i)
     @buffer += char
     if char is "\n"
       @buffer = @buffer.replace("\r\n", "")
       @buffer = @buffer.replace("\n", "")
       @emit "line", @buffer
       console.log "incoming line: #{@buffer}"
       @buffer = ""
     i++

server = new ChatServer() </lang>

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>

Go

<lang go>package main

import ( "os" "fmt" "net" "flag" "bufio" "bytes" )

// Quick and dirty error handling. func error_(err error, r int) { fmt.Printf("Error: %v\n", err)

if r >= 0 { os.Exit(r) } }

// A type for storing the connections. type clientMap map[string]net.Conn

// A method that makes clientMap compatible with io.Writer, allowing it to be // used with fmt.Fprintf(). func (cm clientMap) Write(buf []byte) (n int, err error) { for _, c := range cm { // Write to each client in a seperate goroutine. go c.Write(buf) }

n = len(buf)

return }

// Check if a name exists; if it doesn't, add it. func (cm clientMap) Add(name string, c net.Conn) bool { for k := range cm { if name == k { return false } }

cm[name] = c

return true }

// A clientMap variable. var clients clientMap

func init() { // Initialize the map. clients = make(clientMap) }

func client(c net.Conn) { // Close the connection when this function returns. defer c.Close()

br := bufio.NewReader(c)

fmt.Fprintf(c, "Please enter your name: ")

buf, err := br.ReadBytes('\n') if err != nil { error_(err, -1) return } name := string(bytes.Trim(buf, " \t\n\r\x00"))

if name == "" { fmt.Fprintf(c, "!!! %v is invalid !!!\n", name) }

// Try to add the connection to the map. if !clients.Add(name, c) { fmt.Fprintf(c, "!!! %v is not available !!!\n", name) return }

// Send a message telling the clients who connected. fmt.Fprintf(clients, "+++ %v connected +++\n", name) // Send a disconnected message when the function returns. defer fmt.Fprintf(clients, "--- %v disconnected ---\n", name) // Remove the client from the list. defer delete(clients, name)

for { buf, err = br.ReadBytes('\n') if err != nil { break } buf = bytes.Trim(buf, " \t\n\r\x00")

// Ignore empty messages. if len(buf) == 0 { continue }

switch { // Support for '/me' type messages. case string(buf[0:3]) == "/me": buf = append([]byte(name), buf[3:]...) default: // Prepend the user-name and '> '. buf = append([]byte(name+"> "), buf...) }

// Send the message to all the clients. fmt.Fprintf(clients, "%v\n", string(buf)) } }

func main() { // Flags. Use -help for usage info. var ( port int help bool ) flag.IntVar(&port, "port", 23, "Port to listen on") flag.BoolVar(&help, "help", false, "Display this") flag.Parse()

if help { flag.Usage() return }

// Initialize a new listener. lis, err := net.Listen("tcp", fmt.Sprintf(":%v", port)) if err != nil { error_(err, 1) }

// Begin the main loop. for { c, err := lis.Accept() if err != nil { error_(err, -1) continue }

// Launch a new goroutine to handle the connection. go client(c) } }</lang>

{-# LANGUAGE OverloadedStrings #-} import Network import System.IO import Control.Concurrent import qualified Data.Text as T import Data.Text (Text) import qualified Data.Text.IO as T import qualified Data.Map as M import Data.Map (Map) import Control.Monad.Reader import Control.Monad.Error import Control.Exception import Data.Monoid

type ServerApp = ReaderT ThreadData IO data Speaker = Server | Client Text data ThreadData = ThreadData { threadHandle :: Handle

                            , userTableMV :: MVar (Map Text Handle)}

echoLocal = liftIO . T.putStrLn echoRemote = echoMessage . (">> "<>) echoMessage msg = viewHandle >>= \h -> liftIO $ T.hPutStrLn h msg getRemoteLine = viewHandle >>= liftIO . T.hGetLine putMVarT = (liftIO.) . putMVar takeMVarT = liftIO . takeMVar readMVarT = liftIO . readMVar modifyUserTable fn = viewUsers >>= \mv ->

                    liftIO $ modifyMVar_ mv (return . fn)

viewHandle = ask >>= return . threadHandle viewUsers = ask >>= return . userTableMV

userChat :: ServerApp () userChat = do

   name <- addUser 
   echoLocal name
   h <- viewHandle
   (flip catchError) (\_ -> removeUser name) $
     do echoLocal $ "Accepted " <> name
        forever $ getRemoteLine >>= broadcast (Client name)

removeUser :: Text -> ServerApp () removeUser name = do

   echoLocal $ "Exception with " <> name <> ", removing from userTable"
   broadcast Server $ name <> " has left the server"
   modifyUserTable (M.delete name)

addUser :: ServerApp Text addUser = do

   h <- viewHandle
   usersMV <- viewUsers
   echoRemote "Enter username" 
   name <- getRemoteLine >>= return . T.filter (/='\r')
   userTable <- takeMVarT usersMV
   if name `M.member` userTable
     then do echoRemote "Username already exists!" 
             putMVarT usersMV userTable
             addUser
     else do putMVarT usersMV (M.insert name h userTable)
             broadcast Server $ name <> " has joined the server"
             echoRemote "Welcome to the server!\n>> Other users:"
             readMVarT usersMV >>=
                 mapM_ (echoRemote . ("*" <>) . fst) 
               . filter ((/=name). fst) . M.toList
             return name

broadcast :: Speaker -> Text -> ServerApp () broadcast user msg =

   viewUsers >>= readMVarT >>= mapM_ (f . snd) . fn . M.toList
 where f h = liftIO $ T.hPutStrLn h $ nm <> msg
       (fn, nm) = case user of
                   Server -> (id, ">> ")
                   Client t -> (filter ((/=t) . fst), t <> "> ")

clientLoop socket users = do

   (h, _, _) <- accept socket
   hSetBuffering h LineBuffering
   forkIO $ runReaderT userChat (ThreadData h users)
   clientLoop socket users

main = do

   server <- listenOn $ PortNumber 5002
   T.putStrLn "Server started"
   newMVar (M.empty) >>= clientLoop server

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>

JavaScript

Works with: Node.js

<lang javascript>var net = require("net"); var sys = require("sys"); var EventEmitter = require("events").EventEmitter;

/*******************************************************************************

* ChatServer
*
* Manages connections, users, and chat messages.
******************************************************************************/

function ChatServer() {

 this.chatters = {};
 this.server   = net.createServer(this.handleConnection.bind(this));
 this.server.listen(1212, "localhost");

}

ChatServer.prototype.isNicknameLegal = function(nickname) {

 // A nickname may contain letters or numbers only,
 // and may only be used once.
 if(nickname.replace(/[A-Za-z0-9]*/, ) != "") {
   return false
 }
 for(used_nick in this.chatters) {
   if(used_nick == nickname) {
     return false;
   }
 }
 return true;

};

ChatServer.prototype.handleConnection = function(connection) {

 console.log("Incoming connection from " + connection.remoteAddress);
 connection.setEncoding("utf8");
 var chatter = new Chatter(connection, this);
 chatter.on("chat", this.handleChat.bind(this));
 chatter.on("join", this.handleJoin.bind(this));
 chatter.on("leave", this.handleLeave.bind(this));

};

ChatServer.prototype.handleChat = function(chatter, message) {

 this.sendToEveryChatterExcept(chatter, chatter.nickname + ": " + message);

};

ChatServer.prototype.handleJoin = function(chatter) {

 console.log(chatter.nickname + " has joined the chat.");
 this.sendToEveryChatter(chatter.nickname + " has joined the chat.");
 this.addChatter(chatter);

};

ChatServer.prototype.handleLeave = function(chatter) {

 console.log(chatter.nickname + " has left the chat.");
 this.removeChatter(chatter);
 this.sendToEveryChatter(chatter.nickname + " has left the chat.");

};

ChatServer.prototype.addChatter = function(chatter) {

 this.chatters[chatter.nickname] = chatter;

};

ChatServer.prototype.removeChatter = function(chatter) {

 delete this.chatters[chatter.nickname];

};

ChatServer.prototype.sendToEveryChatter = function(data) {

 for(nickname in this.chatters) {
   this.chatters[nickname].send(data);
 }

};

ChatServer.prototype.sendToEveryChatterExcept = function(chatter, data) {

 for(nickname in this.chatters) {
   if(nickname != chatter.nickname) {
     this.chatters[nickname].send(data);
   }
 }

};

/*******************************************************************************

* Chatter
*
* Represents a single user/connection in the chat server.
******************************************************************************/

function Chatter(socket, server) {

 EventEmitter.call(this);
 this.socket     = socket;
 this.server     = server;
 this.nickname   = "";
 this.lineBuffer = new SocketLineBuffer(socket);
 this.lineBuffer.on("line", this.handleNickname.bind(this));
 this.socket.on("close", this.handleDisconnect.bind(this));
 this.send("Welcome! What is your nickname?");

};

sys.inherits(Chatter, EventEmitter);

Chatter.prototype.handleNickname = function(nickname) {

 if(server.isNicknameLegal(nickname)) {
   this.nickname = nickname;
   this.lineBuffer.removeAllListeners("line");
   this.lineBuffer.on("line", this.handleChat.bind(this));
   this.send("Welcome to the chat, " + nickname + "!");
   this.emit("join", this);
 } else {
   this.send("Sorry, but that nickname is not legal or is already in use!");
   this.send("What is your nickname?");
 }

};

Chatter.prototype.handleChat = function(line) {

 this.emit("chat", this, line);

};

Chatter.prototype.handleDisconnect = function() {

 this.emit("leave", this);

};

Chatter.prototype.send = function(data) {

 this.socket.write(data + "\r\n");

};

/*******************************************************************************

* SocketLineBuffer
*
* Listens for and buffers incoming data on a socket and emits a 'line' event
* whenever a complete line is detected.
******************************************************************************/

function SocketLineBuffer(socket) {

 EventEmitter.call(this);
 this.socket = socket;
 this.buffer = "";
 this.socket.on("data", this.handleData.bind(this));

};

sys.inherits(SocketLineBuffer, EventEmitter);

SocketLineBuffer.prototype.handleData = function(data) {

 for(var i = 0; i < data.length; i++) {
   var char = data.charAt(i);
   this.buffer += char;
   if(char == "\n") {
     this.buffer = this.buffer.replace("\r\n", "");
     this.buffer = this.buffer.replace("\n", "");
     this.emit("line", this.buffer);
     this.buffer = "";
   }
 }

};

// Start the server! server = new ChatServer();</lang>

Objeck

<lang objeck> use System.IO.Net; use System.Concurrency; use Collection;

bundle Default {

 class ChatServer {
   @clients : StringMap;
   @clients_mutex : ThreadMutex;
   
   New() {
     @clients := StringMap->New();
     @clients_mutex := ThreadMutex->New("clients_mutex");
   }
   method : ValidLogin(login_name : String, clients : StringMap) ~ Bool {
     if(clients->Has(login_name)) {
       return false;
     };
     
     return true;
   }
   function : Main(args : String[]) ~ Nil {
     chat_server := ChatServer->New();
     chat_server->Run();
   }
   
   method : public : Broadcast(message : String, sender : Client) ~ Nil {
     client_array : Vector;
     critical(@clients_mutex) {
       client_array := @clients->GetValues();
     };
     each(i : client_array) {
       client := client_array->Get(i)->As(Client);
       if(client <> sender) {
         client->Send(message);
       };
     };      
   }
   method : public : Disconnect(sender : Client) ~ Nil {
     send_name := sender->GetName();
     Broadcast("+++ {$send_name} has left +++", sender);
     critical(@clients_mutex) {
       @clients->Remove(sender->GetName());
     };
     sender->Close();
   }
   method : public : Run() ~ Nil {
     server := TCPSocketServer->New(4661);
     if(server->Listen(5)) {
       while(true) {
         client_sock := server->Accept();
         critical(@clients_mutex) {
           client_sock->WriteString("login: ");
           login_name := client_sock->ReadString();
           if(ValidLogin(login_name, @clients)) {
             client := Client->New(login_name, client_sock, @self);
             @clients->Insert(client->GetName(), client);
             client->Execute(Nil);
           }
           else {
             client_sock->WriteString("+++ login in use +++\r\n");
             client_sock->Close();
           };
         };
       };
     };
     server->Close();
   }
 }
 class Client from Thread {
   @client_sock : TCPSocket;
   @server : ChatServer;
   
   New(login_name : String, client_sock : TCPSocket, server : ChatServer) {
     Parent(login_name);
     @client_sock := client_sock;
     @server := server;
   }
   
   method : public : Close() ~ Nil {
     @client_sock->Close();
   }
   
   method : public : Send(message : String) ~ Nil {
     if(@client_sock->IsOpen() & message->Size() > 0) {
       @client_sock->WriteString("{$message}\r\n");
     }
     else {
       @server->Disconnect(@self);
     };
   }
   method : public : Run(param : Base) ~ Nil {
     client_name := GetName();
     @server->Broadcast("+++ {$client_name} has arrived +++", @self);
     
     message := @client_sock->ReadString();
     while(message->Size() > 0 & message->Equals("/quit") = false) {
       @server->Broadcast("{$client_name}> {$message}", @self);      
       message := @client_sock->ReadString();
     };
     @server->Disconnect(@self);
   }
 }

} </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
  1. 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())

  1. 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>

Racket

This is a very basic chat server, but it does everything that is needed for this task.

<lang racket>

  1. lang racket

(define outs (list (current-output-port))) (define ((tell-all who o) line)

 (for ([c outs] #:unless (eq? o c)) (displayln (~a who ": " line) c)))

(define ((client i o))

 (define nick (begin (display "Nick: " o) (read-line i)))
 (define tell (tell-all nick o))
 (let loop ([line "(joined)"])
   (if (eof-object? line)
     (begin (tell "(left)") (set! outs (remq o outs)) (close-output-port o))
     (begin (tell line) (loop (read-line i))))))

(define (chat-server listener)

 (define-values [i o] (tcp-accept listener))
 (for ([p (list i o)]) (file-stream-buffer-mode p 'none))
 (thread (client i o)) (set! outs (cons o outs)) (chat-server listener))

(void (thread (λ() (chat-server (tcp-listen 12321))))) ((client (current-input-port) (current-output-port))) </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

  1. Start up the server on port 7000
  2. Accept connections for any IP address
  3. Allow up to 100 connections
  4. Send information to stderr
  5. Turn on informational messages

ChatServer.new(7000, '0.0.0.0', 100, $stderr, true).start.join </lang>

Tcl

Works with: Tcl version 8.6

<lang tcl>package require Tcl 8.6

  1. 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 }

   }

}

  1. 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

}

  1. 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"

   }

}

  1. 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>