這是我參與8月更文挑戰的第19天,活動詳情檢視:8月更文挑戰
前言
上一篇Flutter 入門與實戰(五十五):和 Provider 一起玩 WebSocket我們講了使用 socket_client_io 和 StreamProvider
實現 WebSocket
通訊。本篇延續上一篇,來講一下如何實現與其他使用者進行即時聊天。
Socket 訊息推送
在 與服務端Socket
通訊中,呼叫 socket.emit
方法時預設傳送訊息都是給當前連線的 socket
的,如果要實現傳送訊息給其他使用者,服務端需要做一下改造。具體的做法如下:
- 在建立連線後,客戶多傳送訊息將使用者唯一識別符號(例如使用者名稱或
userId
)與連線的socket
物件進行繫結。 - 當其他使用者傳送訊息給該使用者時,找到該使用者繫結的
socket
物件,再通過該socket
的emit
方法傳送訊息就可以搞定了。
因此客戶端需要傳送一個註冊訊息到服務端以便與使用者繫結,同時還應該有一個登出訊息,以解除繫結(可選的,也可以通過斷開連線來自動解除繫結)。整個聊天過程的時序圖如下:
服務端程式碼已經好了,採用了一個簡單的物件來儲存使用者相關的未傳送訊息和 socket
物件。可以到後端程式碼倉庫拉取最新程式碼,
訊息格式約定
Socket
可以傳送字串或Json
物件,這裡我們約定訊息聊天為 Json
物件,欄位如下:
fromUserId
:訊息來源使用者id
;toUserId
:接收訊息使用者id
;contentType
:訊息型別,方便傳送文字、圖片、語音、視訊等訊息。目前只做了文字訊息,其他訊息其實可以在 content 中傳對應的資源 id 後由App 自己處理就好了。content
:訊息內容。
StreamSocket 改造
上一篇的 StreamSocket
改造我們只能傳送字串,為了擴大適用範圍,將該類改造成泛型。這裡需要注意,Socket
的 emit
的資料會呼叫物件的 toJson
將物件轉為 Json
物件傳送,因此泛型的類需要實現 Map<String dynamic> toJson
方法。同時增加了如下屬性和方法:
- recvEvent:接收事件的名稱
- regsiter:註冊方法,將使用者 id傳送到服務端與 socket 繫結,可以理解為上線通知;
- unregister:登出方法,將使用者 id 傳送到服務端與 socket解綁,可以理解為下線通知。
class StreamSocket<T> {
final _socketResponse = StreamController<T>();
Stream<T> get getResponse => _socketResponse.stream;
final String host;
final int port;
late final Socket _socket;
final String recvEvent;
StreamSocket(
{required this.host, required this.port, required this.recvEvent}) {
_socket = SocketIO.io('ws://$host:$port', <String, dynamic>{
'transports': ['websocket'],
'autoConnect': true,
'forceNew': true
});
}
void connectAndListen() {
_socket.onConnect((_) {
debugPrint('connected');
});
_socket.onConnectTimeout((data) => debugPrint('timeout'));
_socket.onConnectError((error) => debugPrint(error.toString()));
_socket.onError((error) => debugPrint(error.toString()));
_socket.on(recvEvent, (data) {
_socketResponse.sink.add(data);
});
_socket.onDisconnect((_) => debugPrint('disconnect'));
}
void regsiter(String userId) {
_socket.emit('register', userId);
}
void unregsiter(String userId) {
_socket.emit('unregister', userId);
}
void sendMessage(String event, T message) {
_socket.emit(event, message);
}
void close() {
_socketResponse.close();
_socket.disconnect().close();
}
}
複製程式碼
聊天頁面
新建一個 chat_with_user.dart
檔案,實現聊天相關的程式碼,其中ChatWithUserPage
為 StatefulWidget
,以便在State
的生命週期管理 Socket
的連線,註冊和登出等操作。目前我們寫死了 App 端的使用者是 user1
,傳送訊息給 user2
。
class _ChatWithUserPageState extends State<ChatWithUserPage> {
late final StreamSocket<Map<String, dynamic>> streamSocket;
@override
void initState() {
super.initState();
streamSocket =
StreamSocket(host: '127.0.0.1', port: 3001, recvEvent: 'chat');
streamSocket.connectAndListen();
streamSocket.regsiter('user1');
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('即時聊天'),
),
body: Stack(
alignment: Alignment.bottomCenter,
children: [
StreamProvider<Map<String, dynamic>?>(
create: (context) => streamSocket.getResponse,
initialData: null,
child: StreamDemo(),
),
ChangeNotifierProvider<MessageModel>(
child: MessageReplyBar(messageSendHandler: (message) {
Map<String, String> json = {
'fromUserId': 'user1',
'toUserId': 'user2',
'contentType': 'text',
'content': message
};
streamSocket.sendMessage('chat', json);
}),
create: (context) => MessageModel(),
),
],
),
);
}
@override
void dispose() {
streamSocket.unregsiter('user1');
streamSocket.close();
super.dispose();
}
}
複製程式碼
其他的和上一篇基本類似,只是訊息物件由 String
換成了 Map<String, dynamic>
。
除錯
訊息的對話介面本篇先不涉及,下一篇我們再來介紹。現在來看一下如何進行除錯。目前 PostMan 的8.x 版本已經支援 WebSocket
除錯了,我們拿PostMan 和手機模擬器進行聯調。Postman 的 WebSocket
除錯介面如下:
使用起來比較簡單,這裡我們已經完成了如下操作:
- 註冊:使用 user2註冊
- 設定傳送訊息為 json,訊息事件(event)為 chat,以便和 app、服務端 保持一致。
現在來看看除錯效果怎麼樣(PostMan 調起來有點手忙腳亂?)?
可以看到模擬器和 PostMan 直接的通訊是正常的。
總結
本篇介紹了通過服務端配合完成兩個不同客戶端的即時通訊的思路,基本介面的實現和Postman
的Socket
除錯。可以看到,有了 StreamProvider
後,我們的業務程式碼和 Socket
的實現是隔離開的,就如同我們之前說的那樣,Provider
的重要特性之一就是降低介面和業務程式碼之間的耦合。有了本篇的基礎,把聊天的介面完成就相對輕鬆了,下一篇我們來介紹聊天介面的搭建。
我是島上碼農,微信公眾號同名,這是Flutter 入門與實戰的專欄文章,對應原始碼請看這裡:Flutter 入門與實戰專欄原始碼。
??:覺得有收穫請點個贊鼓勵一下!
?:收藏文章,方便回看哦!
?:評論交流,互相進步!