В этой статье мы хотели бы рассмотреть реализацию простого чата на языке Dart, используя протокол websocket.
Читать на Habr
Читать на Habr
Для чего нужны вебсокеты?
Вебсокеты могут использоваться в клиент-серверных приложениях, где необходимо создать двухстороннюю связь, чтобы серверная сторона могла инициировать процессы в клиентском приложении. Например, отображать уведомление в клиентском приложении при возникновении событии на сервере, или в случае, когда необходимо в режиме реального времени обновлять данные в клиентском приложении. Это можно использовать, чтобы отображать на карте местоположение и статус водителя. В качестве еще одного применения вебсокета - можно привести настройку из одного клиентского приложения (используемого родителем) через связь по сокету с сервером другого приложения (используемого ребенком).
В этих приложениях, реализованных нашей компанией, успешность бизнес-процесса выстраивалась на последовательной корректной работе двух сокетов (клиент (родительское приложение) - сервер и клиент (используемый ребенком) - сервер).
В этих приложениях, реализованных нашей компанией, успешность бизнес-процесса выстраивалась на последовательной корректной работе двух сокетов (клиент (родительское приложение) - сервер и клиент (используемый ребенком) - сервер).
Пример использования вебсокета
Рассмотрим пример использования вебсокета при реализации простого чата с помощью библиотеки web_socket_channel.
Напишем класс для управления сокетом нашего чата. Он будет содержать объект сокета из библиотеки, подписку на события из этого сокета и комплитер для идентификации состояния процесса подключения. При старте приложения создадим объект этого класса.
Напишем класс для управления сокетом нашего чата. Он будет содержать объект сокета из библиотеки, подписку на события из этого сокета и комплитер для идентификации состояния процесса подключения. При старте приложения создадим объект этого класса.
class SocketApi {
SocketApi();
IOWebSocketChannel? _socket;
StreamSubscription? _socketSubscription;
Completer<bool> _connecting = Completer<bool>();
}Если приложение требует авторизации, то соединение по сокету устанавливаем только в авторизованном состояниии. Для этого делаем в классе геттер и сеттер id профиля пользователя и при инициализации соединения передаем в запросе токен пользователя.
int? _profileId;
Future<void> setProfile({required final int profileId}) async {
_profileId = profileId;
await _connect();
}
void removeProfile() {
_profileId = null;
_close();
}
Future<void> _connect() async {
await _close();
final prefs = await SharedPreferences.getInstance();
final token = prefs.getString('TOKEN');
assert(_profileId != null);
final wsUrl = Uri.parse('ws://my-base-url/ws/chat/$_profileId/?token=$token');
try {
_socket = IOWebSocketChannel.connect(
wsUrl,
pingInterval: const Duration(seconds: 5),
);
_socketSubscription = _socket!.stream.listen(
_onMessage,
onDone: _onDone,
onError: (Object err, StackTrace stackTrace) {
print('Listen err $err, $stackTrace');
},
);
_connecting.complete(true);
debugPrint('connected');
} catch (_) {
debugPrint('cant connected');
}
}Как видно из кода выше, мы создаем подписку на стрим и вешаем обработчики на все события и ошибки.
Напишем метод, который будет закрывать сокет при выходе пользователя из профиля.
Напишем метод, который будет закрывать сокет при выходе пользователя из профиля.
Future<void> _close() async {
_connecting = Completer<bool>();
await _socketSubscription?.cancel();
_socketSubscription = null;
try {
await _socket?.sink.close(status.normalClosure);
} catch (_) {
debugPrint('Socket already closed.');
}
if (_socket != null) {
print(['disconnected', _socket?.closeCode, _socket?.closeReason]);
}
_socket = null;
}Реализуем обработчики событий в сокете.
Future<void> _onDone() async {
await _close();
await Future<void>.delayed(const Duration(seconds: 1), () async {
await _connect();
});
}
void _onMessage(dynamic message) {
debugPrint('✅ RECEIVED: $message');
final msg = jsonDecode(message.toString()) as Map<String, dynamic>;
final chatJson = msg['chat'] as Map<String, dynamic>;
final event = msg['type'] as String;
if (event == 'write_message') {
// Реализуем обработчики различных типов событий чата
} else if (event == 'read_messages') {
// то же
}
}Реализуем метод для отправки сообщений в сокет:
Future<void> _send(
String cmd,
int chatId,
String? message, {
Map<String, dynamic>? data,
}) async {
if (_socket == null) {
await _connect();
}
final connectionResult = await _connecting.future;
if (connectionResult != true) {
return;
}
final d = <String, dynamic>{
'type': cmd,
'chat_id': chatId,
'message': message,
};
d.removeWhere((key, value) => value == null);
final json = jsonEncode(d);
debugPrint('? SEND: $d');
_socket!.sink.add(json);
}Поле "cmd" в этом методе отвечает за тип отправляемого сообщения.
Реализуем примеры методов для отправки событий различного типа в сокет.
Реализуем примеры методов для отправки событий различного типа в сокет.
Future<void> sendTextMessage({
required final String text,
required final int chatId,
}) async {
await _send('write_message', chatId, text);
}
Future<void> setMessagesRead({required final int chatId}) async {
await _send('read_message', chatId, null);
}В итоге мы получили класс, который позволит реализовать работу простого чата с помощью вебсокета.

Делаем простой навигатор с помощью Google Maps на Flutter своими руками