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.

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 dependenciesfile section pubspec.yaml:

dependencies:
  simplechat.common:
    path: ./common

pubspec.yamlI 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.yamlto a directory commonin which we simply indicate the name of our package:

name: simplechat.common

Server


Server files are located in a folder bin. The file main.dartcontains the entry point to the server, and the file contains the server.dartclass 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.dartwe 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.dartwe include a file bin/server.dartthat 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 WebSocketTransformerand 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 createWsis 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 createWsso 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.dartcontains the definition of the class MessageViewthat deals with the display of messages. We will not consider it (the code can be viewed on github ). The file ./controllers/web_socket_controller.dartcontains 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 WebSocketControllerhas 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, userInputand 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 onOpenfires 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 Entermessage 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, keyUpyou 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 onClosefires when the connection is closed. The handler for this event simply displays a message stating that the connection has been dropped.

The event is onMessagetriggered 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 processMessagethat 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 showOnlineand 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.

Also popular now: