Flutter服务提供插件serveme的使用
Flutter服务提供插件Serveme的使用
Serveme 是一个简单而强大的模块化服务器框架。它允许轻松创建用于移动和Web应用程序的后端服务。以下是Serveme框架的一些功能:
- 模块化架构允许通过Serveme模块化API轻松实现服务器的不同部分;
- 支持WebSockets和TCP套接字(TCP套接字支持在v1.1.0版本中实现);
- 内置MongoDB支持,自动数据库完整性验证,便于服务器部署;
- 事件API允许分发和监听应用中的任何内置或自定义事件;
- 调度API允许创建不同任务并安排其执行时间和周期;
- 日志、调试和错误处理工具;
- 控制台API允许处理自定义服务器控制台命令(包括自动完成、命令行格式验证、命令信息等);
- 使用内置或自定义配置文件(通过Config API);
- 客户端连接管理、根据标准广播消息、全局或单独监听客户端数据;
- 集成了PackMe二进制序列化库进行数据传输:非常快速;
- 可以使用JSON实现复杂的数据传输协议(编译为PackMe消息.dart文件);
- 支持不同的消息数据类型:字符串、Uint8List或PackMe消息;
- 使用PackMe消息异步查询数据:SomeResponse response = await client.query(SomeRequest());
使用示例
以下是最简单的基于Serveme的服务器应用示例代码:
import 'package:serveme/serveme.dart';
Future<void> main() async {
final ServeMe<ServeMeClient> server = ServeMe<ServeMeClient>();
await server.run();
}
你需要提供默认的配置文件 config.yaml
才能启动服务器。
port: 8080
debug: true
debug_log: debug.log
error_log: error.log
现在服务器已经准备好运行了!不过此时它什么也没有做。我们需要至少实现一个模块文件,在其中实际发生一些事情。建议保持项目文件结构整洁,并将所有模块文件放在单独的“modules”目录中。
让我们创建一个模块,该模块会监听来自已连接客户端的字符串消息并将其回显:
class MyModule extends Module<ServeMeClient> {
@override
Future<void> init() async {
await Future<void>.delayed(const Duration(seconds: 1)); // 模拟一些初始化过程,例如从数据库加载数据
server.log('Module initialized'); // 将消息记录到控制台和debug.log文件
}
@override
void run() {
server.listen<String>((String message, ServeMeClient client) async {
log('Got a message: $message');
client.send(message);
});
}
@override
Future<void> dispose() async {
await Future<void>.delayed(const Duration(seconds: 1)); // 在服务器关闭前完成所有必要的清理工作
server.log('Module disposed');
}
}
现在我们有了一个模块,但还需要在配置文件中启用它:
modules:
- mymodule
更新我们的主函数:
Future<void> main() async {
final ServeMe<ServeMeClient> server = ServeMe<ServeMeClient>(
modules: <String, Module<ServeMeClient>>{
'mymodule': MyModule(),
},
);
await server.run();
}
现在一切准备就绪!你可以使用浏览器连接到服务器并测试它:
let ws = new WebSocket('ws://127.0.0.1:8080');
ws.onmessage = console.log;
ws.send('Something');
可以通过[]
操作符访问一个模块到另一个模块:
class AnotherModule extends Module<ServeMeClient> {
MyModule get myModule => server['mymodule']! as MyModule;
@override
Future<void> init() async {
log('Here is our main module: $myModule');
}
}
别忘了在配置文件中启用新实现的模块。
WebSockets和TCP套接字
Serveme默认使用WebSockets。但是也可以处理纯TCP套接字:
Future<void> main() async {
final ServeMe<ServeMeClient> server = ServeMe<ServeMeClient>(
type: ServeMeType.tcp,
modules: <String, Module<ServeMeClient>>{
'mymodule': MyModule(),
},
);
await server.run();
}
需要注意的是,通过TCP套接字发送的字符串消息会被转换为Uint8List。因此,为了通过TCP套接字接收字符串,你需要使用listen
方法。
配置文件
默认情况下,Serveme使用config.yaml
文件和ServeMeConfig
类来实例化可从任何模块访问的配置对象。但是可以实现和使用自定义配置类。
class MyConfig extends Config {
MyConfig(String filename) : super(filename) {
optionalNumber = cast<int?>(map['optional'], fallback: null);
greetingMessage = cast<String>(map['greeting'],
errorMessage: 'Failed to load config: greeting message is not set'
);
}
late final int? optionalNumber;
late final String greetingMessage;
}
现在让我们更新我们的配置文件,看看如何使用自定义配置类而不是默认配置类。
port: 8080
debug: true
debug_log: debug.log
error_log: error.log
optional: 42
greeting: Welcome, friend!
modules:
- mymodule
更新主函数:
Future<void> main() async {
final ServeMe<ServeMeClient> server = ServeMe<ServeMeClient>(
configFile: 'config.yaml',
configFactory: (String filename) => MyConfig(filename),
modules: <String, Module<ServeMeClient>>{
'mymodule': MyModule(),
},
);
await server.run();
}
如何从模块中访问自定义配置:
class MyModule extends Module<ServeMeClient> {
// ...
@override
MyConfig get config => super.config as MyConfig;
void printConfig() {
log('optionalNumber: ${config.optionalNumber}, greetingMessage: ${config.greetingMessage}');
}
}
建立客户端连接到远程服务器
Serveme实例允许你创建到远程WebSocket或TCP服务器的客户端连接。
@override
Future<void> init() async {
// 连接到本地的WebSocket
final ServeMeClient wsConnectionClient = await server.connect(
'ws://127.0.0.1:8080',
onConnect: () => log('WebSocket connection established'),
onDisconnect: () => log('Disconnected from WebSocket server'),
);
// 连接到本地的TCP套接字
final ServeMeClient tcpConnectionClient = await server.connect(
InternetAddress('127.0.0.1', type: InternetAddressType.IPv4),
port: 8177,
onConnect: () => log('TCP connection established'),
onDisconnect: () => log('Disconnected from TCP server'),
);
}
由于server.connect()
方法返回一个ServeMeClient
实例,所以所有功能如发送/接收PackMe消息和使用异步查询都是可用的。
泛型客户端类类型
你可能已经注意到ServeMe
和Module
类都有泛型客户端类(默认为<ServeMeClient>
)。它用于某些服务器属性和方法,并且可以实现自定义客户端类。这是一个例子:
import 'dart:io';
class MyClient extends ServeMeClient {
MyClient(ServeMeSocket socket) : super(socket) {
authToken = socket.httpRequest!.headers.value('x-auth-token');
}
late final String? authToken;
}
我们添加了一些自定义属性authToken
,并且为了使用这个类而不是默认的类,需要在ServeMe
构造函数中设置clientFactory
属性:
Future<void> main() async {
final ServeMe<MyClient> server = ServeMe<MyClient>(
clientFactory: (_, __) => MyClient(_, __),
modules: <String, Module<MyClient>>{
'mymodule': MyModule(),
},
);
await server.run();
}
需要注意的是在这种情况下,所有模块都应该声明相同的泛型类类型。
class MyModule extends Module<MyClient> {
// ...
void echoAuthenticatedClients() {
server.listen<String>((String message, MyClient client) async {
if (client.authToken != 'some-valid-token') return;
client.send(message);
});
}
}
模块
每个模块有三个强制方法:init()
、run()
和 dispose()
。
Future<void> init();
异步方法init()
在服务器启动时调用,通常用于预加载模块运行所需的所有数据。
void run();
方法run()
在所有模块成功初始化后调用。这是模块开始处理事物并执行其工作的时刻。
Future<void> dispose();
异步方法dispose()
在服务器关闭时使用,用于适当地结束模块操作(当有必要时)。
日志和错误
每个Serveme模块都有访问以下方法的权利:log()
、debug()
和 error()
。
Future<void> log(String message, [String color = _green]);
方法log()
将消息写入控制台并保存到debug.log文件(配置文件中指定)。
Future<void> debug(String message, [String color = _reset]);
如果配置文件中启用了调试,则debug()
将消息写入控制台并保存到日志文件中。
Future<void> error(String message, [StackTrace? stack]);
方法error()
将错误记录到控制台并在错误.log文件中写入(配置文件中指定)。
控制台命令
默认情况下,只有一个命令可以在服务器控制台中使用:stop
- 用于关闭服务器。然而,可以使用从模块中访问的控制台对象实现其他任何命令:
@override
void run() {
console.on('echo', (String line, List<String> args) async => log(line),
aliases: <String>['say'], // 可选
similar: <String>['repeat', 'tell', 'speak'], // 可选
usage: 'echo <string>\nEchoes specified string (max 20 characters length)', // 可选
validator: RegExp(r'^.{1,20}$'), // 可选
);
}
这段代码将添加一个echo
命令,允许回显指定字符串,最长不超过20个字符。
- String line - 命令参数字符串(不包括命令本身);
- List<String> args - 参数列表;
- aliases - 如果需要将多个命令分配给同一个命令处理器;
- similar - 不会被识别为有效命令的命令列表,但如果输入错误,会显示原始命令的提示;
- usage - 命令格式提示和/或简短描述,将在命令格式无效或命令与–help键一起使用时显示;
- validator - 参数字符串的正则表达式验证。
事件
Serveme支持一些内置事件:
- ReadyEvent - 在所有模块初始化完成后触发,正好在调用模块的
run()
方法之前; - TickEvent - 每秒触发一次;
- StopEvent - 当服务器关闭时触发(无论是由
stop
命令还是POSIX信号触发); - LogEvent - 在每次消息记录事件时触发;
- ErrorEvent - 在发生错误时触发;
- ConnectEvent - 当传入的客户端连接建立时触发;
- DisconnectEvent - 当客户端连接关闭时触发。
你可以使用从模块中访问的事件对象订阅这些事件:
@override
void run() {
events.listen<TickEvent>((TickEvent event) async {
log('${event.counter} seconds passed since server start');
});
}
还可以实现自己的事件并在必要时分发它们。这对于不同模块之间的交互非常有用。
class AnnouncementEvent extends Event {
AnnouncementEvent(this.message) : super();
final String message;
}
现在可以在一个模块中分发AnnouncementEvent
,并在另一个模块中监听它。
// 在某个模块中实现
void makeAnnouncement() {
events.dispatch(AnnouncementEvent('Cheese for everyone!'));
}
// 在另一个模块中实现
@override
void run() {
events.listen<AnnouncementEvent>((AnnouncementEvent event) async {
server.broadcast(event.message); // 向所有已连接的客户端发送数据
});
}
调度
Serveme允许创建和调度任务。有一个可以从模块中访问的调度器对象:
class SomeModule extends Module<ServeMeClient> {
late final Task task;
@override
Future<void> init() async {
task = Task(
DateTime.now()..add(const Duration(minutes: 1)),
(DateTime time) async {
log('Current time is $time');
},
period: const Duration(seconds: 10), // 可选
skip: false, // 可选
);
}
@override
void run() {
scheduler.schedule(task);
}
@override
Future<void> dispose() async {
scheduler.cancel(task);
}
}
此模块创建了一个将在1分钟后启动的周期性任务。注意任务在dispose
时被取消。
- skip - 如果为true,则在上一个返回的Future未解析之前,周期性任务将跳过到下一次。默认值:false。
连接和数据传输
你可以通过实现在Module
类中的clients
对象访问当前所有的客户端连接:
@override
void run() {
for (final ServeMeClient in server.clients) {
// do something, don't use it for broadcasting however, use server.broadcast() instead
}
}
推荐始终使用PackMe消息进行数据交换,因为它提供了一些重要的好处,如描述清晰的通信协议、内置的异步查询支持以及小的数据包大小。 这是一个简单的协议.json文件(位于packme目录),用于某些假设的客户端-服务器应用程序(参见PackMe JSON文档):
{
"get_user": [
{
"id": "string"
},
{
"first_name": "string",
"last_name": "string",
"age": "uint8"
}
]
}
生成dart文件:
# Usage: dart run packme <json_manifests_dir> <generated_classes_dir>
dart run packme packme generated
在监听来自客户端的任何PackMe消息之前,需要注册消息工厂(这会在生成的dart文件中自动创建并声明)。
@override
void run() {
// 必要的,以便Serveme知道如何解析传入的二进制数据
server.register(protocolMessageFactory);
server.listen<GetUserRequest>((GetUserRequest request, ServeMeClient client) {
// GetUserRequest.$response方法返回与当前请求关联的GetUserResponse。
final GetUserResponse response = request.$response(
firstName: 'Alyx',
lastName: 'Vance',
age: 19,
);
});
}
这段代码监听来自客户端的GetUserRequest
消息,并回复GetUserResponse
消息。然而,有时能够为特定客户端添加消息监听器是有用的,例如仅对已登录用户监听:
bool _isAuthorizedToDoSomething(String codePhrase) {
return codePhrase == "I am Iron Man.";
}
@override
void run() {
// 监听来自已连接客户端的某些授权请求。
server.listen<AuthorizeRequest>((AuthorizeRequest request, ServeMeClient client) {
if (_isAuthorizedToDoSomething(request.codePhrase)) {
client.listen<GodModeRequest>(_handleGodModeRequest);
client.listen<AllWeaponsRequest>(_handleAllWeaponsRequest);
client.listen<KillEveryoneRequest>(_handleKillEveryoneRequest);
client.send(request.$response(
allowed: true,
reason: 'Welcome on board!',
));
}
else {
client.send(request.$response(
allowed: false,
reason: 'You are not Iron Man.',
));
// 关闭客户端连接。
client.close();
}
});
}
你可以在前面的例子中看到request.$response()
方法的使用,而不是直接实例化相应的ResponseMessage
。这样做是为了将响应分配给特定的请求,从而允许我们在客户端(或服务器端)使用.query()
。
// 例如,当服务器即将关机时获取一些数据
@override
Future<void> dispose() async {
int ok = 0, notOk = 0;
for (final ServeMeClient client in server.clients) {
// 在真实情况下,你可能希望并行使用异步调用
final AreYouOkResponse response = await client.query<AreYouOkResponse>(AreYouOkRequest());
if (response.ok) ok++;
else notOk++;
}
server.log('$ok clients are OK and $notOk clients are not. Now ready for shutting down.');
}
方法broadcast()
允许向所有已连接的客户端或根据某些条件过滤的客户端发送消息:
// 在服务器关机时向所有客户端说再见
@override
Future<void> dispose() async {
// 向所有已连接的客户端发送字符串消息。
server.broadcast(
'See you later!',
(ServeMeClient client) => true // 可选的标准过滤器
);
}
MongoDB
Serveme使用mongo_dart
包支持MongoDB。要在模块中使用MongoDB,需要在配置文件中指定mongo配置部分:
mongo:
host: 127.0.0.1
database: test_db
或者在使用副本集的情况下:
mongo:
host:
- 192.160.1.101:27017
- 192.160.1.102:27017
- 192.160.1.103:27017
database: test_db
replica: myReplicaSet
可以从模块中访问一个名为db
的对象。这个对象实际上是Future<Db>
。使用Future确保数据库连接处于活动状态且Db对象有效。
import 'package:mongo_dart/mongo_dart.dart';
late final List<Map<String, dynamic>> items;
@override
Future<void> init() async {
// 加载配置文件中指定的价格范围内的所有项
items = await (await db).collection('users')
.find(where.gte('price', config.minPrice).lte('price', config.maxPrice))
.toList();
}
数据库完整性验证
有时需要确保服务器数据库包含所有必要的集合、索引和数据,以便服务器正常运行。为此,Serveme提供了特殊的完整性描述符。它允许在服务器启动时自动创建缺失的索引并创建必需的文档。除了验证之外,它还允许轻松部署服务器,无需额外步骤来设置数据库。
Future<void> main() async {
final ServeMe<ServeMeClient> server = ServeMe<ServeMeClient>(
dbIntegrityDescriptor: <String, CollectionDescriptor>{
'users': CollectionDescriptor(
indexes: <String, IndexDescriptor>{
'login_unique': IndexDescriptor(key: {'login': 1}, unique: true),
'email_unique': IndexDescriptor(key: {'email': 1}, unique: true),
'session': IndexDescriptor(key: {'sessions.key': 1}, unique: true),
}
),
'settings': CollectionDescriptor(
indexes: <String, IndexDescriptor>{
'param_unique': IndexDescriptor(key: {'param': 1}, unique: true),
},
documents: <Map<String, dynamic>>[
{'param': 'online_users_limit', 'value': 5000},
{'param': 'disable_email_login', 'value': false},
]
),
},
modules: <String, Module<ServeMeClient>>{
'mymodule': MyModule(),
},
);
await server.run();
}
支持的平台
目前它仅适用于Dart。目前没有计划将其实现为其他语言。然而,如果开发者发现这个包很有用,未来可能会为Node.JS和C++实现它。
希望你喜欢它 ;)
更多关于Flutter服务提供插件serveme的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
更多关于Flutter服务提供插件serveme的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
当然,以下是一个关于如何在Flutter项目中使用serveme
插件的示例代码案例。serveme
是一个假设的服务提供插件,用于演示目的。请注意,实际中serveme
可能并不存在,以下代码是一个概念性的示例,展示了如何在Flutter中使用自定义服务插件。
Flutter 项目中使用 serveme
插件示例
1. 添加依赖
首先,在你的pubspec.yaml
文件中添加对serveme
插件的依赖(假设它存在于pub.dev上):
dependencies:
flutter:
sdk: flutter
serveme: ^1.0.0 # 假设的版本号
然后运行flutter pub get
来安装依赖。
2. 导入插件
在你的Dart文件中导入serveme
插件:
import 'package:serveme/serveme.dart';
3. 使用插件提供的服务
假设serveme
插件提供了一个简单的服务,比如获取设备的网络信息。以下是如何使用它的示例代码:
import 'package:flutter/material.dart';
import 'package:serveme/serveme.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Serveme Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
String _deviceInfo = 'Loading...';
@override
void initState() {
super.initState();
_loadDeviceInfo();
}
Future<void> _loadDeviceInfo() async {
try {
// 假设Serveme类有一个静态方法getDeviceInfo,返回设备信息
var deviceInfo = await Serveme.getDeviceInfo();
setState(() {
_deviceInfo = deviceInfo;
});
} catch (e) {
setState(() {
_deviceInfo = 'Error: ${e.message}';
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Serveme Demo'),
),
body: Center(
child: Text(
_deviceInfo,
style: TextStyle(fontSize: 20),
),
),
);
}
}
4. 插件方法实现(假设)
虽然通常插件的方法是在原生代码(Android的Java/Kotlin和iOS的Swift/Objective-C)中实现的,但为了完整性,这里假设serveme
插件在Dart层有一个简单的模拟实现:
// 假设这是serveme插件的源代码的一部分
class Serveme {
// 静态方法模拟获取设备信息
static Future<String> getDeviceInfo() async {
// 模拟异步操作,比如从原生代码获取数据
await Future.delayed(Duration(seconds: 2));
return 'Device Info: Simulated Data';
}
}
在实际情况下,Serveme.getDeviceInfo()
方法会调用原生代码来获取设备信息。
总结
上述代码演示了如何在Flutter项目中使用一个假设的服务提供插件serveme
。请注意,实际的插件可能会有更复杂的实现,包括原生代码部分和更多的功能。如果你正在使用一个真实存在的插件,请查阅该插件的官方文档以获取准确的使用方法和API参考。