We already covered in another Article what an HTTP call is and how it works in a Client - Server Architecture. But today we want to have a look how we can keep an connection open between the Client and the Server in order to implement an Real Time Application.
Why do we need Sockets
But what would be the benefit of Sockets compared to the standard HTTP Request / Response method? In modern applications, we want real-time data, and who thinks now at Firebase is correct.
Before we had the benefit of WebSockets or Sockets, we had to wait until the Server finished a task and make another request to get the Data. Because the Server never told us, that we were ready with the new data. Therefore, developers all around the world discovered some quite exciting workarounds like Pooling. At Polling a client made a request every period (200+ ms) to check, if there are new data present. Thanks to WebSockets and Sockets we have the chance to reduce the load of our servers because we can reduce the calls needed. With that the server can directly notify the client of the new information. Real-Time applications are born.
Sockets vs. WebSockets
At the beginning of my journey I tried to use Shelf_WebSockets and connect with my two Terminals and the result was,... nothing. Interesting enough I was able to connect via the terminal clients to the server but whenever I send a message it was just not visible and the server did not show anything.
After a long time of learning and debugging I found out that my Terminal used Dart:io and needs that a Server works with the same TCP Sockets. While the WebSocket Server was able to allow the connections it was unable to understand the messages the Terminals tried to send.
Why is that the case, well WebSockets are an upgraded HTTP request that allows the open connection between a client and a server. WebSockets are usually used to connect from a Browser or an UI to the Server and this made it probalmatic. There are packages out there that allow Terminals to connect against an WebSocket server but I did not use them. Therefore I needed to change my WebSocket Server to an TCP Socket Server.
So the rest of the Tutorial we will only use a Socket Server and Socket Clients but the same thing can be done with WebSocket Server and an WebSocket Client like for example websocket_manager that allows Flutter to connect against a WebSocket Server.
If you want to know more about Sockets vs. WebSockets I can recommend you to read the mdn post about WebSockets and the StackOverflow answer. Great now that we have that out of the way let's see how we can develop our application.
Implementation
The best part of the implementation we will now see is that we do not rely on any package. To make this possible default Dart is enough to make it work and that is by any means awesome. The only package I added is absolutly optional and is called ansicolor and allows to make prints in the console a bit more colorful.
I created a basic barebone Dart project without anything inside but a terminal executable. Because we do not need the boilerplate from the Server app because we want to use Sockets :).
![[Pasted image 20220706134649.png]]
Now I delete everything in the bin folder and created three files client.dart
, server.dart
and terminal_service.dart
. In the client.dart
we will collect all information about the client, how he connects to the server and the message he sends. At the end we will be able to start multiple clients against a single server. The second file is the server.dart
inside here we write everything to receive messages from multiple clients and notify all clients that there happens something new. Last but not least we have the terminal_service.dart
this is just a mini service that contains different print functions for different colors. The result after the tutorial will look something like this.
Setup the Server
First we start as always with a main function inside here we want to find the local ip address and we allow to find dart just an unused IP address. Then we create a ServerSocket and bind the ip and a port in our case the port is fixated on 3000. after that we just let the server listen to incoming connections, so whenever someone connects to the server the callback in server.listen(callback)
will be executed.
Future<void> main() async {
final ip = InternetAddress.anyIPv4;
final server = await ServerSocket.bind(ip, 3000);
print("Server is running on: ${ip.address}:3000");
server.listen((Socket client) {
handleConnection(client);
});
}
Whenever a client is connecting to our Server we call the method handleConnection
inside of which we want now to inform the Server that there is an incoming connection. The Socket we receive in the handleConnection is the information how to communicate to that specific client and if you want for example to create multiple players you can save that socket to have a reference between player and socket to send messages between specific players. Now we have to listen to the client if the client is sending the server some messages.
List<Socket> clients = [];
void handleConnection(Socket client) {
printGreen(
"Server: Connection from ${client.remoteAddress.address}:${client.remotePort}",
);
client.listen( ... );
}
Before we had already server.listen
where the server waited for connections, now we have client.listen
so we on the server wait for the client to send us some notifications. the client.listen
function has some information we have to pass in. The first part is how to handle messages from the client. So what we will do is we receive a Uint8List message that is just a basic ByteString, and because we will be in full control of the client we know that this will be a basic string so we can transcode it with String.fromCharCodes(data)
.
In that message we want that our users will send the player name so we can inform every client that already connected to the server
client.listen(
(Uint8List data) async {
final message = String.fromCharCodes(data);
for(final c in clients) {
c.write("Server: $message joined the party!");
}
clients.add(client);
client.write("Server: You are logged in as: $message");
},
onError: ...
onDone: ...
...
)
Implement the Client
First we create in our Dart Project another file called client.dart
. Here we will implement the client application that will connect to our Server. The client works similar to the Server and should be a standalone Dart application. Therefore, we need another main
function. In this main
function we connect with a Socket to our Server with the IP we get from the server command:
Future<void> main(){
final socket = await Socket.connect("0.0.0.0", 3000);
print('Connected to ${socket.remoteAddress.address}:${socket.remotePort}');
}
When we execute the client.dart
file now without anything further we will get already the information on our Terminal where the Server is running, it should look something like this.
➜ dart ./bin/client.dart
Connected to: 127.0.0.1:3000
➜ dart ./bin/server.dart
Server is running on: 0.0.0.0:3000
Server: Connection from 127.0.0.1:53419
Therefore we can see that the Server already receives the connection, and keeps the connection open for the ip 127.0.0.1:53419. This is in our case also the same computer but the IP and the Port could be completly different for your implementation. Great so the Connection is already stable so lets send some messages from the client to the Server and see how we can handle them. For that we will ask the User for his name and send the information to the Server.
// Ask user for its username
String? username;
do {
print("Client: Please enter your username");
username = stdin.readLineSync();
} while (username == null || username.isEmpty);
socket.write(username);
We first define a variable username
that could be null and in the do-while-loop
we ask the user for a terminal input to enter the username, if the username is null or empty we ask him again until we get the info we need. After that we call socket.write(username);
which sends the information directly to the Server. But if we restart the Server and the client now we will see that nothing has changed. The message is now only send to the server and if we check again the server we see that we send the message to the client.
client.write("Server: You are logged in as: $message");
The client on the other hand does not listen to server messages and is not able to show what the Server send, yet. So let us make the Client smarter and allow it to receive messages send by the Server.
socket.listen(
(Uint8List data) {
final serverResponse = String.fromCharCodes(data);
printGreen("Client $serverResponse");
},
onError: (error) {
print("Client: $error");
socket.destroy();
},
onDone: () {
print('Client: Server left.');
socket.destroy();
},
);
If we check the socket we can see that it provides also the listen
method and also here we have the chance to implement the same three methods like on the Server side. Therefore we implemented them nearly the same. The only difference is the onData
method that is now receiving the message and directly prints them into the console.
If we start up now the Server and Client we should be able to see something like this:
➜ dart ./bin/server.dart
Server is running on: 0.0.0.0:3000
Server: Connection from 127.0.0.1:54384
➜ dart ./bin/client.dart
Server: Connected to: 127.0.0.1:3000
Client: Please enter your username
Max
Client Server: You are logged in as: Max
And if we create another client the following will happen:
➜ dart ./bin/client.dart
Server: Connected to: 127.0.0.1:3000
Client: Please enter your username
Mahtab
Client Server: You are logged in as: Mahtab
➜ dart ./bin/client.dart
Server: Connected to: 127.0.0.1:3000
Client: Please enter your username
Max
Client Server: You are logged in as: Max
Client Server: Mahtab joined the party!
➜ dart ./bin/server.dart
Server is running on: 0.0.0.0:3000
Server: Connection from 127.0.0.1:54384
Server: Connection from 127.0.0.1:54415
Conclusion
Great with that we have now a successful Socket Server running and multiple clients connected. This allows us now to create amazing applications that needs the real time experience. That can be integrated in Games but also in Tools where collaboration is key.
The tutorial project can be found on Codeberg, feel free to check it out and if there are any toughts please let us know. Thanks for reading and till the next post.
Top comments (0)