📜 ⬆️ ⬇️

Simple websocket chat on Dart

Hello!

In this article, I want to describe the creation of a simple websocket chat on Dart in order to show how to work with websockets in Dart. Application code is available on github , and an example of its work can be found here: http://simplechat.rudart.in .

The application will consist of two parts: the server and the client. We will analyze the server part in great detail, and from the client we will consider only what is responsible for working with the connection.
')
The requirements for the application are very simple - sending messages from the user to all or just to selected chat participants.

Application settings


All application settings and constants will be stored in the file common/lib/common.dart . This file contains the definition of the library 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, since if we use relative paths, then when building the application ( pub build ) we can get an error from the pub : Exception: Can not read {build} .

In order to connect the package, located somewhere on our machine, we will use the pub path dependency . To do this, we simply pubspec.yaml definition of our package to the dependencies section of the pubspec.yaml file:

 dependencies: simplechat.common: path: ./common 

I will not give all the contents of the pubspec.yaml file (but you can look at it on github ). You will also need to add the pubspec.yaml file to the common directory in which you simply specify the name of our package:

 name: simplechat.common 

Server


Server files are located in the bin folder. The main.dart file main.dart the entry point to the server, and the server.dart file server.dart class of our server. Let's start by looking at the contents of the main.dart file.

General scheme of the server


Let's talk about how our server will work. The first thing we will do with the server is to launch it. During startup, it will start listening to port 9224 .

When a new user sends a request to this port, the server opens a websocket connection for it, generates a name and saves the name and connection in a hash with open connections. After that, the client will be able to send messages via this connection. The server will be able to send these messages to other users, as well as send notifications about the connection and disconnection of 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 will determine that this is the library simplechat.bin . For the server to work, we will need to connect the dart:async libraries dart:async , dart:convert , dart:io , the route package (set via pub ) and the file with the application settings. Also in bin/main.dart we include the file bin/server.dart , which contains the main code of our server (consider it a bit later).

In the main() function, we create an instance of the server and start 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 listening


Below is the basic server code that will simply bind to the correct 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<String, WebSocket> connections = new Map<String, WebSocket>(); 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 connectServer() function, the function for setting up the router is bindRouter() , which we will discuss below.

Setting up a router and creating a websocket connection


To configure the router, create the bindRouter() function. The incoming stream to / we will change using WebSocketTransformer and listen in the createWs() function.

 /** * 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 createWs() function, we generate a name for the connection using the user_{counter} scheme and save this connection as connections .

Message structure from the server and message creation function


The server sends messages in the form of a Map object (or rather its representations in json) with the following keys:


Here is the function that builds such a message:

 /** * Build message */ String buildMessage(String from, String message) { Map<String, String> data = { 'from': from, 'message': message, 'online': connections.length }; return JSON.encode(data); } 

Sending messages from the server


In order to send a message to the client, you need to use the add () method of the WebSocket class. Below is a 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 name of the connection and the message (about connecting or disconnecting). This feature notifies all active clients in addition to whom this notification is made. Those. if user_3 has joined us, then all users will receive a notification except him. 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 abstract class Iterable .

 /** * 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 greet 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 take a look at the function that processes incoming messages from the user and sends them to all (or just the specified) chat participants. The function sendMessage(String from, String message) takes the name of the sender and his message. If the message body ( message ) specifies the names of the recipients by the mask @{user_name} , then the message will be delivered only to them. Let's look at the sendMessage function 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<Match> 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 closeConnection(String connectionName) function takes the name of the connection that was closed and removes it from the list of connections:

 /** * Close user connections */ closeConnection(String connectionName) { if (connections.containsKey(connectionName)) { connections.remove(connectionName); } } 

Adding Opportunities to the Connection Listener


Let's summarize everything that we have now. The createWs function createWs on the user's connection. send - sends a message to the specified user. sendWelcome - sends a message with a greeting to a new user. notifyAbout - notifies chat participants (except the initiator) about any actions of the initiator (enable / disable). sendMessage - sends a message to all or only specified users.

Let's now change the createWs function so that we can use it all. Last time, we stopped at adding a connection to the list. After that, we need to notify all other members of the chat about the new user, and send the message to the new user with a greeting.

Then we will need to listen to the user's websocket connection to messages from him and send messages to participants. We will also add a handler to close the websocket connection, in which we will 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 it, a simple server is ready. We now turn to the client side.

Customer


Here I will not talk about the layout of the client part and the display of messages . In this part we will talk only about how we open a websocket-connection to the server, send and receive messages.

Client Application Entry Point


The entry point to the client application is in the web/dart/index.dart file. 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 the library. Then we include the necessary files and parts of libraries. The file ./views/message_view.dart contains the definition of the class MessageView , which 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 the definition of the class WebSocketController , which we will focus on in more detail.

The main() function permanently creates an instance of this controller.

WebSocketController - class constructor and connection creation


Let's take a look at the properties and constructor of the WebSocketController class:

 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); }); } // ... } 

From the code it is clear that WebSocketController has the following properties:


The class constructor takes an address at which you can open a websocket connection, selectors for the output , userInput and online elements. At the very beginning he finds the elements in the tree. Then a websocket connection to the server is created using the WebSocket constructor:

 ws = new WebSocket(connectTo); 

Then we assign event handlers for our connection.

The onOpen event onOpen triggered when the connection is successfully established. Its handler displays a message indicating that the connection has been established and sets up a listener of keystroke events on the message input element so that when you press Enter , the message is sent. Here is the bindSending() function code:

 bindSending() { userInput.onKeyUp.listen((KeyboardEvent key) { if (key.keyCode == 13) { key.stopPropagation(); sendMessage(userInput.value); userInput.value = ''; } }); } 

In the body of the keyUp event keyUp you can notice the call to the function sendMessage(String message) , which deals with sending a message. Sending a message via a websocket connection is performed 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 onClose event onClose triggered when the connection is closed. The handler for this event simply displays a message indicating that the connection has been dropped.

The onMessage event fires when a message is received from the server. The listener is passed the MessageEvent object. The event handler for this event passes the data received from the server to the processMessage function, which simply displays the message. Here is its code:

 processMessage(String message) { var data = JSON.decode(message); showOnline(data['online']); showMessage(data['message'], data['from']); } 

I will not give the code for showOnline and showMessage , since nothing particularly interesting happens in them. But if you are interested in their content, 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 report, and I will try to fix everything quickly.

Source: https://habr.com/ru/post/230351/


All Articles