5. chat (socketset, OOP)

import std.socket : InternetAddress, Socket, TcpSocket, SocketOptionLevel,
    SocketOption, Address, SocketSet;
import std.stdio : writefln;
import std.algorithm : remove, startsWith;
import std.string : strip;
import std.format : format;
import std.conv : to;

enum MAX_CONNECTIONS = 40;
enum PORT = 4444;

struct Client {
    Socket sock;
    Address addr;
    string name;
    ubyte[256] buf;

    this(Socket sock) @safe {
        this.sock = sock;
        addr = sock.remoteAddress;
    }

    // bool: is the client still alive?
    bool receive(size_t id, ChatServer server) @safe {
        ptrdiff_t got = sock.receive(buf[]);
        if (got <= 0)
            return false;
        if (name != "") { // named clients can speak
            server.broadcast(id, format!"<%s> %s"(name, cast(char[])(buf[0 .. got])));
        } else if (buf[0 .. got].startsWith("/nick ")) {
            static const prefix = "/nick ".length;
            name = strip(cast(char[])(buf[prefix .. got])).idup;
            writefln!"Connection %s is now known as %s."(addr, name);
            server.broadcast(-1, format!"* %s has joined.\n"(name));
        } else {
            sock.send("* Identify yourself with /nick yourname\n");
        }
        return true;
    }

    void send(string msg) @safe {
        if (name != "") // named clients can hear
            sock.send(msg);
    }
}

class ChatServer {
private:
    TcpSocket listener;
    Client[] clients;
    SocketSet sockets;

public:
    this(short port) @safe {
        listener = new TcpSocket;
        sockets = new SocketSet(MAX_CONNECTIONS + 1);

        listener.blocking = true;
        listener.setOption(SocketOptionLevel.SOCKET, SocketOption.REUSEADDR, 1);
        listener.bind(new InternetAddress(port));
        listener.listen(10);
        writefln!"Listening on %d."(port);
    }

    void broadcast(ptrdiff_t except, string msg) @safe {
        foreach (i; 0 .. clients.length) {
            if (i != except)
                clients[i].send(msg);
        }
    }

    void select() @safe {
        sockets.reset;
        sockets.add(listener);
        foreach (client; clients)
            sockets.add(client.sock);
        Socket.select(sockets, null, null);

        for (size_t i = 0; i < clients.length; i++) {
            if (!sockets.isSet(clients[i].sock))
                continue;
            if (!clients[i].receive(i, this)) {
                writefln!"Lost connection from %s."(clients[i].addr);
                clients[i].sock.close;
                clients = clients.remove(i);
                i--; // restart loop at same i
            }
        }
        if (sockets.isSet(listener)) {
            Socket client = listener.accept;
            if (clients.length < MAX_CONNECTIONS) {
                writefln!"Received connection from %s."(client.remoteAddress);
                clients ~= Client(client);
            } else {
                writefln!"Rejected connection from %s; too many connections (%d)."(client.remoteAddress,
                        clients.length);
                client.close;
            }
        }
    }
}

void main() @safe {
    scope server = new ChatServer(PORT);
    while (true)
        server.select;
}

This server is very similar to brutish chat, but client structs keep track of their name and handle their own I/O, only broadcasting when named.

Server interaction

Listening on 4444.
Received connection from 127.0.0.1:42376.
Connection 127.0.0.1:42376 is now known as bob.
Received connection from 127.0.0.1:42404.
Connection 127.0.0.1:42404 is now known as alice.
Lost connection from 127.0.0.1:42404.
Lost connection from 127.0.0.1:42376.

Client interaction(s)

/nick bob
* bob has joined.
* alice has joined.
hello?
<alice> hi
^C
hi
* Identify yourself with /nick yourname
/nick alice
* alice has joined.
<bob> hello?
hi
^C