Flutter 入门与实战(五十六):让模拟器和和邮递员(Postman)聊聊天

1,981 阅读4分钟

这是我参与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 入门与实战专栏源码

👍🏻:觉得有收获请点个赞鼓励一下!

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

💬:评论交流,互相进步!