Flutter服务提供插件serveme的使用

发布于 1周前 作者 sinazl 来自 Flutter

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消息和使用异步查询都是可用的。

泛型客户端类类型

你可能已经注意到ServeMeModule类都有泛型客户端类(默认为<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

1 回复

更多关于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参考。

回到顶部