基于CefSharp开发浏览器(八)浏览器收藏夹栏 - 咸鱼翻身? - 博客园

mikel阅读(665)

来源: 基于CefSharp开发浏览器(八)浏览器收藏夹栏 – 咸鱼翻身? – 博客园

一、前言

上一篇文章 基于CefSharp开发(七)浏览器收藏夹菜单 简单实现了部分收藏夹功能 如(添加文件夹、添加收藏、删除、右键菜单部分功能)

后续代码中对MTreeViewItem进行了扩展,增加了TextBox用于编辑Item及相应的依赖属性,实现了重命名操作。

浏览器除了有收藏夹菜单,还需要有收藏夹栏用于快捷访问,本章将开发简易的收藏夹栏。

二、收藏夹栏分析

如下面两幅图所示,前者为收藏夹菜单样式,后者为收藏夹栏样式,两者数据结构相同,只是展示形式略有差异。故可采用同一数据结构

观其展示形式与MMenu接近,故此处将采用Menu进行样式扩展开发。

收藏夹栏支持右键功能,故需要添加ContextMenu支持。

收藏夹栏与收藏夹菜单展示的同一数据,属于同一功能,故需增加联动。

下面将逐步完成以上内容的开发,由于样式及结构均与收藏夹菜单接近,故本章不对样式进行叙述,直撸代码

三、创建自定义控件并添加至WebTabControlUc

1、新增定义控件MFavorites、MFavoritesItem

样式可复制MMenu、MMenuItem

MFavorites.xaml

复制代码
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:Cys_CustomControls.Controls">
    <Style TargetType="{x:Type local:MFavorites}">
        <Setter Property="Foreground" Value="{DynamicResource ColorBrush.FontDefaultColor}"/>
        <Setter Property="Background" Value="Transparent"/>
        <Setter Property="Width" Value="Auto"/>
        <Setter Property="Height" Value="35"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:MFavorites}">
                    <Border Background="{TemplateBinding Background}"
                            BorderThickness="{TemplateBinding BorderThickness}"
                            BorderBrush="{TemplateBinding BorderBrush}" 
                            Padding="{TemplateBinding Padding}" 
                            SnapsToDevicePixels="true">
                        <ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>
复制代码

MFavorites.xaml.cs

复制代码
public class MFavorites : Menu
{
    static MFavorites()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(MFavorites), new FrameworkPropertyMetadata(typeof(MFavorites)));
    }
}
复制代码

MFavoritesItem.xaml

复制代码
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:Cys_CustomControls.Controls">

    <ControlTemplate x:Key="MTopLevelHeaderTemplate" TargetType="{x:Type local:MFavoritesItem}">
        <Border x:Name="templateRoot" Background="{TemplateBinding Background}" BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}" CornerRadius="{TemplateBinding CornerRadius}" SnapsToDevicePixels="true">
            <Grid>
                <Grid VerticalAlignment="Center" HorizontalAlignment="Center" x:Name="PART_TextGrid" Opacity="0.8" Margin="10,0">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="Auto"/>
                        <ColumnDefinition Width="Auto"/>
                    </Grid.ColumnDefinitions>
                    <TextBlock x:Name="Icon" FontSize="20" HorizontalAlignment="Center" Text="{TemplateBinding Icon}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="Center" Foreground="{TemplateBinding IconForeground}"/>
                    <ContentPresenter Margin="10,0,0,0" Grid.Column="1" ContentSource="Header" x:Name="PART_Header" HorizontalAlignment="Center" RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="Center"/>
                </Grid>
                <Popup x:Name="PART_Popup" AllowsTransparency="true" Focusable="false" IsOpen="{Binding IsSubmenuOpen, RelativeSource={RelativeSource TemplatedParent}}" 
                   Placement="Bottom" PopupAnimation="{DynamicResource {x:Static SystemParameters.MenuPopupAnimationKey}}" PlacementTarget="{Binding ElementName=templateRoot}" Width="{TemplateBinding PopupWidth}">
                    <Border x:Name="SubMenuBorder" Margin="0 0 5 5" >
                        <Border.Effect>
                            <DropShadowEffect Color="{DynamicResource Color.MenuItemDropShadowBrush}" Opacity="0.3" ShadowDepth="3"/>
                        </Border.Effect>
                        <Border Background="{DynamicResource WebBrowserBrushes.WebMenuBackground}" BorderThickness="1" CornerRadius="5">
                            <ScrollViewer x:Name="SubMenuScrollViewer" Style="{DynamicResource {ComponentResourceKey ResourceId=MenuScrollViewer, TypeInTargetAssembly={x:Type FrameworkElement}}}" Margin="0,5">
                                <Grid RenderOptions.ClearTypeHint="Enabled" Background="Transparent">
                                    <Canvas HorizontalAlignment="Left" Height="0" VerticalAlignment="Top" Width="0">
                                        <Rectangle x:Name="OpaqueRect" Fill="{Binding Background, ElementName=SubMenuBorder}" 
                                                   Height="{Binding ActualHeight, ElementName=SubMenuBorder}"
                                                   Width="{Binding ActualWidth, ElementName=SubMenuBorder}"/>
                                    </Canvas>
                                    <ItemsPresenter x:Name="ItemsPresenter" KeyboardNavigation.DirectionalNavigation="Cycle" 
                                                    Grid.IsSharedSizeScope="true" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" KeyboardNavigation.TabNavigation="Cycle"/>
                                </Grid>
                            </ScrollViewer>
                        </Border>
                    </Border>
                </Popup>
            </Grid>
        </Border>
        <ControlTemplate.Triggers>
            <Trigger Property="IsSuspendingPopupAnimation" Value="true">
                <Setter Property="PopupAnimation" TargetName="PART_Popup" Value="None"/>
            </Trigger>
            <!--<Trigger Property="Icon" Value="{x:Null}">
                <Setter Property="Visibility" TargetName="Icon" Value="Collapsed"/>
            </Trigger>-->
            <Trigger Property="ScrollViewer.CanContentScroll" SourceName="SubMenuScrollViewer" Value="false">
                <Setter Property="Canvas.Top" TargetName="OpaqueRect" Value="{Binding VerticalOffset, ElementName=SubMenuScrollViewer}"/>
                <Setter Property="Canvas.Left" TargetName="OpaqueRect" Value="{Binding HorizontalOffset, ElementName=SubMenuScrollViewer}"/>
            </Trigger>
            <Trigger Property="IsHighlighted" Value="True">
                <Setter TargetName="templateRoot" Property="Background"  Value="{DynamicResource WebBrowserBrushes.WebMenuIsMouseOverBackground}"/>
                <Setter TargetName="PART_TextGrid" Property="Opacity"  Value="1"/>
            </Trigger>
        </ControlTemplate.Triggers>
    </ControlTemplate>

    <ControlTemplate x:Key="MTopLevelItemTemplate" TargetType="{x:Type local:MFavoritesItem}">
        <Border x:Name="templateRoot" Background="{TemplateBinding Background}" BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}" CornerRadius="{TemplateBinding CornerRadius}" SnapsToDevicePixels="true">
            <Grid VerticalAlignment="Center" HorizontalAlignment="Center" x:Name="PART_TextGrid" Opacity="0.8" Margin="10,0">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition Width="Auto"/>
                </Grid.ColumnDefinitions>
                <TextBlock x:Name="Icon" FontSize="20" HorizontalAlignment="Center" Text="{TemplateBinding Icon}" Foreground="{TemplateBinding IconForeground}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="Center"/>
                <ContentPresenter Margin="10,0,0,0" ContentSource="Header" HorizontalAlignment="Center" Grid.Column="1" RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="Center"/>
            </Grid>
        </Border>
        <ControlTemplate.Triggers>
            <!--<Trigger Property="Icon" Value="{x:Null}">
                <Setter Property="Visibility" TargetName="Icon" Value="Collapsed"/>
            </Trigger>-->
            <Trigger Property="IsHighlighted" Value="True">
                <Setter TargetName="templateRoot" Property="Background"  Value="{DynamicResource WebBrowserBrushes.WebMenuIsMouseOverBackground}"/>
                <Setter TargetName="PART_TextGrid" Property="Opacity"  Value="1"/>
            </Trigger>
        </ControlTemplate.Triggers>
    </ControlTemplate>

    <ControlTemplate  x:Key="MSubmenuHeaderTemplate" TargetType="{x:Type local:MFavoritesItem}">
        <Border x:Name="templateRoot" Background="{TemplateBinding Background}" BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}" Height="35" SnapsToDevicePixels="true">
            <Grid >
                <Grid HorizontalAlignment="Left" VerticalAlignment="Center" Margin="10,0">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="Auto"/>
                        <ColumnDefinition Width="Auto"/>
                    </Grid.ColumnDefinitions>
                    <TextBlock Grid.Column="0" x:Name="Icon" Width="20" FontSize="20" HorizontalAlignment="Center" Text="{TemplateBinding Icon}" Foreground="{TemplateBinding IconForeground}"
                               SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="Center"/>
                    <ContentPresenter Margin="10,0,0,0" Grid.Column="1" ContentSource="Header"  HorizontalAlignment="Center" RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="Center"/>
                </Grid>
                <Path x:Name="RightArrow" Data="M 0,0 L 7,7 L 0,14 Z" 
                      Fill="{TemplateBinding Foreground}" 
                      HorizontalAlignment="Right" Margin="0,0,5,0" VerticalAlignment="Center" Opacity="0.6"/>

                <Popup x:Name="PART_Popup" AllowsTransparency="true" Focusable="false" IsOpen="{Binding IsSubmenuOpen, RelativeSource={RelativeSource TemplatedParent}}" 
                   Placement="Right" PopupAnimation="{DynamicResource {x:Static SystemParameters.MenuPopupAnimationKey}}" PlacementTarget="{Binding ElementName=templateRoot}" VerticalOffset="-5" Width="{TemplateBinding PopupWidth}">
                    <Border x:Name="SubMenuBorder" Margin="0 0 5 5" >
                        <Border.Effect>
                            <DropShadowEffect Color="{DynamicResource Color.MenuItemDropShadowBrush}" Opacity="0.3" ShadowDepth="3"/>
                        </Border.Effect>
                        <Border Background="{DynamicResource WebBrowserBrushes.WebMenuBackground}" BorderThickness="1" CornerRadius="5">
                            <ScrollViewer x:Name="SubMenuScrollViewer" Style="{DynamicResource {ComponentResourceKey ResourceId=MenuScrollViewer, TypeInTargetAssembly={x:Type FrameworkElement}}}" Margin="0,5">
                                <Grid RenderOptions.ClearTypeHint="Enabled" Background="Transparent">
                                    <Canvas HorizontalAlignment="Left" Height="0" VerticalAlignment="Top" Width="0">
                                        <Rectangle x:Name="OpaqueRect" Fill="{Binding Background, ElementName=SubMenuBorder}" 
                                                   Height="{Binding ActualHeight, ElementName=SubMenuBorder}"
                                                   Width="{Binding ActualWidth, ElementName=SubMenuBorder}"/>
                                    </Canvas>
                                    <ItemsPresenter x:Name="ItemsPresenter" KeyboardNavigation.DirectionalNavigation="Cycle" 
                                                    Grid.IsSharedSizeScope="true" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" KeyboardNavigation.TabNavigation="Cycle"/>
                                </Grid>
                            </ScrollViewer>
                        </Border>
                    </Border>
                </Popup>
            </Grid>
        </Border>
        <ControlTemplate.Triggers>
            <Trigger Property="IsSuspendingPopupAnimation" Value="true">
                <Setter Property="PopupAnimation" TargetName="PART_Popup" Value="None"/>
            </Trigger>
            <!--<Trigger Property="Icon" Value="{x:Null}">
                <Setter Property="Visibility" TargetName="Icon" Value="Collapsed"/>
            </Trigger>-->
            <Trigger Property="IsChecked" Value="True">
                <Setter Property="Visibility" TargetName="Icon" Value="Collapsed"/>
            </Trigger>
            <Trigger Property="ScrollViewer.CanContentScroll" SourceName="SubMenuScrollViewer" Value="false">
                <Setter Property="Canvas.Top" TargetName="OpaqueRect" Value="{Binding VerticalOffset, ElementName=SubMenuScrollViewer}"/>
                <Setter Property="Canvas.Left" TargetName="OpaqueRect" Value="{Binding HorizontalOffset, ElementName=SubMenuScrollViewer}"/>
            </Trigger>
            <Trigger Property="IsHighlighted" Value="True">
                <Setter TargetName="templateRoot" Property="Background"  Value="{DynamicResource WebBrowserBrushes.WebMenuIsMouseOverBackground}"/>
            </Trigger>
        </ControlTemplate.Triggers>
    </ControlTemplate>

    <ControlTemplate x:Key="MSubmenuItemTemplate" TargetType="{x:Type local:MFavoritesItem}">
        <Border x:Name="templateRoot" Background="{TemplateBinding Background}" BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}" Height="35" SnapsToDevicePixels="true">
            <Grid HorizontalAlignment="Left" VerticalAlignment="Center" Margin="10,0">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition Width="Auto"/>
                </Grid.ColumnDefinitions>
                <TextBlock Grid.Column="0" x:Name="Icon" Width="20" FontSize="20" HorizontalAlignment="Center" Text="{TemplateBinding Icon}" Foreground="{TemplateBinding IconForeground}"
                           SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="Center"/>
                <ContentPresenter Margin="10,0,0,0" Grid.Column="1" x:Name="menuHeaderContainer"  HorizontalAlignment="Center" ContentSource="Header" RecognizesAccessKey="True" 
                                  SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="Center"/>
            </Grid>
        </Border>
        <ControlTemplate.Triggers>
            <!--<Trigger Property="Icon" Value="{x:Null}">
                <Setter Property="Visibility" TargetName="Icon" Value="Collapsed"/>
            </Trigger>-->
            <Trigger Property="IsChecked" Value="True">
                <Setter Property="Visibility" TargetName="Icon" Value="Collapsed"/>
            </Trigger>
            <Trigger Property="IsHighlighted" Value="True">
                <Setter TargetName="templateRoot" Property="Background"  Value="{DynamicResource WebBrowserBrushes.WebMenuIsMouseOverBackground}"/>
            </Trigger>
        </ControlTemplate.Triggers>
    </ControlTemplate>

    <Style TargetType="{x:Type local:MFavoritesItem}">
        <Setter Property="BorderBrush" Value="Transparent"/>
        <Setter Property="CornerRadius" Value="5"/>
        <Setter Property="Background" Value="{DynamicResource WebBrowserBrushes.TabHeaderIsSelectedBackground}"/>
        <Setter Property="Foreground" Value="{DynamicResource ColorBrush.FontPrimaryColor}"/>
        <Setter Property="FontSize" Value="13"/>
        <Setter Property="FontFamily" Value="Microsoft YaHei"/>
        <Setter Property="Cursor" Value="Hand"/>
        <Setter Property="Height" Value="35"/>
        <Setter Property="Width" Value="Auto"/>
        <Setter Property="MinWidth" Value="40"/>
        <Setter Property="HorizontalContentAlignment" Value="{Binding HorizontalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
        <Setter Property="VerticalContentAlignment" Value="{Binding VerticalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
        <Setter Property="ScrollViewer.PanningMode" Value="Both"/>
        <Setter Property="Stylus.IsFlicksEnabled" Value="False"/>
        <Setter Property="Template" Value="{StaticResource MSubmenuItemTemplate}"/>
        <Style.Triggers>
            <Trigger Property="Role" Value="TopLevelHeader">
                <Setter Property="BorderThickness" Value="0,0,1,0"/>
                <Setter Property="PopupWidth" Value="300"/>
                <Setter Property="Template" Value="{StaticResource MTopLevelHeaderTemplate}"/>
            </Trigger>
            <Trigger Property="Role" Value="TopLevelItem">
                <Setter Property="BorderThickness" Value="0,0,1,0"/>
                <Setter Property="Template" Value="{StaticResource MTopLevelItemTemplate}"/>
            </Trigger>
            <Trigger Property="Role" Value="SubmenuHeader">
                <Setter Property="PopupWidth" Value="300"/>
                <Setter Property="Background" Value="Transparent"/>
                <Setter Property="Foreground" Value="{DynamicResource ColorBrush.FontPrimaryColor}"/>
                <Setter Property="Template" Value="{StaticResource MSubmenuHeaderTemplate}"/>
                <Setter Property="Padding" Value="3,0,0,0"/>
            </Trigger>
            <Trigger Property="Role" Value="SubmenuItem">
                <Setter Property="Foreground" Value="{DynamicResource ColorBrush.FontPrimaryColor}"/>
                <Setter Property="Background" Value="Transparent"/>
                <Setter Property="Template" Value="{StaticResource MSubmenuItemTemplate}"/>
                <Setter Property="Padding" Value="3,0,0,0"/>
            </Trigger>
        </Style.Triggers>
    </Style>
</ResourceDictionary>
复制代码

MFavoritesItem.xaml.cs

复制代码
public class MFavoritesItem : MenuItem
{
    static MFavoritesItem()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(MFavoritesItem), new FrameworkPropertyMetadata(typeof(MFavoritesItem)));
    }

    #region == DependencyProperty==
    #region == CornerRadius==
    public static readonly DependencyProperty CornerRadiusProperty = DependencyProperty.Register("CornerRadius", typeof(CornerRadius), typeof(MFavoritesItem),
        new PropertyMetadata(null));

    /// <summary>
    /// CornerRadius
    /// </summary>
    public CornerRadius CornerRadius
    {
        get => (CornerRadius)GetValue(CornerRadiusProperty);
        set => SetValue(CornerRadiusProperty, value);
    }
    #endregion

    #region == PopupWidth==
    public static readonly DependencyProperty PopupWidthProperty = DependencyProperty.Register("PopupWidth", typeof(double), typeof(MFavoritesItem),
        new PropertyMetadata(null));

    /// <summary>
    /// PopupWidth
    /// </summary>
    public double PopupWidth
    {
        get => (double)GetValue(PopupWidthProperty);
        set => SetValue(PopupWidthProperty, value);
    }
    #endregion

    /// <summary>
    /// IconForeground 字体图标前景色
    /// </summary>
    public static readonly DependencyProperty IconForegroundProperty = DependencyProperty.Register("IconForeground", typeof(Brush), typeof(MFavoritesItem));
    public Brush IconForeground
    {
        get => (Brush)GetValue(IconForegroundProperty);
        set => SetValue(IconForegroundProperty, value);
    }

    /// <summary>
    /// ItemMargin 
    /// </summary>
    public static readonly DependencyProperty ItemMarginProperty = DependencyProperty.Register("ItemMargin", typeof(Thickness), typeof(MFavoritesItem));
    public Thickness ItemMargin
    {
        get => (Thickness)GetValue(ItemMarginProperty);
        set => SetValue(ItemMarginProperty, value);
    }

    /// <summary>
    /// TextMaxWidth 
    /// </summary>
    public static readonly DependencyProperty TextMaxWidthProperty = DependencyProperty.Register("TextMaxWidth", typeof(double), typeof(MFavoritesItem));
    public double TextMaxWidth
    {
        get => (double)GetValue(TextMaxWidthProperty);
        set => SetValue(TextMaxWidthProperty, value);
    }
    #endregion

    public int Type { get; set; }

    public int Level { get; set; }
    public int NodeId { get; set; }
}
复制代码

2、接着新增用户控件FavoritesBarUc

用于承接MFavorites代码如下:

FavoritesBarUc.xaml

复制代码
<UserControl x:Class="MWebBrowser.View.FavoritesBarUc"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:controls="clr-namespace:Cys_CustomControls.Controls;assembly=Cys_CustomControls"
             mc:Ignorable="d" 
             d:DesignHeight="40" d:DesignWidth="800" Height="40" Background="{DynamicResource WebBrowserBrushes.TabHeaderIsSelectedBackground}">
    <Grid VerticalAlignment="Center">
        <controls:MFavorites x:Name="MenuParent" ContextMenuOpening="FavoritesTree_OnContextMenuOpening" ScrollViewer.HorizontalScrollBarVisibility="Hidden" PreviewMouseLeftButtonUp="FavoritesTree_OnPreviewMouseLeftButtonUp">
            <controls:MFavorites.ContextMenu>
                <ContextMenu x:Name="FavoritesContextMenu" Style="{DynamicResource WebCustomMenus.DefaultContextMenu}">
                    <controls:MMenuItem Tag="0" x:Name="OpenAllFolder" Header="全部打开(16个)" Icon="&#xe600;"/>
                    <controls:MMenuItem Tag="1" x:Name="OpenNewAllFolder" Header="在新建窗口中全部打开(16个)" Icon="&#xe602;"/>
                    <controls:MMenuItem Tag="2" Header="在新 InPrivate窗口全部打开(16个)" Icon="&#xe68c;"/>
                    <controls:MMenuItem Tag="4" Header="按名称排序" Icon="&#xe606;"/>
                    <controls:MMenuItem Tag="5" x:Name="ReName" Header="重命名" Icon="&#xe712;" Click="ReName_OnClick"/>
                    <controls:MMenuItem Tag="6" x:Name="DeleteNode" Header="删除" Icon="&#xe74e;" IconFontSize="26" Click="Delete_OnClick"/>
                    <controls:MMenuItem Tag="7" Header="将当前标签页添加到文件夹" Icon="&#xe659;" Click="AddFavorites_OnClick"/>
                    <controls:MMenuItem Tag="8" Header="将所有标签页添加到文件夹" Visibility="Collapsed"/>
                    <controls:MMenuItem Tag="9" Header="添加文件夹" Icon="&#xe652;" Click="AddFolder_OnClick"/>
                </ContextMenu>
            </controls:MFavorites.ContextMenu>
        </controls:MFavorites>
        <Popup x:Name="ReNamePop" PopupAnimation="Fade" Placement="Bottom"  PlacementTarget="{Binding ElementName=MenuParent}"
               StaysOpen="False" AllowsTransparency="True" VerticalOffset="-40">
            <Border Background="{DynamicResource WebBrowserBrushes.WebMenuBackground}" CornerRadius="5">
                <Border.Effect>
                    <DropShadowEffect Color="{DynamicResource Color.MenuItemDropShadowBrush}" Opacity="0.3" ShadowDepth="3"/>
                </Border.Effect>
                <Grid Width="320" Height="140">
                    <Grid Margin="20,20,20,0">
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="Auto"/>
                        </Grid.RowDefinitions>
                        <TextBlock Grid.Row="0" Text="编辑文件夹名称" FontSize="18" Foreground="{DynamicResource ColorBrush.FontPrimaryColor}"/>
                        <StackPanel Grid.Row="1" Margin="0,10,0,0" Orientation="Horizontal" VerticalAlignment="Center">
                            <TextBlock Text="名称" Foreground="{DynamicResource ColorBrush.FontPrimaryColor}" VerticalAlignment="Center"/>
                            <TextBox x:Name="FolderName" Height="30" Width="236" Margin="10,0,0,0" Style="{DynamicResource TextBox.ReName}" VerticalAlignment="Center"/>
                        </StackPanel>
                        <StackPanel Grid.Row="2" Margin="0,15,0,0" Orientation="Horizontal" HorizontalAlignment="Right">
                            <Button Content="保存" Style="{DynamicResource Button.ReSave}" Click="ReSave_OnClick"/>
                            <Button Content="取消" Style="{DynamicResource Button.ReCancel}" Click="ReCancel_OnClick" Margin="10,0,0,0"/>
                        </StackPanel>
                    </Grid>
                </Grid>
            </Border>
        </Popup>
    </Grid>
</UserControl>
复制代码

FavoritesBarUc.xaml.cs

复制代码
using Cys_Common;
using Cys_Controls.Code;
using Cys_CustomControls.Controls;
using Cys_Model;
using MWebBrowser.Code.Helpers;
using MWebBrowser.ViewModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;

namespace MWebBrowser.View
{
    /// <summary>
    /// Interaction logic for FavoritesBarUc.xaml
    /// </summary>
    public partial class FavoritesBarUc : UserControl
    {
        private readonly double _textMaxWidth = 300;
        /// <summary>
        /// 记录当前右键选中的Item;
        /// </summary>
        private MFavoritesItem _currentRightItem;
        public Func<WebTabControlViewModel> GetWebUrlEvent;
        public Action<string> OpenNewTabEvent;
        public FavoritesBarUc()
        {
            InitializeComponent();
            this.Loaded += FavoritesBarUc_Loaded;
        }

        private void FavoritesBarUc_Loaded(object sender, System.Windows.RoutedEventArgs e)
        {
            if (this.IsInDesignMode()) return;
            GetFavoritesInfo();
        }

        private void GetFavoritesInfo()
        {
            List<TreeNode> root = GetNodes(-1, GlobalInfo.FavoritesSetting.FavoritesInfos);
            if (root == null || root.Count <= 0 || root[0].ChildNodes.Count <= 0) return;
            foreach (var child in root[0].ChildNodes)
            {
                AddFavoritesItem(null, child, true);
            }
        }

        private List<TreeNode> GetNodes(int parentId, List<TreeNode> nodes)
        {
            List<TreeNode> mainNodes = nodes.Where(x => x.ParentId == parentId).OrderByDescending(x => x.Type).ToList();
            List<TreeNode> otherNodes = nodes.Where(x => x.ParentId != parentId).OrderByDescending(x => x.Type).ToList();
            foreach (TreeNode node in mainNodes)
                node.ChildNodes = GetNodes(node.NodeId, otherNodes);
            return mainNodes;
        }

        /// <summary>
        /// 递归添加子集
        /// </summary>
        /// <param name="parent"></param>
        /// <param name="treeNode"></param>
        /// <param name="isRoot"></param>
        private void AddFavoritesItem(MFavoritesItem parent, TreeNode treeNode, bool isRoot)
        {
            var item = GetNewFavoritesItem(treeNode);
            if (treeNode.ChildNodes.Count > 0)
            {
                foreach (var child in treeNode.ChildNodes)
                {
                    AddFavoritesItem(item, child, false);
                }
            }

            if (!isRoot)
                parent.Items.Add(item);
            else
                MenuParent.Items.Add(item);
        }

        /// <summary>
        /// 获取FavoritesItem
        /// </summary>
        /// <param name="treeNode"></param>
        /// <returns></returns>
        private MFavoritesItem GetNewFavoritesItem(TreeNode treeNode)
        {
            return new MFavoritesItem
            {
                Header = treeNode.NodeName,
                Type = treeNode.Type,
                NodeId = treeNode.NodeId,
                Level = treeNode.Level,
                TextMaxWidth = _textMaxWidth,
                Icon = treeNode.Type == 0 ? "\ueb1e" : "\ue903",
                IconForeground = treeNode.Type == 0 ? new SolidColorBrush(Color.FromRgb(255, 255, 255)) : new SolidColorBrush(Color.FromRgb(255, 205, 44)),
            };
        }

        /// <summary>
        /// 处理右键菜单打开前的行为
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void FavoritesTree_OnContextMenuOpening(object sender, ContextMenuEventArgs e)
        {
            _currentRightItem = ControlHelper.FindVisualParent<MFavoritesItem>(e.OriginalSource as DependencyObject);
            if (null == _currentRightItem)
            {
                e.Handled = true;
                return;
            }
            if (_currentRightItem.Type == 0)
            {
                OpenAllFolder.Visibility = Visibility.Collapsed;
                OpenNewAllFolder.Visibility = Visibility.Collapsed;
                ReName.Visibility = Visibility.Collapsed;
            }
            else
            {
                OpenAllFolder.Visibility = Visibility.Visible;
                OpenNewAllFolder.Visibility = Visibility.Visible;
                ReName.Visibility = Visibility.Visible;
            }
        }

        private void FavoritesTree_OnPreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            var item = ControlHelper.FindVisualParent<MFavoritesItem>(e.OriginalSource as DependencyObject);
            if (item.Type == 1) return;
            if (!GlobalInfo.FavoritesSetting.FavoritesInfos.Exists(x => x.NodeId == item.NodeId)) return;
            var treeNode = GlobalInfo.FavoritesSetting.FavoritesInfos.First(x => x.NodeId == item.NodeId);
            OpenNewTabEvent?.Invoke(treeNode.Url);
        }

        /// <summary>
        /// 添加收藏
        /// </summary>
        /// <param name="sender"></param> 
        /// <param name="e"></param>
        private void AddFavorites_OnClick(object sender, RoutedEventArgs e)
        {
            var model = GetWebUrlEvent?.Invoke();
            if (null == model) return;
            if (_currentRightItem?.Type != 1) return;
            var newTreeNode = GetNewTreeNodeInfo(false, 0, model.Title, model.CurrentUrl);
            _currentRightItem.Items.Add(newTreeNode.Item2);
            GlobalInfo.FavoritesSetting.FavoritesInfos.Add(newTreeNode.Item1);
        }
        /// <summary>
        /// 添加文件夹
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void AddFolder_OnClick(object sender, RoutedEventArgs e)
        {
            var newTreeNode = GetNewTreeNodeInfo(false, 1, "新建文件夹", null);
            if (_currentRightItem != null && _currentRightItem.Type == 1)
            {
                _currentRightItem.Items.Add(newTreeNode.Item2);
                GlobalInfo.FavoritesSetting.FavoritesInfos.Add(newTreeNode.Item1);
            }
        }

        private Tuple<TreeNode, MFavoritesItem> GetNewTreeNodeInfo(bool isRoot, int type, string nodeName, string url)
        {
            int parentId = 0;
            int level = 1;
            if (!isRoot)
            {
                parentId = _currentRightItem.NodeId;
                level = parentId == -1 ? +1 : _currentRightItem.Level + 1;
            }
            int nodeMax = GlobalInfo.FavoritesSetting.FavoritesInfos.Max(x => x.NodeId);
            var treeNode = new TreeNode
            {
                Url = url,
                ParentId = parentId,
                NodeId = nodeMax + 1,
                NodeName = nodeName,
                Type = type,
                Level = level,
            };
            var favoritesItem = GetNewFavoritesItem(treeNode);
            return new Tuple<TreeNode, MFavoritesItem>(treeNode, favoritesItem);
        }

        #region 右键菜单操作

        /// <summary>
        /// 删除当前节点
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Delete_OnClick(object sender, RoutedEventArgs e)
        {
            if (_currentRightItem?.Parent == null) return;
            for (int i = _currentRightItem.Items.Count; i > 0; i--)
            {
                _currentRightItem.Items.Remove(_currentRightItem.Items[^1]);
                if (!GlobalInfo.FavoritesSetting.FavoritesInfos.Exists(x => x.NodeId == _currentRightItem.NodeId))
                    continue;
            }

            if (_currentRightItem.Parent is MFavoritesItem items)
            {
                if (GlobalInfo.FavoritesSetting.FavoritesInfos.Exists(x => x.NodeId == _currentRightItem.NodeId))
                {
                    var currentNode = (GlobalInfo.FavoritesSetting.FavoritesInfos.FirstOrDefault(x => x.NodeId == _currentRightItem.NodeId));
                    GlobalInfo.FavoritesSetting.FavoritesInfos.Remove(currentNode);
                }
                items.Items.Remove(_currentRightItem);
            }

            if (_currentRightItem.Parent is MFavorites parent)
            {
                if (GlobalInfo.FavoritesSetting.FavoritesInfos.Exists(x => x.NodeId == _currentRightItem.NodeId))
                {
                    var currentNode = (GlobalInfo.FavoritesSetting.FavoritesInfos.FirstOrDefault(x => x.NodeId == _currentRightItem.NodeId));
                    GlobalInfo.FavoritesSetting.FavoritesInfos.Remove(currentNode);
                }
                parent.Items.Remove(_currentRightItem);
            }
        }

        #region 重命名

        private void ReName_OnClick(object sender, RoutedEventArgs e)
        {
            if (null == _currentRightItem) return;
            if (_currentRightItem.Type == 0) return;

            ReNamePop.HorizontalOffset = (this.ActualWidth - 320) / 2;
            ReNamePop.IsOpen = true;
        }

        private void ReCancel_OnClick(object sender, RoutedEventArgs e)
        {
            ReNamePop.IsOpen = false;
        }

        private void ReSave_OnClick(object sender, RoutedEventArgs e)
        {
            ReNamePop.IsOpen = false;
            _currentRightItem.Header = FolderName.Text;
            if (!GlobalInfo.FavoritesSetting.FavoritesInfos.Exists(x => x.NodeId == _currentRightItem.NodeId)) return;
            var treeNode = GlobalInfo.FavoritesSetting.FavoritesInfos.First(x => x.NodeId == _currentRightItem.NodeId);
            treeNode.NodeName = FolderName.Text;
        }

        #endregion

        #endregion
    }
}
复制代码

该类中的方法用于初始化MFavorites数据

3、更改WebTabControlUc布局

新增一行用于展示FavoritesBarUc

<webBrowser:FavoritesBarUc Grid.Row="2"/>

四、运行效果

五、源码地址

gitee地址:https://gitee.com/sirius_machao/mweb-browser

项目邀请:如对该项目有兴趣,欢迎联系我共同开发!!!

抽一根烟的时间学会.NET Core 操作RabbitMQ - 青城同学 - 博客园

mikel阅读(730)

来源: 抽一根烟的时间学会.NET Core 操作RabbitMQ – 青城同学 – 博客园

什么是RabbitMQ?

RabbitMQ是由erlang语言开发的一个基于AMQP(Advanced Message Queuing Protocol)协议的企业级消息队列中间件。可实现队列,订阅/发布,路由,通配符等工作模式。

为什么要使用RabbitMQ?

  • 异步处理:比如发送邮件,发送短信等不需要等待处理结果的操作
  • 应用解耦:比如下单成功后,通知仓库发货,不需要等待仓库回应,通过消息队列去通知仓库,降低应用间耦合程序,可并行开发两个功能模块
  • 流量削锋:在抢购或者其他的活动页,服务处于爆发式请求状态,如果直连数据库,数据库容易被拖垮。抢购商品也容易出现库存超卖的情况。通过队列可有效解决该问题。
  • 日志处理:在单机中,日志直接写入到文件目录中,但是在分布式应用中,日志需要有统一的处理机制,可通过消息队列统一由某个消费端做处理。
  • 消息通信:如生产端和消费端可通过队列进行异步通信

如何安装RabbitMQ?

Windows端

  1. 安装erlang语言运行环境
    https://erlang.org/download/otp_win64_23.2.exe
    下载后直接下一步即可
  2. 安装RabbitMQ
    https://www.rabbitmq.com/install-windows.html#installer
    直接点击安装下一步即可按章
  3. 安装RabbitMQ的Web管理平台

RabbitMQ的管理平台是通过插件的形式使用,需要手动启用管理平台
在Windows下,RabbitMQ默认被安装到C:\Program Files\RabbitMQ Server\rabbitmq_server-3.8.14 下。
打开sbin ,在cmd或者powershell中执行
rabbitmq-plugins.bat enable rabbitmq_management

安装完成后,浏览器打开 http://localhost:15672/#/ 即可看到RabbitMQ的管理界面。输入默认账号密码 guest 成功登录。

Linux环境安装

  1. Ubuntu:https://www.rabbitmq.com/install-debian.html
  2. Centos:https://www.rabbitmq.com/install-rpm.html

RabbitMQ的基本概念了解一下?

生产者

发送消息的端

消费者

获取消息并处理的端

Connection

一个终端连接。每一个Connection都可以在RabbitMQ后台看到

Channel

Channel是建立在Connection上的一个虚拟通信管道。一般情况下,往消息队列中写入多条消息,为了不每条消息都建立一个TCP连接,所以RabbitMQ的做法是多条消息可以公用一个Connection,大大提高MQ的负载能力。

Exchange

Exchange是一个虚拟交换机。每一条消息都必须要通过交换机才能能进入对应的队列,可以理解为网络设备中的交换机,是一个意思。

Queue

Queue是一个存储消息的内部对象,所有的Rabbit MQ消息都存储在Queue中。生产者所生产的消息会存储在Queue中,消费者获取的消息也是从Queue中获取。

如何在.NET Core中使用RabbitMQ?

nuget安装

dotnet add package RabbitMQ.Client

创建生产者

const string QUEUENAME = “HELLO_MQ”;
//创建连接对象工厂
var factory = new ConnectionFactory()
{
UserName = “guest”,
Password = “guest”,
HostName = “localhost”,
Port = 5672, //RabbitMQ默认的端口
};
while (true)
{
using var conn = factory.CreateConnection();
var chanel = conn.CreateModel();
chanel.QueueDeclare(QUEUENAME, true, false, false);
Console.WriteLine(“输入生产内容:”);
var input = Console.ReadLine();
chanel.BasicPublish(“”, QUEUENAME, null, Encoding.Default.GetBytes(“hello rabbitmq:” + input));
}

在循环中,输入一个值,按下enter,即可推送一条消息到队列。

也可以直接在RabbitMQ的管理后台查看

可以看到我们发送的消息已经被RabbitMQ存储在Queue中了。只等某个幸运的消费者前来消费。

创建消费者

const string QUEUENAME = “HELLO_MQ”;
var factory = new ConnectionFactory()
{
UserName = “guest”,
Password = “guest”,
HostName = “localhost”,
Port = 5672,
};
var conn = factory.CreateConnection();
var chanel = conn.CreateModel();
chanel.QueueDeclare(QUEUENAME, true, false, false);
EventingBasicConsumer consumer = new EventingBasicConsumer(chanel);
consumer.Received += (a, e) =>
{
Console.WriteLine($“{DateTime.Now.ToString()}接收到消息:” + Encoding.Default.GetString(e.Body.ToArray()));
chanel.BasicAck(e.DeliveryTag, true); //收到回复后,RabbitMQ会直接在队列中删除这条消息
};
chanel.BasicConsume(QUEUENAME, false, consumer);
Console.WriteLine(“启动成功”);
Console.ReadLine();

启动成功后,consumer的Received方法,会收到一条来自MQ的消息,

如果处理完成后,不调用chennel的BasicAck方法,那么这条消息依然会存在,下次有消费者出现,会再次推送给消费者。

简单的RabbitMQ Hello World到这里就算完成了。接下来就是稍微高级一点的应用

RabbitMQ的工作模式

Work Queue 工作队列模式

工作队列模式的意思就是一个生产者对应多个消费者。RabbitMQ会使用轮询去给每个消费者发送消息。

publish/subscribe

发布订阅模式是属于比较用多的一种。

发布订阅,是由交换机发布消息给多个队列。多个队列再对应多个消费者。

发布订阅模式对应的交换机类型的fanout。

消费者

A

const string QUEUENAME = “HELLO_MQ_B”;
const string TESTEXCHANGE = “TESTEXCHANGE”;
var factory = new ConnectionFactory()
{
UserName = “guest”,
Password = “guest”,
HostName = “localhost”,
Port = 5672,
};
var conn = factory.CreateConnection();
var channel = conn.CreateModel();
//定义队列
channel.QueueDeclare(QUEUENAME, true, false, false);
//定义交换机
channel.ExchangeDeclare(TESTEXCHANGE, ExchangeType.Fanout, true, false);
//绑定队列到交换机
channel.QueueBind(QUEUENAME, TESTEXCHANGE, “”);
var consumer = new EventingBasicConsumer(channel);
consumer.Received += (a, e) =>
{
Console.WriteLine($“{DateTime.Now.ToString()}接收到消息:” + Encoding.Default.GetString(e.Body.ToArray()));
channel.BasicAck(e.DeliveryTag, true); //收到回复后,RabbitMQ会直接在队列中删除这条消息
};
channel.BasicConsume(QUEUENAME, false, consumer);
Console.WriteLine(“启动成功”);
Console.ReadLine();

B

const string QUEUENAME = “HELLO_MQ”;
const string TESTEXCHANGE = “TESTEXCHANGE”;
var factory = new ConnectionFactory()
{
UserName = “guest”,
Password = “guest”,
HostName = “localhost”,
Port = 5672,
};
var conn = factory.CreateConnection();
var channel = conn.CreateModel();
//定义队列
channel.QueueDeclare(QUEUENAME, true, false, false);
//定义交换机
channel.ExchangeDeclare(TESTEXCHANGE, ExchangeType.Fanout, true, false);
//绑定队列到交换机
channel.QueueBind(QUEUENAME, TESTEXCHANGE, “”);
var consumer = new EventingBasicConsumer(channel);
consumer.Received += (a, e) =>
{
Console.WriteLine($“{DateTime.Now.ToString()}接收到消息:” + Encoding.Default.GetString(e.Body.ToArray()));
channel.BasicAck(e.DeliveryTag, true); //收到回复后,RabbitMQ会直接在队列中删除这条消息
};
channel.BasicConsume(QUEUENAME, false, consumer);
Console.WriteLine(“启动成功”);
Console.ReadLine();

生产者

const string QUEUENAME = “HELLO_MQ”;
const string QUEUENAME_B = “HELLO_MQ_B”;
const string TESTEXCHANGE = “TESTEXCHANGE”;
//创建连接对象工厂
var factory = new ConnectionFactory()
{
UserName = “guest”,
Password = “guest”,
HostName = “localhost”,
Port = 5672, //RabbitMQ默认的端口
};
using var conn = factory.CreateConnection();
while (true)
{
var channel = conn.CreateModel();
//定义交换机
channel.ExchangeDeclare(TESTEXCHANGE, ExchangeType.Fanout, true, false);
Console.WriteLine(“输入生产内容:”);
var input = Console.ReadLine();
channel.BasicPublish(TESTEXCHANGE,“”, null, Encoding.Default.GetBytes(“hello rabbitmq:” + input));
}

在生产者运行成功后,RabbitMQ后台会出现一个交换机,点击交换机会看到交换机下绑定了两个队列


从生产者发送消息到队列,两个消费者会同时收到消息

routing模式

routing模式对应的交换机类型是direct,和发布订阅模式的区别在于:routing模式下,可以指定一个routingkey,用于区分消息
生产者

var channel = conn.CreateModel();
//定义交换机
channel.ExchangeDeclare(TESTEXCHANGE, ExchangeType.Direct, true, false);
//绑定队列到交换机
Console.WriteLine(“输入生产内容:”);
var input = Console.ReadLine();
channel.BasicPublish(TESTEXCHANGE, “INFO”, null, Encoding.Default.GetBytes(“hello rabbitmq:” + input));

消费者 A

//定义队列
channel.QueueDeclare(QUEUENAME, true, false, false);
//定义交换机
channel.ExchangeDeclare(TESTEXCHANGE, ExchangeType.Direct, true, false);
//绑定队列到交换机
channel.QueueBind(QUEUENAME, TESTEXCHANGE, “INFO”);

消费者 B

//定义队列
channel.QueueDeclare(QUEUENAME, true, false, false);
//定义交换机
channel.ExchangeDeclare(TESTEXCHANGE, ExchangeType.Direct, true, false);
//绑定队列到交换机
channel.QueueBind(QUEUENAME, TESTEXCHANGE, “ERROR”);

绑定成功后,发送消息,消费者A可以收到消息,消费者B无法收到消息。

如果遇到指定routingKey生产一条消息,结果 AB消费者都收到的情况。建议在RabbitMQ后台的交换机下看一下绑定的Queue是否重复绑定了多个routingKey.

topic通配符模式

在通配符模式下,RabbitMQ使用模糊匹配来决定把消息推送给哪个生产者。通配符有两个符号来匹配routingKey

  1. *匹配一个字符 如:*.qq.com 可匹配 1.qq.com
  2. #匹配一个或者多个字符。 如:*.qq.com 可匹配 1.qq.com或者1111.qq.com

其他的操作基本和routing模式一样。

header模式

header模式是把routingkey放到header中.取消掉了routingKey。并使用一个字典传递 K、V的方式来匹配。
比如同时要给用户发送邮件和短信,可直接通过header的键值对来匹配绑定的值,把消息传递给发短信和邮件的生产者.


广告时间:

成都南门这边有招BS方向高级.NET程序员的公司吗? 有的话,请私聊我。或者加我QQ:862640563


博客地址:https://www.cnblogs.com/boxrice/ 转载请注明出处

本文作者:青城同学

本文链接:https://www.cnblogs.com/boxrice/p/14475574.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

在 .NET Core 中应用六边形架构 - SpringLeee - 博客园

mikel阅读(958)

来源: 在 .NET Core 中应用六边形架构 – SpringLeee – 博客园

在本文中,您会看到一个Web API应用的模板,在.NET Core 中应用了六边形架构,并且里面包含了一些基础功能。

介绍

这是一个模板项目,里面集成了一些必备的基础功能,当我们需要开发一个新项目时,我们可以使用这个模板,这样我们可以聚焦业务。

模板集成了一些组件和功能如下:

  • 六边形架构的应用程序
  • Web API
  • Entityframework Core
  • 异常处理
  • NUnit
  • 版多本
  • Swagger UI
  • 日志 — SeriLog
  • Health checks UI
  • JWT authentication

什么是六边形架构

六边形架构(或称其为“端口和适配器架构风格”),将解决传统架构中维护应用程序的问题,而我们过去通常通过以数据库为中心的架构来实现, 所有输入和输出都通过端口进入或离开应用程序的核心内容,并且端口把应用程序和外部技术,工具和交付机制隔离开。

六边形架构带来了哪些好处

  • 插件化
    我们可以在项目中添加或者移除adapter, 比如我们可以替换 Rest adapter 或者 GraphQL 或者 gRPC adapter,并且它不会影响到我们的逻辑。
  • 可测试的能力
    因为它分离了所有层,所以很容易为每个组件编写一个测试用例。
  • 与数据库无关
    由于数据库与数据访问是分开的,因此切换数据库提供程序非常容易。
  • 整洁的代码
    由于业务逻辑和表示层是分开的,因此易于实现UI(例如React,Angular或Blazor)。

六边形架构的各层

  • Domain Api layer

Domain Api layer 在中心实现,并且不依赖于其他层。

  • Domain Layers (业务逻辑层)
  • Rest Adapter layer

Rest Adapter也称为左端口适配器和主适配器,在其中我们实现了静态服务(即GET,POST,PUT,DELETE等)。

  • Persistence Adapter layer
    Persistence Adapter,也称为辅助适配器,我们用了Entity Framework Core,DbContext将是UOW(工作单元)。
  • Bootstrap/Presentation Layer

开始使用六边形架构

首先,我们需要先安装一个模板插件,在VS中,选择 扩展 -> 联机,然后搜索 Hexagonal,并安装

然后选择模板,创建新项目:

它的解决方案是这样的:

您可以浏览下各层的代码结构,也可以启动项目,访问 /healthcheck-ui, 查看健康检查的页面

在本文中,我简单介绍了六角形体系结构,并且通过这个模板,可以快速的在.NET Core 中创建六边形架构的项目应用。

原文链接: https://www.c-sharpcorner.com/article/hexagonal-architecture-in-asp-net-core/

队列工厂之RabbitMQ - 神牛003 - 博客园

mikel阅读(1223)

来源: 队列工厂之RabbitMQ – 神牛003 – 博客园

本次和大家分享的是RabbitMQ队列的用法,前一篇文章队列工厂之(MSMQ)中在描述的时候已经搭建了简单工厂,因此本章内容是在其之上扩充的子项不再过多讲解工厂的代码了;RabbitMQ应该是现在互联网公司消息队列用的最多的一种之一吧,看看招聘基本都会有这个单词的出现,她相比前一篇分享的MSMQ来说配置更多样化,安装步骤数两者都差不多吧,最大差别MSMQ是windows捆绑的服务几乎只能在windows上使用,而Rabbit现目前运行支持的系统比较多;在写队列工厂第二篇文章的时候,其实代码已经都完成了目前队列工厂包括有如下队列(msmq,redis,rabbitmq),你可以去下载源码和测试用例:QueueReposity-队列工厂;希望大家能够喜欢,也希望各位多多”扫码支持“和”推荐“谢谢!

 

» RabbitMQ安装和控制台

» 封装RabbitMQ队列的读和写

» 队列工厂之RabbitMQ测试用例

 

下面一步一个脚印的来分享:

» RabbitMQ安装和控制台

要说RabbitMQ的安装,首先我们要下载对应服务器操作系统的RabbitMQ安装文件,因为她有对应不同操作系统的安装版本,这点需要注意;我本地电脑系统是win7(属于windows)所以去官网下载安装包:https://www.rabbitmq.com/,进入官网后选择“Installation”,能看到很多安装版本的下载源,这里可选择从Downloads on rabbitmq.com下载,点击“windows”即可下载:

目前最新版本地址:

https://www.rabbitmq.com/releases/rabbitmq-server/v3.6.6/rabbitmq-server-3.6.6.exe;

通常我都是进入这个界面的Installation Guides节点后-》With installer (recommended),这个时候进入的是windows系统所需的帮助文档吧,同样可以选择版本下载;进入该界面的主要目的是需要下载一个Erlang Windows的安装文件(更深层原因:RabbitMq运行依赖于Erlang语言),点击Erlang Windows Binary File进入下载界面,然后选择您操作系统对应的版本,如果您也是windows64位的可以直接用这个地址下载:

http://erlang.org/download/otp_win64_19.2.exe

此刻两个必须的东西已经下载完成,先安装erlang语言的exe,再安装rabbitmq-server-3.6.6.exe;有刚开始接触RabbitMQ的朋友会问为什么需要Erlang的支持,因为她就是Erlang开发出来的,Erlang语言是一种通用的面向并发的编程语言,专门用来编写分布式的一种语言;当你安装前面说的那个erlang安装包后,您电脑开始菜单中就有Erlang开发编辑器,有时间您可以用来练练手,就目前而言这种语言单词一般出现在一流大公司的招聘中,中小型一般没有,可能也因为很少中小型公司会涉及到并发的原因吧;到这里安装就完成了,下面需要通过命令行执行一些指令,由于RabbitMQ配置很多这里我捡一定会用到的几个来示范,其他具体可以参考官方文档:

首先找到安装rabbitmq的目录并进入rabbitmq_server-3.6.6找到sbin文件夹-》按住Shift+鼠标右键sbin文件夹-》在此处打开命令窗体-》参考这个地址https://www.rabbitmq.com/management.html的命令:rabbitmq-plugins enable rabbitmq_management -》录入到刚才打开的cmd窗体中:

这个是开启rabbitmq管理器的指令,这个时候你可以在你浏览器中录入http://localhost:15672/ 通过游客账号进入rabbitmq的监控后台:

url:http://localhost:15672/

Username:guest

Password:guest

此刻如果你看到如下图的界面,那恭喜你成功了,搭建RabbitMQ服务成功了:

因为Rabbit不光有队列,还有其他的路由,交换机等功能,所以能看到很多的统计或描述,这里我们只用到Queues的选项,点击进入Queues界面能看到没有任何的数据,但是有一个Add queue的按钮,这个控制台允许你手动添加一个队列数据,当然这不是我们今天的话题:

上面的guest已经够咋们测试使用了,至于剩余的什么管理员账号或密码等操作的设置可以去看这个:

rabbitmqctl操作的文档:https://www.rabbitmq.com/man/rabbitmqctl.1.man.html#

plugins操作文档:https://www.rabbitmq.com/plugins.html

 

» 封装RabbitMQ队列的读和写

C#中运用RabbitMQ官网列举了几种方式,这里我选择直接使用其提供的RabbitMQ.Client的nuget包,就目前这个nuget而言4.0.0及以上版本必须要NETFramework 4.5.1及以上版本或netcore版本才允许使用,笔者这里用的是Framework4.5框架所以引用了此版本的nuget包:

Install-Package RabbitMQ.Client -Version 3.6.6

引用过后就是往前面讲的队列工厂填写代码,首先继承统一配置文件读取类 PublicClass.ConfClass<QRabbitMQ> ,然后实现 IQueue 接口,这里封装了RabbitMq常用的几个操作方法,具体代码:

复制代码
 1  /// <summary>
 2     /// RabbitMq
 3     /// </summary>
 4     public class QRabbitMQ : PublicClass.ConfClass<QRabbitMQ>, IQueue
 5     {
 6         private IConnection con = null;
 7 
 8         public void Create()
 9         {
10             if (string.IsNullOrWhiteSpace(this.ApiUrl) || string.IsNullOrWhiteSpace(this.ApiKey)) { throw new Exception("创建RabbitMq队列需要指定队列:HostName和Port"); }
11 
12             try
13             {
14                 var factory = new ConnectionFactory() { HostName = this.ApiUrl, Port = Convert.ToInt32(this.ApiKey) };
15                 con = con ?? factory.CreateConnection();
16             }
17             catch (Exception ex)
18             {
19                 throw new Exception(ex.Message);
20             }
21         }
22 
23         public long Total(string name = "Redis_01")
24         {
25             if (con == null) { throw new Exception("请先创建队列连接"); }
26             using (var channel = con.CreateModel())
27             {
28                 return channel.MessageCount(name);
29             }
30         }
31 
32         public Message Read(string name = "RabbitMQ_01")
33         {
34             if (con == null) { throw new Exception("请先创建队列连接"); }
35             if (string.IsNullOrWhiteSpace(name)) { throw new Exception("name不能为空"); }
36 
37             var message = new Message();
38             message.Label = name;
39             message.Formatter = new XmlMessageFormatter(new Type[] { typeof(string) });
40             using (var channel = con.CreateModel())
41             {
42                 var baseResult = channel.BasicGet(name, true); //true:获取后删除队列   false:不删除  
43                 if (baseResult == null) { return message; }
44                 var body = baseResult.Body;
45                 message.Body = Encoding.UTF8.GetString(body); 
46             }
47             return message;
48         }
49 
50         public bool Write(string content, string name = "RabbitMQ_Queue01")
51         {
52             if (con == null) { throw new Exception("请先创建队列连接"); }
53             if (string.IsNullOrWhiteSpace(content) || string.IsNullOrWhiteSpace(name)) { throw new Exception("content和name不能为空"); }
54 
55             using (var channel = con.CreateModel())
56             {
57                 channel.QueueDeclare(name, false, false, false, null);
58                 var body = Encoding.UTF8.GetBytes(content);
59 
60                 channel.BasicPublish(string.Empty, name, null, body);
61                 return true;
62             }
63         }
64 
65         public void Dispose()
66         {
67             if (con != null)
68             {
69                 con.Close();
70                 con.Dispose();
71                 con = null;
72             }
73         }
74     }
复制代码

代码主要使用流程是:创建(Create)-》读(Read)|写(Write)-》释放(Dispose);有了具体的RabbitMq实现类,那么在工厂中直接通过泛型映射来获取该实现类的对象:

复制代码
 1 /// <summary>
 2     /// ==================
 3     /// author:神牛步行3
 4     /// des:该列工厂开源,包括队列有MSMQ,RedisMQ,RabbitMQ
 5     /// blogs:http://www.cnblogs.com/wangrudong003/
 6     /// ==================
 7     /// 队列工厂
 8     /// </summary>
 9     public class QueueReposity<T> where T : class,IQueue, new()
10     {
11         public static IQueue Current
12         {
13             get
14             {
15                 return PublicClass.ConfClass<T>.Current;
16             }
17         }
18     }
复制代码

 

» 队列工厂之RabbitMQ测试用例

通过上面配置环境和封装自己的方法,这里写了一个简单的测试用例,分为Server(加入消息队列)和Client(获取消息队列),首先来看Server端的代码:

复制代码
 1  /// <summary>
 2     /// 队列服务端测试用例
 3     /// </summary>
 4     class Program
 5     {
 6         static void Main(string[] args)
 7         {
 8             //Redis_Server();
 9 
10             RabbitMQ_Server();
11 
12             //MSMQ_Server();
13         }
14         private static void RabbitMQ_Server()
15         {
16             //实例化QMsmq对象
17             var mq = QueueReposity<QRabbitMQ>.Current;
18 
19             try
20             {
21                 Console.WriteLine("Server端创建:RabbitMQ实例");
22                 mq.Create();
23 
24                 var num = 0;
25                 do
26                 {
27                     Console.WriteLine("输入循环数量(数字,0表示结束):");
28                     var readStr = Console.ReadLine();
29                     num = string.IsNullOrWhiteSpace(readStr) ? 0 : Convert.ToInt32(readStr);
30 
31                     Console.WriteLine("插入数据:");
32                     for (int i = 0; i < num; i++)
33                     {
34                         var str = "我的编号是:" + i;
35                         mq.Write(str);
36                         Console.WriteLine(str);
37                     }
38                 } while (num > 0);
39             }
40             catch (Exception ex)
41             {
42             }
43             finally
44             {
45                 Console.WriteLine("释放。");
46                 mq.Dispose();
47             }
48             Console.ReadLine();
49         }
50     }
复制代码

通过:创建(Create)-》读(Read)|写(Write)-》释放(Dispose) 的流程来使用我们的队列工厂,感觉挺简单的,此时我们运行下这个Server端,然后录入参数:

这个时候就往RabbitMq队列中加入了11条数据,我们通过她的后台去找刚才添加的队列:

能够看到我们刚刚插入的队列总数和名称,如果你想看里面具体内容,可以点击名字“mq_01”进入某一个队列的界面,往下面拉滚动条找到“Get messages”选项,默认查看Messages是1我们修改为10,再点击get messages就能够看到如下图我们刚才插入的具体内容了:

截图有点长哦,不知道dudu会不会怪我哈哈,到这里能看到队列插入是成功的,然后我们来通过client端消费队列,具体代码:

复制代码
 1  /// <summary>
 2     /// 队列客户端测试用例
 3     /// </summary>
 4     class Program
 5     {
 6         static void Main(string[] args)
 7         {
 8             //RedisMQ_Client();
 9 
10             RabbitMQ_Client();
11 
12             //MSMQ_Client();
13         }
14 
15         private static void RabbitMQ_Client()
16         {
17             //实例化QMsmq对象
18             var mq = QueueReposity<QRabbitMQ>.Current;
19             try
20             {
21                 Console.WriteLine("Client端创建:RabbitMQ实例");
22                 mq.Create();
23 
24                 while (true)
25                 {
26                     try
27                     {
28                         var total = mq.Total();
29                         if (total > 0) { Console.WriteLine("队列条数:" + total); }
30 
31                         var result = mq.Read();
32                         if (result.Body == null) { continue; }
33                         Console.WriteLine(string.Format("接受队列{0}:{1}", result.Label, result.Body));
34                     }
35                     catch (Exception ex)
36                     { Console.WriteLine("异常信息:" + ex.Message); }
37                 }
38             }
39             catch (Exception ex)
40             {
41                 throw ex;
42             }
43             finally
44             {
45                 Console.WriteLine("释放。");
46                 mq.Dispose();
47             }
48         }
49     }
复制代码

再来咋们运行exe看下效果:

此刻刚刚加入队列中的数据就读取出来了,这个时候我们再看Rabbitmq控制台,get messages已经获取不出来具体的内容信息了,因为这个客户端消费了数据,队列中的数据自动清除了,至于是否想清除数据这个设置在代码:

1 var baseResult = channel.BasicGet(name, true); //true:获取后删除队列   false:不删除

以上对封装RabbitMQ的代码分享和环境搭建讲解,希望能给您带来好的帮助,谢谢阅读;

rabbitmq消息队列原理_一群专业码农的笔记本-CSDN博客_rabbitmq原理

mikel阅读(676)

来源: rabbitmq消息队列原理_一群专业码农的笔记本-CSDN博客_rabbitmq原理

一、rabbitmq架构

RabbitMQ是一个流行的开源消息队列系统,是AMQP(高级消息队列协议)标准的实现,由以高性能、健壮、可伸缩性出名的Erlang语言开发,并继承了这些优点。rabbitmq简单架构如下:

上图简单展示了rabbitmq的架构,从图中看到几个关键字:vhost、exchange、route key、queue等,后面会介绍这些概念。

下面看下rabbitmq的进程模型:

看到这个图,相信大家应该很熟悉,没错就是事件驱动模型(或者说反应堆模型),这是一种高性能的非阻塞io线程模型,不过在Erlang中称为进程模型。

tcp_acceptor进程接收客户端连接,创建rabbit_reader、rabbit_writer、rabbit_channel进程。
rabbit_reader接收客户端连接,解析AMQP帧;rabbit_writer向客户端返回数据;
rabbit_channel解析AMQP方法,对消息进行路由,然后发给相应队列进程。
rabbit_amqqueue_process是队列进程,在RabbitMQ启动(恢复durable类型队列)或创建队列时创建。
rabbit_msg_store是负责消息持久化的进程。

在整个系统中,存在一个tcp_accepter进程,一个rabbit_msg_store进程,有多少个队列就有多少个rabbit_amqqueue_process进程,每个客户端连接对应一个rabbit_reader和rabbit_writer进程。
二、关于AMQP协议

1.AMQP帧组件

AMQP帧由五个不同的组件组成:

帧类型
信道编号
以字节为单位的帧大小
帧有效载荷payload
结束字节标志(ASCII值206)
1
2
3
4
5

2.帧类型

AMQP规范定义了五种类型的帧:协议头帧、方法帧、内容帧、消息体帧及心跳帧。每种帧类型都有明确的目的,有些帧的使用频率比其他的高很多:

协议头帧用于连接到rabbitmq,进使用一次。
方法帧携带发送给rabbitmq或者从rabbitmq接收到的rpc请求或者响应
内容头包含一条消息的大小和属性。
消息体帧包含消息的内容
心跳帧在客户端与rabbitmq直接进行传递,作为一种校验机制确保连接的两端都可用并且正常工作。
1
2
3
4
5
3.将消息编组成帧

我们使用方法帧、内容头帧和消息体帧组成一个完整的rabbitmq消息。方法头帧携带命令和执行它所需要的参数(如交换器和路由键)、内容帧包含消息的基本属性以及消息的大小,消息体帧也就是携带我们真正需要发送的消息内容。

4.方法帧结构

5.内容头帧结构

内容头包含的具体属性如下:

content-type:消息体的报文编码,如application/json
expiration:消息过期时间
reply-to:响应消息的队列名
content-encoding:报文压缩的编码,如gzip
message-id:消息的编号
correlation-id:链路id
deliver-mode:告诉rabbitmq将消息写入磁盘还是内存
user-id:投递消息的用户(发送消息时不要设置该值)
timestamp:投递消息的时间
headers:定义一些属性,可用于实现rabbitmq路由(比如exchange类型是headers的时候用到)

6.消息体帧结构

7.几个概念:
Broker:简单来说就是消息队列服务器实体
Exchange:消息交换机,它指定消息按什么规则,路由到哪个队列
Queue:消息队列载体,每个消息都会被投入到一个或多个队列,队列类型又分为临时队列,持久化队列,排他队列
Binding:绑定,它的作用就是把exchange和queue按照路由规则绑定起来
Routing Key:路由关键字,exchange根据这个关键字进行消息投递
vhost:虚拟主机,一个broker里可以开设多个vhost,用作不同用户的权限分离
producer:消息生产者,就是投递消息的程序
consumer:消息消费者,就是接受消息的程序
channel:消息通道,在客户端的每个连接里,可建立多个channel,每个channel代表一个会话任务

四、通讯过程
1.启动会话

2.声明交换器

3.声明队列

4.绑定队列到exchange

5.发送消息-使用事务机制

对事务的支持是AMQP协议的一个重要特性。假设当生产者将一个持久化消息发送给服务器时,假如使用no_ack模式,所以即使服务器崩溃,没有持久化该消息,生产者也无法获知该消息已经丢失。如果此时使用事务,即通过txSelect()开启一个事务,然后发送消息给服务器,然后通过txCommit()提交该事务,即可以保证,如果txCommit()提交了,则该消息一定会持久化,如果txCommit()还未提交即服务器崩溃,则该消息不会服务器就收。当然Rabbit MQ也提供了txRollback()命令用于回滚某一个事务。但是使用事务,会导致性能下降,它使得生产者发布消息后必须等到消息真正持久化后服务端响应了才结束本次连接,所以需要在实际应用中平衡性能与安全的问题。

6.发送消息-非事务方式

使用事务固然可以保证只有提交的事务,才会被服务器执行。但是这样同时也将客户端与消息服务器同步起来,这背离了消息队列解耦的本质。Rabbit MQ提供了一个更加轻量级的机制来保证生产者可以感知服务器消息是否已被路由到正确的队列中——Confirm。如果设置channel为confirm状态,则通过该channel发送的消息都会被分配一个唯一的ID,然后一旦该消息被正确的路由到匹配的队列中后,服务器会返回给生产者一个Confirm,该Confirm包含该消息的ID,这样生产者就会知道该消息已被正确分发。对于持久化消息,只有该消息被持久化后,才会返回Confirm。

Confirm机制的最大优点在于异步,生产者在发送消息以后,即可继续执行其他任务(也就是异步监听服务端的ack即可)。而服务器返回Confirm后,会触发生产者的回调函数,生产者在回调函数中处理Confirm信息。如果消息服务器发生异常,导致该消息丢失,会返回给生产者一个nack,表示消息已经丢失,这样生产者就可以通过重发消息,保证消息不丢失。Confirm机制在性能上要比事务优越很多。

但是Confirm机制,无法进行回滚,就是一旦服务器崩溃,生产者无法得到Confirm信息,生产者其实本身也不知道该消息吃否已经被持久化,只有继续重发来保证消息不丢失,但是如果原先已经持久化的消息,并不会被回滚,这样队列中就会存在两条相同的消息,系统需要支持去重。

7.消费消息

五、使用delivery-mode平衡速度和安全
delivery-mode有两个值:1表示非持久化,2表示持久化消息

1.发送消息到纯内存队列中,delivery-mode = 1

特点:非持久化的消息在服务宕机的时候会丢失数据,但是由于不需要磁盘io,尽可能地降低消息投递的延迟性,性能较高。

2.发布消息到支持磁盘存储的队列,delivery-mode = 2

特点:持久化的消息安全性较高,尽管服务宕机,数据也不会丢失,但是在投递消息的过程中需要发生磁盘io,性能相对纯内存投递的方式低,但是尽管是产生了磁盘io,由于日志的记录方式是直接追加到消息日志文件的末尾,属于顺序io,没有随机io,所以性能还是可以接受的。

大概原理:
所有队列中的消息都以append的方式写到一个文件中,当这个文件的大小超过指定的限制大小后,关闭这个文件再创建一个新的文件供消息的写入。文件名(*.rdq)从0开始然后依次累加。当某个消息被删除时,并不立即从文件中删除相关信息,而是做一些记录,当垃圾数据达到一定比例时,启动垃圾回收处理,将逻辑相邻的文件中的数据合并到一个文件中。

消息的读写及删除:
rabbitmq在启动时会创建msg_store_persistent,msg_store_transient两个进程,一个用于持久消息的存储,一个用于内存不够时,将存储在内存中的非持久化数据转存到磁盘中。所有队列的消息的写入和删除最终都由这两个进程负责处理,而消息的读取则可能是队列本身直接打开文件进行读取,也可能是发送请求由msg_store_persisteng/msg_store_transient进程进行处理。

在进行消息的存储时,rabbitmq会在ets表中记录消息在文件中的映射,以及文件的相关信息。消息读取时,根据消息ID找到该消息所存储的文件,在文件中的偏移量,然后打开文件进行读取。消息的删除只是从ets表删除指定消息的相关信息,同时更新消息对应存储的文件的相关信息(更新文件有效数据大小)。
六、消息路由模式
1.fanout模式
fanout类型的Exchange路由规则非常简单,它会把所有发送到该Exchange的消息路由到所有与它绑定的Queue中。

上图中,生产者发送到Exchange的所有消息都会路由到图中的两个Queue,并最终被两个消费者(C1与C2)消费。
2.direct模式
direct类型的Exchange路由规则也很简单,它会把消息路由到那些binding key与routing key完全匹配的Queue中。如图,生产者发送消息的routing key=key1的时候,只有绑定了key1的queue才能收到信息

3.topic模式
topic类型的Exchange在匹配规则上进行了扩展,它与direct类型的Exchage相似,也是将消息路由到binding key与routing key相匹配的Queue中,但这里的匹配规则有些不同,它约定:
routing key为一个句点号“. ”分隔的字符串(我们将被句点号“. ”分隔开的每一段独立的字符串称为一个单词),如“image.new.profile”.
binding key与routing key一样也是句点号“. ”分隔的字符串
binding key中可以存在两种特殊字符“”与“#”,用于做模糊匹配,其中“”用于匹配下一个据点前的所有字符,“#”用于匹配所有字符,包括句点(可以是零个)

如图,生产者以routing key为image.new.profile发布消息,这key可以被image.*.profile以及image.#匹配到,所有这两个队列都可以收到消息。由此可见,topic的路由方式更加灵活。
3.headers模式
headers类型的Exchange不依赖于routing key与binding key的匹配规则来路由消息,而是根据发送的消息内容中的headers属性进行匹配。
在绑定Queue与Exchange时指定一组键值对以及x-match参数,x-match参数是字符串类型,可以设置为any或者all。如果设置为any,意思就是只要匹配到了headers表中的任何一对键值即可,all则代表需要全部匹配。

七、rabbitmq流量控制

RabbitMQ可以对内存和磁盘使用量设置阈值,当达到阈值后,生产者将被阻塞(block),直到对应项恢复正常。除了这两个阈值,RabbitMQ在正常情况下还用流控(Flow Control)机制来确保稳定性。
Erlang进程之间并不共享内存(binaries类型除外),而是通过消息传递来通信,每个进程都有自己的进程邮箱。Erlang默认没有对进程邮箱大小设限制,所以当有大量消息持续发往某个进程时,会导致该进程邮箱过大,最终内存溢出并崩溃。
在RabbitMQ中,如果生产者持续高速发送,而消费者消费速度较低时,如果没有流控,很快就会使内部进程邮箱大小达到内存阈值,阻塞生产者(得益于block机制,并不会崩溃)。然后RabbitMQ会进行page操作,将内存中的数据持久化到磁盘中。
为了解决该问题,RabbitMQ使用了一种基于信用证的流控机制。消息处理进程有一个信用组{InitialCredit,MoreCreditAfter},默认值为{200, 50}。消息发送者进程A向接收者进程B发消息,每发一条消息,Credit数量减1,直到为0,A被block住;对于接收者B,每接收MoreCreditAfter条消息,会向A发送一条消息,给予A MoreCreditAfter个Credit,当A的Credit>0时,A可以继续向B发送消息。

八、 RabbitMQ 多层消息队列

RabbitMQ完全实现了AMQP协议,类似于一个邮箱服务。Exchange负责根据ExchangeType和RoutingKey将消息投递到对应的消息队列中,消息队列负责在消费者获取消息前暂存消息。在RabbitMQ中,MessageQueue主要由两部分组成,一个为AMQQueue,主要负责实现AMQP协议的逻辑功能。另外一个是用来存储消息的BackingQueue。
为了高效处理入队和出队的消息、避免不必要的磁盘IO,BackingQueue进程为消息设计了4种状态和5个内部队列。
(1) 4种状态包括:

alpha,消息的内容和索引都在内存中;
beta,消息的内容在磁盘,索引在内存;
gamma,消息的内容在磁盘,索引在磁盘和内存中都有;
delta,消息的内容和索引都在磁盘。
1
2
3
4
对于持久化消息,RabbitMQ先将消息的内容和索引保存在磁盘中,然后才处于上面的某种状态(即只可能处于alpha、gamma、delta三种状态之一)。
(2) 5个内部队列

包括:q1、q2、delta、q3、q4。q1和q4队列中只有alpha状态的消息;q2和q3包含beta和gamma状态的消息;delta队列是消息按序存盘后的一种逻辑队列,只有delta状态的消息。所以delta队列并不在内存中,其他4个队列则是由erlang queue模块实现。

消息从q1入队,q4出队,在内部队列中传递的过程一般是经q1顺序到q4。实际执行并非必然如此:开始时所有队列都为空,消息直接进入q4(没有消息堆积时);内存紧张时将q4队尾部分消息转入q3,进而再由q3转入delta,此时新来的消息将存入q1(有消息堆积时)。

当内存紧张时触发paging,paging将大量alpha状态的消息转换为beta和gamma;如果内存依然紧张,继续将beta和gamma状态转换为delta状态。Paging是一个持续过程,涉及到大量消息的多种状态转换,所以Paging的开销较大,严重影响系统性能。

九、高可用队列(HA)
在生产环境下,一般都不会允许rabbitmq这种消息中间件单点,以免单点故障导致服务不可用,那么rabbitmq同样可以集群部署来保证服务的可用性,在rabbitmq集群中,我们可以定义HA队列,可以在web管理平台设置,也可以通过AMQP接口设置,当我们定义某个HA队列的时候,会在集群的各个节点上都建立该队列,发布消息的时候,直接发送至master服务,当master服务受到消息后,把消息同步至各个从节点,假如开启事务的情况下,是需要在消息被同步到各个节点之后才算完成事务,所以会带来一定的性能损耗,所以还是回到之前说的,性能和安全直接,需要根据实际业务的需要找到平衡点。

当master服务宕机之后,其中一个slaver节点会升级为master,消息不会丢失(因为已经完成了事务的消息都会在各个节点有备份)
ha-队列可以跨越集群的每台服务,或者仅使用其中一批独立节点。如果是全部节点都为副本的时候,将x-ha-policy参数设置为all,否则设置为nodes,然后在设置另一个参数:x-ha-nodes,该参数指定ha队列所在的节点列表。思考下,rabbitmq的集群节点是不是越多越好?
————————————————
版权声明:本文为CSDN博主「软件开发随心记」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/vipshop_fin_dev/article/details/81612935

Redis 实现消息队列 MQ_weixin_34179968的博客-CSDN博客

mikel阅读(720)

来源: Redis 实现消息队列 MQ_weixin_34179968的博客-CSDN博客

Redis 2.4版本之后就内置队列的功能了,如果是日常比较简单的队列应用,可以选择Redis , 效率还很高的!!

 

Redis 还能实现 有序 和 无序 两种队列(只讨论生产者和消费者这种模式的队列):

一、有序队列:

1、生产者:

  1. $redis = new Redis();
  2. $redis->pconnect(‘127.0.0.1’, 6379);
  3. $redis->zAdd(‘MQ’, 1, ‘need to do 1’); $redis->zAdd(‘MQ’, 2, ‘need to do 2’);

2、消费者:

  1. while (true) {
  2. $pid = pcntl_fork();
  3. if ($pid == -1) {
  4. //创建子进程失败,不处理 } else if ($pid == 0) { $redis = new Redis(); $redis->connect(‘127.0.0.1’, 6379); //执行有序查询,取出排序前10进行处理 $redis->zRevRangeByScore(‘MQ’, ‘+inf’, ‘-inf’, array(‘withscores’=>false, ‘limit’=>array(0,10))); exit; } else { //主进行执行中,等待 pcntl_wait($status); } }

 

二、无序队列:

1、生产者:

  1. $redis = new Redis();
  2. $redis->pconnect(‘127.0.0.1’, 6379);
  3. $redis->LPUSH(‘MQ’, 1, ‘need to do 1’); $redis->LPUSH(‘MQ’, 2, ‘need to do 2’);

2、消费者:

  1. while (true) {
  2. $pid = pcntl_fork();
  3. if ($pid == -1) {
  4. //创建子进程失败,不处理 } else if ($pid == 0) { $redis = new Redis(); $redis->connect(‘127.0.0.1’, 6379); //执行出队处理,BLPOP是阻塞的出队方式,其实还可以用LPOP,不过用lPOP就要自行判断数据是否为空了 $mq = $redis->BLPOP(‘MQ’) //do something } else { //主进行执行中,等待 pcntl_wait($status); } }

 

简单版就是这样了~~当然,如果应用规模大,还是建议用正规的MQ,例如:RabbitMQ

手把手教你用redis实现一个简单的mq消息队列(java) - 烦嚣的人 - 博客园

mikel阅读(1273)

来源: 手把手教你用redis实现一个简单的mq消息队列(java) – 烦嚣的人 – 博客园

众所周知,消息队列是应用系统中重要的组件,主要解决应用解耦,异步消息,流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构。目前使用较多的消息队列有 ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaMQ,RocketMQ.

但是如果你不想为你的系统引入一个重量级(相对 redis 来说)的 mq,但是想要享受解耦、异步消息等特性,通过本文你就 get 到了,通过 redis 实现一个简单版的 mq。

为什么是 redis

  • redis 通常作为缓存服务引入,因此大部分系统都会有 redis
  • redis 本身的资源消耗是极小的,符合我们的轻量要求
  • redis 速度很快,几乎不会出现速度瓶颈
  • redis 有持久化方案,调整配置项可以在数据安全和速度间进行取舍(参考这篇)[https://segmentfault.com/a/1190000002906345]

如何实现

利用 redis 的队列结构来实现消息队列。redis 单个队列最多支持 2*32-1 条数据,对于大部分应用是完全够用的。

简单来说就是:

  • 每个 topic 对应一条队列
  • 从队列一段写入数据,从另一端读取数据
  • 消费失败,重新将消息放入队列

注意:代码仅供个人尝鲜使用,请勿用于真实生产环境

代码仅可在 springboot 环境中使用

首先定义注解和接口类

注解代码如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface MqConsumer {
    /**
     * 队列主题
     */
    String topic() default "default_es_topic";
}

被该注解修饰的类,将会接收 topic 下的消息。

接口代码如下:

public interface RedisConsumer {

    /**
     * 功能描述: 消费方法,消费者类必须继承此方法
     *
     * @param message 数据载体
     * @author 123
     * @date 2020/3/28 22:41
     */
    void deal(String message);
}

本接口用于定于接受消息的处理方法。

扫描注解修饰类

本部分为核心代码,首先需要获取代码中被注解修饰的类,然后建立一个循环从 redis 队列中取数据,最后调用类对象的 deal 方法消费消息,如果 deal 方法抛出错误,认为消费失败,重新将该数据放入队列中。

  1. 扫描部分代码如下:
/**
 *  MqConfiguration.java
 */
@Override
public void run(ApplicationArguments args) {
    Map<String, Object> map = context.getBeansWithAnnotation(MqConsumer.class);
    map.values().forEach(item -> {
        if (!(item instanceof RedisConsumer)) {
            log.warn("注意检测到被@EsConsumer注解的类{}未实现RedisConsumer接口", item.getClass().getCanonicalName());
            return;
        }
        MqConsumer[] annotations = item.getClass().getAnnotationsByType(MqConsumer.class);
        MqConsumer annotation = annotations[0];
        String topic = annotation.topic();
        if (topicMap.containsKey(topic)) {
            log.error("多个消费者{},消费同一个消息:{},已忽略", item.getClass().getCanonicalName(), topic);
        } else {
            topicMap.put(topic, (RedisConsumer) item);
        }

    });
    log.info("redis订阅信息汇总完毕!!!!!!");
    //由一个线程始终循环获取es队列数据
    threadPoolExecutor.execute(loop());
}

run 方法在 spring 扫描完毕后调用,通过实现ApplicationRunner接口实现,通过 spring 的方法来获取所有被MqConsumer接口注解的类(否则需要自己写类加载器)。数据汇总完毕后使用一个线程来进行无线循环从 redis 队列中取数据。

  1. 执行线程部分代码如下:
private Runnable loop() {
    return () -> {
        while (true) {
            AtomicInteger count = new AtomicInteger(0);
            topicMap.forEach((k, v) -> {
                try {
                    String message = mqUtil.getRedisTemplate().opsForList().rightPop(k);
                    if (message == null) {
                        count.getAndIncrement();
                    } else {
                        pushTask(v, message, k);
                    }
                } catch (RedisConnectionFailureException connException) {
                    log.error("redis无法连接,10s后重试", connException);
                    sleep(10);
                } catch (Exception e) {
                    log.error("redis消息队列异常", e);
                }
            });
            if (count.get() == topicMap.keySet().size()) {
                //当所有的队列都为空时休眠1s
                sleep(1);
            }
        }
    };
}
private void pushTask(RedisConsumer item, String value, String key) {
    threadPoolExecutor.execute(() -> {
        try {
            item.deal(value);
        } catch (Exception e) {
            log.error("执行消费任务出错", e);
            //非广播消息进行数据回补
            mqUtil.getRedisTemplate().opsForList().rightPush(key, value);
        }
    });
}

loop 方法无限循环根据 topic 从 redis 中取数据,如果取到数据,调用 pushTask 方法执行,如果执行报错将会进行数据回补。

完整代码见本文结尾

测试

运行项目后调用,MainController中的接口即可测试。

完整代码:github

本文原创发布于:手把手教你用 redis 实现一个简单的 mq 消息队列

支撑性服务 & 自动化 - _小码甲 - 博客园

mikel阅读(596)

来源: 支撑性服务 & 自动化 – _小码甲 – 博客园

连载传送门:

  • 什么是云原生?
  • 云原生设计理念
  • .NET 微服务
  • 谈到云原生,绕不开“容器化”

Backing services#

云原生系统依赖于许多不同的辅助资源,例如数据存储、消息队列、监视和身份服务。这些服务统称为支撑性服务。

下图显示了云原生系统使用的许多常见支撑性服务

支撑性服务帮助实现了“十二要素应用”中的Statelessness原则

要素6提到:“每个微服务应在独立隔离的进程中执行,将所需状态信息作为外部支撑性服务,例如分布式缓存或数据存储”

最佳实践是将支撑性服务视为附加资源,并使用外部挂载的方式将配置(URL和凭据)动态绑定到微服务。

要素4指出: “支撑性服务“应通过可寻址的URL公开,这样做解耦了将资源与应用”
要素3指出: “将配置信息从微服务中移出并外挂”

Stateless和支撑性服务,这样松散的设计使你可以将一项支撑性服务换成另一项支撑性服务,或将您的代码移至其他公有云,而无需更改主线服务代码。

支撑性服务将在第5章“云原生数据模式”和第4章“云原生通信模式”中详细讨论。


自动化#

如你所见,云原生依赖(微服务、容器和现代设计理念)来实现速度和敏捷性。
但是,那只是故事的一部分,你如何配置运行这些系统的云环境?你如何快速部署应用程序功能和更新?

被广泛认可的作法是基础设施即代码(IaC)

借助IaC,你可以自动化平台配置和应用程序部署,你将诸如测试和版本控制之类的软件工程实践应用于您的DevOps实践。你的基础架构和部署是自动化,一致且可重复的。

Automating infrastructure#

在底层,IaC是幂等的,这意味着你可以一遍又一遍地运行相同的脚本,而不会产生副作用。
如果团队需要进行更改,可以编辑并重新运行脚本,(仅)需要更新的资源受到影响。

在《基础架构即代码》一书中,作者Sam Guckenheimer指出:“实施IaC的团队可以大规模、快速、稳定地交付。团队不用手动配置环境,通过代码表示的所需环境状态,来增强交付预期。使用IaC进行基础架构部署是可重复的,可防止由于配置差异或缺少依赖关系而导致运行时问题”。

Automating deployments#

“十二要素应用”指出了从代码开发到交付落地的原则

要素5指出:“严格区分构建、发行和运行阶段。每个发行阶段都应标有唯一的ID,并支持回滚功能。”

现代CI/CD实现了这一原则。它们提供的独立部署步骤,确保将一致的、高质量的代码交付给用户。

下图演示了独立的部署过程:

在上图中,要特别注意任务分离。

开发人员在其开发环境中创建feature分支,反复迭代“inner loop”(运行和调试)。
完成后,该代码将被推送到代码存储库中,例如GitHub,Azure DevOps或BitBucket。

推送触发自动构建,构建阶段将代码转换为二进制产物。这项工作是通过持续集成(CI)管道实现的,它会自动生成,测试和打包应用程序。

发布阶段拾取前面的二进制产物,加上外部应用程序和环境配置信息,产生不可变更的发行版。该版本将会部署到指定的环境。这项工作是通过持续交付(CD)管道实现的。每个版本都应该是可识别、可追溯的。你可以说:“这次部署的是应用程序的Release 2.1.1版本”。

最后,发布的版本放在目标执行环境中运行。版本不可变,这意味着任何更改都必须创建一个新版本。

应用这些实践,从根本上发展了软件发布方式。许多人已经从季度发布转为按需更新。通过集成过程的一致性,团队可以更频繁地提交代码更改,从而改善协作和软件质量。

误用.Net Redis客户端CSRedisCore,自己挖坑自己填 - _小码甲 - 博客园

mikel阅读(894)

来源: 误用.Net Redis客户端CSRedisCore,自己挖坑自己填 – _小码甲 – 博客园

前导  #

上次Redis MQ分布式改造完成之后, 编排的容器稳定运行了一个多月,昨天突然收到ETL端同事通知,没有采集到解析日志了。

赶紧进服务器看了一下,用于数据接收的receiver容器挂掉了, 尝试docker container start [containerid],  几分钟后该容器再次崩溃。

 

Redis连接超限#

docker log [containerid]  查看容器日志; 重点:CSRedis.RedisException: ERR max number of clients reached

docker logs [containerid]

日志上显示连接Redis服务器的客户端数量超限,头脑快速思考,目前编排的某容器使用CSRedisCore 对于16个Redis DB实例化了16个客户端,但Redis服务器也不至于这么不经折腾吧。

赶紧进redis.io官网搜集相关资料

After the client is initialized, Redis checks if we are already at the limit of the number of clients that it is possible to handle simultaneously (this is configured using the maxclients configuration directive, see the next section of this document for further information).

In case it can’t accept the current client because the maximum number of clients was already accepted, Redis tries to send an error to the client in order to make it aware of this condition, and closes the connection immediately. The error message will be able to reach the client even if the connection is closed immediately by Redis because the new socket output buffer is usually big enough to contain the error, so the kernel will handle the transmission of the error.

大致意思是:Redis服务器maxclients配置了最大客户端连接数, 如果当前连接的客户端超限,Redis会回发一个错误消息给客户端,并迅速关闭客户端连接。

立刻登录Redis服务器查看默认配置,确认当前Redis服务器maxclients=10000(这是一个动态值,由maxclients和最大进程文件句柄决定),

# Set the max number of connected clients at the same time. By default
# this limit is set to 10000 clients, however if the Redis server is not
# able to configure the process file limit to allow for the specified limit
# the max number of allowed clients is set to the current file limit
# minus 32 (as Redis reserves a few file descriptors for internal uses).
#
# Once the limit is reached Redis will close all the new connections sending
# an error ‘max number of clients reached’.
# maxclients 10000

 左图表明:通过Redis-Cli 登录进服务器立即就被踢下线。

基本可认定redis客户端使用方式有问题。

 

CSRedisCore使用方式#

继续查看相关资料,可在redis服务器上利用redis-cli命令:info clients、client list仔细分析客户端连接。

info clients 命令显示现场确实有10000的连接数;

client list命令显示连接如下:

官方对client list命令输出字段的解释:

  • addr: The client address, that is, the client IP and the remote port number it used to connect with the Redis server.
  • fd: The client socket file descriptor number.
  • name: The client name as set by CLIENT SETNAME.
  • age: The number of seconds the connection existed for.
  • idle: The number of seconds the connection is idle.
  • flags: The kind of client (N means normal client, check the full list of flags).
  • omem: The amount of memory used by the client for the output buffer.
  • cmd: The last executed command.

根据以上解释,表明 Redis服务器收到很多ip=172.16.1.3(故障容器在网桥内的Ip 地址)的客户端连接,这些连接最后发出的是ping命令(这是一个测试命令)

故障容器使用的Redis客户端是CSRedisCore,该客户端只是单纯将 Msg 写入Redis list 数据结构,CSRedisCore上相关github issue给了我一些启发。

发现自己将CSRedisClient实例化代码写在 .netcore api Controller构造函数,这样每次请求构造Controller时都实例化一次Redis客户端,最终Redis客户端连接数达到最大允许连接值。

依赖注入三种模式: 单例(系统内单一实例,一次性注入);瞬态(每次请求产生实例并注入);自定义范围。

有关dotnet apicontroller 以瞬态模式 注入,请查阅链接

还有一个疑问? 为什么Redis服务器没有释放空闲的 客户端连接,如果空闲连接被释放了,即使我写了low代码也不至于如此吧?

查询官方:

By default recent versions of Redis don’t close the connection with the client if the client is idle for many seconds: the connection will remain open forever.

However if you don’t like this behavior, you can configure a timeout, so that if the client is idle for more than the specified number of seconds, the client connection will be closed.

You can configure this limit via redis.conf or simply using CONFIG SET timeout <value>.

大致意思是最近的Redis服务端版本 默认不会释放空闲的客户端连接:

# Close the connection after a client is idle for N seconds (0 to disable)
timeout 0

可通过修改Redis配置释放 空闲客户端连接。

我们最佳实践当然不是修改Redis idle timeout 配置,问题核心还是因为我实例化了多客户端,赶紧将CSRedisCore实例化代码移到startup.cs并注册为单例。

 

大胆求证#

info clients命令显示稳定在53个Redis连接。

client list命令显示:172.16.1.3(故障容器)建立了50个客户端连接,编排的另一个容器webapp建立了2个连接,redis-cli命令登录到服务器建立了1个连接。

client list命令显示连接

那么问题来了,修改之后,receiver容器为什么还稳定建立了50个redis连接?

进一步与CSRedisCore原作者沟通,确定CSRedisCore有预热机制,默认在连接池中预热了50个连接。

bingo,故障和困惑全部排查清楚。

 

总结#

经此一役,在使用CSRedisCore客户端时, 要深入理解

① Stackexchange.Redis 使用的多路复用连接机制(使用时很容易想到注册到单例),CSRedisCore开源库采用连接池机制,在高并发场景下强烈建议注册为单例, 否则在生产使用中可能会误用在瞬态请求中实例化,导致redis客户端几天之后被占满。

② CSRedisCore会默认建立连接池,预热50个连接, 开发者心里要有数。

额外的方法论: 尽量不要从某度找答案,要学会问问题,并尝试从官方、stackoverflow 、github社区寻求解答,你挖过的坑也许别人早就挖过并踏平过。

 

——————————update  多说两句———————————————

很多博友说问题在于我没有细看CSRedisCore官方readme(readme推荐使用单例),使用方式上我确实没有做成单例:

③ 一般连接池都会有空闲释放回收机制 (CSRedisCore也是连接池机制),所以当时并没有把 单例放在心上

④ 本次重要知识点:Redis默认并不会释放空闲客户端连接(但是又设置了最大连接数),这也直接促成了本次容器崩溃事故。

嗯,坑是自己挖的。

Redis 模糊查询删除操作 - 01234567 - 博客园

mikel阅读(949)

来源: Redis 模糊查询删除操作 – 01234567 – 博客园

创建一条测试 数据 查询 (默认是 DB 0 )

创建:
set name xiaoming
查询:
get name

1、模糊搜索查询 (redis 默认有16个DB , 0-15 )

复制代码
Redis 模糊搜索
1、keys *   匹配数据库中所有 key 
2、keys h?llo   匹配 hello , hallo 和 hxllo 等。
3、keys h*llo   匹配 hllo 和 heeello 等。
4、keys h[ae]llo   匹配 hallo 和 hello ,但不匹配 hillo;特殊符号用 \ 隔开。
redis> keys *o*
1) "four"
2) "two"
3) "one"
redis> keys t??
1) "two"
redis> keys t[w]*
1) "two"
redis> keys *    # 匹配数据库内所有 key
1) "four"
2) "three"
3) "two"
4) "one"
redis-cli  进入默认是第一个DB 0 ; select 切换 DB 
> select 2;
复制代码

2、删除指定key :

复制代码
# 删除所有以 user 开头的key 可以这样实现:
# redis-cli keys "user*"
1) "user1"
2) "user2"

# redis-cli keys "user*" | xargs redis-cli del
(integer) 2
# 删除成功

# 删除当前数据库中的所有Key 
> flushdb 

# 删除所有数据库中的key 
> flushall
# 删除单个 key
redis> SET name zhangsan
OK
redis> DEL name
(integer) 1

# 删除一个不存在的 key
redis> EXISTS lisi
(integer) 0

redis> DEL phone   # 失败,没有 key 被删除
(integer) 0

# 同时删除多个 key
redis> SET name "redis"
OK
redis> SET type "key-value store"
OK
redis> SET website "redis.com"
OK
redis> DEL name type website
(integer) 3
复制代码
# 批量删除匹配通配符的key用到了Linux中的管道和xargs参数:
redis-cli keys "s*" | xargs redis-cli del
复制代码
# 如果需要制定数据库,需要用到 -n 数据库编号 参数,下面是删除 2数据库中 s开头的键:

redis-cli -n 2 keys "s*" | xargs redis-cli -n 2 del

redis-cli keys "*" | xargs redis-cli del 

# 如果redis-cli没有设置成系统变量,需要指定redis-cli的完整路径 
如:
/opt/redis/redis-cli keys "*" | xargs /opt/redis/redis-cli del
复制代码

3、Redis Sortedset 数据查询

复制代码
redis sortedset 数据查询:

172.16.12.36:6003> zrank qa:hall 103228953392713728
(integer) 10021

172.16.12.36:6003> ZCARD qa:hall
(integer) 10022
复制代码

TTL key : 以秒为单位,返回给定 key 的剩余生存时间(TTL, time to live)。

返回值:
当 key 不存在时,返回 -2 。
当 key 存在但没有设置剩余生存时间时,返回 -1 。
否则,以秒为单位,返回 key 的剩余生存时间。
在 Redis 2.8 以前,当 key 不存在,或者 key 没有设置剩余生存时间时,命令都返回 -1 。

查询检测 ttl 值:

复制代码
# 不存在的 key
redis> FLUSHDB
OK
redis> TTL key
(integer) -2

# key 存在,但没有设置剩余生存时间
redis> SET key value
OK
redis> TTL key
(integer) -1

# 有剩余生存时间的 key
redis> EXPIRE key 10086
(integer) 1

redis> TTL key
(integer) 10010
复制代码

5、redis type key

TYPE key : 返回 key 所储存的值的类型。

复制代码
返回值:
none (key不存在)
string (字符串)
list (列表)
set (集合)
zset (有序集)
hash (哈希表)
复制代码

示例:

复制代码
# 字符串
redis> SET weather "sunny"
OK
redis> TYPE weather
string

# 列表
redis> LPUSH book_list "programming in scala"
(integer) 1
redis> TYPE book_list
list

# 集合
redis> SADD pat "dog"
(integer) 1
redis> TYPE pat
set
复制代码