0
\$\begingroup\$

I'm trying to write game server core that I can easy extend to use for various games. From simple online Blackjack session, to MMORPG. Long story short, I came up with this solution:

Firstly, we have common classes for both Server and Client. Those are simple data structures, probably will need to wrap them with Builders, but for this presentation I guess I can skip this.

public abstract class DataPacket implements Serializable {
}

final public class LoginRequest extends DataPacket implements Serializable {
    private final String accountNumber;
    private final String password;

    public LoginRequest(String accountNumber, String password) {
        this.accountNumber = accountNumber;
        this.password = password;
    }

    public String getAccountNumber() {
        return accountNumber;
    }

    public String getPassword() {
        return password;
    }
}

final public class LoginResponse extends DataPacket implements Serializable {
    private final String message;

    public LoginResponse(String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}

Now lets go to the core, ServerController, which is kind of Server API, that has all the public methods. Config is just a Property loader class, in the future, probably will change to *.ini file instead of *.property, so I'm using DI here.

public class ServerController {
    private Config config;
    private Dispatcher dispatcher;

    @Autowired
    public ServerController(Config config) {
        this.config = config;
        dispatcher = new Dispatcher(config.getPort(), config.getMaxHosts());
    }

    public void start() {
        dispatcher.start();
    }

    public void stop() {
        dispatcher.stopAccepting();
    }
}

Then there is Dispatcher that listen for incoming connections and creates new ServerWorker for each one.

class Dispatcher extends Thread {
    private ServerSocket serverSocket;
    private List<ServerWorker> serverWorkers;
    private Status status;
    private final int maxHosts;

    Dispatcher(int port, int maxHosts) {
        serverWorkers = new ArrayList<>();
        status = Status.DOWN;
        this.maxHosts = maxHosts;
        try {
            this.serverSocket = new ServerSocket(port);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        status = Status.UP_ACCEPTING;
        while(status == Status.UP_ACCEPTING) {
            acceptIncomingConnections();
        }
    }

    void stopAccepting() {
        status = Status.UP_NOT_ACCEPTING;
        this.interrupt();
    }

    void disconnectClient(ServerWorker worker) {
        serverWorkers.remove(worker);
        worker.interrupt();
    }

    private void acceptIncomingConnections() {
        try {
            Socket socket = serverSocket.accept();
            ServerWorker worker = new ServerWorker(socket, this);
            worker.start();
            serverWorkers.add(worker);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Which leads us to ServerWorker which is listening for requests and redirecting them to further process.

class ServerWorker extends Thread {
    private ObjectInputStream inputStream;
    private ObjectOutputStream outputStream;
    private Dispatcher dispatcher;

    ServerWorker(Socket socket, Dispatcher dispatcher){
        this.dispatcher = dispatcher;
        try {
            inputStream = new ObjectInputStream(socket.getInputStream());
            outputStream = new ObjectOutputStream(socket.getOutputStream());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        try {
            listen();
        } catch (IOException | NullPointerException e) {
            e.printStackTrace();
            dispatcher.disconnectClient(this); //Once Client disconnects, this Exception is thrown
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    private void listen() throws IOException, ClassNotFoundException {
        DataPacket dataPacket;
        while((dataPacket = (DataPacket)inputStream.readObject()) != null ) {
            process(dataPacket);
        }
    }

    private void process(DataPacket dataPacket) throws IOException {
        Optional<Event> event = EventFactory.getEvent(dataPacket);
        event.ifPresent(e -> {
            e.process();
            send(e.getProcessedData());
        });
    }

    private void send(DataPacket dataPacket) throws IOException {            
        outputStream.flush();
        outputStream.writeObject(dataPacket);            
    }
}

And that leaves us with final part, Events. Not sure if I can avoid using instanceof here, since it translates incoming data into event, which are 2 different objects.

public interface Event {
    void process();
    DataPacket getProcessedData();
}

public class EventFactory {
    public static Optional<Event> getEvent(DataPacket dataPacket){
        if(dataPacket instanceof LoginRequest) {
            return Optional.of(new LoginEvent((LoginRequest) dataPacket));
        }
        return Optional.empty();
    }
}

public class LoginEvent implements Event {
    private LoginRequest request;
    private LoginResponse response;

    LoginEvent(LoginRequest loginRequest) {
        request = loginRequest;
    }

    @Override
    public void process() {
        if(isValid(request.getAccountNumber(), request.getPassword())) {
            response = new LoginResponse("You successfully logged in!");
        } else {
            response = new LoginResponse("Something went wrong...");
        }
    }

    @Override
    public DataPacket getProcessedData() {
        return response;
    }

    private boolean isValid(String accountNumber, String password) {
        return true;
    }
}
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

I'm trying to write game server core that I can easy extend to use for various games. From simple online Blackjack session, to MMORPG.

I'm fairly certain, that won't work as easy as you think. It's a huge difference in requirements between exchanging data once a second between few players, and exchanging data 15/30/60 times a second with a .. 'massive' amount of players.

class DataPacket

You can make DataPacket an interface which extends Serializable.

class LoginResponse

Contains a String as response. This means, first of all, it's not internationalized, if that is a problem, second, the client has to parse a String to verify, if the login attempt was successful. What's wrong with a boolean?

class Dispatcher

  • Dispatcher of what?
  • It is usually better to favor composition over inheritance.
  • maxHosts is never used, therefore dead code.
  • e.printStackTrace: Nope, we don't do that. In the constructor it is especially bad, since the program keeps running and fails again during acceptIncomingConnections()
  • Field status must be declared volatile.
  • Thread.interrupt(): It's one of things where you have to know exactly what you're doing.
  • You make a new instance of ServerWorker, pass this as argument. That's a bidirectional dependency and must be avoided.

class ServerWorker

  • run() catches IOE and NPE and executes the same code. I don't think your program should reach that point, so that a NPE is possible.
  • listen(): Listen to what? And it doesn't exactly listen, it's more something 'readAndProcessDataPacket()', to be precise.
  • process(): Why are you using Optional? My interpretation of this is, that it is possible to send data which is unknown to the server, and if that happens, nothing happens. I don't think, this is what you want.
  • send(): You have to flush after writing the object, to guarantee, that the data has been flushed, not before.

interface Event

Here again, you have a bidirectional dependency between the networking layer and the event layer. In the long run, this will be horrible to maintain. I'd recommend, to try to make the even system running without the networking layer first. Think more of the API, which you'd like to call from a layer above.

Hope this helps,

slowy

\$\endgroup\$
3
  • \$\begingroup\$ Thanks for your anserw, its really helpful. I got just 2 more questions if you dont mind. Bidirectional dependency in ServerWorker, how can I avoid that? I can't throw exception from run() method and catch it in Dispatcher above. And second one, Events. Same thing, bidirectional dependency, but here I thought it's inevitable. I pass data into the some kind of Event and I expect data to return in processed form. You got any ideas how to avoid that? \$\endgroup\$
    – JohnDoe
    Sep 25, 2018 at 6:57
  • \$\begingroup\$ @CleanCoder Exception: What can happen atm: The ServerController creates a Dispatcher during initialisation, the Dispatcher throws an exception, which is swallowed, so the Dispatcher is in a unknown state, e.g. the serverSocket isn't up and running, this is something you can easily avoid. \$\endgroup\$
    – slowy
    Sep 25, 2018 at 18:56
  • \$\begingroup\$ @CleanCoder: Bidirectional dependencies: Maybe this helps: stackoverflow.com/questions/946834/…. Another tip: Create different modules (intellij) or projects (eclipse) and manage the module dependencies like this, at least intellij warns about circular dependencies. Imo this helps beginners to think more about the api and the design of their software, and of course, avoids this problem in the first place... \$\endgroup\$
    – slowy
    Sep 25, 2018 at 19:01

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.