注册

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

前言


上一篇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 直接的通讯是正常的。




0 个评论

要回复文章请先登录注册