17 December 2017

HttpClient

HttpClient定义了HTTP请求和响应相关的类型实现,简化了网络请求的编码方式,完全支持异步调用(async/await),通过使用HttpClient及相关的一系列类型定义,可以很方便地构造请求和接收响应消息。

GET | POST | PUT | DELETE

我们知道根据不同的请求动词,可以指定不同的请求操作。下面来看一下HttpClient中以上这四个请求动词是如何使用的。

GET

比如一个简单的获取这篇文章内容的Http请求可以这样写:

public async Task GetStringAsync()
{
    using(var httpClient = new HttpClient())
    {
        string content = await httpClient.GetStringAsync("http://kanlei.github.io/2017/12/17/learn-to-use-httpclient");
        Console.WriteLine(content);
    }
}

由于HttpClient实现了IDisposable接口,所以可以使用using语句来释放不再使用的底层Socket连接。

通常,在项目中我们只会定义一个HttpClient实例,所有的请求都复用这个实例,这样做不仅避免了在每次请求时都指定请求头、Cookie、缓存等其它配置项,还复用了底层的Socket连接,避免不必要的重复创建和释放的过程,而且HttpClient是保证线程安全的,也不存在多线程竞争的问题;否则甚至可能会导致SocketException

除了GetStringAsync方法,类库还提供了GetStreamAsync获取流,GetByteArrayAsync获取字节数组,以及GetAsync获取完整的响应消息,该方法还提供了HttpCompletionOption以及CancellationToken重载,用于指定响应的条件,比如在响应头可用时即识别出是否支持当前响应的处理,以及支持取消操作。

向下面这样,如果响应成功则读取消息内容:

public async Task GetStringAsync()
{
    HttpResponseMessage responseMessage = await httpClient.GetAsync("http://kanlei.github.io/2017/12/17/learn-to-use-httpclient");
    if (responseMessage.IsSuccessStatusCode)  // [200-299]
    {
        string content = await responseMessage.Content.ReadAsStringAsync();
    }
}

POST

HttpClient提供了多种POST请求格式,比如使用FormUrlEncodedContent提交表单,通过对提交的内容进行URL编码后添加到请求体内,提交给服务器。

// application/x-www-form-urlencoded
public async Task PostFormAsync()
{
    var keyValues = new List<KeyValuePair<string, string>>()
    {
        new KeyValuePair<string, string>("name", "value")
    };
    var formContent = new FormUrlEncodedContent(keyValues);
    HttpResponseMessage responseMessage = await httpClient.PostAsync("uri", formContent);
}

PostAsync方法接收一个HttpContent类型的参数,该类型是一个实现了IDisposable的抽象类。相关子类定义如下:

HttpContent -> ByteArrayContent -> FormUrlEncodedContent
                                -> StringContent
HttpContent -> StreamContent
HttpContent -> MultipartContent -> MultipartFormDataContent

使用StringContent提交JSON文本到服务器:

// application/json
public async Task PostStringAsync()
{
    var stringContent = new StringContent("{ \"title\"=\"post\", \"content\"=\"string\" }", Encoding.UTF8, "application/json");
    HttpResponseMessage responseMessage = await httpClient.PostAsync("uri", stringContent);
}

使用MultipartFormDataContent上传文件:

// multipart/form-data
public async Task PostFileAsync(Stream stream)
{
    stream.Seek(0, SeekOrigin.Begin);
    var streamContent = new StreamContent(stream);
    var formDataContent = new MultipartFormDataContent();
    formDataContent.Add(streamContent, "upload", "file.png");
    HttpResponseMessage responseMessage = await httpClient.PostAsync("uri", streamContent);
}

注意,这里是通过读取流的方式来上传文件,所以首先要确保流的读取位置为起始位置,其次流使用完毕后需要手动释放,该操作可以在请求响应后的上层逻辑中释放。

HttpClient大大简化了代码量,比如完整的上传代码实现需要添加像这样的参数:

private async Task PostFileByStream(Stream stream)
{
    stream.Seek(0, SeekOrigin.Begin);
    var streamContent = new StreamContent(stream);
    streamContent.Headers.ContentLength = stream.Length;
    streamContent.Headers.ContentDisposition = ContentDispositionHeaderValue.Parse("form-data");
    streamContent.Headers.ContentDisposition.Parameters.Add(new NameValueHeaderValue("name", "upload"));
    streamContent.Headers.ContentDisposition.Parameters.Add(new NameValueHeaderValue("filename", "file.png"));
    streamContent.Headers.ContentType = MediaTypeHeaderValue.Parse("image/png");

    var multipartForm = new MultipartFormDataContent("--WebKit" + DateTime.Now.Ticks);
    multipartForm.Add(streamContent);
    HttpResponseMessage result = await httpClient.PostAsync("uri", multipartForm);
}

以上仍然是简化后的版本,如果完全手动拼接格式,则会向下面这样(iOS的实现):

var sessionDelegate = new SessionDelegate (progressCallback);
var defaultSession = NSUrlSession.FromConfiguration (NSUrlSessionConfiguration.DefaultSessionConfiguration, sessionDelegate, new NSOperationQueue ());

var header = $"Content-Disposition: form-data; name=\"file\"; filename=\"{fileName}\"\r\nContent-Type: {mineType}\r\n\r\n";
var boundary = "--WebKitFormBoundary" + DateTime.Now.Ticks;
var urlRequest = new NSMutableUrlRequest (NSUrl.FromString (url));
urlRequest.HttpMethod = "POST";
urlRequest ["Content-Type"] = "multipart/form-data; boundary=" + boundary;

stream.Seek(0, SeekOrigin.Begin);
NSData imageData = NSData.FromStream (stream);
NSMutableData mutableData = new NSMutableData ();
mutableData.AppendData (NSData.FromString ($"\r\n--{boundary}\r\n"));
mutableData.AppendData (NSData.FromString (header));
mutableData.AppendData (imageData);
mutableData.AppendData (NSData.FromString ($"\r\n--{boundary}\r\n"));

urlRequest ["Content-Length"] = mutableData.Length.ToString ();

NSUrlSessionDataTaskRequest dataTaskRequest = await defaultSession.CreateUploadTaskAsync (urlRequest, mutableData);
defaultSession.FinishTasksAndInvalidate ();

我们手动指定了请求头的Content-Typemultipart/form-data以及分割内容边界的boundary,该boundary不能在内容中出现;指定内容头Content-DispositionnamefilenameContent-Type,并谨慎的在合适的位置上插入换行符和边界标识符以及空行。以上的代码最终会转化为类似如下的请求格式发送给服务器:

POST image/upload HTTP/1.1
HOST:
Content-Type: multipart/form-data; boundary="--WebKitFormBoundary636491486584734080"
Content-Length: 1124

--WebKitFormBoundary636491486584734080
Content-Disposition: form-data; name="file"; filename="file.png"
Content-Type: image/png"
Content-Length: 1024

10101000101010100010101001...  // image data

--WebKitFormBoundary636491486584734080

以上遵循了 rfc2388 的定义,如果需要上传多个内容,则需要更多的boundary用于标识边界,由此可见,HttpClient极大地提高了我们的编码效率,减少了手动拼接字符格式的错误次数,提升了开发效率。

PUT 与 DELETE

PUTPOST的使用方式相似;而DELETE则与GET的使用方式相似。

public async Task PutAndDeleteAsync()
{
    HttpResponseMessage putResponse = await httpClient.PutAsync("uri", new StringContent(""));

    HttpResponseMessage deleteResponse = await httpClient.DeleteAsync("uri");
}

Send

其实上面所有便捷的请求方式,最终都会转化为调用SendAsync,该方法提供了一个HttpRequestMessage类型的参数重载,该类型可以指定所有与请求相关的内容,包括请求类型与请求体,如:

public async Task SendAsync()
{
    var requestMessage = new HttpRequestMessage(HttpMethod.Get, "uri");
    requestMessage.Content = new StringContent("hello world");
    HttpResponseMessage deleteResponse = await httpClient.SendAsync(requestMessage);
}

请求处理 HttpMessageHandler

在构造HttpClient实例时,可以选择传递两个参数,一个是HttpMessageHandler类型的handler,还有一个是bool类型的disposeHandler

var client = new HttpClient(new HttpClientHandler(), false);

disposeHandler

false则表示在HttpClient上调用Dispose方法并不会释放当前handler,以及其所关联的Socket连接;true则相反。当我们想复用同一个HttpMessageHandler给多个HttpClient实例使用时,可以通过该参数控制释放的行为方式。

handler

其实HttpClient并不处理请求,而是将请求交付给handler来处理,HttpMessageHandler定义如下:

public abstract class HttpMessageHandler : IDisposable
{
    protected HttpMessageHandler ();

    public void Dispose ();

    protected virtual void Dispose (bool disposing);

    protected internal abstract Task<HttpResponseMessage> SendAsync (HttpRequestMessage request, CancellationToken cancellationToken);
}

SendAsync方法定义则说明了该类型的实际用途,HttpClientSendAsync实际是调用了handler中的SendAsync方法定义。其相关的子类型定义如下:

HttpMessageHandler -> DelegatingHandler -> MessageProcessingHandler
HttpMessageHandler -> HttpClientHandler
HttpMessageHandler -> SocketsHttpHandler

HttpClient默认handler构造是HttpClientHandler,其定义了如 Cookie、Credentials、Proxy 等功能。DelegatingHandler则提供了一个InnerHandler属性,使得我们可以串联现有的handler,每个handler仅负责处理请求或响应过程中的一个阶段。MessageProcessingHandler则提供了更方便的ProcessRequestProcessResponse方法用于子类实现。

至此,HttpClient简单用法和构造已介绍完,这里可以查看更多的真实案例。

Call a Web API From a .NET Client / httpclientwrong / HttpClient