C# IHttpHandler 和将 httpclient 用于 Web 服务

C# IHttpHandler and the use of httpclient for a web service

我的任务是维护一个 C# Web 服务,该服务几乎可以归结为以下与 IIS 8 结合使用的代码:

public class Handler1 : IHttpHandler
{
    public void ProcessRequest(HttpContext context) 
    {
    // here we process the request and return it after fetching some data
    }
}

现在我需要创建另一个 Web 服务,它又会请求另一个 Web 服务来获取数据。我收集到的是 C# 中的 HttpClient() 是新的亮点,但是在我的案例中找出如何实现它并不是那么容易(或者如果那是一个好的解决方案)。

我的解决方案是这样的

public class Handler1 : IHttpHandler
{
    public void ProcessRequest(HttpContext context) 
    {
    // make sure that the incoming request is valid
    HttpClient client = new HttpClient();
    // do stuff with ^client and request the other service
    [..]
    // return the data from the response from ^client to the first request
    }
}

我是在正确的轨道上还是这会是一场灾难?我很乐意接受相关文档的提示或指示。

更好的解决方案示例:

public class YourApp
{
    private static HttpClient Client = new HttpClient();
    public static void Main(string[] args)
    {
        Console.WriteLine("Requests about to start!");
        for(int i = 0; i < 5; i++)
        {
            var result = Client.GetAsync("http://www.whosebug.com").Result;
            Console.WriteLine(result);
        }
        Console.WriteLine("Requests are finished!");
        Console.ReadLine();
    }
}

您的方法有两个问题。

首先,正如其他人所说,不能为每个请求都创建 HttpClient,因为这会导致您的服务器为每个请求创建一个新的 TCP 连接并且不处理它从您的连接池直到他们达到他们的 MaxIdleTimeout。

这是因为 HttpClient 的每个实例(或者更确切地说,HttpClientHandler 的每个实例)在 ConnectionPool 中创建一个唯一的 ConnectionGroup,因此两个 HttpClient(以这种方式创建)不会共享相同的连接。

您可以很容易地耗尽您的端口(因为连接保持活动状态很长一段时间),即使没有它,您也会遇到停用保活时可能会发现的所有性能问题(通常是在 HTTPs 上连接时服务器)。

至于解决方案,您可以在您的处理程序中添加一个私有只读 HttpClient 字段,您将按照您在代码中所做的那样对其进行初始化(我相信 HttpHandler 由 IIS 实例化一次,而不是 per-request,只要它说它是可重复使用的)。 (编辑:注意 HttpClient 实例是 thread-safe)。

您会发现的另一个问题是 IHttpHandler 在设计上是同步的。这意味着您必须在代码中 return task.Result 。这将阻塞当前线程(我们称它为 A),以及出站请求的所有后续处理(发送请求的 body,读取响应的 header 和读取 body 的响应)将使用线程池中的一个线程来处理这个(我们称它们为 B、C 和 D,尽管它们都可以是同一个线程,因为时间上没有步骤重叠)。

但在 Asp.net 中,您不能安全地这样做。当您的主线程 A 正在处理 Asp.net 入站请求时,简单地说,它将锁定当前上下文。当线程 B、C 和 D 将被生成以处理出站请求时,它们也会尝试获取并锁定当前上下文。但是因为你的主线程A被阻塞了,上下文一直没有释放。这将有效地导致死锁(A锁定上下文并等待B/C/D,B/C/D等待上下文)。

要解决此问题,我建议您改用 IHttpAsyncHandler。异步进程不是用来同步的,HttpClient 只能以异步方式使用。

但是,这对于第一个使用场景来说可能更具挑战性。从 WebApi 服务器的上下文中使用 HttpClient 会容易得多。但是,在不知道为什么必须使用 HttpHandler 的情况下,我不知道这是否在可接受的答案范围内。

还有其他方法可以 运行 异步任务并同步等待它们(例如临时更改 CurrentContext),但它们都非常危险。

附带说明一下,HttpClient 是新出现的东西,并且与 "all-the-way async infrastructure" 等其他新出现的东西配合得很好。但是尝试在较旧的基础架构中使用 HttpClient 并不那么容易。