Flutter 入門與實戰(五十六):讓模擬器和和郵遞員(Postman)聊聊天

島上碼農發表於2021-08-19

這是我參與8月更文挑戰的第19天,活動詳情檢視:8月更文挑戰

前言

上一篇Flutter 入門與實戰(五十五):和 Provider 一起玩 WebSocket我們講了使用 socket_client_io 和 StreamProvider實現 WebSocket 通訊。本篇延續上一篇,來講一下如何實現與其他使用者進行即時聊天。

Socket 訊息推送

在 與服務端Socket 通訊中,呼叫 socket.emit 方法時預設傳送訊息都是給當前連線的 socket的,如果要實現傳送訊息給其他使用者,服務端需要做一下改造。具體的做法如下:

  • 在建立連線後,客戶多傳送訊息將使用者唯一識別符號(例如使用者名稱或 userId)與連線的 socket 物件進行繫結。
  • 當其他使用者傳送訊息給該使用者時,找到該使用者繫結的 socket 物件,再通過該 socketemit 方法傳送訊息就可以搞定了。

因此客戶端需要傳送一個註冊訊息到服務端以便與使用者繫結,同時還應該有一個登出訊息,以解除繫結(可選的,也可以通過斷開連線來自動解除繫結)。整個聊天過程的時序圖如下:

時序圖.png

服務端程式碼已經好了,採用了一個簡單的物件來儲存使用者相關的未傳送訊息和 socket 物件。可以到後端程式碼倉庫拉取最新程式碼,

訊息格式約定

Socket 可以傳送字串或Json 物件,這裡我們約定訊息聊天為 Json 物件,欄位如下:

  • fromUserId:訊息來源使用者 id
  • toUserId:接收訊息使用者 id
  • contentType:訊息型別,方便傳送文字、圖片、語音、視訊等訊息。目前只做了文字訊息,其他訊息其實可以在 content 中傳對應的資源 id 後由App 自己處理就好了。
  • content:訊息內容。

StreamSocket 改造

上一篇的 StreamSocket 改造我們只能傳送字串,為了擴大適用範圍,將該類改造成泛型。這裡需要注意,Socketemit 的資料會呼叫物件的 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 檔案,實現聊天相關的程式碼,其中ChatWithUserPageStatefulWidget,以便在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 除錯介面如下: image.png 使用起來比較簡單,這裡我們已經完成了如下操作:

  • 註冊:使用 user2註冊
  • 設定傳送訊息為 json,訊息事件(event)為 chat,以便和 app、服務端 保持一致。

現在來看看除錯效果怎麼樣(PostMan 調起來有點手忙腳亂?)?

螢幕錄製2021-08-19 下午9.35.45.gif

可以看到模擬器和 PostMan 直接的通訊是正常的。

總結

本篇介紹了通過服務端配合完成兩個不同客戶端的即時通訊的思路,基本介面的實現和PostmanSocket除錯。可以看到,有了 StreamProvider 後,我們的業務程式碼和 Socket 的實現是隔離開的,就如同我們之前說的那樣,Provider 的重要特性之一就是降低介面和業務程式碼之間的耦合。有了本篇的基礎,把聊天的介面完成就相對輕鬆了,下一篇我們來介紹聊天介面的搭建。


我是島上碼農,微信公眾號同名,這是Flutter 入門與實戰的專欄文章,對應原始碼請看這裡:Flutter 入門與實戰專欄原始碼

??:覺得有收穫請點個贊鼓勵一下!

?:收藏文章,方便回看哦!

?:評論交流,互相進步!

相關文章