分享在Linux下使用OSGi.NET插件框架快速实现一个分布式服务集群的方法

本文涉及的产品
传统型负载均衡 CLB,每月750个小时 15LCU
网络型负载均衡 NLB,每月750个小时 15LCU
应用型负载均衡 ALB,每月750个小时 15LCU
简介:

在这篇文章我分享了如何使用分层与模块化的方法来设计一个分布式服务集群。这个分布式服务集群是基于DynamicProxy、WCF和OSGi.NET插件框架实现的。我将从设计思路、目标和实现三方面来描述。

1 设计思路

首先,我来说明一下设计思路。我们先来看看目前OSGi.NET插件框架的服务。在这里,服务不是远程服务,它是轻量级的服务,由接口和实现类组成,如下图所示。服务契约插件定义了服务接口,服务实现插件向服务总线注册服务,服务调用插件利用服务契约(接口)从服务总线获取实现的服务并调用,服务实现插件和服务调用插件都依赖于服务契约,但二者并未有依赖。服务是插件间松耦合的调用方式。

clip_image001

我们希望在不更改现有的通讯机制的情况下,将以前定义的在同一个框架下的服务能够直接不更改代码情况下,变成远程服务。此时,基于远程服务的通讯方式变成如下图所示的方式。

clip_image002

这时候,在不更改服务定义、服务注册的代码下,在OSGi.NET框架中安装一个远程服务宿主插件,它直接将服务总线的服务暴露成远程服务;OSGi.NET插件框架安装一个远程服务客户端插件,就可以使用服务契约来获取并调用远程服务。

接下来,我们希望能更进一步在不需要更改服务定义和注册代码情况下,来实现透明的集群支持。

clip_image004

在这里,我们引入了负载均衡插件。首先,一个OSGi.NET插件框架安装了远程服务负载均衡插件,它管理所有远程服务的负载状况,并为集群提供了统一的访问和负载均衡支持;接着,所有安装了远程服务宿主插件的OSGi.NET框架,会安装一个负载均衡客户端插件,它用于将远程服务注册到负载均衡器;服务调用端安装了远程服务客户端插件,它通过负载均衡器来调用远程服务。

这个思路可以简单描述如下:

A)本地服务 = OSGi.NET插件框架 + 服务契约插件 + 服务实现插件 + 服务调用插件;服务实现和服务调用在同一个OSGi.NET插件框架内,在同一个进程。

B)远程服务实现 = OSGi.NET插件框架 + 服务契约插件 + 服务实现插件 + 远程服务宿主插件,远程服务调用 = OSGi.NET插件框架 + 服务契约插件 + 远程服务客户端插件;服务实现和服务调用可以在不同的OSGi.NET插件框架,在不同进程内。

C)负载均衡器 = OSGi.NET插件框架 + 远程服务负载均衡插件,负载均衡远程服务实现 = OSGi.NET插件框架 + 服务契约插件 + 服务实现插件 + 远程服务宿主插件 + 负载均衡客户端插件,远程服务调用 = OSGi.NET插件框架 + 服务契约插件 + 远程服务客户端插件; 负载均衡器、远程服务、服务调用均可以在不同OSGi.NET插件框架和不同进程,远程服务可以在多台机器中,注册到负载均衡器。

2 设计目标

远程服务和负载均衡的实现基于模块化思路,如下图所示。

clip_image006

(1)不更改本地服务的定义、注册和使用方法;

(2)在本地服务的基础上,安装远程服务宿主插件,服务就暴露成远程服务;

(3)在远程服务的基础上,安装负载均衡客户端插件和负载均衡器,远程服务就支持集群及负载均衡。

以一个简单的服务ISayHelloService为例,下面将描述如何通过以上方式来实现远程服务和服务集群。

2.1 远程服务示例

2.1.1 远程服务注册及实现

如下图所示,SayHelloServiceContract插件定义了一个ISayHelloService服务接口,SayHelloService定义了SayHelloServiceImpl服务实现,在OSGi.NET插件框架安装了UIShell.RemoteServiceHostPlugin插件,这样我们就将本地服务暴露成远程服务了。

clip_image008

下图是SayHelloServiceImpl服务的实现和服务注册。

clip_image010

下图则是服务注册。

clip_image012

你可以发现,为了支持远程服务,我们仅仅是安装了一个远程服务宿主插件,而没有更改服务的实现和注册方法。

2.1.2 远程服务调用

远程服务调用如下所示,在OSGi.NET插件框架安装了远程服务客户端插件。服务调用插件使用服务契约,来调用远程服务。

clip_image014

调用远程服务的步骤为:

1 使用远程服务客户端插件的IRemoteServiceProxyService来获取远程服务;

2 直接调用远程服务的方法。

因此,你可以发现,远程服务的定义和使用都非常的简单。接下来我们再看看负载均衡远程服务的使用。

2.2 负载均衡远程服务示例

2.2.1 负载均衡器

负载均衡器相当于远程服务注册表,它用于注册暴露远程服务的所有机器以及每一个机器每一个服务的负载均衡状况,并提供负载均衡支持。下图是负载均衡器的实现。

clip_image016

负载均衡器向外暴露一个固定的IP地址和端口号,用于注册远程服务,并提供负载均衡。

2.2.2 负载均衡远程服务

支持负载均衡的远程服务需要安装一个负载均衡客户端插件,如下所示。负载均衡客户端插件用于将远程服务注册到负载均衡器,从而,负载均衡器可以来管理远程服务的负载情况,当发生故障时可以实现负载转移和实现负载均衡。

clip_image018

这里,远程负载均衡器客户端插件会连接到负载均衡服务器,向其注册本机器的远程服务。

2.2.3 负载均衡远程服务调用

调用负载均衡远程服务与直接调用远程服务方法类似,如下图所示。它使用GetFirstOrDefaultLoadBalancerService接口来访问负载均衡器,获取经过负载均衡的远程服务。

clip_image020

你可以发现,调用负载均衡远程服务的方法也非常简单。下面,我来介绍一下如何实现。

3 设计实现

首先,我们先来看看远程服务的实现。

3.1 远程服务的实现

远程服务的实现可以归结为以下几点:(1)远程服务宿主插件用于暴露一个WCF服务,这个WCF服务相当于本地服务的Bridge,即客户端对远程服务的调用先中专到这个WCF服务,再由WCF服务来调用本地服务,然后返回给客户端;(2)客户端使用DynamicProxy为服务契约生成一个代理,对这个代理的方法调用将会被拦截,然后调用远程服务宿主插件的WCF服务,将调用结果再返回。

3.1.1 远程服务宿主插件实现

该插件首先定义了一个IRemoteServiceInvoker的WCF服务接口,这个接口及参数的定义如下。

clip_image022

它的作用就是通过调用这个WCF远程服务来调用OSGi.NET框架本地服务,达到将本地服务暴露成远程服务的目的。

这个WCF的实现代码如下所示,其目的就是对WCF的调用转换成对本地服务方法的调用并返回。

复制代码
  1 using System;
  2 using System.Collections.Generic;
  3 using System.Linq;
  4 using System.Text;
  5 using System.Threading;
  6 using System.Reflection;
  7 using System.Threading.Tasks;
  8 using fastJSON;
  9 using Fasterflect;
 10 using UIShell.OSGi.Utility;
 11 
 12 namespace UIShell.RemoteServiceHostPlugin
 13 {
 14     public class RemoteServiceInvoker : IRemoteServiceInvoker
 15     {
 16         public static ReaderWriterLock Locker = new ReaderWriterLock();
 17         public static Dictionary<string, System.Tuple<MethodInfo, Type[], MethodInvoker>> InvokerCache = new Dictionary<string, System.Tuple<MethodInfo, Type[], MethodInvoker>> (); 
 18         
 19         public string InvokeService(RemoteServiceInvocation invocation)
 20         {
 21             AssertUtility.NotNull(invocation);
 22             AssertUtility.ArgumentHasText(invocation.ContractName, "service contract name");
 23             AssertUtility.ArgumentHasText(invocation.MethodName, "service method name");
 24             
 25             var service = Activator.Context.GetFirstOrDefaultService(invocation.ContractName);
 26             string msg = string.Empty;
 27             if(service == null)
 28             {
 29                 msg = string.Format ("Remote Service '{0}' not found.", invocation.ContractName);
 30                 FileLogUtility.Warn (msg);
 31                 throw new Exception(msg);
 32             }
 33 
 34             System.Tuple<MethodInfo, Type[], MethodInvoker> invokerTuple;
 35             using (var locker = ReaderWriterLockHelper.CreateReaderLock (Locker)) 
 36             {
 37                 InvokerCache.TryGetValue (invocation.Key, out invokerTuple);
 38             }
 39 
 40             if (invokerTuple == null) 
 41             {
 42                 Type serviceType = service.GetType ();
 43 
 44                 var serviceMethodInfo = serviceType.GetMethod (invocation.MethodName);
 45                 if (serviceMethodInfo == null) 
 46                 {
 47                     msg = string.Format ("The method '{1}' of the remote service '{0}' not found.", invocation.ContractName, invocation.MethodName);
 48                     FileLogUtility.Warn (msg);
 49                     throw new Exception (msg);
 50                 }
 51 
 52                 if (invocation.JsonSerializedParameters == null) {
 53                     invocation.JsonSerializedParameters = new List<string> ();
 54                 }
 55 
 56                 var parameterInfos = serviceMethodInfo.GetParameters ();
 57                 if (invocation.JsonSerializedParameters.Count != parameterInfos.Length) 
 58                 {
 59                     msg = string.Format ("The parameters count is not match with the method '{0}' of service '{1}'. The expected count is {2}, the actual count is {3}.", invocation.MethodName, invocation.ContractName, parameterInfos.Length, invocation.JsonSerializedParameters.Count);
 60                     FileLogUtility.Warn (msg);
 61                     throw new Exception (msg);
 62                 }
 63 
 64                 var parameterTypes = new Type[parameterInfos.Length];
 65                 for (int i = 0; i < parameterInfos.Length; i++) 
 66                 {
 67                     parameterTypes [i] = parameterInfos [i].ParameterType;
 68                 }
 69 
 70                 try
 71                 {
 72                     var methodInvoker = serviceType.DelegateForCallMethod (invocation.MethodName, parameterTypes);
 73 
 74                     invokerTuple = new System.Tuple<MethodInfo, Type[], MethodInvoker> (serviceMethodInfo, parameterTypes, methodInvoker);
 75 
 76                     using (var locker = ReaderWriterLockHelper.CreateWriterLock (Locker)) 
 77                     {
 78                         if (!InvokerCache.ContainsKey (invocation.Key)) 
 79                         {
 80                             InvokerCache [invocation.Key] = invokerTuple;
 81                         }
 82                     }
 83                 }
 84                 catch(Exception ex)
 85                 {
 86                     msg = string.Format ("Failed to create delegate method for the method '{0}' of service '{1}'.", invocation.MethodName, invocation.ContractName);
 87                     FileLogUtility.Warn (msg);
 88                     FileLogUtility.Warn (ex);
 89                     throw new Exception (msg, ex);
 90                 }
 91             }
 92 
 93             var paramters = new object[invokerTuple.Item2.Length];
 94 
 95             for(int i = 0; i < invokerTuple.Item2.Length; i++)
 96             {
 97                 try
 98                 {
 99                     paramters[i] = JSON.ToObject(invocation.JsonSerializedParameters[i], invokerTuple.Item2[i]);
100                 }
101                 catch(Exception ex) 
102                 {
103                     msg = string.Format ("Failed to unserialize the '{0}'th parameter for the method '{1}' of service '{2}'.", i + 1, invocation.MethodName, invocation.ContractName);
104                     FileLogUtility.Warn (msg);
105                     FileLogUtility.Warn (ex);
106                     throw new Exception (msg, ex);
107                 }
108             }
109 
110             try
111             {
112                 return JSON.ToJSON(invokerTuple.Item3(service, paramters));
113             }
114             catch(Exception ex) 
115             {
116                 msg = string.Format("Failed to invoke the method '{0}' of service '{1}'.", invocation.MethodName, invocation.ContractName);
117                 FileLogUtility.Warn (msg);
118                 FileLogUtility.Warn (ex);
119                 throw new Exception (msg, ex);
120             }
121         }
122     }
123 }
复制代码

 

3.1.2 远程服务客户端插件的实现

接下来,我们看看远程服务客户端插件的实现。它定义了一个IRemoteServiceProxyService服务,暴露了两个接口分别用于对远程服务和负载均衡远程服务的调用。

clip_image024

该服务的远程服务获取实现如下所示。

clip_image026

它仅仅时通过DynamicProxy创建远程服务代理类,此时,对代理类方法的调用会转换成远程服务调用。下面看看拦截机的实现。

复制代码
 1 class RemoteServiceProxyInterceptor : IInterceptor, IDisposable
 2 {
 3     private RemoteServiceContext _remoteServiceContext;
 4     private RemoteServiceClient _remoteServiceClient;
 5     public RemoteServiceProxyInterceptor(RemoteServiceContext context)
 6     {
 7         _remoteServiceContext = context;
 8         _remoteServiceClient = new RemoteServiceClient(_remoteServiceContext.IPAddress, _remoteServiceContext.Port);
 9         _remoteServiceClient.Start();
10     }
11     public void Intercept(IInvocation invocation)
12     {
13         try
14         {
15             var jsonParameters = new List<string>();
16             foreach (var param in invocation.Arguments)
17             {
18                 jsonParameters.Add(JSON.ToJSON(param));
19             }
20 
21             var resultJson = _remoteServiceClient.Invoke(invocation.Method.DeclaringType.FullName, invocation.Method.Name, jsonParameters);
22 
23             if (!invocation.Method.ReturnType.FullName.Equals("System.Void"))
24             {
25                 invocation.ReturnValue = JSON.ToObject(resultJson, invocation.Method.ReturnType);
26             }
27             else
28             {
29                 invocation.ReturnValue = null;
30             }
31         }
32         catch(Exception ex) 
33         {
34             FileLogUtility.Error (string.Format("Failed to invoke the remote service 'Remote Service: {0}, Method: {1}.'", 
35                 invocation.Method.DeclaringType.FullName, invocation.Method.Name));
36             throw;
37         }
38     }
39 
40     public void Dispose()
41     {
42         if (_remoteServiceClient != null)
43         {
44             _remoteServiceClient.Stop();
45             _remoteServiceClient = null;
46         }
47     }
48 }
复制代码

 

拦截机的代码很简单,对远程服务代理类方法的调用将直接转换成对远程服务宿主插件的WCF服务的调用。下面看看负载均衡远程服务的实现。

3.2 负载均衡远程服务的实现

有了以上的技术,关于负载均衡远程服务的实现,就简单多了。负载均衡远程服务的目的就是将所有远程服务统一在负载均衡服务器进行注册,并实现负载的动态管理。因此,需要在远程服务基础上,创建一个负载均衡服务器和负载均衡客户端。负载均衡服务器用于管理所有远程服务及提供远程服务的机器,管理所有远程服务的负载情况,并实现负载均衡及故障转移;负载均衡客户端的目的时将远程服务注册到负载均衡器,并且当远程服务关闭时从负载均衡器卸载。下面,看看负载均衡器的实现。

3.2.1 负载均衡器实现

负载均衡服务器暴露了如下WCF服务。这个服务用于提供远程服务注册和卸载以及负载均衡请求。

clip_image028

这个服务的实现如下所示。

clip_image030

它使用远程服务注册表来实现远程服务的管理和负载均衡的实现。

3.2.2 负载均衡客户端的实现

负载均衡客户端的目的时实现远程服务的注册与卸载,通过该插件将远程服务暴露到负载均衡服务器。这样服务调用者就可以通过负载均衡器来调用远程服务。

clip_image032

3.2.3 负载均衡远程服务调用

负载均衡远程服务的调用方式的实现和远程服务类似。它由远程服务代理服务的GetFirstOrDefaultLoadBalancerService接口来实现。

clip_image034

该接口的实现如下所示,主要时创建代理和方法拦截机。

clip_image036

这个方法调用拦截机会将方法调用转化为:(1)从负载均衡服务器获取均衡的远程服务主机;(2)直接调用该远程服务主机的服务,如果调用失败则尝试进行重新负载均衡。其实现如下所示。

复制代码
  1 class RemoteServiceLoadBalancerProxyInterceptor : IInterceptor, IDisposable
  2 {
  3     private string LoadBalancerHost
  4     {
  5         get
  6         {
  7             return ConfigurationSettings.AppSettings["LoadBalancerHost"];
  8         }
  9     }
 10 
 11     private string LoadBalancerPort
 12     {
 13         get
 14         {
 15             return ConfigurationSettings.AppSettings["LoadBalancerPort"];
 16         }
 17     }
 18 
 19     private LoadBalancerContext _remoteServiceLoadBalancerContext;
 20     private RemoteServiceClient _remoteServiceClient;
 21     private RemoteServiceLoadBalancerAccessClient _remoteServiceLoadBalancerAccessClient;
 22     private bool _initialized;
 23 
 24     public RemoteServiceLoadBalancerProxyInterceptor(LoadBalancerContext context)
 25     {
 26         _remoteServiceLoadBalancerContext = context;
 27 
 28         if (string.IsNullOrEmpty(LoadBalancerHost))
 29         {
 30             throw new Exception("You need to specified the load balancer host (HostName or IP Address) by app setting 'LoadBalancerHost'.");
 31         }
 32 
 33         int loadBalancerPortInt;
 34         if (!int.TryParse(LoadBalancerPort, out loadBalancerPortInt))
 35         {
 36             throw new Exception("You need to specified the load balancer port by app setting 'LoadBalancerPort'.");
 37         }
 38 
 39         try
 40         {
 41             _remoteServiceLoadBalancerAccessClient = new RemoteServiceLoadBalancerAccessClient (LoadBalancerHost, loadBalancerPortInt);
 42             _remoteServiceLoadBalancerAccessClient.Start ();
 43         }
 44         catch(Exception ex)
 45         {
 46             FileLogUtility.Error (string.Format("Faild to connect to load balancer '{0}'.", _remoteServiceLoadBalancerContext));
 47             FileLogUtility.Error (ex);
 48             throw;
 49         }
 50     }
 51 
 52     private bool Initialize(string serviceContractName)
 53     {
 54         if(_remoteServiceClient != null)
 55         {
 56             _remoteServiceClient.Stop();
 57         }
 58                 
 59         RemoteServiceHost remoteHost = null;
 60         try
 61         {
 62             remoteHost = _remoteServiceLoadBalancerAccessClient.Balance(serviceContractName);
 63             FileLogUtility.Inform(string.Format("Get the remote service host '{0}' by load balancer '{1}'.", remoteHost, _remoteServiceLoadBalancerContext));
 64         }
 65         catch(Exception ex) 
 66         {
 67             FileLogUtility.Error (string.Format("Faild to get a remote service host by load balancer '{0}'.", _remoteServiceLoadBalancerContext));
 68             FileLogUtility.Error (ex);
 69             return false;
 70         }
 71         if (remoteHost != null) 
 72         {
 73             _remoteServiceClient = new RemoteServiceClient (remoteHost.IPAddress, remoteHost.Port);
 74             try
 75             {
 76                 _remoteServiceClient.Start ();
 77                 return true;
 78             }
 79             catch(Exception ex) 
 80             {
 81                 FileLogUtility.Error (string.Format("Failed to connect to the remote service host '{0}' by using load balancer '{1}'.", remoteHost, _remoteServiceLoadBalancerContext));
 82             }
 83         }
 84 
 85         return false;
 86     }
 87 
 88     public void Intercept(IInvocation invocation)
 89     {
 90         var serviceContractName = invocation.Method.DeclaringType.FullName;
 91         if (!_initialized)
 92         {
 93             _initialized = Initialize (serviceContractName);
 94             if (!_initialized) 
 95             {
 96                 invocation.ReturnValue = null;
 97                 return;
 98             }
 99         }
100 
101         var jsonParameters = new List<string>();
102         foreach (var param in invocation.Arguments)
103         {
104             jsonParameters.Add(JSON.ToJSON(param));
105         }
106 
107         int tryTimes = 1;
108 
109         for (int i = 0; i < tryTimes; i ++ )
110         {
111             try
112             {
113                 var resultJson = _remoteServiceClient.Invoke(serviceContractName, invocation.Method.Name, jsonParameters);
114 
115                 if (!invocation.Method.ReturnType.FullName.Equals("System.Void"))
116                 {
117                     invocation.ReturnValue = JSON.ToObject(resultJson, invocation.Method.ReturnType);
118                 }
119                 else
120                 {
121                     invocation.ReturnValue = null;
122                 }
123                 return;
124             }
125             catch(Exception ex) 
126             {
127                 FileLogUtility.Error (string.Format("Failed to invoke the remote service 'Remote Service: {0}, Method: {1}.'", 
128                     serviceContractName, invocation.Method.Name));
129                 FileLogUtility.Error (ex);
130                 if (i == tryTimes) 
131                 {
132                     throw;
133                 }
134                 if (!((_initialized = Initialize (serviceContractName)) == true))  // 重新Balance
135                 {
136                     throw;
137                 }
138             }
139         }
140     }
141 
142     public void Dispose()
143     {
144         if (_remoteServiceClient != null)
145         {
146             _remoteServiceClient.Stop();
147             _remoteServiceClient = null;
148         }
149     }
150 }
复制代码

 

4 小结

在这篇文章,我详细介绍了支持集群的远程服务的实现。你可以发现,整体实现完全按照模块化组装的方式。你也可以尝试来考虑以模块化组装的方法实现一个远程服务集群。多谢支持~~。


本文转自道法自然博客园博客,原文链接:http://www.cnblogs.com/baihmpgy/p/3765525.html,如需转载请自行联系原作者

相关实践学习
SLB负载均衡实践
本场景通过使用阿里云负载均衡 SLB 以及对负载均衡 SLB 后端服务器 ECS 的权重进行修改,快速解决服务器响应速度慢的问题
负载均衡入门与产品使用指南
负载均衡(Server Load Balancer)是对多台云服务器进行流量分发的负载均衡服务,可以通过流量分发扩展应用系统对外的服务能力,通过消除单点故障提升应用系统的可用性。 本课程主要介绍负载均衡的相关技术以及阿里云负载均衡产品的使用方法。
目录
相关文章
|
25天前
|
XML JSON API
ServiceStack:不仅仅是一个高性能Web API和微服务框架,更是一站式解决方案——深入解析其多协议支持及简便开发流程,带您体验前所未有的.NET开发效率革命
【10月更文挑战第9天】ServiceStack 是一个高性能的 Web API 和微服务框架,支持 JSON、XML、CSV 等多种数据格式。它简化了 .NET 应用的开发流程,提供了直观的 RESTful 服务构建方式。ServiceStack 支持高并发请求和复杂业务逻辑,安装简单,通过 NuGet 包管理器即可快速集成。示例代码展示了如何创建一个返回当前日期的简单服务,包括定义请求和响应 DTO、实现服务逻辑、配置路由和宿主。ServiceStack 还支持 WebSocket、SignalR 等实时通信协议,具备自动验证、自动过滤器等丰富功能,适合快速搭建高性能、可扩展的服务端应用。
86 3
|
6天前
|
机器学习/深度学习 自然语言处理 并行计算
DeepSpeed分布式训练框架深度学习指南
【11月更文挑战第6天】随着深度学习模型规模的日益增大,训练这些模型所需的计算资源和时间成本也随之增加。传统的单机训练方式已难以应对大规模模型的训练需求。
28 3
|
10天前
|
存储 缓存 监控
解决分布式系统演进过程中数据一致性问题的方法
【10月更文挑战第24天】解决分布式系统演进过程中数据一致性问题是一个复杂而又重要的任务。需要综合运用多种方法和技术,根据具体的系统需求和场景,选择合适的解决方案。同时,不断地进行优化和改进,以适应不断变化的分布式系统环境。
28 4
|
10天前
|
机器学习/深度学习 并行计算 Java
谈谈分布式训练框架DeepSpeed与Megatron
【11月更文挑战第3天】随着深度学习技术的不断发展,大规模模型的训练需求日益增长。为了应对这种需求,分布式训练框架应运而生,其中DeepSpeed和Megatron是两个备受瞩目的框架。本文将深入探讨这两个框架的背景、业务场景、优缺点、主要功能及底层实现逻辑,并提供一个基于Java语言的简单demo例子,帮助读者更好地理解这些技术。
30 2
|
25天前
|
开发框架 前端开发 API
C#/.NET/.NET Core优秀项目和框架2024年9月简报
C#/.NET/.NET Core优秀项目和框架2024年9月简报
|
25天前
|
Linux C# Android开发
.NET开源跨平台桌面和移动应用的统一框架 - Eto.Forms
.NET开源跨平台桌面和移动应用的统一框架 - Eto.Forms
114 1
|
25天前
|
开发框架 JavaScript 前端开发
一个适用于 ASP.NET Core 的轻量级插件框架
一个适用于 ASP.NET Core 的轻量级插件框架
|
25天前
|
开发框架 缓存 前端开发
WaterCloud:一套基于.NET 8.0 + LayUI的快速开发框架,完全开源免费!
WaterCloud:一套基于.NET 8.0 + LayUI的快速开发框架,完全开源免费!
|
缓存 Shell Linux
分布式--Linux 基础
1. Linux 安装 1). 工具下载 VMware Workstationcentos 2). 安装VMware工具--(傻瓜式安装) 图1.
1232 0
|
16天前
|
Linux
Linux系统之expr命令的基本使用
【10月更文挑战第18天】Linux系统之expr命令的基本使用
51 4