首页 文章 精选 留言 我的

精选列表

搜索[学习],共10000篇文章
优秀的个人博客,低调大师

Hadoop概念学习系列之为什么hadoop/spark执行作业时,输出路径必须要不存在?(三十九)

很多人只会,但没深入体会和想为什么要这样? 拿Hadoop来说,当然,spark也一样的道理。 输出路径由Hadoop自己创建,实际的结果文件遵守part-nnnn的约定。 如何指定一个已有目录作为Hadoop作业的输出路径,作业将无法进行,并会抛出异常抗议一个已经存在的目录。如果想让Hadoop将输出存储到一个目录,它必须是不存的目录。其实,我们可以把这个特点当做Hadoop的一种安全机制,它可以防止Hadoop重写有用的文件以及用户总是忘记弄清的事。 本文转自大数据躺过的坑博客园博客,原文链接:http://www.cnblogs.com/zlslch/p/6092396.html,如需转载请自行联系原作者

优秀的个人博客,低调大师

AgileEAS.NET SOA 中间件平台5.2版本下载、配置学习(一):下载平台并基于直连环境运行

一、前言 AgileEAS.NET SOA 中间件平台是一款基于基于敏捷并行开发思想和Microsoft .Net构件(组件)开发技术而构建的一个快速开发应用平台。用于帮助中小型软件企业建立一条适合市场快速变化的开发团队,以达到节省开发成本、缩短开发时间,快速适应市场变化的目的。 AgileEAS.NET SOA中间件平台提供了敏捷快速开发软件工程的最佳实践,通过提供大量的基础支撑功能如IOC、ORM、SOA、分布式体系及敏捷并发开发方法所支撑的插件开发体系,以及提供了大量的实体、数据模型设计生成工具、代码生成工具,用于帮助中小软件开发商快速成长。 AgileEAS.NET平台充分把握目前软件行业快速发展的新趋势,基于敏捷并行开发、快速适应市场这样淳朴的软件工程实践,采用业界广泛使用的Microsoft .Net构件(组件)开发技术实践了这种开发思想,帮助软件企业实现“敏捷变化、快速适合”的目标,从而帮助软件企业在激烈的市场竞争中赢得先机并获得更高的回报。 二、下载AgileEAS.NET SOA 中间件平台5.2 版本 通过AgileEAS.NET SOA 中间件平台官方网站最新下载页面进行下载: 可以选择通过下载打好的RAR压缩包,也可以选择通过SVN进行下载,一版情况下我们对AgileEAS.NET SOA 中间件平台进行改动和修正之后都会把最新的上传到SVN传大家下载,RAR包也会及时的重新打包,但是有是可以因为某些原因没有及时对RAR压缩包进行更新,所以最新的还是在SVN上,如果有条件的情况尽量通过SVN更新最新版本。 下载完成平台之后,有关于发布包之中的目录结构及程序集介绍请参考AgileEAS.NET SOA 中间件平台 5.2 发布说明一文。 三、初始化数据库并直接方式运行WinClient运行容器 运行bin\dotnet目录之中的EAS.DbInitializer.exe程序,即AgileEAS.NET SOA中间件数据库初始化工具,数据库初始化工具会帮助你完成AgileEAS.NET SOA中间件平台运行所必须的表结构及数据: 运行界面如下: 关于选择何种数据库请根据你的喜欢和你的擅长,假设我们选择SQL Server(2005/2008),然后进行下一步: 我们选择创建一个新库,在新库上创建AgileEAS.NET SOA 中间件的运行环境: 我们填写数据库名称,然后点“开始创建数据库”,完成后会有如下提示: 点确定后返回到如下界面: 我们点击完成之后开始AgileEAS.NET SOA中间件平台的数据库初始化工作: 数据库初始化工作包括创建数据库结构及建立初始化数据,初始化完成后会有完成提示(上图)。 如果选中了“完成后启动AgileEAS.NET SOA平台WinClient运行容器”,则会在初始化数据完成之后,我们点击确定之后启动AgileEAS.NET SOA中间件平台WinClient运行容器并以刚初始化完毕的数据库为基础进行运行,如下登录界面: 界面Administrator(管理员)初始密码:sa进入AgileEAS.NET SOA 中间件平台WinClient运行容器: 至此,AgileEAS.NET SOA 中间件平台的数据库初始化及直连数据库环境配置完成。 四、直连数据库的配置文件介绍 AgileEAS.NET SOA中间件平台的系统配置文档,其核心是AgileEAS.NET SOA中间件平台IOC框架的配置文件,其中配置了运行环境所依赖的数据库连接、数据访问、ORM数据存取等相关的组件的动态配置,以下为本例直接数据库的配置文件内容: 1: <?xml version="1.0" encoding="utf-8"?> 2: <configuration> 3: <configSections> 4: <section name="eas" type="EAS.ConfigHandler,EAS.MicroKernel" /> 5: </configSections> 6: <startup useLegacyV2RuntimeActivationPolicy="true"> 7: <supportedRuntime version="v4.0"/> 8: </startup> 9: <eas> 10: <configurations> 11: <item name="Key" value="Value" /> 12: </configurations> 13: <objects> 14: <object name="DbProvider" assembly="EAS.Data" type="EAS.Data.Access.SqlClientDbProvider" LifestyleType="Thread"> 15: <property name="ConnectionString" type="string" value="Data Source=.;Initial Catalog=eas_demo;User ID=sa;Password=sa1q2w3e4r;Connect Timeout=0" /> 16: </object> 17: <object name="DataAccessor" assembly="EAS.Data" type="EAS.Data.Access.DataAccessor" LifestyleType="Thread"> 18: <property name="DbProvider" type="object" value="DbProvider"/> 19: <property name="Language" type="object" value="TSqlLanguage"/> 20: </object> 21: <object name="OrmAccessor" assembly="EAS.Data" type="EAS.Data.ORM.OrmAccessor" LifestyleType="Thread"> 22: <property name="DataAccessor" type="object" value="DataAccessor"/> 23: </object> 24: <!--查询语言--> 25: <object name="TSqlLanguage" assembly="EAS.Data" type="EAS.Data.Linq.TSqlLanguage" LifestyleType="Thread"/> 26: <!--服务桥--> 27: <object name="ServiceBridger" assembly="EAS.MicroKernel" type="EAS.Services.DirectServiceBridger" LifestyleType="Singleton" /> 28: <!--日志管理--> 29: <object name="Logger" assembly="EAS.MicroKernel" type="EAS.Loggers.TextLogger" LifestyleType="Singleton" /> 30: <!--资源--> 31: <!--<object name="EAS.Explorer.Resource" assembly="EAS.Explorer.Res" type="EAS.Explorer.Res.Resources" LifestyleType="Singleton" />--> 32: </objects> 33: </eas> 34: </configuration> 35: 其中eas/configurations中配置的内容供EAS.Configuration.Config对象进行读取的一些基本配置,供开发人员在处理系统的一些配置参数时候,在AgileEAS.NET SOA 平台内部没有使用。 其中eas/objects中配置的内容供EAS.Objects.ComponentConfig对象进行读取的IOC框架的对象配置信息,因为AgileEAS.NET SOA中间件平台在设计时支持多种数据库系统以及连接、分布式结构,所以系统的主体结果是不会变的,当换不同的数据的时候,只需要改变一下IOC之中的数据库连接对象的配置信息就能随便的切换,同样,系统在进行直连与分布式切换的时候,只需要通过修改配置文件就可以达到这样的目录。 本例的配置文件之中,以下配置内容,定义了系统使用的数据库连接对象为EAS.Data.Access.SqlClientDbProvider,即为SQLServer的连接: 1: <object name="DbProvider" assembly="EAS.Data" type="EAS.Data.Access.SqlClientDbProvider" LifestyleType="Thread"> 2: <property name="ConnectionString" type="string" value="Data Source=.;Initial Catalog=eas_demo;User ID=sa;Password=sa1q2w3e4r;Connect Timeout=0" /> 3: </object> 如需要切换其数据库我们只需要改变对象“DbProvider”的类库信息及修改数据库的连接字符串描述,比如做如下修改: 1: <object name="DbProvider" assembly="EAS.Data.Provider" type="EAS.Data.Access.SqliteProvider" LifestyleType="Thread"> 2: <property name="ConnectionString" type="string" value="Data Source=..\db\Chat.db;" /> 3: </object> 就变成了基于Sqlite数据库的定义了,当然了,改变数据库光改编“DbProvider”的定义还不够,还需要改变一下“DataAccessor”对象的属性“Language”定义,即需要把以下配置内容: 1: <object name="DataAccessor" assembly="EAS.Data" type="EAS.Data.Access.DataAccessor" LifestyleType="Thread"> 2: <property name="DbProvider" type="object" value="DbProvider"/> 3: <property name="Language" type="object" value="TSqlLanguage"/> 4: </object> 5: <!--查询语言--> 6: <object name="TSqlLanguage" assembly="EAS.Data" type="EAS.Data.Linq.TSqlLanguage" LifestyleType="Thread"/> 改变为: 1: <object name="DataAccessor" assembly="EAS.Data" type="EAS.Data.Access.DataAccessor" LifestyleType="Thread"> 2: <property name="DbProvider" type="object" value="DbProvider"/> 3: <property name="Language" type="object" value="SqliteLanguage"/> 4: </object> 5: <!--Linq查询语言--> 6: <object name="SqliteLanguage" assembly="EAS.Data.Provider" type="EAS.Data.Linq.SqliteLanguage" LifestyleType="Thread"/> 这样我们就把基于SQLServer的数据环境改变为基于Sqlite数据库的数据环境了。 另外一个很重要的配置是系统是工做于数据库直联还是工作于分布式,其决定是由“ServiceBridger”对象所决定,直练环境使用如下配置信息: 1: <object name="ServiceBridger" assembly="EAS.MicroKernel" type="EAS.Services.DirectServiceBridger" LifestyleType="Singleton" /> 分布式环使用如下配置: 1: <object name="ServiceBridger" assembly="EAS.Distributed" type="EAS.Distributed.ServiceBridger" LifestyleType="Singleton"/> 当然了,变不是只改变这一个对象,改变为分布式是还需要改变“DataAccessor”、“OrmAccessor”对象的定义,在些就不做一一界面,愿意的朋友请使用AgileEAS.NET SOA中间件平台的配置文件定义工具(EAS.Configure.exe)多试试。 五、联系我们 为了完善、改进和推广AgileEAS.NET而成立了敏捷软件工程实验室,是一家研究、推广和发展新技术,并致力于提供具有自主知识产权的业务基础平台软件,以及基于业务基础平台了开发的管理软件的专业软件提供商。主要业务是为客户提供软件企业研发管理解决方案、企业管理软件开发,以及相关的技术支持,管理及技术咨询与培训业务。 AgileEAS.NET平台自2004年秋呱呱落地一来,我就一直在逐步完善和改进,也被应用于保险、医疗、电子商务、房地产、铁路、教育等多个应用,但一直都是以我个人在推广,2010年因为我辞职休息,我就想到把AgileEAS.NET推向市场,让更多的人使用。 技术团队成员都是合作多年的老朋友,因为这个平台是免费的,我们的营运开支主要靠为客户提供咨询服务所得,我们都是因为程序员的那种理想与信念坚持,在此我感谢一起奋斗的朋友和一直支持我们工作的客户、朋友。 团队网站:http://www.agilelab.cn, AgileEAS.NET网站:http://www.smarteas.net 官方博客:http://eastjade.cnblogs.com QQ:47920381 AgileEAS.NET QQ群: 113723486(AgileEAS SOA 平台)/上限1000人 199463175(AgileEAS SOA 交流)/上限1000人 120661978(AgileEAS.NET 平台交流)/上限1000人 212867943(AgileEAS.NET研究)/上限500人 147168308(AgileEAS.NET应用)/上限500人 172060626(深度AgileEAS.NET平台)/上限500人 116773358(AgileEAS.NET 平台)/上限500人 125643764(AgileEAS.NET探讨)/上限500人 193486983(AgileEAS.NET 平台)/上限500人 邮件:james@agilelab.cn,mail.james@qq.com, 电话:18629261335。 作者:魏琼东出处:http://www.cnblogs.com/eastjade关于作者:有13年的软件从业经历,专注于中小软件企业软件开发过程研究,通过在技术与管理帮助中小软件企业实现技术层面开源节流的目的。熟悉需求分析、企业架构、项目管理。现主要从事基于AgileEAS.NET平台的技术咨询工作,主要服务于医疗卫生、铁路、电信、物流、物联网、制造、零售等行业。如有问题或建议,请多多赐教!本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,如有问题,可以通过mail.james@qq.com联系我,也可以加入QQ群:113723486、199463175、116773358、116773358、212867943、147168308、59827496、193486983、15118502和大家共同讨论,非常感谢。 本文转自魏琼东博客园博客,原文链接:http://www.cnblogs.com/eastjade/p/3401374.html,如需转载请自行联系原作者

优秀的个人博客,低调大师

学习ASP.NET Core,怎能不了解请求处理管道[2]: 服务器在管道中的“龙头”地位

ASP.NET Core管道由注册的服务器和一系列中间件构成。我们在上一篇中深入剖析了中间件,现在我们来了解一下服务器。服务器是ASP .NET Core管道的第一个节点,它负责完整请求的监听和接收,最终对请求的响应同样也由它完成。[本文已经同步到《ASP.NET Core框架揭秘》之中] 服务器是我们对所有实现了IServer接口的所有类型以及对应对象的统称。如下面的代码片段所示,这个接口具有一个只读属性Features返回描述自身特性集合的FeatureCollection对象,另一个Start方法用于启动服务器。 1: public interface IServer : IDisposable 2: { 3: IFeatureCollection Features { get; } 4: void Start<TContext>(IHttpApplication<TContext> application); 5: } 当我们Start方法启动指定的Server的时候,必须指定一个类型为IHttpApplication<TContext>的参数,我们将实现才接口的所有类型及其对应对象统称为HttpApplication。当服务器在接收到抵达的请求之后,它会直接交给这个HttpApplication对象来处理,所以我们需要先来认识一下这个对象。 一、HttpApplication 对于ASP.NET Core管道来说,HttpApplication对会接管服务器接收的请求,后续的请求完全由它来负责。如下图所示,HttpApplication从服务器获得请求之后,会利用注册的中间件注册对请求进行处理,并最终将请求递交给应用程序。HttpApplication针对请求的处理实际上会在一个执行上下文中完成,这个上下文为应用对单一请求的整个处理过程定义了一个边界。单纯描述HTTP请求的HttpContext是这个执行上下文中最为核心的部分,除此之外,我们还可以根据需要将其他相关的信息定义其中,所以IHttpApplication<TContext>接口采用泛型参数的形式来表示定义这个上下文的类型。 HttpApplication不仅仅需要在这个执行上下文中处理服务器转发给它的请求,这个上下文对象的创建和回收释放同样需要由它来完成。如下面的代码片段所示,IHttpApplication<TContext>接口的CreateContext和DisposeContext方法分别体现了针对执行上下文的创建和释放,CreateContext方法的参数contextFeatures表示描述原始上下文的特性集合。在此上下文中针对请求的处理实现在另一个方法ProcessRequestAsync之中。 1: public interface IHttpApplication<TContext> 2: { 3: TContext CreateContext(IFeatureCollection contextFeatures); 4: void DisposeContext(TContext context, Exception exception); 5: Task ProcessRequestAsync(TContext context); 6: } 在默认情况下创建的HttpApplication是一个HostingApplication对象。对于HostingApplication来说,它创建的执行上下文的类型是一个具有如下定义的结构Context。对于这个Context对象表示的针对当前请求的执行上下文来说,描述当前HTTP请求的HttpContext是最为核心的部分。除了这个HttpContext属性之外,Context还具有额外两个属性,其中Scope是为追踪诊断而创建的日志上下文范围,该范围将针对同一个请求的多项日志记录进行关联,而另一个属性StartTimestamp表示应用开始处理请求的时间戳。 1: public class HostingApplication : IHttpApplication<Context> 2: { 3: //省略成员 4: public struct Context 5: { 6: public HttpContext HttpContext { get; set; } 7: public IDisposable Scope { get; set; } 8: public long StartTimestamp { get; set; } 9: } 10: } 由于HostingApplication针对请求的处理是通过注册的中间件来完成的,而这些中间件最终会利用上面介绍的ApplicationBuilder对象转换成一个类型为RequestDelegate的委托对象,所有中间件对请求的处理通过执行这个委托对象来完成。我们在创建HostingApplication的时候需要提供这么一个RequestDelegate对象。由HostingApplication创建的Context对象包含表示HTTP上下文的HttpContext对象,而后者是通过对应的工厂HttpContextFactory创建的,所以HttpContextFactory在创建时也是必须要提供的。如下面的代码片段所示,HostingApplication类型的构造函数需要将这两个对象作为输入参数,至于另外两个参数(logger和diagnosticSource),它们与日志记录有关。 1: public class HostingApplication : IHttpApplication<HostingApplication.Context> 2: { 3: private readonly RequestDelegate _application; 4: private readonly DiagnosticSource _diagnosticSource; 5: private readonly IHttpContextFactory _httpContextFactory; 6: private readonly ILogger _logger; 7: 8: public HostingApplication(RequestDelegate application, ILogger logger, DiagnosticSource diagnosticSource, IHttpContextFactory httpContextFactory) 9: { 10: _application = application; 11: _logger = logger; 12: _diagnosticSource = diagnosticSource; 13: _httpContextFactory = httpContextFactory; 14: } 15: } 下面给出的代码片段基本体现了HostingApplication创建和释放Context对象,以及在此上下文中处理请求的逻辑。在CreateContext方法中,它直接利用初始化提供的HttpContextFactory创建一个HttpContext并将其作为Context对象的同名属性,至于Context额外两个属性(Scope和StartTimestamp)该作何设置,我们会在本节后续部分对此作专门介绍。实现在ProcessRequestAsync方法中针对请求的处理最终体现在对构造时指定的这个RequestDelegate对象的执行。当DisposeContext方法被执行的时候,Context的Scope属性会率先被释放,在此之后HttpContextFactory的Dispose方法被调用以完成对Context对象自身的回收释放。 1: public class HostingApplication : IHttpApplication<HostingApplication.Context> 2: { 3: public Context CreateContext(IFeatureCollection contextFeatures) 4: { 5: //省略其他实现代码 6: return new Context 7: { 8: HttpContext = _httpContextFactory.Create(contextFeatures), 9: Scope = ..., 10: StartTimestamp = ... 11: }; 12: } 13: 14: public Task ProcessRequestAsync(Context context) 15: { 16: Return _application(context.HttpContext); 17: } 18: 19: public void DisposeContext(Context context, Exception exception) 20: { 21: //省略其他实现代码 22: context.Scope.Dispose(); 23: _httpContextFactory.Dispose(context.HttpContext); 24: } 25: } 二、KestrelServer 跨平台是ASP.NET Core一个显著的特性,而KestrelServer是目前微软推出了唯一一个能够真正跨平台的服务器。KestrelServer利用一个名为KestrelEngine的网络引擎实现对请求的监听、接收和响应。KetrelServer之所以具有跨平台的特质,源于KestrelEngine是在一个名为libuv的跨平台网络库上开发的。说起libuv,就不得不谈谈libev,后者是Unix系统一个针对事件循环和事件模型的网络库。libev因其具有的高性能成为了继lievent和Event perl module之后一套最受欢迎的网络库。由于Libev不支持Windows,有人在libev之上创建了一个抽象层以屏蔽平台之间的差异,这个抽象层就是libuv。libuv在Windows平台上是采用IOCP的形式实现的,下图揭示了libuv针对Unix和Windows的跨平台实现原理。到目前为止,libuv支持的平台已经不限于Unix和Windows了,包括Linux(2.6)、MacOS和Solaris (121以及之后的版本)在内的平台在libuv支持范围之内。 如下所示的代码片段体现了KestrelServer这个类型的定义。除了实现接口IServer定义的Features属性之外,KestrelServer还具有一个类型为KestrelServerOptions的只读属性Options。这个属性表示对KestrelServer所作的相关设置,我们在调用构造函数时通过输入参数options所代表的IOptions<KestrelServerOptions>对象对这个属性进行初始化。构造函数还具有另两个额外的参数,它们的类型分别是IApplicationLifetime和ILoggerFactory,后者用于创建记录日志的Logger,前者与应用的生命周期管理有关。 1: public class KestrelServer : IServer 2: { 3: public IFeatureCollection Features { get; } 4: public KestrelServerOptions Options { get; } 5: 6: public KestrelServer(IOptions<KestrelServerOptions> options, IApplicationLifetime applicationLifetime, ILoggerFactory loggerFactory); 7: public void Dispose(); 8: public void Start<TContext>(IHttpApplication<TContext> application); 9: } 注册的KetrelServer在管道中会以依赖注入的方式被创建,并采用构造器注入的方式提供其构造函数的参数options,由于这个参数类型为IOptions<KestrelServerOptions>,所以我们利用Options模型以配置的方式来指定KestrelServerOptions对象承载的设置。比如我们可以将KestrelServer的相关配置定义在如下一个JSON文件中。 1: { 2: "noDelay" : false, 3: "shutdownTimeout" : "00:00:10", 4: "threadCount" : 10 5: } 为了让应用加载这么一个配置文件(文件名假设为“KestrelServerOptions.json”),我们只需要按照如下的方式利用ConfigurationBuilder加载这个配置文件并生成相应的Configuration对象,最后按照Options模型的编程方式完成KestrelServerOptions类型和该对象的映射即可。针对KestrelServerOptions的服务注册也可以定义在启动类型的ConfigureServices方法中。 1: IConfiguration config = new ConfigurationBuilder() 2: .AddJsonFile("KestrelServerOptions.json") 3: .Build(); 4: 5: new WebHostBuilder() 6: .UseKestrel() 7: .ConfigureServices(services=>services.Configure<KestrelServerOptions>(config)) 8: .Configure(app => app.Run(async context => await context.Response.WriteAsync("Hello World"))) 9: .Build() 10: .Run(); 我们一般通过调用WebHostBuilder的扩展方法UseKestrel方法来完成对KestrelServer的注册。如下面的代码片段所示,UseKestrel方法具有两个重载,其中一个具有同一个类型为Action<KestrelServerOptions>的参数,我们可以利用这个参数直接完成对KestrelServerOptions的设置。 1: public static class WebHostBuilderKestrelExtensions 2: { 3: public static IWebHostBuilder UseKestrel(this IWebHostBuilder hostBuilder); 4: public static IWebHostBuilder UseKestrel(this IWebHostBuilder hostBuilder, Action<KestrelServerOptions> options); 5: } 由于服务器负责请求的监听、接收和响应,所以Server是影响整个Web应用响应能力和吞吐量最大的因素之一,为了更加有效地使用服务器,我们往往针对具体的网络负载状况对其作针对性的设置。对于KestrelServer来说,在构造函数中作为参数指定的KestrelServerOptions对象代表针对它所做的设置。我们针对KestrelServer所做的设置主要体现在KestrelServerOptions类型的如下5个属性上。 1: public class KestrelServerOptions 2: { 3: //省略其他成员 4: public int MaxPooledHeaders { get; set; } 5: public int MaxPooledStreams { get; set; } 6: public bool NoDelay { get; set; } 7: public TimeSpan ShutdownTimeout { get; set; } 8: public int ThreadCount { get; set; } 9: } 三、ServerAddressesFeature 在演示的实例中,我们实际上并不曾为注册的KestrelServer指定一个监听地址,从运行的效果我们不难看出,WebHost在这种情况下会指定“http://localhost:5000”为默认的监听地址。服务器的监听地址自然可以显式指定。在介绍如何通过编程的方式为服务器指定监听地址之前,我们有先来认识一个名为ServerAddressesFeature的特性。 我们知道表示服务器的接口IServer中定义了一个类型为IFeatureCollection 的只读属性Features,它表示用于描述当前服务器的特性集合,ServerAddressesFeature作为一个重要的特性,就包含在这个集合之中。我们所说的ServerAddressesFeature对象是对所有实现了IServerAddressesFeature接口的所有类型及其对应对象的统称,该接口具有一个唯一的只读属性返回服务器的监听地址列表。ASP.NET Core默认使用的ServerAddressesFeature是具有如下定义的同名类型。 1: public interface IServerAddressesFeature 2: { 3: ICollection<string> Addresses { get; } 4: } 5: 6: public class ServerAddressesFeature : IServerAddressesFeature 7: { 8: public ICollection<string> Addresses { get; } 9: } 对于WebHost在通过依赖注入的方式创建的服务器,由它的Features属性表示的特性集合中会默认包含这么一个ServerAddressesFeature对象。如果没有一个合法的监听地址被添加到这个 ServerAddressesFeature对象的地址列表中,WebHost会将显式指定的地址(一个或者多个)添加到该列表中。我们显式指定的监听地址实际上是作为WebHost的配置保存在一个Configuration对象上,配置项对应的Key为“urls”,WebHostDefaults的静态只读属性ServerUrlsKey返回的就是这么一个Key。 1: new WebHostBuilder() 2: .(WebHostDefaults.ServerUrlsKey, "http://localhost:3721/") 3: .UseMyKestrel() 4: .UseStartup<Startup>() 5: .Build() 6: .Run(); WebHost的配置最初来源于创建它的WebHostBuilder,后者提供了一个UseSettings方法来设置某个配置项的值,所以我们可以采用如上的方式来指定监听地址(“http://localhost:3721/”)。不过,针对监听地址的显式设置,最直接的编程方式还是调用WebHostBuilder的扩展方法UseUrls,如下面的代码片段所示,该方法的实现逻辑与上面完全一致。 1: public static class WebHostBuilderExtensions 2: { 3: public static IWebHostBuilder UseUrls(this IWebHostBuilder hostBuilder, params string[] urls) 4: =>hostBuilder.UseSetting(WebHostDefaults.ServerUrlsKey, string.Join(ServerUrlsSeparator, urls)) ; 5: } 作者:蒋金楠 微信公众账号:大内老A 微博: www.weibo.com/artech 如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号 蒋金楠的自媒体将会停用)。 本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。 原文链接

优秀的个人博客,低调大师

【分布式技术专题】「Zookeeper中间件」给大家学习一下Zookeeper的”开发伴侣”—Curator-Framework(基础篇)

CuratorFramework基本介绍 CuratorFramework是Netflix公司开源的一套Zookeeper客户端框架,它作为一款优秀的ZooKeeper客户端开源工具,主要提供了对客户端到服务的连接管理和连接重试机制,以及一些扩展功能,它解决了很多ZooKeeper客户端非常底层的细节开发工作。 主要的功能包括:连接重连、反复注册Watcher和NodeExistsException异常等,目前已经成为了Apache的顶级项目,是全世界范围内使用最广泛的ZooKeeper客户端之一,Patrick Hunt(ZooKeeper代码的核心提交者)以一句 “Guava is to Java what Curator is to ZooKeeper” (Curator对于ZooKeeper,可以说就像Guava工具集对于Java平台一样,作用巨大)对其进行了高度评价。 除此之外,Curator中还提供了ZooKeeper各种应用场景(Recipe,如共享锁服务、Master选举机制和分布式计数器等)的抽象封装。 CuratorFramework编程特点 除了封装一些开发人员不需要特别关注的底层细节之外,Curator还在ZooKeeper原生API的基础上进行了包装,提供了一套易用性和可读性更强的Fluent风格的客户端API框架。 CuratorFramework项目组件 Recipes:Zookeeper典型应用场景的实现,这些实现是基于Curator Framework。 Framework:Zookeeper API的高层封装,简化Zookeeper客户端编程,添加了例如Zookeeper连接管理、重试机制等。 Utilities:为Zookeeper提供的各种实用程序。 Client:Zookeeper client的封装,用于取代原生的Zookeeper客户端(ZooKeeper类),提供一些非常有用的客户端特性。 Errors:Curator如何处理错误,连接问题,可恢复的例外等。 官方资源 官方文档 版本列表 官方文章 Maven依赖说明 由以下几个artifact的组成,但大多数情况下只用引入curator-recipes即可。 CuratorFramework简单使用 CuratorFramework的jar包在Maven仓库中心是可以找到 ,使用Maven,Gradle,Ant等可以很轻松简单的将Curator包含到项目当中。 很多用户会想要使用Curtor预编译的一些工具,所以Curator提供了curator-recipes,如果你仅仅想使用Zooeeper的简单包装,包括链接管理和重试机制,那么使用curator-framework就足够了。 Maven依赖配置 <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> <version>2.12.0</version> </dependency> 创建会话 使用CuratorFrameworkFactory这个工厂类的两个静态方法来创建一个客户端: static CuratorFramework newClient(String connectString, RetryPolicy retryPolicy); static CuratorFramework newClient(String connectString, int sessionTimeoutMs, int connectionTimeoutMs, RetryPolicy retryPolicy); 构造方法中的各参数 connectString:zk的server地址,多个server之间使用英文逗号分隔开 connectionTimeoutMs:连接超时时间,如上是30s,默认是15s sessionTimeoutMs:会话超时时间,如上是50s,默认是60s retryPolicy:失败重试策略 Session会话超时 该方法配置重连retryPolicy以及回话有效时间sessionTimeoutMs,重连就是当客户端与zookeeper 连接异常的时候,如网络波动,断开链接,支持重新连接,会话有效这个与节点的属性有关。那么zookeeper 有哪些节点属性。 重试策略 CuratorFramework通过一个接口RetryPolicy来让用户实现自定义的重试策略。在RetryPolicy来让用户实现自定义的重试策略。在RetryPolicy接口中定义了一个方法: boolean allowRetry(int retryCount, long elapsedTimeMs, RetrySleeper sleeper) RetryPolicy接口参数 默认提供了以下实现,分别为ExponentialBackoffRetry、BoundedExponentialBackoffRetry、RetryForever、RetryNTimes、RetryOneTime、RetryUntilElapsed。 通过调用CuratorFramework中的start()方法来启动会话。 获取Zookeeper连接会话 Curator链接实例(CuratorFramework)由CuratorFrameworkFactory获取,对于一个Zk集群,仅仅需要一个CuratorFramework实例: RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000,3); CuratorFramework Client = CuratorFrameworkFactory.builder() .connectString("ip:2181,ip2:2181,ip3:2181") .sessionTimeoutMs(3000) .connectionTimeoutMs(5000) .retryPolicy(retryPolicy) .build(); client.start(); client.blockUntilConnected(); 这将会使用默认的值创建一个到ZK集群的链接,唯一需要特别指定单参数是重试机制,从例子上看,你需要使用: RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3); CuratorFramework client = CuratorFrameworkFactory.newClient(zookeeperConnectionString, retryPolicy); client.start(); 获得到的CuratorFramework实例在使用之前需要调用其start方法,在不许要使用的时候需要调用close方法。 在上面这个示例程序中,我们首先创建了一个名为ExponentialBackoffRetry的重试策略,该重试策略是Curator默认提供的几种重试策略之一,其构造方法如下: ExponentialBackOffRetry(int baseSleepTimeMs, int maxRetries); ExponentialBackOffRetry(int baseSleepTimeMs, int maxRetries, int maxSleepMs); ExponentialBackoffRetry构造方法参数: 构造器含有三个参数 ExponentialBackoffRetry(int baseSleepTimeMs, int maxRetries, int maxSleepMs) baseSleepTimeMs:初始的sleep时间,用于计算之后的每次重试的sleep时间,计算公式:当前sleep时间=baseSleepTimeMs*Math.max(1, random.nextInt(1<<(retryCount+1))) maxRetries:最大重试次数 maxSleepMs:最大sleep时间,如果上述的当前sleep计算出来比这个大,那么sleep用这个时间 org.apache.curator.RetryPolicy接口 start() 开始创建会话。 blockUntilConnected() 直到连接成功或超时。 ExponentialBackoffRetry的重试策略 给定一个初始sleep时间baseSleepTimeMs,在这个基础上结合重试次数,通过以下公式计算出当前需要sleep的时间: 当前sleep时间 = baseSleepTimeMs * Math.max(1, random.nextInt(1 << (retryCount + 1))) 随着重试次数的增加,计算出的sleep时间会越来越大。如果该sleep时间在maxSleepMs的范围之内,那么就使用该sleep时间,否则使用maxSleepMs。另外,maxRetries参数控制了最大重试次数,以避免无限制的重试。 CuratorFrameworkFactory工厂在创建出一个客户端CuratorFramework实例之后,实质上并没有完成会话的创建,而是需要调用CuratorFramework的start()方法来完成会话的创建。 创建一个初始内容为空的节点 一旦你拥有了CuratorFramework实例,你可以直接调用Zookeeper,这类似ZK发布版本中提供的原生的ZooKeeper对象, client.create().forPath(path); 创建一个包含内容的节点 client.create().forPath(path,"数据欸日".getBytes()); 创建临时节点,并递归创建父节点 client.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath(path); 此处Curator和ZkClient一样封装了递归创建父节点的方法。在递归创建父节点时,父节点为持久节点。 client.create().forPath("/my/path", myData) 删除节点 删除一个子节点 client.delete().forPath(path); 删除节点并递归删除其子节点 client.delete().deletingChildrenIfNeeded().forPath(path); 指定版本进行删除 client.delete().withVersion(1).forPath(path); //如果版本不存在,则删除异常,信息如下: org.apache.zookeeper.KeeperException$BadVersionException: KeeperErrorCode = BadVersion for 强制保证删除一个节点 client.delete().guaranteed().forPath(path); 读取数据 读取节点数据内容API相当简单,Curator提供了传入一个Stat,使用节点当前的Stat替换到传入的Stat的方法,查询方法执行完成之后,Stat引用已经执行当前最新的节点Stat。 普通查询 client.getData().forPath(path); 包含状态查询 Stat stat = new Stat(); client.getData().storingStatIn(stat()).forPath(path); 更新数据 更新数据,如果未传入version参数,那么更新当前最新版本,如果传入version则更新指定version,如果version已经变更,则抛出异常。 普通更新 client.setData().forPath(path,"新内容".getBytes()); 指定版本更新 client.setData().withVersion(1).forPath(path); 更新出错,版本不一致异常: org.apache.zookeeper.KeeperException$BadVersionException: KeeperErrorCode = BadVersion for 异步接口 在使用以上针对节点的操作API时,我们会发现每个接口都有一个inBackground()方法可供调用。此接口就是Curator提供的异步调用入口。对应的异步处理接口为BackgroundCallback。此接口指提供了一个processResult的方法,用来处理回调结果。其中processResult的参数event中的getType()包含了各种事件类型,getResultCode()包含了各种响应码。 重点说一下inBackground的以下接口: public T inBackground(BackgroundCallback callback, Executor executor); //此接口就允许传入一个Executor实例,用一个专门线程池来处理返回结果之后的业务逻辑。 /** * 异步创建节点 * * 注意:如果自己指定了线程池,那么相应的操作就会在线程池中执行,如果没有指定, * 那么就会使用Zookeeper的EventThread线程对事件进行串行处理 * */ client.create().withMode(CreateMode.EPHEMERAL).inBackground(new BackgroundCallback() { public void processResult(CuratorFramework client, CuratorEvent event) throws Exception { System.out.println("当前线程:" + Thread.currentThread().getName() + ",code:" + event.getResultCode() + ",type:" + event.getType()); } }, Executors.newFixedThreadPool(10)).forPath("/async-node01"); client.create().withMode(CreateMode.EPHEMERAL).inBackground(new BackgroundCallback() { public void processResult(CuratorFramework client, CuratorEvent event) throws Exception { System.out.println("当前线程:" + Thread.currentThread().getName() + ",code:" + event.getResultCode() + ",type:" + event.getType()); } }).forPath("/async-node02"); 创建含隔离命名空间的会话 使用Curator的好处是Curator帮助我们管理客户端到ZK的链接,并且在出现网络链接的问题的时候将会执行指定的重试机制。为了实现不同的ZooKeeper业务之间的隔离,往往会为每个业务分配一个独立的命名空间,即指定一个ZooKeeper根路径。 下面所示的代码片段中定义了某一个客户端的独立命名空间为/base,那么该客户端对ZooKeeper上数据节点的任何操作,都是基于该相对目录进行的: CuratorFrameworkFactory.builder().connectString("domain1.book.zookeeper:2181") .sessionTimeoutMs(5000).retryPolicy(retryPolicy).namespace("base").build(); 参考资料 https://www.cnblogs.com/a-du/p/9892108.html

优秀的个人博客,低调大师

Hadoop概念学习系列之关于hadoop-2.2.0和hadoop2.6.0的winutils.exe、hadoop.dll版本混用(易出...

问题详情是 2016-12-10 23:24:13,317 INFO [org.apache.hadoop.metrics.jvm.JvmMetrics] - Initializing JVM Metrics with processName=JobTracker, sessionId= 2016-12-10 23:24:14,281 WARN [org.apache.hadoop.mapreduce.JobSubmitter] - Hadoop command-line option parsing not performed. Implement the Tool interface and execute your application with ToolRunner to remedy this. 2016-12-10 23:24:14,284 WARN [org.apache.hadoop.mapreduce.JobSubmitter] - No job jar file set. User classes may not be found. See Job or Job#setJar(String). 2016-12-10 23:24:14,316 INFO [org.apache.hadoop.mapreduce.lib.input.FileInputFormat] - Total input paths to process : 1 2016-12-10 23:24:14,389 INFO [org.apache.hadoop.mapreduce.JobSubmitter] - Cleaning up the staging area file:/tmp/hadoop-Administrator/mapred/staging/Administrator2004170506/.staging/job_local2004170506_0001 2016-12-10 23:24:14,391 WARN [org.apache.hadoop.fs.FileUtil] - Failed to delete file or dir [D:\tmp\hadoop-Administrator\mapred\staging\Administrator2004170506\.staging\job_local2004170506_0001\.job.split.crc]: it still exists. 2016-12-10 23:24:14,392 WARN [org.apache.hadoop.fs.FileUtil] - Failed to delete file or dir [D:\tmp\hadoop-Administrator\mapred\staging\Administrator2004170506\.staging\job_local2004170506_0001\job.split]: it still exists. Exception in thread "main" java.lang.UnsatisfiedLinkError:org.apache.hadoop.util.NativeCrc32.nativeComputeChunkedSumsByteArray(II[BI[BIILjava/lang/String;JZ)V at org.apache.hadoop.util.NativeCrc32.nativeComputeChunkedSumsByteArray(Native Method) at org.apache.hadoop.util.NativeCrc32.calculateChunkedSumsByteArray(NativeCrc32.java:86) at org.apache.hadoop.util.DataChecksum.calculateChunkedSums(DataChecksum.java:430) at org.apache.hadoop.fs.FSOutputSummer.writeChecksumChunks(FSOutputSummer.java:202) at org.apache.hadoop.fs.FSOutputSummer.flushBuffer(FSOutputSummer.java:163) at org.apache.hadoop.fs.FSOutputSummer.flushBuffer(FSOutputSummer.java:144) at org.apache.hadoop.fs.ChecksumFileSystem$ChecksumFSOutputSummer.close(ChecksumFileSystem.java:400) at org.apache.hadoop.fs.FSDataOutputStream$PositionCache.close(FSDataOutputStream.java:72) at org.apache.hadoop.fs.FSDataOutputStream.close(FSDataOutputStream.java:106) at org.apache.hadoop.mapreduce.split.JobSplitWriter.createSplitFiles(JobSplitWriter.java:80) at org.apache.hadoop.mapreduce.JobSubmitter.writeNewSplits(JobSubmitter.java:603) at org.apache.hadoop.mapreduce.JobSubmitter.writeSplits(JobSubmitter.java:614) at org.apache.hadoop.mapreduce.JobSubmitter.submitJobInternal(JobSubmitter.java:492) at org.apache.hadoop.mapreduce.Job$10.run(Job.java:1296) at org.apache.hadoop.mapreduce.Job$10.run(Job.java:1293) at java.security.AccessController.doPrivileged(Native Method) at javax.security.auth.Subject.doAs(Subject.java:415) at org.apache.hadoop.security.UserGroupInformation.doAs(UserGroupInformation.java:1628) at org.apache.hadoop.mapreduce.Job.submit(Job.java:1293) at org.apache.hadoop.mapreduce.Job.waitForCompletion(Job.java:1314) at zhouls.bigdata.myMapReduce.Anagram.Anagram.run(Anagram.java:103) at org.apache.hadoop.util.ToolRunner.run(ToolRunner.java:70) at zhouls.bigdata.myMapReduce.Anagram.Anagram.main(Anagram.java:119) 这是hadoop-2.6.0版本的 知识点: 2.4之前的和自后的需要的不一样,需要选择正确的版本(包括操作系统的版本) 本文转自大数据躺过的坑博客园博客,原文链接:http://www.cnblogs.com/zlslch/p/6158704.html,如需转载请自行联系原作者

优秀的个人博客,低调大师

学习ASP.NET Core,怎能不了解请求处理管道[1]: 中间件究竟是个什么东西?

ASP.NET Core管道虽然在结构组成上显得非常简单,但是在具体实现上却涉及到太多的对象,所以我们在 “通过重建Hosting系统理解HTTP请求在ASP.NET Core管道中的处理流程”(上篇、中篇、下篇) 中围绕着一个经过极度简化的模拟管道讲述了真实管道构建的方式以及处理HTTP请求的流程。在本系列 中,我们会还原构建模拟管道时可以舍弃和改写的部分,向读者朋友们呈现一个真是的HTTP请求处理管道。 ASP.NET Core 的请求处理管道由一个服务器与一组有序排列的中间件构成,前者仅仅完成请求监听、接收和响应这些与底层网络相关的工作,至于请求接收之后和响应之前的所有工作都交给中间件来完成。ASP.NET Core的中间件通过一个类型Func<RequestDelegate, RequestDelegate>的委托对象来表示,而RequestDelegate也是一个委托,它代表一项请求处理任务。 [本文已经同步到《ASP.NET Core框架揭秘》之中] 目录 一、RequestDelegate 二、HttpContext FeatureCollection DefaultHttpContext HttpContextFactory 三、ApplicationBuilder ApplicationBuilderFactory 中间件类型 中间件类型的注册 一、RequestDelegate 服务器接受到抵达的HTTP请求之后会构建一个描述当前请求的原始上下文,服务器的类型决定了这个原始上下文的类型,比如在我们模拟管道默认采用的HttpListenerServer由于采用HttpListener来监听、接收并响应请求,所以它对应的原始上下文是一个HttpListenerContext对象。但是对于管道的后续部分,即由注册的中间件构建的链表,它们需要采用统一的方式来处理请求,所以服务器最终会根据原始的上下文来创建一个抽象的HTTP上下文,后者通过抽象类HttpContext来表示。 我们不仅可以利用这个HttpContext获取描述当前请求的上下文信息,同样可以利用它来实现对响应的控制。针对当前请求的任何处理操作总是在这么一个上下文中进行,所以一项请求处理任务完全可以抽象成一个类型Func<HttpContext,Task>的委托来表示,实际上具有如下定义的RequestDelegate委托具有类似的定义。 1: public delegate Task RequestDelegate(HttpContext context); 每个中间件都承载着独立的请求处理任务,它本质上也体现了在当前HttpContext下针对请求的处理操作,那么为什么中间件不直接通过一个RequestDelegate对象来表示,而是表示为一个类型为Func<RequestDelegate, RequestDelegate>的委托对象呢?原因很简单,中间件并不孤立地存在,所有注册的中间件最终会根据注册的先后顺序组成一个链表,每个中间件不仅仅需要完成各自的请求处理任务外,还需要驱动链表中的下一个中间件。 如上图所示,对于一个由多个Func<RequestDelegate, RequestDelegate>对象组成的中间链表来说,某个中间件会将后一个Func<RequestDelegate, RequestDelegate>对象的返回值作为输入,而自身的返回值则作为前一个中间件的输入。某个中间件执行之后返回的RequestDelegate对象不仅仅体现了自身对请求的处理操作,而是体现了包含自己和后续中间件一次对请求的处理。那么对于第一个中间件来说,它执行后返回的RequestDelegate对象实际上体现了整个应用对请求的处理逻辑。 二、 HttpContext 对当前上下文的抽象解除了管道对具体服务器类型的依赖, 这使我们可以为ASP.NET Core应用自由地选择承载(Hosting)方式,而不是像传统的ASP.NET应用一样只能寄宿在IIS之中。抽象HTTP上下文的目的是为了实现对请求处理流程的抽象,只有这样我们才能将针对请求的某项操作体现在一个标准的中间件上,有了这个这个标准化的中间件才有所谓的请求处理管道。 ASP.NET Core通过具有如下所示的HttpContext类来表示这么一个抽象的HTTP上下文。对于一个HttpContext对象来说,它的核心体现在用于描述请求和响应的Request和Response属性之上。除此之外,我们还可以通过它获取与当前请求相关的其他上下文信息,比如用来控制用户认证的AuthenticationManager对象和代表当前请求用户的ClaimsPrincipal对象,以及描述当前HTTP连接的ConnectionInfo对象和用于控制WebSocket的WebSocketManager。我们可以获取并控制当前会话,也可以获取或者设置调试追踪的ID。 1: public abstract class HttpContext 2: { 3: 4: public abstract HttpRequest Request { get; } 5: public abstract HttpResponse Response { get; } 6: 7: public abstract AuthenticationManager Authentication { get; } 8: public abstract ClaimsPrincipal User { get; set; } 9: public abstract ConnectionInfo Connection { get; } 10: public abstract WebSocketManager WebSockets { get; } 11: public abstract ISession Session { get; set; } 12: public abstract string TraceIdentifier { get; set; } 13: public abstract CancellationToken RequestAborted { get; set; } 14: public abstract IDictionary<object, object> Items { get; set; } 15: 16: public abstract IServiceProvider RequestServices { get; set; } 17: public abstract IFeatureCollection Features { get; } 18: } 当需要中指对请求的处理时,我们可以通过为RequestAborted属性设置一个CancellationToken对象从而将终止通知发送给管道。如果需要对整个管道共享一些与当前上下文相关的数据,我们可以将它保存在通过Items属性表示的字典中。我们一再提到依赖注入被广泛地应用ASP.NET Core管道中,HttpContext的RequestServices属性返回的根据在应用启动时注册的服务而创建的ServiceProvider。只要相应的服务被预先注册到指定的服务接口上,我们就可能利用这个ServiceProvider根据这个接口得到对应的服务对象。 1: public abstract class HttpRequest 2: { 3: public abstract HttpContext HttpContext { get; } 4: public abstract string Method { get; set; } 5: public abstract string Scheme { get; set; } 6: public abstract bool IsHttps { get; set; } 7: public abstract HostString Host { get; set; } 8: public abstract PathString PathBase { get; set; } 9: public abstract PathString Path { get; set; } 10: public abstract QueryString QueryString { get; set; } 11: public abstract IQueryCollection Query { get; set; } 12: public abstract string Protocol { get; set; } 13: public abstract IHeaderDictionary Headers { get; } > 14: public abstract IRequestCookieCollection Cookies { get; set; } 15: public abstract string ContentType { get; set; } 16: public abstract Stream Body { get; set; } 17: public abstract bool HasFormContentType { get; } 18: public abstract IFormCollection Form { get; set; } 19: 20: public abstract Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken); 21: } 如上所示的是抽象类HttpRequest是对HTTP请求的描述,它是HttpContext的只读属性Request的返回类型。我们可以利用这个对象获取到描述当前请求的各种相关信息,比如请求的协议(HTTP或者HTTPS)、HTTP方法、地址,也可以获取代表请求的HTTP消息的首部和主体。 在了解了表示请求的抽象类HttpRequest之后,我们再来认识一个与之相对的用于描述响应HttpResponse类型。如下面的代码片断所示,HttpResponse依然是一个抽象类,我们可以通过定义在它之上的属性和方法来控制对请求的响应。从原则上讲,我们对请求的所做的任意类型的响应都可以利用它来说实现。当我们通过表示当前上下文的HttpContext对象得到表示响应的HttpResponse之后,我们不仅仅可以将希望的内容写入响应消息的主体,还可以设置响应状态码以及添加相应的首部。 1: public abstract class HttpResponse 2: { 3: public abstract HttpContext HttpContext { get; } 4: public abstract int StatusCode { get; set; } 5: public abstract IHeaderDictionary Headers { get; } 6: public abstract Stream Body { get; set; } 7: public abstract long? ContentLength { get; set; } 8: public abstract IResponseCookies Cookies { get; } 9: public abstract bool HasStarted { get; } 10: 11: public abstract void OnStarting(Func<object, Task> callback, object state); 12: public virtual void OnStarting(Func<Task> callback); 13: public abstract void OnCompleted(Func<object, Task> callback, object state); 14: public virtual void RegisterForDispose(IDisposable disposable); 15: public virtual void OnCompleted(Func<Task> callback); 16: public virtual void Redirect(string location); 17: public abstract void Redirect(string location, bool permanent); 18: } FeatureCollection HttpContext的另一个只读属性Features返回一组“特性”对象。在ASP.NET Core管道式处理设计中,特性是一个非常重要的概念,特性是实现抽象化HttpContext的途径。具体来说,服务器在接收到请求之后会创建一个由自身类型决定的原始的上下文,管道不仅仅利用这个原始上下文来获取与请求相关的信息,它对请求的最终响应实际上也是通过这个原始上下文来完成的。所以对一个HttpContext对象来说,由它描述的上下文信息不仅仅来源于这个原始的上下文,我们针对HttpContext所做的任何响应操作最终都需要分发给这个原始上下文来完成, 否则是不会生效的。抽象的HttpContext和原始上下文之间的“双向绑定”究竟是如何实现的呢? 这个所谓的“双向绑定”即使其实很简单。当原始上下文被创建出来之后,服务器会将它封装成一系列标准的特性对象,HttpContext正是对这些特性对象的封装。一般来说,这些特性对象所对应的类型均实现了某个预定义的标准接口,接口中不仅仅定义相应的属性来读写原始上下文中描述的信息,还定义了相应的方法来操作原始上下文。HttpContext的属性Features返回的就是这组特性对象的集合,它的返回类型为IFeatureCollection,我们将实现了该接口的类型以及对应的对象统称为FeatureCollection。 1: public interface IFeatureCollection : IEnumerable<KeyValuePair<Type, object>> 2: { 3: TFeature Get<TFeature>(); 4: void Set<TFeature>(TFeature instance); 5: 6: bool IsReadOnly { get; } 7: object this[Type key] { get; set; } 8: int Revision { get; } 9: } 一个FeatureCollection对象本质上就是一个Key和Value分别为Type和Object类型的字段,话句话说,特性对象通过对应的接口类型注册到HttpContext之上。我们通过调用Set方法将一个特性对象针对指定的类型(一般为特性接口)注册到这个字典对象上,并通过Get方法根据注册的类型获取它。特性对象的注册和获取也可以利用定义的索引来完成。如果IsReadOnly属性返回True,我们将不能注册新的特性或者修改已经注册的特性。 整数类型的之都属性Revision可以视为整个FeatureCollection对象的版本,不论是采用何种方式注册新的特性还是修改现有的特性,这个属性的值都将改变。 具有如下定义的FeatureCollection类实现了IFeatureCollection接口,我们默认使用的FeatureCollection就是这么一个类型的对象。FeatureCollection具有两个构造函数重载,默认无参构造函数帮助我们创建一个空的特性集合,另一个构造函数则需要指定一个FeatureCollection对象来提供默认特性。对于采用第二个构造函数重载创建的 FeatureCollection对象来说,当我们通过指定某个特性接口类型试图获取对应的特性对象时,如果对应的特性没有注册到当前FeatureCollection对象上,而是注册到提供默认特性的FeatureCollection对象上,后者将会提供最终的特性。 1: public class FeatureCollection : IFeatureCollection 2: { 3: //其他成员 4: public FeatureCollection(); 5: public FeatureCollection(IFeatureCollection defaults); 6: } 对于FeatureCollection类型来说,它 的IsReadOnly总是返回False,所以它永远是可读可写的。对于调用默认无参构造函数创建的FeatureCollection对象来说,它 的Revision默认返回零。如果我们通过指定另一个FeatureCollection对象为参数调用第二个构造函数来创建一个FeatureCollection对象,前者的Revision属性值将成为后者同名属性的默认值。不论我们采用何种形式(调用Set方法或者索引)添加一个新的特性或者改变了一个已经注册的特性,FeatureCollection对象的Revision属性都将自动递增。上述的这些关于FeatureCollection的特性都体现在如下所示的代码片段中。 1: FeatureCollection defaults = new FeatureCollection(); 2: Debug.Assert(defaults.Revision == 0); 3: 4: defaults.Set<IFoo>(new Foo()); 5: Debug.Assert(defaults.Revision == 1); 6: 7: defaults[typeof(IBar)] = new Bar(); 8: Debug.Assert(defaults.Revision == 2); 9: 10: FeatureCollection features = new FeatureCollection(defaults); 11: Debug.Assert(features.Revision == 2); 12: Debug.Assert(features.Get<IFoo>().GetType() == typeof(Foo)); 13: 14: features.Set<IBaz>(new Baz()); 15: Debug.Assert(features.Revision == 3); DefaultHttpContext ASP.NET Core默认使用的HttpContext类型为DefaultHttpContext,上面我们介绍的针对描述原始上下文“特性集合”来创建HttpContext的策略就体现在这个类型之上。DefaultHttpContext具有一个如下的构造函数,作为参数的FeatureCollection对象就是这么一个特性集合。 1: public class DefaultHttpContext : HttpContext 2: { 3: public DefaultHttpContext(IFeatureCollection features); 4: } 不论是组成管道的中间件还是建立在管道上的应用,在默认的情况下都是利用这个DefaultHttpContext对象来获取当前请求的相关信息,并利用这个对象来控制最终发送的响应。但是DefaultHttpContext对象这个这个过程中仅仅是一个“代理”,针对它的调用(属性或者方法)最终都需要转发给由具体服务器创建的那个原始上下文,在构造函数中指定的这个FeatureCollection对象所代表的特性集合成为了这两个上下文对象进行沟通的唯一渠道。对于定义在DefaultHttpContext中的所有属性,它们几乎都具有一个对应的特性,这些特性都对应着一个接口。表1列出了部分特性接口以及DefaultHttpContext对应的属性。 表1 描述原始HTTP上下文的特性接口 接口 属性 描述 IHttpRequestFeature Request 获取描述请求的基本信息。 IHttpResponseFeature Response 控制对请求的响应。 IHttpAuthenticationFeature AuthenticationManger/User 提供完成用户认证的AuthenticationHandler对象和表示当前用户的ClaimsPrincipal对象 IHttpConnectionFeature Connection 提供描述当前HTTP连接的基本信息。 IItemsFeature Items 提供用户存放针对当前请求的对象容器。 IHttpRequestLifetimeFeature RequestAborted 传递请求处理取消通知和中止当前请求处理。 IServiceProvidersFeature RequestServices 提供根据服务注册创建的ServiceProvider。 ISessionFeature Session 提供描述当前会话的Session对象。 IHttpRequestIdentifierFeature TraceIdentifier 为追踪日志(Trace)提供针对当前请求的唯一标识。 IHttpWebSocketFeature WebSockets 管理WebSocket 对于上面列出的众多特性接口,我们在后续相关章节中都会涉及到,目前来说我们只需要了解一下两个最重要的特性接口,即表示请求和响应的IHttpRequestFeature和IHttpResponseFeature。从下面给出的代码片断我们不难看出,这两个接口的定义分别与抽象类HttpRequest和HttpResponse具有一致的定义。对于DefaultHttpContext类型来说,它的Request和Response属性分别返回的是一个DefaultHttpRequest和DefaultHttpResponse对象。DefaultHttpRequest和DefaultHttpResponse分别继承自HttpRequest和HttpResponse,它们分别利用这个两个特性实现了从基类继承下来的所有抽象成员。 1: public interface IHttpRequestFeature 2: { 3: Stream Body { get; set; } 4: IHeaderDictionary Headers { get; set; } 5: string Method { get; set; } 6: string Path { get; set; } 7: string PathBase { get; set; } 8: string Protocol { get; set; } 9: string QueryString { get; set; } 10: string Scheme { get; set; } 11: } 12: 13: public interface IHttpResponseFeature 14: { 15: Stream Body { get; set; } 16: bool HasStarted { get; } 17: IHeaderDictionary Headers { get; set; } 18: string ReasonPhrase { get; set; } 19: int StatusCode { get; set; } 20: 21: void OnCompleted(Func<object, Task> callback, object state); 22: void OnStarting(Func<object, Task> callback, object state); 23: } 对于实现请求监听、接收和响应的服务器来说,它们都需要通过实现上面这些特性接口来定义针对性的特性类。如下图所示,当成功接收到请求之后,服务器会创建相应的特性并将它们组合成一个FeatureCollection对象,最后创建出一个DefaultHttpContext对象,我们注册的所有中间件针对这个DefaultHttpContext完成各自的请求处理工作。 HttpContextFactory 在服务器接收到抵达的请求时,它并不会直接利用原始的上下文去创建HttpContext对象,HttpContext在管道中的创建是间接地通过HttpContextFactory来完成的。 HttpContextFactory是对所有实现了IHttpContextFactory接口的所有类型及其对象的统称。如下面的代码片段所示,IHttpContextFactory接口除了定义创建HttpContext对象的Create方法之外,还定义了另一个方法Dispose来释放指定的HttpContext对象。HttpContextFactory类是该接口的默认实现者,由它的Create方法创建并返回的自然是一个DefaultHttpContext对象。 1: public interface IHttpContextFactory 2: { 3: HttpContext Create(IFeatureCollection featureCollection); 4: void Dispose(HttpContext httpContext); 5: } 6: 7: public class HttpContextFactory : IHttpContextFactory 8: { 9: //省略其他成员 10: public HttpContext Create(IFeatureCollection featureCollection); 11: public void Dispose(HttpContext httpContext); 12: } 综上所述,组成管道的所有中间件在一个标准化的上下文中完整对请求的处理,这个上下文通过抽象类HttpContext表示,ASP.NET Core默认使用的是它的子类DefaultHttpContext。一个DefaultHttpContext对象是根据描述原始上下文的特性集合,每个特性对应的类型都实现了标准的接口,接口IHttpRequestFeature和IHttpResponseFeature分别代表针对请求和响应的特性。HttpContext默认情况下是通过注册的工厂创建的,该工厂通过接口IHttpContextFactory表示,默认使用的HttpContext工厂类型为HttpContextFactory,它也是DefaultHttpContext对象的创建者。 三、ApplicationBuilder 以类型为Func<RequestDelegate, RequestDelegate>的委托对象表示的中间件需要在启动的时候注册到应用程序上。所有注册的中间件最终会转换成一个RequestDelegate类型的委托对象,它们按照注册顺序对请求的处理流程最终体现在对这个委托对象的执行。不论是最终将中间件转换成RequestDelegate对象,还是最初对它们的注册,都是通过一个名为ApplicationBuilder的对象来完成的。 ApplicationBuilder是我们对所有实现了IApplicationBuilder接口的所有类型以及对应对象的统称。接口IApplicationBuilder定义如下,中间件的注册和RequestDelegate对象的生成分别通过调用它的Use和Build方法来完成。除了这两个核心方法,IApplicationBuilder接口还定义了三个属性,其中ApplicationServices返回根据最初服务注册生成的ServiceProvider对象,而ServerFeatures属性返回的FeatureCollection对象是描述Server的特性集合。字典类型的Properties属性用户存储任意自定义的属性,而New方法会根据自己“克隆”出一个新的ApplicationBuilder对象,这两个ApplicationBuilder对象应用具有相同的属性集合。 1: public interface IApplicationBuilder 2: { 3: IServiceProvider ApplicationServices { get; set; } 4: IFeatureCollection ServerFeatures { get; } 5: IDictionary<string, object> Properties { get; } 6: 7: RequestDelegate Build(); 8: IApplicationBuilder New(); 9: IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware); 10: } 具有如下定义的ApplicationBuilder类型是对IApplicationBuilder接口的默认实现。ApplicationBuilder类型利用一个List<Func<RequestDelegate, RequestDelegate>>对象来保存注册的中间件,所以Use方法只需要将指定的中间件添加到这个列表中即可,而Build方法只需要逆序调用这些注册的中间件对应的Func<RequestDelegate, RequestDelegate>对象就能得到我们需要的RequestDelegate对象。值得一提的是,Build方法实际上在中间件链条的尾部添加了一个额外的中间件,该中间件会负责将响应状态码设置为404,如果我们没有注册一个中间件对请求作最终的响应(这样的中间件将不会试图调用后续中间件),整个管道比较回复一个状态码为404的响应。 1: public class ApplicationBuilder : IApplicationBuilder 2: { 3: private readonly IList<Func<RequestDelegate, RequestDelegate>> middlewares = new List<Func<RequestDelegate, RequestDelegate>>(); 4: 5: public IDictionary<string, object> Properties { get; } 6: 7: public IServiceProvider ApplicationServices 8: { 9: get { return GetProperty<IServiceProvider>("application.Services"); } 10: set { SetProperty<IServiceProvider>("application.Services", value); } 11: } 12: 13: public IFeatureCollection ServerFeatures 14: { 15: get { return GetProperty<IFeatureCollection>("server.Features"); } 16: } 17: 18: 19: public ApplicationBuilder(IServiceProvider serviceProvider) 20: { 21: this.Properties = new Dictionary<string, object>(); 22: ApplicationServices = serviceProvider; 23: } 24: 25: public ApplicationBuilder(IServiceProvider serviceProvider, object server) 26: : this(serviceProvider) 27: { 28: SetProperty("server.Features", server); 29: } 30: 31: public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware) 32: { 33: middlewares.Add(middleware); 34: return this; 35: } 36: 37: public IApplicationBuilder New() 38: { 39: return new ApplicationBuilder(this); 40: } 41: 42: public RequestDelegate Build() 43: { 44: RequestDelegate app = context => 45: { 46: context.Response.StatusCode = 404; 47: return Task.FromResult(0); 48: }; 49: foreach (var component in middlewares.Reverse()) 50: { 51: app = component(app); 52: } 53: return app; 54: } 55: 56: private ApplicationBuilder(ApplicationBuilder builder) 57: { 58: this.Properties = builder.Properties; 59: } 60: 61: private T GetProperty<T>(string key) 62: { 63: object value; 64: return Properties.TryGetValue(key, out value) ? (T)value : default(T); 65: } 66: 67: private void SetProperty<T>(string key, T value) 68: { 69: this.Properties[key] = value; 70: } 71: } 通过上面的代码片段我们不难看到,不论是通过ApplicationServices属性返回的ServiceProvider对象,还是通过ServerFeatures属性返回的用于描述Server特性的FeatureCollection对象,它们实际上都保存在通过Properties属性返回字典对象上。ApplicationBuilder具有两个公共构造函数重载,它们具有一个公共的参数,即用来初始化ApplicationServices属性的参数serviceProvider。 一个构造函数具有一个名为server的参数,但是这个参数并不是表示管道使用的服务器,而是承载服务器相关特性的FeatureCollection对象,不过这个参数类型被定义成Object,而不是IFeatureCollection接口。New方法直接调用私有构造函数创建出一个新的ApplicationBuilder对象,这个对象与自己的Properties属性共享同一个字典对象,由于ApplicationServices和ServerFeatures属性的返回值也存放在这个字典对象上,所以New方法得到的ApplicationBuilder对象与自身对象其实是完全等效的。 ApplicationBuilderFactory ApplicationBuilderFactory是ASP.NET Core它用来创建ApplicationBuilder的工厂,它是对所有实现了接口IApplicationBuilderFactory的所有类型以及对应对象的统称。如下面的代码片段所示,该接口定义了唯一个方法CreateBuilder根据提供的FeatureCollection对象创建出对应的ApplicationBuilder对象,这个FeatureCollection对象正是承载与服务器相关特性的集合。ApplicationBuilderFactory类型是该接口的默认实现者,当CreateBuilder方法被调用的时候,它会直接将构造时提供ServiceProvider对象和serverFeatures参数表示的FeatureCollection对象来创建返回的ApplicationBuilder对象。 1: public interface IApplicationBuilderFactory 2: { 3: IApplicationBuilder CreateBuilder(IFeatureCollection serverFeatures); 4: } 5: 6: public class ApplicationBuilderFactory : IApplicationBuilderFactory 7: { 8: private readonly IServiceProvider _serviceProvider; 9: 10: public ApplicationBuilderFactory(IServiceProvider serviceProvider) 11: { 12: this._serviceProvider = serviceProvider; 13: } 14: 15: public IApplicationBuilder CreateBuilder(IFeatureCollection serverFeatures) 16: { 17: return new ApplicationBuilder(_serviceProvider, serverFeatures); 18: } 19: } 中间件类型 虽然中间件最终体现为一个类型为 Func<RequestDelegate, RequestDelegate>的委托对象,但是我们在大部分情况下都会将中间件定义成一个单独的类型。虽然这样的中间件类型不要求实现某个预定义的接口或者继承某个预定义的基类,但是却要遵守几个必要的约定。接下来我们直接如下这个ContentMiddleware类说说一个合法的中间件类型应该如何定义。 1: public class ContentMiddleare 2: { 3: public RequestDelegate _next; 4: public byte[] _content; 5: public string _contentType; 6: 7: public ContentMiddleare(RequestDelegate next, byte[] content, string contentType) 8: { 9: _next = next; 10: _content = content; 11: _contentType = contentType; 12: } 13: 14: public async Task Invoke(HttpContext context, ILoggerFactory loggerFactory) 15: { 16: loggerFactory.CreateLogger<ContentMiddleare>().LogInformation($"Write content ({_contentType})"); 17: context.Response.ContentType = _contentType; 18: await context.Response.Body.WriteAsync(_content,0, _content.Length); 19: } 20: } 如上所示的这个中间件(ContentMiddleware)可以帮助我们将任何类型的内容响应给客户端,它的两个字段_content和_contentType分别代表响应内容和媒体类型(内容类型或者MIME类型),它体现了一个典型中间件类型的定义规则或者约定: 应该定义成实例类,不能定义成静态类。 具有一个有效的公共构造函数。这个构造函数的第一个参数类型必须为RequestDelegate,代表对请求的后续操作(可以视为下一个注册的中间件),至于后续参数的个数和类型则不作要求。 针对请求的处理定义在一个名为Invoke的公共实例方法,其返回类型为Task。该方法的第一个参数类型为HttpContext,代表当前HTTP上下文。我们可以为这个方法定义任意数量和类型的后续参数,当这个方法被执行的时候,系统将会采用依赖注入的方式提供响应的服务来为这个参数赋值。 中间件类型的注册 中间件类型的注册可以通过调用 IApplicationBuilder接口的扩展方法UseMiddleware 和UseMiddleware < TMiddleware >进行注册。如下面的代码片断所示,除了指定中间件的类型之外,我们还需要按照顺序指定调用目标构造函数的全部或者部分参数。不过不过的参数列表不需要提供作为第一个参数的RequestDelegate,如果仅仅指定了部分参数,缺失的参数将会自动通过ServiceProvider来提供。 1: public static class UseMiddlewareExtensions 2: { 3: public static IApplicationBuilder UseMiddleware<TMiddleware>(this IApplicationBuilder app, params object[] args); 4: public static IApplicationBuilder UseMiddleware(this IApplicationBuilder app, Type middleware, params object[] args); 5: } 对于上面定义的这个 ContentMiddleare类型,我们按照如下的方式对它进行了注册。当这个中间件执行的时候,它会响应客户端一张PNG图片。如果客户端是能够支持图片呈现的浏览器,这张图片会直接显示在浏览器上。 1: new WebHostBuilder() 2: .Configure(app=>app.UseMiddleware<ContentMiddleare>(File.ReadAllBytes("girl.png"),"image/png")) 3: ... 虽然中间件可以定义成任何一个遵循约定的类型,但是中间件自身在ASP.NET Core框架中总是体现为一个类型为Func<RequestDelegate, RequestDelegate>的委托对象,所以上述的这个UseMiddleware方法在执行的时候需要在内部根据注册的中间件类型和指定的参数列表创建这么一个Func<RequestDelegate, RequestDelegate>对象。其中的逻辑并不复杂,它之需要将中间件对象的创建和针对Invoke方法的调用实现在返回的委托对象中就可以了。值得一提的是,针对Invoke方法的调用并没有直接通过反射的方式来实现,而是采用表达式,后者具有更好的性能。在如下所示的代码片段中,我采用最精简的代码模拟了UseMiddleware方法的实现。 1: public static class WebHostBuilderExtensions 2: { 3: private static MethodInfo GetServiceMethod = typeof(WebHostBuilderExtensions).GetMethod("GetService", BindingFlags.Static | BindingFlags.NonPublic); 4: 5: public static IApplicationBuilder UseMiddleware<TMiddleware>(this IApplicationBuilder app, params object[] args) 6: { 7: return UseMiddleware2(app, typeof(TMiddleware), args); 8: } 9: 10: public static IApplicationBuilder UseMiddleware(this IApplicationBuilder app, Type middlewareType, params object[] args) 11: { 12: return app.Use(next => 13: { 14: return context => { 15: var factory = Compile<object>(middlewareType.GetMethod("Invoke", BindingFlags.Instance | BindingFlags.Public)); 16: object middleware = CreateMiddleware(app, middlewareType, next, args); 17: return factory(middleware, context, app.ApplicationServices); 18: }; 19: }); 20: } 21: 22: private static object CreateMiddleware(IApplicationBuilder app, Type middlewareType, RequestDelegate next, params object[] args) 23: { 24: object[] arguments = new object[args.Length + 1]; 25: arguments[0] = next; 26: args.CopyTo(arguments, 1); 27: return ActivatorUtilities.CreateInstance(app.ApplicationServices, middlewareType, arguments); 28: } 29: 30: //将对Invoke方法的调用转换成一个Func<TMiddleware, HttpContext, IServiceProvider, Task>对象 31: private static Func<TMiddleware, HttpContext, IServiceProvider, Task> Compile<TMiddleware>(MethodInfo invokeMethod) 32: { 33: ParameterExpression middleware = Expression.Parameter(typeof(TMiddleware), "middleware"); 34: ParameterExpression httpContext = Expression.Parameter(typeof(HttpContext), "httpContext"); 35: ParameterExpression serviceProvider = Expression.Parameter(typeof(IServiceProvider), "serviceProvider"); 36: 37: var arguments = from parameter in invokeMethod.GetParameters() 38: select GetArgument(httpContext, serviceProvider, parameter.ParameterType); 39: 40: Expression instance = middleware; 41: if (invokeMethod.DeclaringType != typeof(TMiddleware)) 42: { 43: instance = Expression.Convert(instance, invokeMethod.DeclaringType); 44: } 45: 46: Expression invoke = Expression.Call(instance, invokeMethod, arguments.ToArray()); 47: return Expression.Lambda<Func<TMiddleware, HttpContext, IServiceProvider, Task>>(invoke, middleware, httpContext, serviceProvider).Compile(); 48: } 49: 50: //生成调用Invoke方法的参数表达式 51: private static Expression GetArgument(Expression httpContext, Expression serviceProvider, Type parameterType) 52: { 53: if (parameterType == typeof(HttpContext)) 54: { 55: return httpContext; 56: } 57: Expression serviceType = Expression.Constant(parameterType, typeof(Type)); 58: Expression callGetService = Expression.Call(GetServiceMethod, serviceProvider, serviceType); 59: return Expression.Convert(callGetService, parameterType); 60: } 61: 62: private static object GetService(IServiceProvider serviceProvider, Type serviceType) 63: { 64: return serviceProvider.GetService(serviceType); 65: } 66: } 作者:蒋金楠 微信公众账号:大内老A 微博: www.weibo.com/artech 如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号 蒋金楠的自媒体将会停用)。 本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。 原文链接

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

用户登录
用户注册