Ratpong
WEB Pong written in Plain Java
Install / Use
/learn @javaFunAgain/RatpongREADME
ratpong
This is POUNK game server implementation which is network version of a classic PONG.
For client code go to : https://github.com/jarekratajski/scalajspounk
java 9
In order to run this code in Java 9 please add this switch to jvm. --add-modules java.activation
Running?
Just call gradle run
Navigate to http://localhost:9000
As a user you can register with user and password.
After that you can login.
You can create new game and enter game...
but once another player joins it. Yes you need 2 players,
you may open second browser window and register a second player.
Controls:
- Q move paddle up,
- A move paddle down,
Purpose
The goal of this project is to prepare clean Java example of non blocking server architecture. System uses as little mutability as possible, no magic frameworks, application servers and simply starts with main method ( as Fat Jar).
For a moment system uses such technologies:
- Ratpack providing REST server library,
- JavaSlang for immutable data structures (vavr :-) ),
- Airomem / prevayler for simple persistence
- Junit5 for simple tests,
Notice that system does not use any special dependency injection frameworks or containers. It is 2017 - we do not need it anymore! :smile:
Legal issue
Please, bear in mind that PONG is a registered trademark that probably belongs to Atari SA.
Architecture
System is divided into 3 main modules.
- users - user registration and login/session logic
- games - created games list, states of games
- scores - registered scores
Typical module consists of:
- service class - which defines REST methods -api package that defines JSON structures for client and server
- module class - which contains configuration
- repositories - various repositories implementations
Concepts
Dependency injection
Code does not use any dependency injection container. For it is 2017. Once you use modern web server such as Ratpack you do not have technical limitations that happen with Servlet architecture that make use of dependency injection by container handy. You are in full control of our objects creation and you can use this power. :metal:
It does not mean there is no dependency injection working. It is. Only done the easy way: with constructor. (Notice that if you follow Oliver Gierke advice field injection evil you get almost same code...)
Check for instance ScoresService class. It has one dependency ScoresRepositoryProcessor which allows us to inject different persistence engine if needed (or for tests).
public ScoresService(ScoresRepositoryProcessor nonBlockingRepo) {
this.nonBlockingRepo = nonBlockingRepo;
}
Btw.: ScoresRepositoryProcessor has indeed further dependencies... (but this is not the problem).
In order to not get lost in "new" hell there is a factory which handles some modules defaults. See ScoresModule. This is exactly one place which decides explicitly what is default persistence etc. This code is more or less same as you would define in spring-beans.xml or with @Component annotations. This time however fully type safe, debuggable, testable. And without magic.
You can check the same concept working with more complicated classes as GamesService/GameModule More dependencies? No problem. Use java to control them all. :beers:
Service
It is easy to define REST api. Lets talk about Users.
Thare are 2 operations there. POST /users/USER_ID -register new user by POSTing data (password) POST /sessions/USER_ID - login user by posting password (it is POST because we create session)
There is usersApi method in UsersService which basically defines how to handle both operations. See how easy we do it with lambdas: :sunglasses:
public Action<Chain> usersApi() {
return apiChain -> apiChain
.prefix("users", users())
.prefix("sessions", sessions());
}
private Action<Chain> users() {
return chain -> chain
.post(":id", addUser());
}
Then real addUser implementation can be written:
private Handler addUser() {
return ctx -> {
final String userId = ctx.getPathTokens().get("id");
ctx.parse(NewUser.class).then(
newUser -> {
final Promise result = Promise.async(
d -> d.accept(usersRepo.addUser(userId, newUser.password).thenApply(Jackson::json)
));
ctx.render(result);
}
);
};
See how simple is to convert JSON from input (ctx.parse) and then our result to output JSON (Jackson::json).
Promises
One thing that may look unfamiliar in the code above is this Promise thing. :confused: Ratpack is a non blocking server. What does it mean: one should not block processing of request (by calling and waiting for IO ). So if you want to ask the database and then wait for result you are generally destroying the whole concept of non blocking architecture. :boom:
So how to work with that?
The answer are Promises (which are some form of Futures that you could have probably heard about). Instead of just doing the blocking thing we return Promise object that will be completed somewhere in the future. Possibly when result from our database is read. Ratpack makes life easy here providing handy implementation of Promise. ctx.render accepts generally a String in case we know how to answer or a Promise which will be resolved somewhere in the future (which is more likely).
So what is this?
final Promise result = Promise.async(
d -> d.accept(usersRepo.addUser(userId, newUser.password).thenApply(Jackson::json)
));
This is in fact conversion between JAVA promise called CompletableFuture and Ratpack Promise. (Ratpack was created before CompletableFuture was defined in Java API). (Btw. this operation can be easily extracted)
So: you know how to render Promise (or CompletableFuture) - but how to get it?
Here comes to help the class called UserRepositoryProcessor. The responsibility of this class is to cooperate with a blocking persistence engine and delegate processing to other threads so that the request processing thread is not blocked.
You may wonder: what sense does it make? - not blocking the request thread but instead to block some other thread... where is the point?
That is good point indeed. If you had some kind of nonblocking DB such as Cassandra or even MongoDB you could leverage that in be truly non blocking way. But probably you do not have. Still nothing wrong happens! It is possible to use Blocking IO with non blocking server and still get some gain.
What you can do (win) is the control of your application threads! Imagine 5000 users trying to get access to the rest service simultaneously.
In a classical (blocking) architecture it would mean 5000 threads are created, all of them making connection to database, waiting for answer, holding memory.
This does not sound good. Imagine what your database feels.
As an experienced human you know that it is typically better to do tasks one by one - than to start thousands of them concurrently.
This is exactly what can be done with blocking DB. You can limit how many concurrent queries (or generally operations) you perform and simply queue all the rest. This is what "Processor" class does. It uses very nice tool from standard Java lib called Executor. Simply create an executor and tell how many threads should it use (lets say one!). Then just queue all subsequent operations using it:
```
writesExecutor.execute( ()-> {
result.complete(this.usersRepository.addUser(login, pass));
});
```
See? That executor operation returns CompletableFuture - and this is exactly what we need.
Notice also that this is called wr
Related Skills
node-connect
341.8kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
84.6kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
341.8kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
commit-push-pr
84.6kCommit, push, and open a PR
