6

ASP.NET Core 6框架揭秘实例演示[28]:自定义一个服务器

 2 years ago
source link: https://www.cnblogs.com/artech/p/inside-asp-net-core-6-28.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

作为ASP.NET Core请求处理管道的“龙头”的服务器负责监听和接收请求并最终完成对请求的响应。它将原始的请求上下文描述为相应的特性(Feature),并以此将HttpContext上下文创建出来,中间件针对HttpContext上下文的所有操作将借助于这些特性转移到原始的请求上下文上。学习ASP.NET Core框架最有效的方式就是按照它的原理“再造”一个框架,了解服务器的本质最好的手段就是试着自定义一个服务器。现在我们自定义一个真正的服务器。在此之前,我们再来回顾一下表示服务器的IServer接口。(本篇提供的实例已经汇总到《ASP.NET Core 6框架揭秘-实例演示版》)

一、IServer
二、请求和响应特性
三、StreamBodyFeature
四、HttpListenerServer

一、IServer

作为服务器的IServer对象利用如下所示的Features属性提供了与自身相关的特性。除了利用StartAsync<TContext>和StopAsync方法启动和关闭服务器之外,它还实现了IDisposable接口,资源的释放工作可以通过实现的Dispose方法来完成。StartAsync<TContext>方法将IHttpApplication<TContext>类型的参数作为处理请求的“应用”,该对象是对中间件管道的封装。从这个意义上讲,服务器就是传输层和这个IHttpApplication<TContext>对象之间的“中介”。

public interface IServer : IDisposable
{
    IFeatureCollection Features { get; }

    Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken) where TContext : notnull;
    Task StopAsync(CancellationToken cancellationToken);
}

虽然不同服务器类型的定义方式千差万别,但是背后的模式基本上与下面这个以伪代码定义的服务器类型一致。如下这个Server利用IListener对象来监听和接收请求,该对象是利用构造函数中注入的IListenerFactory工厂根据指定的监听地址创建出来的。StartAsync<TContext>方法从Features特性集合中提取出IServerAddressesFeature特性,并针对它提供的每个监听地址创建一个IListener对象。该方法为每个IListener对象开启一个“接收和处理请求”的循环,循环中的每次迭代都会调用IListener对象的AcceptAsync方法来接收请求,我们利用RequestContext对象来表示请求上下文。

public class Server : IServer
{
    private readonly IListenerFactory _listenerFactory;
    private readonly List<IListener> _listeners = new();

    public IFeatureCollection Features { get; } = new FeatureCollection();

    public Server(IListenerFactory listenerFactory) => _listenerFactory = listenerFactory;

    public async Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken) where TContext : notnull
    {
        var addressFeature = Features.Get<IServerAddressesFeature>()!;
        foreach (var address in addressFeature.Addresses)
        {
            var listener = await _listenerFactory.BindAsync(address);
            _listeners.Add(listener);
            _ = StartAcceptLoopAsync(listener);
        }

        async Task StartAcceptLoopAsync(IListener listener)
        {
            while (true)
            {
                var requestContext = await listener.AcceptAsync();
                _ = ProcessRequestAsync(requestContext);
            }
        }

        async Task ProcessRequestAsync(RequestContext requestContext)
        {
            var feature = new RequestContextFeature(requestContext);
            var contextFeatures = new FeatureCollection();
            contextFeatures.Set<IHttpRequestFeature>(feature);
            contextFeatures.Set<IHttpResponseFeature>(feature);
            contextFeatures.Set<IHttpResponseBodyFeature>(feature);

            var context = application.CreateContext(contextFeatures);
            Exception? exception = null;
            try
            {
                await application.ProcessRequestAsync(context);
            }
            catch (Exception ex)
            {
                exception = ex;
            }
            finally
            {
                application.DisposeContext(context, exception);
            }
        }
    }
    public Task StopAsync(CancellationToken cancellationToken) => Task.WhenAll(_listeners.Select(listener => listener.StopAsync()));

    public void Dispose() => _listeners.ForEach(listener => listener.Dispose());
}

public interface IListenerFactory
{
    Task<IListener> BindAsync(string listenAddress);
}

public interface IListener : IDisposable
{

    Task<RequestContext> AcceptAsync();
    Task StopAsync();
}

public class RequestContext
{
     ...
}

public class RequestContextFeature : IHttpRequestFeature, IHttpResponseFeature, IHttpResponseBodyFeature
{
    public RequestContextFeature(RequestContext requestContext);
    ...
}

StartAsync<TContext>方法接下来利用此RequestContext上下文将RequestContextFeature特性创建出来。RequestContextFeature特性类型同时实现了IHttpRequestFeature, IHttpResponseFeature和 IHttpResponseBodyFeature这三个核心接口,我们特性针对这三个接口将特性对象添加到创建的FeatureCollection集合中。特性集合随后作为参数调用IHttpApplication<TContext>的CreateContext方法将TContext上下文创建出来,后者将进一步作为参数调用另一个ProcessRequestAsync方法将请求分发给中间件管道进行处理。待处理结束,IHttpApplication<TContext>对象的DisposeContext方法被调用,创建的TContext上下文承载的资源得以释放。

二、请求和响应特性

接下来我们将采用类似的模式来定义一个基于HttpListener的服务器。提供的HttpListenerServer的思路就是利用自定义特性来封装表示原始请求上下文的HttpListenerContext对象,我们使用HttpRequestFeature和HttpResponseFeature这个两个现成特性。

public class HttpRequestFeature : IHttpRequestFeature
{
    public string 		Protocol { get; set; }
    public string 		Scheme { get; set; }
    public string 		Method { get; set; }
    public string 		PathBase { get; set; }
    public string 		Path { get; set; }
    public string 		QueryString { get; set; }

    public string 		RawTarget { get; set; }
    public IHeaderDictionary 	Headers { get; set; }
    public Stream 		Body { get; set; }
}
public class HttpResponseFeature : IHttpResponseFeature
{
    public int 		StatusCode { get; set; }
    public string? 		ReasonPhrase { get; set; }
    public IHeaderDictionary 	Headers { get; set; }
    public Stream 		Body { get; set; }
    public virtual bool 	HasStarted => false;

    public HttpResponseFeature()
    {
        StatusCode = 200;
        Headers = new HeaderDictionary();
        Body = Stream.Null;
    }

    public virtual void OnStarting(Func<object, Task> callback, object state){}
    public virtual void OnCompleted(Func<object, Task> callback, object state){}
}

如果我们使用HttpRequestFeature来描述请求,意味着HttpListener在接受到请求之后需要将请求信息从HttpListenerContext上下文转移到该特性上。如果使用HttpResponseFeature来描述响应,待中间件管道在完成针对请求的处理后,我们还需要将该特性承载的响应数据应用到HttpListenerContext上下文上。

三、StreamBodyFeature

现在我们有了描述请求和响应的两个特性,还需要一个描述响应主体的特性,为此我们定义了如下这个StreamBodyFeature特性类型。StreamBodyFeature直接使用构造函数提供的Stream对象作为响应主体的输出流,并根据该对象创建出Writer属性返回的PipeWriter对象。本着“一切从简”的原则,我们并没有实现用来发送文件的SendFileAsync方法,其他成员也采用最简单的方式进行了实现。

public class StreamBodyFeature : IHttpResponseBodyFeature
{
    public Stream 	Stream { get; }
    public PipeWriter 	Writer { get; }

    public StreamBodyFeature(Stream stream)
    {
        Stream = stream;
        Writer = PipeWriter.Create(Stream);
    }

    public Task CompleteAsync() => Task.CompletedTask;
    public void DisableBuffering() { }
    public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellationToken = default)=> throw new NotImplementedException();
    public Task StartAsync(CancellationToken cancellationToken = default) => Task.CompletedTask;
}

四、HttpListenerServer

在如下这个自定义的HttpListenerServer服务器类型中,与传输层交互的HttpListener体现在_listener字段上。服务器在初始化过程中,它的Features属性返回的IFeatureCollection对象中添加了一个ServerAddressesFeature特性,因为我们需要用它来存放注册的监听地址。实现StartAsync<TContext>方法将监听地址从这个特性中取出来应用到HttpListener对象上。

public class HttpListenerServer : IServer
{
    private readonly HttpListener _listener = new();
    public IFeatureCollection Features { get; } = new FeatureCollection();

    public HttpListenerServer() => Features.Set<IServerAddressesFeature>(new ServerAddressesFeature());
    public Task StartAsync<TContext>(IHttpApplication<TContext> application,CancellationToken cancellationToken) where TContext : notnull
    {
        var pathbases = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        var addressesFeature = Features.Get<IServerAddressesFeature>()!;
        foreach (string address in addressesFeature.Addresses)
        {
            _listener.Prefixes.Add(address.TrimEnd('/') + "/");
            pathbases.Add(new Uri(address).AbsolutePath.TrimEnd('/'));
        }
        _listener.Start();

        while (true)
        {
            var listenerContext = _listener.GetContext();
            _ = ProcessRequestAsync(listenerContext);
        }

        async Task ProcessRequestAsync( HttpListenerContext listenerContext)
        {
            FeatureCollection features = new();
            var requestFeature = CreateRequestFeature(pathbases, listenerContext);
            var responseFeature = new HttpResponseFeature();
            var body = new MemoryStream();
            var bodyFeature = new StreamBodyFeature(body);
            features.Set<IHttpRequestFeature>(requestFeature);
            features.Set<IHttpResponseFeature>(responseFeature);
            features.Set<IHttpResponseBodyFeature>(bodyFeature);

            var context = application.CreateContext(features);
            Exception? exception = null;
            try
            {
                await application.ProcessRequestAsync(context);

                var response = listenerContext.Response;
                response.StatusCode = responseFeature.StatusCode;
                if (responseFeature.ReasonPhrase is not null)
                {
                    response.StatusDescription = responseFeature.ReasonPhrase;
                }
                foreach (var kv in responseFeature.Headers)
                {
                    response.AddHeader(kv.Key, kv.Value);
                }
                body.Position = 0;
                await body.CopyToAsync(listenerContext.Response.OutputStream);
            }
            catch (Exception ex)
            {
                exception = ex;
            }
            finally
            {
                body.Dispose();
                application.DisposeContext(context, exception);
                listenerContext.Response.Close();
            }
        }
    }
    public void Dispose() => _listener.Stop();

    private static HttpRequestFeature CreateRequestFeature(HashSet<string> pathbases,HttpListenerContext listenerContext)
    {
        var request 		= listenerContext.Request;
        var url 		= request.Url!;
        var absolutePath 	= url.AbsolutePath;
        var protocolVersion 	= request.ProtocolVersion;
        var requestHeaders 	= new HeaderDictionary();
        foreach (string key in request.Headers)
        {
            requestHeaders.Add(key, request.Headers.GetValues(key));
        }

        var requestFeature = new HttpRequestFeature
        {
            Body 		= request.InputStream,
            Headers 		= requestHeaders,
            Method 		= request.HttpMethod,
            QueryString 	                = url.Query,
            Scheme 		= url.Scheme,
            Protocol 		= $"{url.Scheme.ToUpper()}/{protocolVersion.Major}.{protocolVersion.Minor}"
        };
        var pathBase = pathbases.First(it => absolutePath.StartsWith(it, StringComparison.OrdinalIgnoreCase));
        requestFeature.Path = absolutePath[pathBase.Length..];
        requestFeature.PathBase = pathBase;
        return requestFeature;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _listener.Stop();
        return Task.CompletedTask;
    }
}

在调用Start方法将HttpListener启动后,StartAsync<TContext>方法开始“请求接收处理”循环。接收到的请求上下文被封装成HttpListenerContext上下文,其承载的请求信息利用CreateRequestFeature方法转移到创建的HttpRequestFeature特性上。StartAsync<TContext>方法创建的“空”HttpResponseFeature对象来描述响应,另一个描述响应主体的StreamBodyFeature特性则根据创建的MemoryStream对象构建而成,意味着中间件管道写入的响应主体的内容将暂存到这个内存流中。我们将这三个特性注册到创建的FeatureCollection集合上,并将后者作为参数调用了IHttpApplication<TContext>对象的CreateContext方法将TContext上下文创建出来。此上下文进一步作为参数调用了IHttpApplication<TContext>对象的ProcessRequestAsync方法,中间件管道得以接管请求。

待中间件管道的处理工作完成后,响应的内容还暂存在两个特性中,我们还需要将它们应用到代表原始HttpListenerContext上下文上。StartAsync<TContext>方法从HttpResponseFeature特性提取出响应状态码和响应报头转移到HttpListenerContext上下文上,然后上述这个MemoryStream对象“拷贝”到HttpListenerContext上下文承载的响应主体输出流中。

using App;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.Extensions.DependencyInjection.Extensions;

var builder = WebApplication.CreateBuilder(args);
builder.Services.Replace(ServiceDescriptor.Singleton<IServer, HttpListenerServer>());
var app = builder.Build();
app.Run(context => context.Response.WriteAsync("Hello World!"));
app.Run("http://localhost:5000/foobar/");

我们采用上面的演示程序来检测HttpListenerServer能否正常工作。我们为HttpListenerServer类型创建了一个ServiceDescriptor对象将现有的服务器的服务注册替换掉。在调用WebApplication对象的Run方法时显式指定了具有PathBase(“/foobar”)的监听地址“http://localhost:5000/foobar/”,如图1所示的浏览器以此地址访问应用,会得到我们希望的结果。

图1 HttpListenerServer返回的结果


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK