国庆假期,整整七天,我使用Flutter终于做出了即时通信!!!?
前言:在这个假期,我完成了一个小Demo,Flutter 与 Springboot 进行websocket的通讯,为啥想要去做这个Demo呢,主要是在各大平台以及google搜索后发现,没有一个详细的例子来教大家进行一对一、一对多的通讯,大多数都是教你怎么连接,却没有教你怎么去进行下一步的功能实现,于是我利用了五天的假期,踩了无数的坑,终于是完成了它,所以,点个赞吧,不容易啊,兄弟们😭
源码在文章最后,直接运行就完事,服务端我都帮兄弟们架包打好了,运行一下就行,运行方法在文末简单叙述了😎
服务端分析:Springboot WebSocket 即时通讯
先上效果图(我自己搜索这样功能性的问题时,没有效果图基本上都是不想看的):
即时通讯最重要的功能是完成了(发送文字信息)
阅读本文的注意点:
1.需要一点WebSocket的原理知识
2.Flutter使用WebSocket的方式,本文使用 'dart:io' ,大家也可以使用插件
正文:
1.WebSocket的简单原理
很多同学在第一次碰到这个协议时会发现它与HTTP相似,于是就会问,我们已经有了 HTTP 协议,为什么还需要WebSocket?它有什么特殊的地方呢?
其实是因为 HTTP 协议有一个缺陷:通信只能由客户端发起。
而WebSocket首先是一个持久化的协议,它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,这个协议非常适合即时通讯或者消息的推送。
2.Flutter中怎么使用WebSocket
有两种方式:
1.Flutter自带的 'dart:io'
连接WebSocket服务器
Future<WebSocket> webSocketFuture = WebSocket.connect('ws://192.168.13.32:9090'); //connect中放服务端地址
存放WebSocket.connect返回的对象
static WebSocket _webSocket;
发送消息
_webSocket.add('发送消息内容');
监听接收消息,调用listen方法
void onData(dynamic content) {
print('收到消息:'+content);
}
_webSocket.listen(onData, onDone: () {
print('onDone');
}, onError: () {
print('onError');
}, cancelOnError: true);例子:
webSocketFuture.then((WebSocket ws) {
_webSocket = ws;
void onData(dynamic content) {
print('收到新的消息');
}
// 调用add方法发送消息
_webSocket.add('message');
// 监听接收消息,调用listen方法
_webSocket.listen(onData, onDone: () {
print('onDone');
}, onError: () {
print('onError');
}, cancelOnError: true);
});关闭WebSocket连接
_webSocket.close();
2.第三方插件库实现 WebSocket
基本使用步骤也都是:连接 WebSocket 服务器、发送消息、接收消息、关闭 WebSocket 连接。
- 在项目的 pubspec.yaml 里加入引用:
dependencies:
web_socket_channel: 官网最新版本
- 导入包:
import 'package:web_socket_channel/io.dart';
- 连接 WebSocket 服务器:
var channel = IOWebSocketChannel.connect("ws://192.168.13.32:9090");
通过IOWebSocketChannel我们便可以进行各种操作
- 发送消息:
channel.sink.add("connected!");
- 监听接收消息:
channel.stream.listen((message) { print('收到消息:' + message); });
- 关闭 WebSocket 连接:
channel.sink.close();
以上就是 Flutter 通过第三方插件库实现 WebSocket 通信功能的基本步骤。
3.联系人界面以及对话界面的UI实现
我的小部件都进行了封装,源码请看文章最后,分析部分只放重要代码
对话框ui处理
顶部使用appbar,包含一个返回按钮,用户信息以及状态,还有一个设置按钮(没有什么难点就不放代码了)
- 双方信息处理
这里有个ui处理难点,就是分析信息是谁发出的,是自己还是对方呢
这里我选择在每条信息Json格式的末尾加上一个判断符:messageType,”receiver“代表对方发的信息,”sender“代表是自己发的
ui处理:
ListView.builder( itemCount: UserMessage.messages.length, //总发送信息的条数 shrinkWrap: true, padding: const EdgeInsets.only(top: 10, bottom: 10), physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) { return Container( padding: const EdgeInsets.only( left: 14, right: 14, top: 10, bottom: 10), child: Align( alignment: (UserMessage.messages[index].messageType == "receiver" ? Alignment.topLeft : Alignment.topRight), child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), color: (UserMessage.messages[index].messageType == "receiver" ? Colors.grey.shade200 : Colors.blue[200]), ), padding: const EdgeInsets.all(16), child: Text( UserMessage.messages[index].messageContent, style: TextStyle(fontSize: 15), ))), ); },),
单个联系人的ui
一行为一个联系人模块,其内包含用户头像,用户姓名,即时通讯内容,以及上一次对话的时间,点击每行跳转到相对应的聊天框。
封装处理:
class ConversationList extends StatefulWidget { String name; //用户姓名 String messageText; //即时内容 String imageUrl; //用户头像 String time;//上一次对话时间 bool isMessageRead; //用于处理字体大小 ConversationList( {Key? key, required this.name, required this.messageText, required this.imageUrl, required this.time, required this.isMessageRead}) : super(key: key); @override _ConversationListState createState() => _ConversationListState();}
详细布局:
return GestureDetector( onTap: () { Navigator.push(context, MaterialPageRoute(builder: (context){ return ChatDetailPage(name:widget.name,userImageUrl:widget.imageUrl); })); }, child: Container( padding: const EdgeInsets.only(left: 16, right: 16, top: 10, bottom: 10), child: Row( children: <Widget>[ Expanded( child: Row( children: <Widget>[ CircleAvatar( backgroundImage: AssetImage(widget.imageUrl), maxRadius: 30, ), const SizedBox( width: 16, ), Expanded( child: Container( color: Colors.transparent, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Text( widget.name, style: const TextStyle(fontSize: 16), ), const SizedBox( height: 6, ), Text( widget.messageText, style: TextStyle( fontSize: 13, color: Colors.grey.shade600, fontWeight: widget.isMessageRead ? FontWeight.bold : FontWeight.normal), ), ], ), ), ), ], ), ), Text( widget.time, style: TextStyle( fontSize: 12, fontWeight: widget.isMessageRead ? FontWeight.bold : FontWeight.normal), ), ], ), ), );}
列表实现:
这里简单使用ListView.builder
ListView.builder( itemCount: chatUsers.length, shrinkWrap: true, padding: const EdgeInsets.only(top: 16), physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index){ return ConversationList( name: chatUsers[index].name, messageText: chatUsers[index].messageText, imageUrl: chatUsers[index].imageURL, time: chatUsers[index].time, isMessageRead: (index == 0 || index == 3)?true:false, ); },),
ui的难点部分在这里就分析完成了
4.Flutter WebSocket处理
因为是个Demo,封装的信息比较简单
对发送的信息进行封装
class ChatMessage { String messageContent; String messageType; ChatMessage({required this.messageContent, required this.messageType});}
对基本信息,以及用户信息进行封装,
class UserMessage{ static int userId = 0; static String socketUrl = "ws://192.168.10.104:9090/websocket/"; ///将对话暂存在这里 static List<ChatMessage> messages = [ ];}
初始化时连接服务器并且对其进行监听
当数据返回时,保存到内存中
//连接wensocket并且监听void connect(String userName) { Future<WebSocket> futureWebSocket = WebSocket.connect(UserMessage.socketUrl + "/$userName"); //socket地址 futureWebSocket.then((WebSocket ws) { _webSocket = ws; _webSocket.readyState; // 监听事件 void onData(dynamic content) { print('收到消息:' + content); setState(() { if (content.toString().substring(0, 1) == UserMessage.userId.toString()) { ///自己发送的消息(服务端没有完善) } else { UserMessage.messages.add(ChatMessage( messageContent: content.toString().substring( 2, ), messageType: "receiver")); } }); } _webSocket.listen(onData, onError: (a) => print("error"), onDone: () => print("done")); });}
发送信息处理
对数据需要将json对象转换为json字符串
// 向服务器发送消息void sendMessage(dynamic message) { print(convert.jsonEncode(message)); _webSocket.add(convert.jsonEncode(message);}
onPressed: () { var toUser = "0"; //服务端没有完善,这里固定用户id了 if (UserMessage.userId == 0) { toUser = "1"; } else { toUser = "0"; } var message = { "msg": _controller.text, "toUser": toUser, "type": 1 }; ///传递信息 sendMessage(message); //发送信息 setState(() { //更新ui UserMessage.messages.add( ChatMessage( messageContent: _controller.text, messageType: "sender"), ); _controller.clear(); //清除输入框内文字 });},
在退出页面时,关闭WebSocket连接
@overridevoid dispose() { // TODO: implement dispose super.dispose(); closeSocket();}
5.该文章项目运行步骤
下载代码压缩包,解压后的目录是这样的:
其中images是图片,lib是源代码,jar包是服务端
第一步:创建一个新项目,源代码是使用了空安全的,也可以创建不是空安全的项目,改一下代码即可
第二步:将images与lib复制进去
第三步:在pubspec.yaml中配置静态图片
assets: - images/
第四步:运行.jar包
切换到包存放的地址,服务端的端口为9090,如果与各位的端口冲突可以修改服务端代码,或者停止你在使用9090的这个端口
java -jar 包名.jar
查找端口:
netstat -aon|findstr 9090
查看指定 PID 的进程
tasklist|findstr 10021
结束进程
强制(/F参数)杀死 pid 为 10021的所有进程包括子进程(/T参数):
taskkill /T /F /PID 9088
第五步:在cmd中查找自己的ip,然后在代码的这里修改
第六步:运行后的操作
运行后会先进入登录界面,这里第一只手机输入0 ,第二只手机输入1,因为服务端默认从0开始(给用户分配id),因为没有数据库。
这样进入就可以了,然后就可以像效果图一样开始交流
服务端有问题可以参考这篇文章:Springboot WebSocket 即时通讯
作者:阿Tya
链接:https://juejin.cn/post/7016606314025451557