做为一个Android开发,基本没怎么接触后台开发的东西,对这方面也有点兴趣,一直都想写套接口实现下简单的后端服务玩一玩。 Flutter也学习了快一年了,加上以前看了下闲鱼的一篇文章Flutter & Dart三端一体化开发,兴趣就来了,有兴趣就有学习热情。因而将Dart的HttpServer学习了一下,实现了一个简单的聊天室应用。html
作这个应用还有其余的目的:linux
Github源码,包括了客户端和服务端的代码。android
clone项目后,能够在本地运行个人客户端代码。基本上是一套代码,目前只适配了app和desktop平台。(web遇到点问题,因此还没弄好)git
因为时间关系,也只是作了下一些最基本的功能,后面有空再继续完善。github
这是生成的接口文档地址:web
ApplicationChannel(应用通道),每一个aqueduct应用程序会根据isolate数目去启动相应数量的ApplicationChannel(一个isolate会建立一个ApplicationChannel)。sql
不一样的HTTP请求,会根据Router配置的路径,由不一样的Controller进行处理。每一个里面都有相应的逻辑去处理HTTP请求。数据库
能够连接多个controller处理,造成子通道。好比实现一个获取好友列表的接口,很明显,前提是咱们须要在请求接口的时候带上用户信息(好比token)。这样的话,就能够考虑一个Authorizer controller,用来验证请求的受权凭据是否正确。再加一个FriendController来获取好友列表数据做为response。json
好比下面的代码,定义了注册接口和登陆接口的路由。ubuntu
router
.route("/register")
.link(() => RegisterController(authServer, context));
router.route("/login").link(() => LoginController(context));
复制代码
针对不一样的接口,定义Controller进行相应的处理。下面的登陆接口的相关代码。
class LoginController extends ResourceController {
final ManagedContext context;
LoginController(this.context);
@Operation.post()
Future<Response> login(@Bind.body() User user) async {
String msg = "登陆异常";
//查询数据库是否存在这个用户
var query = Query<User>(context)
..where((u) => u.username).equalTo(user.username);
User result = await query.fetchOne();
if (result == null) {
msg = "用户不存在";
} else {
//经过auth/token获取token。登陆成功的话,返回token
var clientId = "com.donggua.chat";
var clientSecret = "dongguasecret";
var body =
"username=${user.username}&password=${user.password}&grant_type=password";
var clientCredentials =
Base64Encoder().convert("$clientId:$clientSecret".codeUnits);
res.Response response =
await http.post("http://127.0.0.1:8888/auth/token",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": "Basic $clientCredentials"
},
body: body);
if (response.statusCode == 200) {
var map = json.decode(response.body);
return Response.ok(
BaseResult(
code: 1,
msg: "登陆成功",
data: {
'userId': result.id,
'access_token': map['access_token'],
'userName': result.username
},
),
);
}
}
return Response.ok(
BaseResult(
code: 1,
msg: msg,
),
);
}
}
复制代码
//跟服务器创建链接
router
.route("/connect")
.link(() => Authorizer.bearer(authServer))
.linkFunction((request) async {
//链接的用户id
int userId = request.authorization.ownerID;
var socket = await WebSocketTransformer.upgrade(request.raw);
print("userId:$userId的用户跟服务器创建链接");
socket.listen((event) {
print("server listen:${event}");
handleEvent(event, fromUserId: userId);
messageHub.add(
{
"event": "websocket_broadcast",
"message": event,
'fromUserId': userId,
},
);
}, onDone: () {
//socket链接断了的话,移除链接
connections.remove(userId);
});
//保存链接
connections[userId] = socket;
print("当前链接用户有${connections.length}个");
connections.keys.forEach((userId) {
print("userId:$userId");
});
return null;
});
复制代码
项目目录下有一个config.yaml文件,用来实现一些信息的配置,好比数据库方面的配置。
database:
host: localhost
port: 5432
username: donggua
password: password
databaseName: database_chat
复制代码
在项目中初始化数据库。在prepare()方法中,进行数据库的链接,并获取到数据库的上下文ManagedContext对象。将ManagedContext保存到一个context的成员变量中,而后能够传给须要数据库操做的controller的构造函数,这样的话,咱们就能够在controller里面进行一些数据库方面的操做。
@override
Future prepare() async {
final config = CustomConfig(options.configurationFilePath);
final dateModel = ManagedDataModel.fromCurrentMirrorSystem();
final persistentStore = PostgreSQLPersistentStore.fromConnectionInfo(
config.database.username,
config.database.password,
config.database.host,
config.database.port,
config.database.databaseName);
context = ManagedContext(dateModel, persistentStore);
复制代码
在服务器上面安装Dart sdk,这里的服务器建议是ubuntu,能够直接安装官网的Dart SDK。若是是centOS的话,须要本身下载dart sdk源码并进行编译构建,好麻烦,并且可能还会遇到其余问题。(因此我最后重装系统,搞成ubuntu系统了)
将本地的服务器代码,放置到服务器上面。用到两个工具,SecureCRT和FileZilla,SecureCRT用来搞远程登陆,FileZilla用来搞文件传输。具体使用百度一下。
在服务器上面安装dart sdk和aqueduct框架
pub global activate aqueduct
复制代码
这块具体也能够百度一下,这里就不细说了。建立配置的信息,要和咱们的服务端项目中的配置信息保持一致就行。
aqueduct serve
复制代码
通常状况下,当咱们关闭远程窗口的话,项目就跟着退出运行了。因此可使用Screen来让咱们在关闭ssh链接的状况下,让程序继续在后台运行。screen命令能够实现当前窗口与任务分离,咱们即便离线了,服务器仍在后台运行任务。当咱们从新登陆服务器,能够读取窗口线程,从新链接任务窗口。
推荐一篇文章,了解下什么是Screen。linux 技巧:使用 screen 管理你的远程会话。
客户端实现,Flutter。客户端这边的实现比较简单,为了快点体验出三端一体化的快感,用了一些第三方库加快节奏。UI的话就是一个登陆注册页面,再加上一个聊天列表和聊天窗口页面。
只是作简单Demo,因此总体的代码架构比较简单,后期再优化下。
UI的代码就不展现了,无非就是两个文本框加个登陆按钮。 看一下以前在前面的项目中,LoginController定义好的登陆接口返回的结构:
//登陆成功
{
"code": 1,
"msg": “登陆成功”,
"data":{
"userId":"12345",
“access_token”:"abcdefg",
"userName":"donggua"
}
}
//登陆失败
{
"code":1,
"msg":"登陆异常:具体缘由"
}
复制代码
点击按钮,使用Dio调用以前定义好的后端接口。
void login() async {
Dio dio = Dio(BaseOptions(baseUrl: GetIt.instance<AppConfig>().apiHost));
Response<Map<String, dynamic>> response =
await dio.post<Map<String, dynamic>>(
"/login",
data: {
'username': username_controller.text.toString(),
'password': password_controller.text.toString(),
},
);
print("登陆结果:$response");
if (response != null &&
response.data != null &&
response.data['code'] == 1 &&
response.data['data']['access_token'] != null) {
//登陆成功
String token = response.data['data']['access_token'];
int fromUserId = response.data['data']['userId'];
String userName = response.data['data']['userName'];
Navigator.of(context).push(MaterialPageRoute(builder: (context) {
return ChatListPage(
token: token,
fromUserId: fromUserId,
userName: userName,
);
}));
}
}
复制代码
登陆成功,进入到聊天列表页面。
1.请求聊天列表接口/chat_list,获取聊天列表并展现。(后台定义接口类ChatListController,注意客户端接口请求是要带上token的,由于服务端会作token验证。若token无效,则返回401错误码)。
getChatList() async {
Dio dio = Dio(BaseOptions(
baseUrl: GetIt.instance<AppConfig>().apiHost,
headers: {'Authorization': 'Bearer ${widget.token}'}));
Response<Map<String, dynamic>> response =
await dio.get<Map<String, dynamic>>(
"/chat_list",
);
if (response != null &&
response.data != null &&
response.data['code'] == 1) {
List list = response.data['data'];
list?.forEach((json) {
userList.add(User.fromJson(json));
});
setState(() {});
}
}
复制代码
2.使用SocketManager建立WebSocket链接,使客户端和服务器之间能够进行通讯。
void initState(){
socketManager.connectWithServer(widget.token).then((bool) {
if (bool) {
showToast("链接服务器成功");
} else {
showToast("链接服务器失败");
}
});
}
复制代码
1.打开聊天详情页面,获取历史聊天记录(App这边暂时没作数据保存,因此数据全是在后端的数据库中)。这里就不展现代码了,跟前面的同样,请求接口,获取数据后进行展现。有一点就是要根据是不是当前用户,消息item的展现会有所区别。
2.在文本框中输入内容,使用socket进行发送消息。
sendMessage() async {
//发送消息
Map<String, dynamic> data = {
'toUserId': widget.toUser.id,
'msg_content': inputController.text.toString(),
'msg_type': 1,
};
GetIt.instance<SocketManager>().sendMessage(data);
//清空editText
inputController.clear();
debugPrint("向服务器发送消息:$data");
}
复制代码
3.监听服务器的消息。当接收到服务端的消息后,往ListView的数据源中添加一条消息。
void initState(){
listener = (Map<String, dynamic> json) {
if (mounted) {
setState(() {
print("messageList增长一条消息");
Message newMessage = Message.fromJson(json);
//消息是本身发的,或者是别人要发给本身的,才进行展现
if (newMessage.fromUserId == widget.fromUserId ||
newMessage.toUserId == widget.fromUserId) {
messageList.add(newMessage);
}
});
Future.delayed(Duration(milliseconds: 50), () {
scrollController.jumpTo(scrollController.position.maxScrollExtent);
});
}
};
//添加监听
GetIt.instance<SocketManager>().addListener(listener);
}
复制代码
这里介绍下以前写过的一篇文章Flutter之支持不一样的屏幕尺寸和方向。
总体的思路是相似Android的Fragment。咱们须要作的就是定义两个Widget,一个用于显示主列表,一个用于显示详细视图。实际上,这些就是相似的fragments。
咱们只须要检查设备是否具备足够的宽度来处理列表视图和详细视图。若是是,咱们在同一屏幕上显示两个widget。若是设备没有足够的宽度来包含两个界面,那咱们只须要在屏幕中展现主列表,点击列表项后导航到独立的屏幕来显示详细视图。