
Всем привет, на связи PurplePlane! В этой статье мы хотели бы рассмотреть реализацию простого чата на языке Dart, используя протокол websocket.
Для чего еще используются сокеты? Вебсокеты могут использоваться в клиент-серверных приложениях, где необходимо создать двухстороннюю связь, чтобы серверная сторона могла инициировать процессы в клиентском приложении. Например, отображать уведомление в клиентском приложении при возникновении событии на сервере, или в случае, когда необходимо в режиме реального времени обновлять данные в клиентском приложении. Это можно использовать, чтобы отображать на карте местоположение и статус водителя. В качестве еще одного применения вебсокета - можно привести настройку из одного клиентского приложения (используемого родителем) через связь по сокету с сервером другого приложения (используемого ребенком). В этих приложениях, реализованных нашей компанией, успешность бизнес-процесса выстраивалась на последовательной корректной работе двух сокетов (клиент (родительское приложение) - сервер и клиент (используемый ребенком) - сервер).
Рассмотрим пример использования вебсокета при реализации простого чата с помощью библиотеки 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); }
В итоге мы получили класс, который позволит реализовать работу простого чата с помощью вебсокета.
