6
\$\begingroup\$

I'd like to build a multi user dungeon in Java/C# (also open to other suggestions). Below I have listed two ways of handling connections, and would like to know which would be the recommended way?

I think a good way to build it would be something like this:

MUD design

Currently, I'm working on the connection handling portion (shown in the picture on the left, heavy black rectangle). It would take care of the connection handling between multiple clients and basically shove commands received from clients (denoted by newline char) into a queue, which the game engine can then pull off of and process. If there is a reply, the game engine would shove it into another queue so the connection handler can send it back to the client.

I see two ways to work this part:

Thread per connection

class Server {
    public void Listen() {
        // configure
        final int portNumber = 81;

        try {
            ServerSocket serverSocket = new ServerSocket(portNumber);

            while (true) {
                Socket socket = serverSocket.accept();
                new ProcessThread(socket).start();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class ProcessThread extends Thread {
    protected Socket socket;

    public ProcessThread(Socket clientSocket) {
        this.socket = clientSocket;
    }

    public void run() {
        try {
            OutputStream os = socket.getOutputStream();
            PrintWriter pw = new PrintWriter(os, true);
            
            while (true) {
                BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                String str = br.readLine();

                // do processing here
            }
        }
    }
}

Pros:

  • Easier implementation

Cons:

  • Thread per connection not scalable
  • Multithreading = potential yuckiness

Async socket

public class Server {
    // use hashmap to keep track of current input
    private HashMap<SelectionKey, StringBuilder> strings = new HashMap<>();

    public void Listen() {
        // configure
        Selector selector = Selector.open();
        ServerSocketChannel serverSocket = ServerSocketChannel.open();
        serverSocket.bind(new InetSocketAddress("localhost", 5454));
        serverSocket.configureBlocking(false);
        serverSocket.register(selector, SelectionKey.OP_ACCEPT);
        ByteBuffer buffer = ByteBuffer.allocate(256);

        while (true) {
            selector.select();
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> iter = selectedKeys.iterator();
            while (iter.hasNext()) {

                SelectionKey key = iter.next();

                if (key.isAcceptable()) {
                    // register new connection
                    register(selector, serverSocket);
                }

                if (key.isReadable()) {
                    // process input
                    // add input to hashmap, if not newline terminated
                    process(buffer, key);
                }
                iter.remove();
            }
        }
    }
}

Pros:

  • Will definitely process in order received
  • Less resource intensive (?)

Cons:

  • Handling readline is more difficult

What would be the recommended way?

I'm leaning towards the thread per connection, as it seems to be slightly easier to implement. For a MUD, I would expect less than 200 simultaneous connections (50 might be a more accurate estimate), which would not be too resource intensive.

\$\endgroup\$
4
  • 2
    \$\begingroup\$ Speaking from someone with a bit of a history on social MUDs: There was a span where 200 simultaneous connections was a crazily high number, and then a span where 200 would be considered a quiet time. I know of a few places that still regularly poll in the few-hundreds, though that's definitely in nice-problem-to-have territory. I would personally lean towards the async approach, but I'd also look at existing MUD codebases for guidance - there are plenty out there where you can see how people have handled it. \$\endgroup\$ Commented Mar 5, 2019 at 23:57
  • \$\begingroup\$ @mmking Is your primary purpose in authoring this MUD to learn coding techniques, to make a successful and widely adopted MUD, or to have fun adding features and color even if not many people play it? I ask because this is probably what should guide your decisions-- your technical constraints do not demand one solution over the other. \$\endgroup\$
    – SAyotte
    Commented Mar 6, 2019 at 21:35
  • \$\begingroup\$ @SAyotte mostly for my own fun/learning. I think it'll be a cool project. I'll be sharing it with a few friends but no intention of making it "widely played" or commercial. \$\endgroup\$
    – mmking
    Commented Mar 7, 2019 at 3:19
  • 1
    \$\begingroup\$ @mmking, if you're doing this in C# you should probably look at using Tasks instead of Threads. Actually, if you haven't used Tasks, or goroutines in Golang, or processes in Erlang, or the Actor pattern somewhere else... I'd encourage you to pick it up in a hobby project soon. As a learning tool, it'll really help you tease apart concurrent-processes from parallel-execution, which aren't really the same thing.... threads are aligned with the latter. But if we do concurrency cleanly, we can easily scale it from uniprocessor to many cores with no/little change in the code. \$\endgroup\$
    – SAyotte
    Commented Mar 9, 2019 at 21:50

3 Answers 3

2
\$\begingroup\$

Think about this: it's not much code to rewrite the details of the I/O handling later after you've built a lot more of the system, because this is a fairly straightforwardly separable module. But there's a choice to be made which decides which one is better to pick for the moment:

Do you want your game logic to have to be thread-safe, or would you rather run it on a single thread?

If you want single-threaded game logic, then it makes sense to use async I/O to go with it, because threads would just have to use some kind of message queue. And if you want to have multithreaded game logic then it makes sense to have a thread for each connection/session.

Now, to answer the new question for you, you almost certainly want single-threaded game logic. Systems which have multiple threads accessing the same objects, where those objects are not specifically and carefully designed to be inter-thread communication/coordination/shared-data-storage mechanisms, are very subtle and frustrating to debug when you make mistakes — and you will.


On the other hand, there are some advantages to be had with using a thread per connection even if your game logic is single-threaded. Not only is implementing line reading easier, but you can also more directly implement special modes, like multiline text editing or mini-games, as methods which don't return until the user exits that mode. So, given that you don't need optimum performance for your fun project, a thread per connection may be a practical way to go.

…but using the call stack like that has some caveats; for example, if the connection is dropped you need to make sure that each of those modes on the stack cleans up after itself when it sees an IO error, rather than (say) perpetually trying again to read text until it sees a newline.

Also, the more code you run on the per-connection threads, the more code has to correctly communicate with the single-threaded game logic (using some kind of message passing or Executor-style mechanism). If you have async IO, or your IO threads only do passing command lines to the game thread, then you don't have this problem.

\$\endgroup\$
0
\$\begingroup\$

I'll build on what @kevin-reid said, which is all pretty sound advice.

Single-threading is the traditional approach to game development, and even today many new games don't leverage additional cores for their heavy lifting. That is, there's a solid chance you can get away with one thread for all your heavy lifting, even for a triple-A game. Additional threads for things like I/O are small potatoes, and as @kevin-reid said you can easily separate that later if you choose.

But, since this is a learning project, learning techniques to leverage additional cores for your heavy lifting is perhaps a goal worth considering... Processors are getting wider, not faster, after all. To that end, a single game-thread which handles everything is not a great starting point, as you'll enjoy the benefit of assumptions you don't even realize you're making.

If you think that you might parallelize later on, you should consider early-on which axis you'd parallelize on... would you allow events to happen in parallel, or computations of outcomes / AI decisions? All of those are legitimate options, but each presents its own synchronization problems:

  • Parallel events cannot affect the same entities at the same time; put differently, events affecting the same entities must be serialized.
    • You can naively achieve this with a mutex on every entity, but you'll quickly find that enforcing entity-relationship invariants forces you to either invent something akin to transactions (yuck) or choose a larger group of things than a single entity to protect. A popular term for such a group of things is an "aggregate".
  • Parallel computations may still need to be sequenced.
    • For instance, in a 3D game you'd need to compute all movement before computing any collision-detection. Similarly, in a MUD you'd need to process combat and movement serially, even if combat calculations are parallelized, so that e.g. you can't be hit by a sword-blow when you're already in the next room.

The lesson you should expect to take away is that, when designing a system for parallel processing, you'll need to relax some of the assumptions you'd make in a single-threaded design. So e.g. you may choose to let AI running in a different thread to operate on a not-100%-consistent view of the world; without this relaxation you'd have to either freeze the main thread so it stops updating the game-state, or take a snapshot of the entire game-state for the AI to operate on. The exercise of identifying what can be relaxed, what can't, and where that leaves room for parallel processing is a good one that should translate well into non-hobby projects you work on.

\$\endgroup\$
0
\$\begingroup\$

I've already created a game server and I used netty for I/O handling. My design is use multi-thread for I/O and single thread for game logic. It looks like your image, where netty is connection handler, input/output queues I used LMAX Disruptor's ring buffer. With this design, thread-safety is not problem because game logic is handled in a single thread -> no multi-threading bug :D

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .