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;
}
}