2024年11月

XAML平台和跨平台开发策略

本文基于Vicky&James 2024年10月22日在韩国Microsoft总部BMW meetup会议上的演讲内容重新整理而成。这次研讨会我们深入探讨了基于XAML的各种平台、跨平台战略以及为有效的项目架构设计所需的核心技术。

介绍

大家好,我们是中韩Microsoft MVP夫妇Vicky&James。我们从WPF开始就对包括Uno Platform在内的基于XAML的框架和项目架构设计有着深入的兴趣和经验。大家可以在我们的GitHub仓库中查看和下载各种项目的源代码:
GitHub - jamesnetgroup


目录

  1. XAML平台和跨平台概述
  2. 考虑跨平台的.NET版本选择策略
  3. View和ViewModel的连接策略分析
  4. 框架设计的必要功能及实现方案
  5. 在其他平台上有效使用WPF技术的核心策略
  6. 分布式项目管理的Bootstrapper设计方法论
  7. 在桌面跨平台中最大化利用WPF技术的策略


1. XAML平台和跨平台概述

XAML是一种用于以声明方式定义UI的标记语言,被用于多个平台。XAML具有由对象(即类)组成的层次结构,使开发人员能够以面向对象的方式设计和管理UI。由于这种结构,开发人员直接处理XAML是很自然的。

当WPF首次出现时,它强调了开发人员和设计师之间的协作,但实际上XAML领域通常也由开发人员负责。这是因为XAML不仅仅是简单的设计,而是形成了基于对象的层次结构,在复杂的自定义控件实现中也发挥着重要作用。这种面向开发人员的设计方法促使XAML不仅在WPF中,而且在随后出现的许多平台中都成为核心组件。

特别是,WPF对所有基于XAML的平台都产生了重大影响,并成为这些平台最重要的参考。

1.1 主要的XAML框架

  • WPF
    : 用于Windows桌面应用程序开发的强大框架,提供丰富的UI和图形功能。
  • Silverlight
    : 用于在web浏览器中运行的互联网应用程序的平台,目前已停止支持。它是WPF的轻量级版本,以插件方式运行。由于Web标准政策的变化,基于插件的Web平台消失,在Silverlight 2中引入了
    VisualStateManager(VSM)
    来弥补Trigger的缺点。
  • Xamarin.Forms
    : 支持iOS、Android和Windows的移动应用开发平台。作为基于Mono的第一个基于XAML的跨平台,被Microsoft战略性收购并成为.NET Core的基础。
  • UWP (Universal Windows Platform)
    : 用于开发运行在Windows 10及以上版本的应用程序的平台。需要注册Microsoft Store,并有WinAPI使用限制等约束。支持与WPF相同的自定义控件设计。
  • WinUI 3
    : Windows原生UI框架,是最新Windows应用开发的下一代UI平台。继承了UWP的所有优点,同时解决了其限制,并采纳了WPF的可扩展性。
  • MAUI (.NET Multi-platform App UI)
    : 从.NET 6开始引入的跨平台UI框架,可以在单一项目中开发移动和桌面应用。
  • Uno Platform
    : 允许在各种平台上使用UWP和WinUI的API的框架,支持Web(WebAssembly)、移动和桌面。支持几乎所有平台,并提供与WPF相同的自定义控件设计。
  • Avalonia UI
    : 允许在跨平台上使用WPF风格XAML的开源UI框架。支持与WPF相同的自定义控件设计,通过独特的技术扩展支持各种平台。
  • OpenSilver
    : 为将旧的Silverlight迁移到OpenSilver而优化的开源平台。以与Silverlight几乎相同的方式运行,也为WPF开发人员提供熟悉的环境。

2. 考虑跨平台的.NET版本选择策略

在跨平台应用程序开发中,需要谨慎选择要使用的.NET版本。因为这将直接影响兼容性、功能性和目标平台支持。

2.1 .NET版本选项

  • .NET Framework
    : 仅用于Windows,主要用于现有的WPF和WinForms应用程序。
  • .NET Standard 2.0 / 2.1
    : 为提供各种.NET实现之间的兼容性而设计的标准。
  • .NET (Core) 3.0及以上
    : 支持Windows、macOS、Linux的跨平台.NET实现,包含最新功能和性能改进。

2.2 选择标准和考虑因素

如果需要跨平台支持,应该选择
.NET Core
或最新的
.NET
。如果与现有.NET Framework库或包的兼容性很重要,那么则应该使用
.NET Standard 2.0
。如果想要最新功能和性能改进,就需要考虑
.NET 5及以上
版本。

此外,跨平台框架从.NET 5.0开始考虑兼容性,并且基于最新版本持续进行功能改进,因此建议大家选择最新的.NET版本。

战略建议
:

  • 将通用库编写为
    .NET Standard 2.0
    以确保最大兼容性。
  • 为每个平台创建项目并引用通用库。
  • 如果可能,使用
    .NET 6及以上
    版本以获得最新功能和性能改进。

3. View和ViewModel的连接策略分析

在MVVM(Model-View-ViewModel)模式中,View和ViewModel的连接是核心部分。连接方式的不同会导致使用MVVM的方式完全不同。因此,我们需要根据使用MVVM的目的来决定DataContext分配方式。

3.1 传统的DataContext直接分配方式

在代码后台创建ViewModel并直接分配给View的DataContext。

public MainWindow()
{
    InitializeComponent();
    DataContext = new MainViewModel();
}

优点
:

  • 实现简单直观
  • 可以明确控制视图模型的创建时机
  • 可以向构造函数传递所需参数

缺点
:

  • View和ViewModel之间产生强耦合
  • 单元测试时难以模拟(Mock)ViewModel
  • 难以利用依赖注入(DI)
  • 需要直接指定DataContext分配时机,可能难以保持一致性

3.2 在XAML中创建ViewModel实例

在XAML中设置DataContext来实例化ViewModel。

<Window x:Class="MyApp.MainWindow"
        xmlns:local="clr-namespace:MyApp.ViewModels">
    <Window.DataContext>
        <local:MainViewModel />
    </Window.DataContext>
    <!-- Window content -->
</Window>

优点
:

  • XAML的智能感知支持可以减少绑定错误
  • 可以在设计器中预览实际的数据绑定
  • 明确表达View和ViewModel之间的关系

缺点
:

  • ViewModel创建时难以进行依赖注入
  • 复杂的初始化逻辑或参数传递受限
  • DataContext分配时机被强制化,降低了灵活性

3.3 ViewModel直接创建及依赖传递

在代码后台创建ViewModel直接创建及依赖传递时直接传递所需的依赖。

public MainWindow()
{
    InitializeComponent();
    var dataService = new DataService();
    var loggingService = new LoggingService();
    DataContext = new MainViewModel(dataService, loggingService);
}

优点
:

  • 可以在创建ViewModel时明确传递所需的依赖
  • 可以实现复杂的初始化逻辑
  • 可以根据Runtime灵活创建ViewModel实例

缺点
:

  • View需要了解ViewModel的依赖关系
  • 随着依赖增加,代码后台会变得复杂
  • View和ViewModel之间的耦合度仍然很高
  • 需要直接指定DataContext分配时机,可能难以保持一致性
  • 由于不使用DI管理项目,可能会出现依赖关系混乱的情况

3.4 活用依赖注入(DI)容器

使用DI容器来管理ViewModel及其依赖可以降低View和ViewModel之间的耦合度。

public MainWindow()
{
    InitializeComponent();
    DataContext = ServiceProvider.GetService<MainViewModel>();
}

优点
:

  • 降低View和ViewModel之间的耦合度
  • 依赖关系集中管理,提高可维护性
  • 便于进行单元测试
  • 可以在运行时灵活更改依赖关系

缺点
:

  • DI容器的初始配置可能较为复杂
  • 团队成员需要理解DI模式
  • 仍然需要直接在DataContext中创建视图模型,分配时机的一致性可能难以保持
  • 需要决定是将视图模型作为单例还是实例来管理,并考虑视图的生命周期。需要制定明确的规则并严格遵守

3.5 View的自动ViewModel创建策略

为了解决上述问题,我们可以考虑在View创建创建的约定时间点,通过依赖注入创建ViewModel并且分配给DataContext。例如,设计一个基于ContentControl的Veiw,自动创建ViewModel就是一个有效的方法。

public class UnoView : ContentControl
{
    public UnoView()
    {
        this.Loaded += (s, e) =>
        {
            var viewModelType = ViewModelLocator.GetViewModelType(this.GetType());
            DataContext = ServiceProvider.GetService(viewModelType);
        };
    }
}

优点
:

  • 可以保持DataContext分配时机的一致性
  • 降低View和ViewModel之间的耦合度
  • ViewModel的创建和依赖注入自动处理
  • View不需要知道自己应该使用哪个ViewModel

这几乎是一个没有缺点的方法,通过管理单一的View,可以统一处理时机和处理逻辑。在结构性完善和扩展方面也能保证很好的设计。

不过需要View和ViewModel之间的Mapping,可以使用Dictionary或Mapping Table 来实现。这样可以集中管理View和ViewModel之间的连接信息。关于连接管理的映射方法,我们将在后面的
Bootstrapper设计方法论
中详细讨论。

4. 框架设计的必要功能及实现方案

在设计应用程序架构时,构建考虑
可重用性和可扩展性
的框架非常重要。为此,使用依赖注入(DI)容器是必不可少的。

4.1 依赖注入(DI)容器的使用

DI是现代软件开发中不可或缺的模式,对依赖关系管理和降低耦合度有很大帮助。然而,在像WPF这样的桌面应用程序中,Web应用程序中常用的
Microsoft.Extensions.DependencyInjection
等DI容器可能并不完全适合。

4.1.1 Microsoft.Extensions.DependencyInjection的使用和注意事项

Microsoft.Extensions.DependencyInjection
是.NET官方提供的DI容器,可以说是.NET Foundation的标准。它被用于ASP.NET Core、EntityFrameworkCore、MAUI等各种平台和框架中的几乎所有系统中使用,并提供
Transient

Scoped

Singleton
等生命周期管理功能。

然而,在WPF中,这种标准DI的生命周期可能和WPF实际情况并不完全匹配。

注意事项
:

  • 在WPF等桌面应用程序中,可能不需要
    Scoped
    生命周期
  • Transient

    Singleton
    的概念是为服务或Web应用程序设计的,在WPF中某些功能可能不适用
  • 可能带来不必要的复杂性,对于WPF的使用场景来说,更简单轻量的DI容器可能更合适

当然,即使不使用
Transient
这样的生命周期也可以使用DI,但准确理解这些要点是非常重要的。

4.1.2 CommunityToolkit.Mvvm的DI

CommunityToolkit.Mvvm
并不直接提供像
Microsoft.Extensions.DependencyInjection
这样的DI。这可能是因为
Microsoft.Extensions.DependencyInjection
和WPF的生命周期特性不完全匹配。

但是,
CommunityToolkit.Mvvm
通过提供
Ioc.Default
允许开发者使用任何想要的DI容器。任何实现了
System.IServiceProvider
接口的DI容器都可以注册使用。

因此,使用
CommunityToolkit.Mvvm
时可以选择DI。最常用的DI之一无疑是
Microsoft.Extensions.DependencyInjection
,使用
Prism
这样的DI也是非常有效的组合。

4.1.3 直接设计DI容器的优势

基于
IServiceProvider
接口设计DI的方法可以注册到
CommunityToolkit.Mvvm

Ioc.Default
中,实现内部功能的连接和兼容。由于
IServiceProvider
只要求实现
GetService
等最基本的功能,因此可以实现非常简单的DI。

优点
:

  • 实现只包含必要功能的简单DI容器,降低项目复杂性
  • 可以在内部设计、控制和扩展各种功能
  • 可以精确构建整体框架架构和项目设计
  • 提供不依赖特定平台的统一DI容器,有利于跨平台开发

示例代码
:

// 基于IServiceProvider的DI容器实现
public class SimpleServiceProvider : IServiceProvider
{
    private readonly Dictionary<Type, Func<object>> _services = new();

    public void AddService<TService>(Func<TService> implementationFactory)
    {
        _services[typeof(TService)] = () => implementationFactory();
    }

    public object GetService(Type serviceType)
    {
        return _services.TryGetValue(serviceType, out var factory) ? factory() : null;
    }
}

// DI容器注册和使用
var serviceProvider = new SimpleServiceProvider();
serviceProvider.AddService<IMainViewModel>(() => new MainViewModel());

Ioc.Default.ConfigureServices(serviceProvider);

这样基于
IServiceProvider
接口实现简单的DI容器,就可以注册到
CommunityToolkit.Mvvm

Ioc.Default
中,实现内部功能的连接和兼容。如果觉得使用
Microsoft.Extensions.DependencyInjection

Prism
等主流DI太繁重,自己直接来实现一个是非常有吸引力的选择。

注意
:
如果不遵循
IServiceProvider

System.ComponentModel
标准,可能会失去和
CommunityToolkit.Mvvm

Ioc
兼容性。但是我们可以将
CommunityToolkit.Mvvm
仅作为MVVM相关的模块,创建一个更专业、更统一的、不依赖特定平台或框架的DI容器。这对于创建可以在跨平台等多个XAML平台上通用的框架是非常适合的。

5. WPF技术在其他平台上的有效使用策略

要在其他XAML平台上最大限度地利用WPF强大的功能,我们需要了解一些历史背景和核心策略。同时也需要详细了解可以直接使用WPF技术的平台特性。

5.1 平台间的特征和差异理解

  • UWP和WinUI 3的差异
    : UWP作为Windows 10的专用平台,由于应用商店注册指南和WinAPI限制等原因,与WPF和WinForms等传统平台的兼容性较差。因此,WinUI 3应运而生,它不仅继承了UWP的所有优点,还改进了其存在的问题,发展成为了一个像WPF一样具有高自由度的平台。

  • Uno Platform桌面版与WinUI 3的一致性
    : Uno Platform支持Windows、macOS、Linux的桌面平台完全遵循WinUI 3的方式。因此,WinUI 3直接使用UWP的核心库,而Uno Platform也同样采用WinUI 3的方式,这意味着所有以
    Microsoft.*
    开头的DLL库都可以共享使用。

理解这些平台间的特征可以让我们认识到
Uno Platform Desktop
是一个非常高效且具有吸引力的平台。因此,在WPF和Uno Platform之间进行技术共享和转换的策略非常有效且高效,因为它们与WinUI 3和UWP都有着紧密的联系。

5.2 充分利用VisualStateManager(VSM)

由于不是所有平台都可以直接使用WPF的Trigger,所以我们就需要一个替代策略。
VisualStateManager(VSM)
在解决这个问题上起着核心作用。

VSM是在Silverlight 2.0中引入的,用来弥补Trigger不足的,对自定义控件和XAML之间的状态处理进行了优化。随后在.NET 4.0中,VSM也被引入到WPF中,WPF的Button、CheckBox、DataGrid、Window等所有CustomControl的内部设计都从Trigger改为了VSM。

优点
:

  • 在不能直接使用Trigger的平台上可以通过VSM实现相同功能
  • 可以有效实现UI状态管理和动画
  • 可以通过VSM统一不同平台的不同行为

最终,通过集中使用VSM,就可以实现在WPF、Uno Platform Desktop、WinUI 3、UWP等平台上构建相同的XAML和CustomControl,源代码也可以完全共享。

5.3 灵活运用IValueConverter

IValueConverter
是一个允许在数据绑定时转换值的接口,对于抽象化平台间差异非常有用。

策略性使用
:

  • 可以实现和替代几乎与Trigger相同的功能,编写简单有效的源代码
  • 由于每次都需要创建Converter,而且且重用性标准模糊,建议不要过分追求重用性,而是灵活使用
  • 即使没有重用性也要直观使用,重要的是通过明确的命名来尽量减少分支,专门化使用

局限和补充
:

  • 仅使用
    IValueConverter
    是有限的
  • IValueConverter
    应用于简单转换,复杂场景的管理会带来负担,这时我们应通过
    VSM
    解决
  • 复杂的状态处理最好使用
    VisualStateManager

总之,
IValueConverter
补充了VSM的不足,对于简单直接的转换工作应该直观灵活使用,不要过分追求重用性。

6. 分布式项目管理的Bootstrapper设计方法论

随着应用程序变得复杂和模块化,初始化过程和依赖管理变得越来越重要。
Bootstrapper模式
在集中管理这些初始化逻辑方面非常有用。

虽然所有平台都以Application设计为基础,但由于各平台的特性和方式不同,Application设计各不相同。因此,为了在所有平台上保持相同的开发方式,使用
Bootstrapper结构设计
是非常有效的。

6.1 Bootstrapper的作用和必要性

Bootstrapper的功能
:

  • 依赖注入设置
    :初始化DI容器,注册必要的服务、View和ViewModel。
  • 管理视图和视图模型的连接
    :通过依赖注入注册View,管理View和ViewModel之间的映射。
  • 集中化配置管理
    :所有配置都在Bootstrapper中管理,使应用程序项目只执行其角色,其余功能实现通过项目分布式和模块化来管理。
  • 此外,还可以灵活地扩展集中化管理项目,且不会影响Application。

优点
:

  • 通过分离应用程序的初始化逻辑来提高代码的可读性和可维护性。
  • 通过项目分布式和模块化,可以独立开发功能实现。
  • 最小化平台之间的结构差异,保持一致的架构。

6.2 Bootstrapper的设计方案

示例代码
:

namespace Jamesnet.Core;

public abstract class AppBootstrapper
{
    protected readonly IContainer Container;
    protected readonly ILayerManager Layer;
    protected readonly IViewModelMapper ViewModelMapper;

    protected AppBootstrapper()
    {
        Container = new Container();
        Layer = new LayerManager();
        ViewModelMapper = new ViewModelMapper();
        ContainerProvider.SetContainer(Container);
        ConfigureContainer();
    }

    protected virtual void ConfigureContainer()
    {
        Container.RegisterInstance<IContainer>(Container);
        Container.RegisterInstance<ILayerManager>(Layer);
        Container.RegisterInstance<IViewModelMapper>(ViewModelMapper);
        Container.RegisterSingleton<IViewModelInitializer, DefaultViewModelInitializer>();
    }

    protected abstract void RegisterViewModels();
    protected abstract void RegisterDependencies();

    public void Run()
    {
        RegisterViewModels();
        RegisterDependencies();
        OnStartup();
    }

    protected abstract void OnStartup();
}

通过这样的抽象化,可以明确强调管理结构,并通过虚方法控制时间点和顺序。这有助于灵活扩展和完善,并且不影响Application,使其在各种平台上以一致的方式运行。

7. 在跨平台桌面环境中最大化WPF技术的策略

通过在其他基于XAML的跨平台框架中最大限度地利用WPF中使用的技术和模式,可以提高开发效率。

7.1 实现可在所有平台上运行的框架

Jamesnet.Core是基于
.NET Standard 2.0
的框架,允许在WPF、Uno Platform和WinUI 3中实现相同的项目设计。该框架具有以下特点:

  • DI设计:利用基于IServiceProvider的DI容器,可以与CommunityToolkit.Mvvm配合使用。
  • MVVM Bootstrapper:集中管理项目的初始化和依赖注入。
  • View和ViewModel之间的连接管理:通过层管理等方式降低View和ViewModel的耦合度。
  • 在所有基于XAML的平台上统一运行。
  • 直接引用存储库源代码,便于调试、功能实现、扩展和研究。

优点
:

  • 无论使用WPF、Uno Platform还是WinUI 3开发,都可以保持相同的架构。
  • 使用
    Uno Platform Desktop
    可以在macOS和Linux上进行开发和运行。
  • 可以使用
    JetBrains Rider
    构建跨平台开发环境。

7.2 实际实现案例分析

英雄联盟客户端重构项目
利用Jamesnet.Core框架,在WPF、Uno Platform和WinUI 3等不同平台上使用相同的代码库和架构实现。




战略方法
:

  • 通过
    Jamesnet.Core框架
    保持
    项目设计的统一性
  • 利用
    DI容器和Bootstrapper
    集中管理视图和视图模型。
  • 使用
    VisualStateManager(VSM)
    替代Trigger,在不同平台上以相同方式管理UI状态。

成果
:

  • 97%以上的代码共享
    ,最大化了向其他平台扩展的可能性。
  • 在各种平台上提供
    一致的用户体验和开发方法论
    ,使技术转换更容易。
  • 通过项目分散化、模块化和管理集中化,大大降低了开发和维护成本。
  • 通过
    CustomControl的模块化开发
    提高了重构和扩展性,在GPT、Claude等
    AI应用
    方面,分散的视图系统也更加有效。

结论

WPF技术和模式仍然强大,将其应用于跨平台开发可以提高开发效率和代码重用性。特别是使用
Jamesnet.Core框架
,通过DI容器的集中化管理策略和引入Bootstrapper,有助于降低视图和视图模型之间的耦合度,提高可维护性。

此外,通过积极使用
VisualStateManager

IValueConverter
,可以最小化平台之间的差异并保持一致的设计。通过这些策略,可以超越WPF基础,战略性地实现跨平台技术扩展。

特别是,
UWP

WinUI 3

Uno Platform
之间100%相同地使用XAML相关DLL,因此这些平台之间几乎没有差异。因此,对WPF开发者来说,
使用Uno Platform桌面版非常有效且具有战略意义
。这是因为从WPF转换到Uno可以在几小时内完成,转换到WinUI 3也非常容易。

未来,WPF技术和基于XAML的框架将继续发展,利用这些进行跨平台开发将变得更加重要。开发人员需要很好地把握这些趋势,制定适当的策略,开发高质量的应用程序。

参考

主要仓库

目前已更新的WPF教程(自定义控件)

《Rust编程与项目实战》(朱文伟,李建英)【摘要 书评 试读】- 京东图书 (jd.com)

在Rust中,结构体(Struct)是一种自定义数据类型,它允许我们将多个相关的值组合在一起,形成一个更复杂的数据结构。结构体在Rust中被广泛应用于组织和管理数据,具有灵活性和强大的表达能力。本节将详细介绍Rust中结构体的概念、定义语法、方法以及相关特性,并提供代码示例来帮助读者更好地理解结构体的使用方法。

8.3.1 结构体的定义

Rust 中的结构体与元组都可以将若干类型不一定相同的数据捆绑在一起形成整体,但结构体的每个成员和其本身都有一个名字,这样访问它的成员的时候就不用记住下标了。元组常用于非定义的多值传递,而结构体用于规范常用的数据结构。结构体的每个成员叫作“字段”。

在Rust中,我们可以使用struct关键字定义一个结构体。结构体允许我们定义多个字段(Fields),每个字段都有自己的类型和名称。通过将字段组合在一起,可以创建自己的数据类型,以便更好地表示和操作数据。以下是一个简单的结构体定义的示例:

structPoint {
x: i32,
y: i32,
}

在上述示例中,我们定义了一个名为Point的结构体,它具有两个字段x和y,分别是i32类型的整数。再来看一个结构体定义:

structSite {
domain: String,
name: String,
nation: String,
found: u32
}

注意:如果你常用C/C++,请记住在Rust中struct语句仅用来定义,不能声明实例,结尾不需要“;” 符号,而且每个字段定义之后用“,”分隔。

8.3.2 结构体实例化

一旦定义了结构体,可以通过实例化结构体来创建具体的对象。可以通过以下两种方式实例化结构体:

1)声明式实例化

let p = Point { x: 10, y: 20 };

在上述示例中,我们使用Point结构体的定义创建了一个名为p的实例,同时指定了字段x和y 的值。

2)可变实例化

如果需要修改结构体的字段值,可以在定义结构体变量时加上mut,代码如下:

let mut p = Point { x: 10, y: 20};
p.x
= 30;
p.y
= 40;

在上述示例中,我们创建了一个可变实例p,并修改了字段x和y的值。

Rust很多地方受JavaScript的影响,在实例化结构体的时候用JSON对象的key: value语法来实现,比如:

let mysite =Site {
domain: String::
from("www.qq.com"),
name: String::
from("qq"),
nation: String::
from("China"),
found:
2024};

如果你不了解 JSON 对象,可以不用管它,记住格式就可以了:

结构体类名 {
字段名 : 字段值,
...
}

这样的好处是不仅使程序更加直观,还不需要按照定义的顺序来输入成员的值。如果正在实例化的结构体有字段名称和现存变量名称一样,可以简化书写:

let domain = String::from("www.qq.com");
let name
= String::from("qq");
let runoob
=Site {
domain,
//等同于 domain : domain, name, //等同于 name : name, nation: String::from("China"),
traffic:
2024};

有这样一种情况:想要新建一个结构体的实例,其中大部分属性需要被设置成与现存的一个结构体属性一样,仅需更改其中一两个字段的值,可以使用结构体更新语法:

let site =Site {
domain: String::
from("www.qq.com"),
name: String::
from("qq"),
..qq
};

注意:..qq后面不可以有逗号。这种语法不允许一成不变地复制另一个结构体实例,意思就是至少重新设定一个字段的值,才能引用其他实例的值。

8.3.3 结构体的方法
在Rust中,结构体可以拥有自己的方法。方法是与结构体关联的函数,可以通过结构体实例调用。以下是一个结构体方法的示例:

structRectangle {
width: u32,
height: u32,
}

impl Rectangle {
//使用关键字impl来定义结构体的一个或多个方法 fn area(&self) -> u32 { //用关键字fn定义具体的函数 self.width *self.height
}
}

fn main() {
let rect
= Rectangle { width: 10, height: 20};
let area
=rect.area();
println
!("Area: {}", area);
}

在上述示例中,我们定义一个名为Rectangle的结构体,并为其实现一个area方法,用于计算矩形的面积。在main函数中,我们创建一个Rectangle实例rect,然后通过调用area方法计算矩形的面积并打印出来。

8.3.4 结构体的关联函数

除实例方法外,结构体还可以定义关联函数(Associated Functions)。关联函数是直接与结构体关联的函数,不需要通过结构体实例来调用。以下是一个关联函数的示例:

structCircle {
radius: f64,
}

impl Circle {
fn
new(radius: f64) ->Circle {
Circle { radius }
}

fn area(
&self) ->f64 {
std::f64::consts::PI
* self.radius *self.radius
}
}

fn main() {
let circle
= Circle::new(5.0);
let area
=circle.area();
println
!("Area: {}", area);
}

在上述示例中,我们定义一个名为Circle的结构体,并为其实现一个关联函数new,用于创建一个新的Circle实例。在main函数中,我们通过调用Circle::new关联函数创建一个Circle实例circle,然后通过调用area方法计算圆的面积并打印出来。

8.3.5 结构体的特性

Rust的结构体具有两个特性:元组结构体(Tuple Struct)和类单元结构体(Unit-Like Struct)。

元组结构体是一种特殊类型的结构体,它没有命名的字段,只有字段的类型。元组结构体使用圆括号而不是花括号来定义。比如:

struct Color(i32, i32, i32);

在上述示例中,我们定义了一个名为Color的元组结构体,它包含3个i32类型的字段。

类单元结构体是一种没有字段的结构体,类似于空元组。比如:

struct Empty;

在上述示例中,我们定义了一个名为Empty的类单元结构体。

前段时间在搬迁项目的时候,遇到一个问题,就是用sqlsugar调用oracle的存储过程的时候调用不了;

当时卡了一整天,现在有空了把这个问题记录分享一下。

先去nuget上安装一下sqlsugar的包:

再安装一个oracle的驱动:

添加一下Json包:

再去创建一下连接

再创建一个测试用的存储过程

create or replace procedure pr_test(i_name   invarchar2,
i_age
invarchar2,o_resultout sys_refcursor) asbegin

open o_result
for select * fromdual;

end pr_test;

创建一个类来接受存储过程返回的数据

    public classPeople
{
public string Dummy { get; set; }
}

单独把存储过程里面的那句sql拿出来执行,会得到下面的结果:

dual这个表是oracle提供的一个表,里面就一个X,一般可以用这个来测试数据库连接是不是正常。

调用的方式如下:

里面那个
游标
的入参必须是个空字符,我之前尝试过object,null,就是没想到过会是一个空字符。

当时也是没想到一个空字符,就把我卡了一个下午,这个坑应该是不会再踩了。

Kubernetes 中实现 MySQL 的读写分离

在 Kubernetes 中实现 MySQL 的读写分离,可以通过主从复制架构来实现。在这种架构中,MySQL 主节点(Master)负责处理所有写操作,而 MySQL 从节点(Slave)负责处理所有读操作。下面是一个详细的步骤指南:

步骤 1:创建 Kubernetes 集群

确保你有一个运行良好的 Kubernetes 集群,建议有3个以上的节点,以便更好地分配资源并实现高可用性。

步骤 2:创建 MySQL 主从复制 Docker 镜像

  1. 首先,需要构建一个支持主从复制的 MySQL 镜像,或直接使用现有支持主从复制的 MySQL 镜像。

  2. 如果要自己配置,可以从 MySQL 官方镜像开始,通过设置 my.cnf 文件来支持主从复制。

  3. 主要的配置如下:


    • 主节点配置(Master):设置 server-id,并启用二进制日志(log-bin)。

    • 从节点配置(Slave):设置不同的 server-id,并配置为从属节点。

步骤 3:创建 Kubernetes Secret 存储 MySQL 密码

为了安全性,我们可以使用 Kubernetes Secret 来存储 MySQL 密码。

apiVersion: v1
kind: Secret
metadata:
name: mysql-secret
type: Opaque
data:
mysql-root-password: <base64编码的root密码>
mysql-replication-user: <base64编码的replication用户名>
mysql-replication-password: <base64编码的replication密码>

步骤 4:部署 MySQL 主节点

  1. 创建主节点的配置文件
    mysql-master-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql-master
spec:
replicas: 1
selector:
  matchLabels:
    app: mysql
    role: master
template:
  metadata:
    labels:
      app: mysql
      role: master
  spec:
    containers:
    - name: mysql
      image: mysql:5.7
      env:
      - name: MYSQL_ROOT_PASSWORD
        valueFrom:
          secretKeyRef:
            name: mysql-secret
            key: mysql-root-password
      - name: MYSQL_REPLICATION_USER
        valueFrom:
          secretKeyRef:
            name: mysql-secret
            key: mysql-replication-user
      - name: MYSQL_REPLICATION_PASSWORD
        valueFrom:
          secretKeyRef:
            name: mysql-secret
            key: mysql-replication-password
      ports:
      - containerPort: 3306
      volumeMounts:
      - name: mysql-persistent-storage
        mountPath: /var/lib/mysql
    volumes:
    - name: mysql-persistent-storage
      persistentVolumeClaim:
        claimName: mysql-pv-claim
  1. 创建 MySQL 主节点的 Service:

apiVersion: v1
kind: Service
metadata:
name: mysql-master
spec:
ports:
- port: 3306
  targetPort: 3306
selector:
  app: mysql
  role: master

步骤 5:部署 MySQL 从节点

  1. 创建从节点的配置文件
    mysql-slave-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql-slave
spec:
replicas: 2
selector:
  matchLabels:
    app: mysql
    role: slave
template:
  metadata:
    labels:
      app: mysql
      role: slave
  spec:
    containers:
    - name: mysql
      image: mysql:5.7
      env:
      - name: MYSQL_ROOT_PASSWORD
        valueFrom:
          secretKeyRef:
            name: mysql-secret
            key: mysql-root-password
      - name: MYSQL_REPLICATION_USER
        valueFrom:
          secretKeyRef:
            name: mysql-secret
            key: mysql-replication-user
      - name: MYSQL_REPLICATION_PASSWORD
        valueFrom:
          secretKeyRef:
            name: mysql-secret
            key: mysql-replication-password
      - name: MYSQL_MASTER_HOST
        value: "mysql-master"
      ports:
      - containerPort: 3306
      volumeMounts:
      - name: mysql-persistent-storage
        mountPath: /var/lib/mysql
    volumes:
    - name: mysql-persistent-storage
      persistentVolumeClaim:
        claimName: mysql-pv-claim
  1. 创建 MySQL 从节点的 Service:

apiVersion: v1
kind: Service
metadata:
name: mysql-slave
spec:
ports:
- port: 3306
  targetPort: 3306
selector:
  app: mysql
  role: slave

步骤 6:设置主从复制

在从节点启动后,执行以下命令来配置主从复制:

  1. 登录主节点,创建用于复制的用户:

    CREATE USER 'replication'@'%' IDENTIFIED BY 'replication_password';
    GRANT REPLICATION SLAVE ON *.* TO 'replication'@'%';
    FLUSH PRIVILEGES;
  2. 获取主节点状态:

    SHOW MASTER STATUS;
  3. 登录到从节点,将其配置为主节点的从属节点:

    CHANGE MASTER TO
      MASTER_HOST='mysql-master',
      MASTER_USER='replication',
      MASTER_PASSWORD='replication_password',
      MASTER_LOG_FILE='<上一步中获取的 File>',
      MASTER_LOG_POS=<上一步中获取的 Position>;
    START SLAVE;
  4. 检查从节点状态以确认同步是否成功:

    SHOW SLAVE STATUS\G

步骤 7:配置读写分离

在 Kubernetes 中,可以使用一个自定义的 Service 来实现读写分离:

  1. 创建 MySQL 读写分离的 Service:

    apiVersion: v1
    kind: Service
    metadata:
    name: mysql-read-write
    spec:
    ports:
    - port: 3306
      targetPort: 3306
    selector:
      app: mysql
      role: master
    ---
    apiVersion: v1
    kind: Service
    metadata:
    name: mysql-read-only
    spec:
    ports:
    - port: 3306
      targetPort: 3306
    selector:
      app: mysql
      role: slave
  2. 通过应用层(例如应用代码)选择访问不同的 Service 来实现读写分离:


    • 写操作:通过
      mysql-read-write
      Service 连接。

    • 读操作:通过
      mysql-read-only
      Service 连接。

步骤 8:测试读写分离

  1. 将写操作请求发送到
    mysql-read-write
    服务,验证数据是否被正确写入。

  2. 将读操作请求发送到
    mysql-read-only
    服务,确保从节点上能够读到主节点写入的数据。

步骤 9:监控与维护

可以通过 Prometheus 和 Grafana 对 MySQL 集群进行监控,关注主从复制的延迟和节点的健康状态,以便及时处理故障。

总结

主节点负责处理写操作,从节点负责处理读操作,应用可以根据需求连接到不同的 Service 来实现高效的数据库读写分离。

web 21——弱口令爆破&custom iterator

进去要求输入账号密码,账号输入
admin
,一般来说管理员用户名都会是这个,密码随便输,然后burpsuite抓包
可以看到账号密码在
Authorization
传输,形式是
账号:密码
的base64加密,把他发到
Intruder
模块

模式选
sniper
,因为要对整个账号密码字符进行加密,不能分开爆破,选中要爆破的地方

选择
custom iterator
模式,在位置1写入
admin
,分隔符写
:

位置2导入提供的字典

添加
base64
加密,取消选中Palyload Encoding编码,因为在进行base64加密的时候在最后可能存在
==
这样就会影响base64加密的结果

开始攻击,点击状态码进行筛选,找到爆破出的密码,将密码解密后为
shark63
,输入即可得到flag

web 22——子域名爆破&oneforall

OneForAll,是 shmilylty 在 Github 上开源的子域收集工具,可以实现对子域名的爆破

python oneforall.py --target ctf.show run

可以看到爆破出了很多结果,不过这题的域名失效了,不然应该会有一个
flag.ctf.show

web 23——md5爆破&burp&python

看一下代码,通过
get
方式提交一个
token
参数,要求MD5 加密结果的第 2 位、第 15 位、第 18 位字符是否相等,且这三位字符的数字之和除以第 2 位字符的值是否等于第 32 位字符的数字值

方法1——burpsuite爆破

不管他到底什么条件,直接burpsuite爆破数字0-500,发现422的时候返回长度不同,422就是满足条件的

方法2——python脚本爆破

通过遍历二字符的字符串,寻找符合条件的字符串,得到两个可用字符串
3j

ZE

import hashlib  
  
dic = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"  
for a in dic:  
    for b in dic:  
        t = str(a) + str(b)  
        md5 = hashlib.md5(t.encode('utf-8')).hexdigest()  
  
        if md5[1] == md5[14] == md5[17]:  # 确保这些位置的字符相同  
            # 确保这些字符是数字  
            if 48 <= ord(md5[1]) <= 57 and 48 <= ord(md5[14]) <= 57 and 48 <= ord(md5[17]) <= 57:  
                # 确保md5[31]也是数字,并符合数学关系  
                if 48 <= ord(md5[31]) <= 57:  
                    num1 = int(md5[1])  
                    num14 = int(md5[14])  
                    num17 = int(md5[17])  
                    num31 = int(md5[31])  
  
                    # 判断除数是否为零  
                    if num1 == 0:  
                        continue  # 跳过当前循环  
  
                    if (num1 + num14 + num17) / num1 == num31:  
                        print(t)

web 24——初探伪随机数

本题考察的是php伪随机数,通过
mt_srand(1);
播种后,再通过同一随机数算法计算出来的随机数值是固定的,因此只要看一下服务器php版本,然后在本地起一下以下代码就可以得到随机数了,各位师傅也可以尝试刷新,会发现每次给出来的值都是同一个。

<?php
mt_srand(372619038);
echo "随机数:".mt_rand();
?>

web 25——伪随机数&种子爆破

要获得flag,必须输入
token
为第二、三个随机数的和,因此必须知道
seed
是什么

传入
?r=0
就可以获得第一个随机数的负值,为
-449307572

接下来就要爆破
seed
,这里我们使用php_mt_seed工具,下载与使用方法请自行百度。可以看到爆出来很多
seed
,由于php版本不同产生的随机数会略有区别,因此需要选择与服务器php版本对应的
seed

看一眼php版本,选择1103714832,这里可能得几个都试试,博主试了后两个都没出来

写个php脚本输出需要的随机数

<?php
    mt_srand(1103714832);
    echo mt_rand()."\n";
    $result = mt_rand()+mt_rand();
    echo $result;
?>

提交
r

token
,得到flag

web 26——数据库密码爆破

本题还是弱口令爆破,就是换到了系统安装的场景,直接对密码进行数字的爆破即可,答案是7758521,爆破的量还挺大的
另外这题的代码逻辑有点问题,什么都不填点安装然后抓包就会发现flag直接在返回包里了,不过这样就没有爆破的味道了,还是建议按上面的方法爆一下

web 27——门户网站爆破

看到一个登陆界面,但是现在啥信息都没有,肯定不能直接爆破,看到下面有录取名单和学籍信息查询系统

看到这里,猜测是爆破身份证号(这里是生日被隐藏了),然后通过录取查询获得密码

抓包,这题很奇怪,火狐好像很难抓到包,要么用谷歌抓,或者用火狐一直点,总归能抓到。给对生日进行爆破,payload类型选日期,选择开始与结束的年月日,选择日期格式,y代表年,M代表月,d代表日

找到长度不同的数据包

返回信息需要Unicode解码一下,结果给出学号和密码,登陆得到flag

# 原始字符串
encoded_str = r"\u606d\u559c\u60a8\uff0c\u60a8\u5df2\u88ab\u6211\u6821\u5f55\u53d6\uff0c\u4f60\u7684\u5b66\u53f7\u4e3a02015237 \u521d\u59cb\u5bc6\u7801\u4e3a\u8eab\u4efd\u8bc1\u53f7\u7801"
# 使用 unicode_escape 解码
decoded_str = encoded_str.encode('utf-8').decode('unicode_escape')
print(decoded_str)

web 28——目录爆破

看到url是
/0/1/2.txt
,猜测是对目录中的数字进行爆破,删掉2.txt,对
0

1
爆破,用
cluster bomb
模式

设置payload set 1和2都为数字0-99

爆破,找到能访问的目录,看一眼返回包就是flag