Flutter 入门与实战:让模拟器和和邮递员(Postman)聊聊天
前言
上一篇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 直接的通讯是正常的。