Simple websocket chat on Dart
Hello!
In this article I want to describe how to create a simple websocket chat on Dart in order to show how to work with web sockets in Dart. The application code is available on github , and an example of its operation can be found here: http://simplechat.rudart.in .
The application will consist of two parts: server and client. We will analyze the server part in great detail, and from the client we will only consider what is responsible for working with the connection.
The requirements for the application are very simple - sending messages from the user to all or only selected chat participants.
All application settings and constants will be stored in a file
We will connect the file itself as a package, because if we use relative paths, then when building the application (
In order to connect a package located somewhere on our machine, we will use pub path dependency . To do this, we simply add the definition of our package to the
Server files are located in a folder
Let's talk about how our server will work in general. The first thing we will do with the server is to start it. During startup, it will start listening to the port
When a new user sends a request to this port, the server will open a websocket connection for it, generate a name and save the name and connection to a hash with open connections. After that, the client will be able to send messages on this connection. The server will be able to send these messages to other users, as well as send notifications about connecting and disconnecting clients.
If the user closes the connection, the server will remove it from the hash with active connections.
At the very beginning of the file,
In the function,
Below is the basic server code, which will simply be bound to the desired port.
At the end of the function
To configure the router, create a function
In the function,
The server sends messages as a Map object (or rather, its representation in json) with the following keys:
Here is the function that builds such a message:
In order to send a message to a client, you need to use the add () method of the WebSocket class . Below is the function that will send messages to the user:
Our server can send notifications to all active clients about connecting or disconnecting a user. Let's look at the function for this. The function
Also, after joining a new user, we will welcome him:
Let's now see a function that processes incoming messages from a user and sends them to all (or only specified) chat participants. The function
When the user closes the connection, we must remove it from the list of active connections. The function
To summarize everything that we now have. The function
Let's now change the function
Then we will need to listen to the user's websocket connection to messages from him and send messages to the participants. We will also add a handler to close the websocket connection, in which we remove it from the list and notify all participants to disconnect.
That's all, a simple server is ready. Now let's move on to the client side.
Here I will not talk about the layout of the client part and the display of messages . In this part, we will only talk about how we open a websocket connection to the server, send and receive messages.
The client application entry point is in the file
In the first line we declare a library. Then we connect the necessary files and parts of the libraries. The file
A function
Let's take a look at the properties and constructor of the class
The code shows that it
The constructor of the class accepts the address where you can open the websocket connection, selectors for elements
Then we assign event handlers to our connection.
The event
In the body of the event handler,
The event
The event is
I will not cite the function code
That's all. This is all the main functionality of the client part.
You can see the working application here: http://simplechat.rudart.in .
If I made any mistakes and inaccuracies, then let me know, and I will try to fix everything quickly.
In this article I want to describe how to create a simple websocket chat on Dart in order to show how to work with web sockets in Dart. The application code is available on github , and an example of its operation can be found here: http://simplechat.rudart.in .
The application will consist of two parts: server and client. We will analyze the server part in great detail, and from the client we will only consider what is responsible for working with the connection.
The requirements for the application are very simple - sending messages from the user to all or only selected chat participants.
Application settings
All application settings and constants will be stored in a file
common/lib/common.dart
. This file contains the library definition simplechat.common
.library simplechat.common;
const String ADDRESS = 'simplechat.rudart.in';
const int PORT = 9224;
const String SYSTEM_CLIENT = 'Simple Chat';
We will connect the file itself as a package, because if we use relative paths, then when building the application (
pub build
) we can get an error from pub
: Exception: Cannot read {file} because it is outside of the build environment . In order to connect a package located somewhere on our machine, we will use pub path dependency . To do this, we simply add the definition of our package to the
dependencies
file section pubspec.yaml
:dependencies:
simplechat.common:
path: ./common
pubspec.yaml
I won’t give the
entire contents of the file (but you can see it on github ). You will also need to add a file pubspec.yaml
to a directory common
in which we simply indicate the name of our package:name: simplechat.common
Server
Server files are located in a folder
bin
. The file main.dart
contains the entry point to the server, and the file contains the server.dart
class of our server. Let's start by looking at the contents of the file main.dart
.General server operation scheme
Let's talk about how our server will work in general. The first thing we will do with the server is to start it. During startup, it will start listening to the port
9224
. When a new user sends a request to this port, the server will open a websocket connection for it, generate a name and save the name and connection to a hash with open connections. After that, the client will be able to send messages on this connection. The server will be able to send these messages to other users, as well as send notifications about connecting and disconnecting clients.
If the user closes the connection, the server will remove it from the hash with active connections.
Server entry point
At the very beginning of the file,
bin/main.dart
we determine that it is a library simplechat.bin
. For the server, we need to connect the library dart:async
, dart:convert
, dart:io
, the package route
(put it through pub
) and a file with application settings. Also in bin/main.dart
we include a file bin/server.dart
that contains the main code of our server (we will consider it a bit later). In the function,
main()
we create an instance of the server and run it.
library simplechat.bin;
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:route/server.dart' show Router;
import 'package:simplechat.common/common.dart';
part 'server.dart';
/**
* Entry point
*/
main() {
Server server = new Server(ADDRESS, PORT);
server.bind();
}
Server base class, port wiretap
Below is the basic server code, which will simply be bound to the desired port.
part of simplechat.bin;
/**
* Class [Server] implement simple chat server
*/
class Server {
/**
* Server bind port
*/
int port;
/**
* Server address
*/
var address;
/**
* Current server
*/
HttpServer _server;
/**
* Router
*/
Router _router;
/**
* Active connections
*/
Map connections = new Map();
int generalCount = 1;
/**
* Server constructor
* param [address]
* param [port]
*/
Server([
this.address = '127.0.0.1',
this.port = 9224
]);
/**
* Bind the server
*/
bind() {
HttpServer.bind(address, port).then(connectServer);
}
/**
* Callback when server is ready
*/
connectServer(server) {
print('Chat server is running on "$address:$port"');
_server = server;
bindRouter();
}
}
At the end of the function
connectServer()
, the function to configure the router is called bindRouter()
, which we will consider below.Configuring a router and creating a websocket connection
To configure the router, create a function
bindRouter()
. /
We will change the input stream using WebSocketTransformer
and listen in the function createWs()
.
/**
* Bind routes
*/
bindRouter() {
_router = new Router(_server);
_router.serve('/')
.transform(new WebSocketTransformer())
.listen(this.createWs);
}
createWs(WebSocket webSocket) {
String connectionName = 'user_$generalCount';
++generalCount;
connections.putIfAbsent(connectionName, () => webSocket);
}
In the function,
createWs()
we generate a name for the connection according to the scheme user_{counter}
and save this connection to connections
.Message structure from the server and message creation function
The server sends messages as a Map object (or rather, its representation in json) with the following keys:
- from - from whom the message is from;
- message - message text;
- online - the number of users online.
Here is the function that builds such a message:
/**
* Build message
*/
String buildMessage(String from, String message) {
Map data = {
'from': from,
'message': message,
'online': connections.length
};
return JSON.encode(data);
}
Sending messages from the server
In order to send a message to a client, you need to use the add () method of the WebSocket class . Below is the function that will send messages to the user:
/**
* Sending message
*/
void send(String to, String message) {
connections[to].add(message);
}
Our server can send notifications to all active clients about connecting or disconnecting a user. Let's look at the function for this. The function
notifyAbout(String connectionName, String message)
accepts the connection name and message (about connecting or disconnecting). This function notifies all active clients in addition to whom this notification is made. Those. if user_3 has joined us , then all users except him will receive a notification. In order to filter clients by a certain condition (in our case, we need to get the names of all clients that do not match the current one) we will use the where () method of the Iterable abstract class .
/**
* Notify users
*/
notifyAbout(String connectionName, String message) {
String jdata = buildMessage(SYSTEM_CLIENT, message);
connections.keys
.where((String name) => name != connectionName)
.forEach((String name) {
send(name, jdata);
});
}
Also, after joining a new user, we will welcome him:
/**
* Sending welcome message to new client
*/
void sendWelcome(String connectionName) {
String jdata = buildMessage(SYSTEM_CLIENT, 'Welcome to chat!');
send(connectionName, jdata);
}
Let's now see a function that processes incoming messages from a user and sends them to all (or only specified) chat participants. The function
sendMessage(String from, String message)
accepts the name of the sender and its message. If the message body ( message
) specify the recipient names by mask @{user_name}
, then the message will be delivered only to them. Let's look at the function code sendMessage
:
/**
* Sending message to clients
*/
sendMessage(String from, String message) {
String jdata = buildMessage(from, message);
// search users that the message is intended
RegExp usersReg = new RegExp(r"@([\w|\d]+)");
Iterable users = usersReg.allMatches(message);
// if users found - send message only them
if (users.isNotEmpty) {
users.forEach((Match match) {
String user = match.group(0).replaceFirst('@', '');
if (connections.containsKey(user)) {
send(user, jdata);
}
});
send(from, jdata);
} else {
connections.forEach((username, conn) {
conn.add(jdata);
});
}
}
When the user closes the connection, we must remove it from the list of active connections. The function
closeConnection(String connectionName)
takes the name of the connection that was closed and removes it from the connection list:
/**
* Close user connections
*/
closeConnection(String connectionName) {
if (connections.containsKey(connectionName)) {
connections.remove(connectionName);
}
}
Add features to the connection listener
To summarize everything that we now have. The function
createWs
is listening to the user's connection. send
- sends a message to the specified user. sendWelcome
- sends a greeting message to a new user. notifyAbout
- notifies the chat participants (except the initiator) of any actions of the initiator (connection / disconnection). sendMessage
- sends a message to all or only specified users. Let's now change the function
createWs
so that we can use all of this. The last time we settled on adding a connection to the list. After that, we need to notify all other chat participants about the new user, and send a greeting message to the new user.Then we will need to listen to the user's websocket connection to messages from him and send messages to the participants. We will also add a handler to close the websocket connection, in which we remove it from the list and notify all participants to disconnect.
createWs(WebSocket webSocket) {
String connectionName = 'user_$generalCount';
++generalCount;
connections.putIfAbsent(connectionName, () => webSocket);
// Уведомим всех о новом подключении
notifyAbout(connectionName, '$connectionName joined the chat');
// Отправим новому пользователю приветствие
sendWelcome(connectionName);
webSocket
.map((string) => JSON.decode(string))
.listen((json) {
sendMessage(connectionName, json['message']);
}).onDone(() {
closeConnection(connectionName);
notifyAbout(connectionName, '$connectionName logs out chat');
});
}
That's all, a simple server is ready. Now let's move on to the client side.
Client
Here I will not talk about the layout of the client part and the display of messages . In this part, we will only talk about how we open a websocket connection to the server, send and receive messages.
Client Application Entry Point
The client application entry point is in the file
web/dart/index.dart
. Let's look at its contents:
library simplechat.client;
import 'dart:html';
import 'dart:convert';
import 'package:simplechat.common/common.dart';
part './views/message_view.dart';
part './controllers/web_socket_controller.dart';
main() {
WebSocketController wsc = new WebSocketController('ws://$ADDRESS:$PORT', '#messages', '#userText .text', '#online');
}
In the first line we declare a library. Then we connect the necessary files and parts of the libraries. The file
./views/message_view.dart
contains the definition of the class MessageView
that deals with the display of messages. We will not consider it (the code can be viewed on github ). The file ./controllers/web_socket_controller.dart
contains a class definition WebSocketController
, which we will dwell on in more detail. A function
main()
instantiates an instance of this controller.WebSocketController - class constructor and connection creation
Let's take a look at the properties and constructor of the class
WebSocketController
:
class WebSocketController {
WebSocket ws;
HtmlElement output;
TextAreaElement userInput;
DivElement online;
WebSocketController(String connectTo, String outputSelector, String inputSelector, String onlineSelector) {
output = querySelector(outputSelector);
userInput = querySelector(inputSelector);
online = querySelector(onlineSelector);
ws = new WebSocket(connectTo);
ws.onOpen.listen((e){
showMessage('Сonnection is established', SYSTEM_CLIENT);
bindSending();
});
ws.onClose.listen((e) {
showMessage('Connection closed', SYSTEM_CLIENT);
});
ws.onMessage.listen((MessageEvent e) {
processMessage(e.data);
});
ws.onError.listen((e) {
showMessage('Connection error', SYSTEM_CLIENT);
});
}
// ...
}
The code shows that it
WebSocketController
has the following properties:WebSocket ws
- here we store our websocket connection;HtmlElement output
- an element in which we will display messages;TextAreaElement userInput
- The text area into which the user enters messages;DivElement online
- an element in which the number of active users is displayed.
The constructor of the class accepts the address where you can open the websocket connection, selectors for elements
output
, userInput
and online
. At the very beginning, he finds elements in a tree. Then websocket connection to the server is created using the constructor WebSocket
:ws = new WebSocket(connectTo);
Then we assign event handlers to our connection.
The event
onOpen
fires when the connection is successfully established. Its handler displays a message that the connection is established and puts the listener of keystrokes on the message entry element so that when you click on the Enter
message will be sent. Here is the function code bindSending()
:
bindSending() {
userInput.onKeyUp.listen((KeyboardEvent key) {
if (key.keyCode == 13) {
key.stopPropagation();
sendMessage(userInput.value);
userInput.value = '';
}
});
}
In the body of the event handler,
keyUp
you can notice the call to the function sendMessage(String message)
that is engaged in sending the message. Sending a message over a websocket connection is done using the send () method of the WebSocket class . Here is the code for this function:
sendMessage(String message) {
Map data = {
'message': message
};
String jdata = JSON.encode(data);
ws.send(jdata);
}
The event
onClose
fires when the connection is closed. The handler for this event simply displays a message stating that the connection has been dropped. The event is
onMessage
triggered when a message is received from the server. The listener is passed a MessageEvent object . The handler of this event passes the data received from the server to a function processMessage
that simply displays the message. Here is her code:
processMessage(String message) {
var data = JSON.decode(message);
showOnline(data['online']);
showMessage(data['message'], data['from']);
}
I will not cite the function code
showOnline
and showMessage
, since nothing particularly interesting happens in them. But if you are interested in their contents, then you can always find the full controller code on github . That's all. This is all the main functionality of the client part.
You can see the working application here: http://simplechat.rudart.in .
If I made any mistakes and inaccuracies, then let me know, and I will try to fix everything quickly.