Читать статью на Habr
Всем привет, на связи 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);
}
В итоге мы получили класс, который позволит реализовать работу простого чата с помощью вебсокета.