Здравствуйте!
В этой статье я хочу описать создание простого websocket-чата на Dart с целью показать, как работать с вебсокетами в Dart. Код приложения доступен на github, а пример его работы можно посмотреть здесь: http://simplechat.rudart.in.
Приложение будет состоять из двух частей: сервера и клиента. Серверную часть мы разберем очень подробно, а из клиентской рассмотрим только то, что отвечает за работу с соединением.
Требования к приложению очень простые — отправка сообщений от пользователя всем или только выбранным участникам чата.
Все настройки приложения и константы будут храниться в файле
Сам файл мы будем подключать как пакет, т.к. если будем использовать относительные пути, то при сборке приложения (
Для того, чтобы подключить пакет, находящийся где-то на нашей машине, мы будем использовать pub path dependency. Для этого мы просто допишем в секцию
Все содержимое файла
Файлы сервера располагаются в папке
Давайте поговорим о том, как вообще будет работать наш сервер. Первое, что мы будем делать с сервером — это запускать его. Во время запуска он начнет слушать порт
Когда новый пользователь отправит запрос на этот порт, то сервер откроет для него websocket-соединение, сгенерирует имя и сохранит имя и соединение в хэш с открытыми соединениями. После этого клиент сможет отправлять сообщения по этому соединению. Сервер сможет передавать эти сообщения другим пользователям, а также отправлять уведомления о подключении и отключении клиентов.
Если пользователь закроет соединение, то сервер удалит его из хэша с активными соединениями.
В самом начале файла
В функции
Ниже приведен базовый код сервера, который будет просто привязываться на нужный порт.
В конце функции
Для настройки роутера создадим функцию
В функции
Сервер отправляет сообщения в виде объекта Map (а точнее его представления в json) со следующими ключами:
Вот функция, которая строит такое сообщение:
Для того, чтобы отправить сообщение клиенту, нужно воспользоваться методом add() класса WebSocket. Ниже приведена функция, которая будет отправлять сообщения пользователю:
Наш сервер может отправлять уведомления всем активным клиентам о подключении или отключении пользователя. Давайте рассмотрим функцию для этого. Функция
Также, после присоединения нового пользователя мы поприветствуем его:
Давайте теперь посмотрим функцию, которая обрабатывает входящие сообщения от пользователя и отправляет их всем (или только указанным) участникам чата. Функция
Когда пользователь закроет соединение, то мы должны удалить его из списка активных соединений. Функция
Подытожим все, что мы сейчас имеем. Функция
Давайте теперь изменим функцию
Затем нам нужно будет прослушивать websocket-соединение пользователя на сообщения от него и отправлять сообщения участникам. Также мы добавим обработчик на закрытие websocket-соединения, в котором удалим его из списка и уведомим об отключении всех участников.
Вот и все, простой сервер готов. Теперь перейдем к клиентской части.
Здесь я не стану рассказывать о верстке клиентской части и об отображении сообщений. В этой части мы поговорим только о том, как мы открываем websocket-соединение с сервером, посылаем и принимаем сообщения.
Точка входа в клиентское приложение находится в файле
В первой строке мы объявляем библиотеку. Затем подключаем необходимые файлы и части библиотек. В файле
В функции
Давайте взглянем на свойства и конструктор класса
Из кода видно, что
Конструктор класса принимает адрес, по которому можно открыть websocket-соединение, селекторы для элементов
Затем мы назначаем обработчики событий для нашего соединения.
Событие
В теле обработчика события
Событие
Событие
Я не стану приводить код функций
Вот и все. Это весь основной функционал клиентской части.
Вы можете посмотреть работающее приложение здесь: http://simplechat.rudart.in.
Если я допустил какие-нибудь ошибки и неточности, то сообщайте, а я постараюсь все быстро поправить.
В этой статье я хочу описать создание простого websocket-чата на Dart с целью показать, как работать с вебсокетами в Dart. Код приложения доступен на github, а пример его работы можно посмотреть здесь: http://simplechat.rudart.in.
Приложение будет состоять из двух частей: сервера и клиента. Серверную часть мы разберем очень подробно, а из клиентской рассмотрим только то, что отвечает за работу с соединением.
Требования к приложению очень простые — отправка сообщений от пользователя всем или только выбранным участникам чата.
Настройки приложения
Все настройки приложения и константы будут храниться в файле
common/lib/common.dart
. В этом файле находится определение библиотеки simplechat.common
.library simplechat.common;
const String ADDRESS = 'simplechat.rudart.in';
const int PORT = 9224;
const String SYSTEM_CLIENT = 'Simple Chat';
Сам файл мы будем подключать как пакет, т.к. если будем использовать относительные пути, то при сборке приложения (
pub build
) мы можем получить ошибку от pub
: Exception: Cannot read {file} because it is outside of the build environment.Для того, чтобы подключить пакет, находящийся где-то на нашей машине, мы будем использовать pub path dependency. Для этого мы просто допишем в секцию
dependencies
файла pubspec.yaml
определение нашего пакета:dependencies:
simplechat.common:
path: ./common
Все содержимое файла
pubspec.yaml
я приводить не буду (но его можно посмотреть на github). Также нужно будет добавить файл pubspec.yaml
в директорию common
в котором просто укажем имя нашего пакета:name: simplechat.common
Сервер
Файлы сервера располагаются в папке
bin
. В файле main.dart
находится точка входа в сервер, а в файле server.dart
— класс нашего сервера. Начнем с рассмотрения содержимого файла main.dart
.Общая схема работы сервера
Давайте поговорим о том, как вообще будет работать наш сервер. Первое, что мы будем делать с сервером — это запускать его. Во время запуска он начнет слушать порт
9224
. Когда новый пользователь отправит запрос на этот порт, то сервер откроет для него websocket-соединение, сгенерирует имя и сохранит имя и соединение в хэш с открытыми соединениями. После этого клиент сможет отправлять сообщения по этому соединению. Сервер сможет передавать эти сообщения другим пользователям, а также отправлять уведомления о подключении и отключении клиентов.
Если пользователь закроет соединение, то сервер удалит его из хэша с активными соединениями.
Точка входа в сервер
В самом начале файла
bin/main.dart
мы определим, что это библиотека simplechat.bin
. Для работы сервера нам понадобится подключить библиотеки dart:async
, dart:convert
, dart:io
, пакет route
(его ставим через pub
) и файл с настройками приложения. Также в bin/main.dart
мы подключаем файл bin/server.dart
, который содержит основной код нашего сервера (рассмотрим его чуть позже). В функции
main()
мы создаем экземпляр сервера и запускаем его.
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();
}
Базовый класс сервера, прослушка порта
Ниже приведен базовый код сервера, который будет просто привязываться на нужный порт.
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();
}
}
В конце функции
connectServer()
вызывается функция для настройки роутера — bindRouter()
, которую мы рассмотрим ниже.Настройка роутера и создание websocket-соединения
Для настройки роутера создадим функцию
bindRouter()
. Входящий поток на /
мы будем изменять с помощью WebSocketTransformer
и прослушивать в функции 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);
}
В функции
createWs()
мы генерируем имя для соединения по схеме user_{counter}
и сохраняем это соединение в connections
.Структура сообщения от сервера и функция создания сообщения
Сервер отправляет сообщения в виде объекта Map (а точнее его представления в json) со следующими ключами:
- from — от кого сообщение;
- message — текст сообщения;
- online — количество пользователей онлайн.
Вот функция, которая строит такое сообщение:
/**
* Build message
*/
String buildMessage(String from, String message) {
Map<String, String> data = {
'from': from,
'message': message,
'online': connections.length
};
return JSON.encode(data);
}
Отправка сообщений с сервера
Для того, чтобы отправить сообщение клиенту, нужно воспользоваться методом add() класса WebSocket. Ниже приведена функция, которая будет отправлять сообщения пользователю:
/**
* Sending message
*/
void send(String to, String message) {
connections[to].add(message);
}
Наш сервер может отправлять уведомления всем активным клиентам о подключении или отключении пользователя. Давайте рассмотрим функцию для этого. Функция
notifyAbout(String connectionName, String message)
принимает имя соединения и сообщение (о подключении или отключении). Эта функция уведомляет всех активных клиентов кроме того, о ком делается это уведомление. Т.е. если к нам присоединился пользователь user_3, то уведомление получат все пользователи, кроме него. Для того, чтобы отфильтровать клиентов по определенному условию (в нашем случае нам нужно получить имена всех клиентов, которые не совпадают с текущим) мы воспользуемся методом where() абстрактного класса 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);
});
}
Также, после присоединения нового пользователя мы поприветствуем его:
/**
* Sending welcome message to new client
*/
void sendWelcome(String connectionName) {
String jdata = buildMessage(SYSTEM_CLIENT, 'Welcome to chat!');
send(connectionName, jdata);
}
Давайте теперь посмотрим функцию, которая обрабатывает входящие сообщения от пользователя и отправляет их всем (или только указанным) участникам чата. Функция
sendMessage(String from, String message)
принимает имя отправителя и его сообщение. Если теле сообщения (message
) указать имена получателей по маске @{user_name}
, то сообщение будет доставлено только им. Давайте посмотрим на код функции 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);
});
}
}
Когда пользователь закроет соединение, то мы должны удалить его из списка активных соединений. Функция
closeConnection(String connectionName)
принимает имя соединения, которое было закрыто и удаляет его из списка соединений:
/**
* Close user connections
*/
closeConnection(String connectionName) {
if (connections.containsKey(connectionName)) {
connections.remove(connectionName);
}
}
Добавляем возможности к слушателю соединения
Подытожим все, что мы сейчас имеем. Функция
createWs
занимается прослушкой соединения пользователя. send
— отправляет сообщение указанному пользователю. sendWelcome
— отправляет сообщение с приветствием новому пользователю. notifyAbout
— уведомляет участников чата (кроме инициатора) о каких-либо действиях инициатора (подключение/отключение). sendMessage
— отправляет сообщение всем или только указанным пользователям.Давайте теперь изменим функцию
createWs
так, чтобы мы могли использовать все это. В предыдущий раз мы остановились на том, что добавили соединение в список. После этого нам необходимо уведомить всех остальных участников чата о новом пользователе, а новому пользователю отправить сообщение с приветствием.Затем нам нужно будет прослушивать websocket-соединение пользователя на сообщения от него и отправлять сообщения участникам. Также мы добавим обработчик на закрытие websocket-соединения, в котором удалим его из списка и уведомим об отключении всех участников.
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');
});
}
Вот и все, простой сервер готов. Теперь перейдем к клиентской части.
Клиент
Здесь я не стану рассказывать о верстке клиентской части и об отображении сообщений. В этой части мы поговорим только о том, как мы открываем websocket-соединение с сервером, посылаем и принимаем сообщения.
Точка входа в клиентское приложение
Точка входа в клиентское приложение находится в файле
web/dart/index.dart
. Давайте посмотрим на его содержимое:
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');
}
В первой строке мы объявляем библиотеку. Затем подключаем необходимые файлы и части библиотек. В файле
./views/message_view.dart
находится определение класса MessageView
, который занимается отображением сообщений. Его мы рассматривать не будем (код можно посмотреть на github). В файле ./controllers/web_socket_controller.dart
находится определение класса WebSocketController
, на котором мы остановимся более подробно.В функции
main()
посто создается экземпляр этого контроллера.WebSocketController — конструктор класса и создание соединения
Давайте взглянем на свойства и конструктор класса
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);
});
}
// ...
}
Из кода видно, что
WebSocketController
имеет следующие свойства:WebSocket ws
— здесь мы храним наше websocket-соединение;HtmlElement output
— элемент, в который будем выводить сообщения;TextAreaElement userInput
— текстовая область, в которую пользователь вводит сообщения;DivElement online
— элемент, в который выводится количество активных пользователей.
Конструктор класса принимает адрес, по которому можно открыть websocket-соединение, селекторы для элементов
output
, userInput
и online
. В самом начале он находит элементы в дереве. Затем создается websocket-соединение с сервером с помощью конструктора WebSocket
:ws = new WebSocket(connectTo);
Затем мы назначаем обработчики событий для нашего соединения.
Событие
onOpen
срабатывает тогда, когда соединение успешно установлено. Его обработчик показывает сообщение о том, что соединение установлено и ставит слушателя событий нажатия клавиш на элементе ввода сообщений так, чтобы при нажатии на Enter
происходила отправка сообщения. Вот код функции bindSending()
:
bindSending() {
userInput.onKeyUp.listen((KeyboardEvent key) {
if (key.keyCode == 13) {
key.stopPropagation();
sendMessage(userInput.value);
userInput.value = '';
}
});
}
В теле обработчика события
keyUp
можно заметить вызов функции sendMessage(String message)
, которая занимается отправкой сообщения. Отправка сообщения по websocket-соединению просходит с помощью метода send() класса WebSocket. Вот код этой функции:
sendMessage(String message) {
Map data = {
'message': message
};
String jdata = JSON.encode(data);
ws.send(jdata);
}
Событие
onClose
срабатывает тогда, когда соединение закрывается. Обработчик этого события просто отображает сообщение о том, что соединение сброшено.Событие
onMessage
срабатывает при получении сообщения от сервера. Слушателю передается объект MessageEvent. Обработчик этого события передает данные, поступившие от сервера в функцию processMessage
, которая просто отображает сообщение. Вот ее код:
processMessage(String message) {
var data = JSON.decode(message);
showOnline(data['online']);
showMessage(data['message'], data['from']);
}
Я не стану приводить код функций
showOnline
и showMessage
, т.к. в них ничего особо интересного не происходит. Но если вам интересно их содержание, то вы всегда можете найти полный код контроллера на github.Вот и все. Это весь основной функционал клиентской части.
Вы можете посмотреть работающее приложение здесь: http://simplechat.rudart.in.
Если я допустил какие-нибудь ошибки и неточности, то сообщайте, а я постараюсь все быстро поправить.