Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
393 views
in Technique[技术] by (71.8m points)

.net - Understand HttpWebRequest in KeepAlive mode

I know that the topic has been discussed many times but I need to understand how to write code in the correct way.

I use more times the same HttpWebRequest (to the same url) with protocol version HTTP 1.1.

Method = "POST"
KeepAlive = True

But every time I need to send a different request, and get a different response.

(NB. This next code it's not correct and throw an exception)

Private Sub SendHttpWebReq()
    Dim httpWebReq = CType(Net.WebRequest.Create("http://www.contoso.com/"), Net.HttpWebRequest)
    httpWebReq.Method = "POST"
    httpWebReq.KeepAlive = True
    httpWebReq.ContentType = "application/x-www-form-urlencoded"
    Dim myRequestString As New List(Of String) From {"abc", "def"}
    Dim ContentList As New List(Of String)
    For a = 0 To 1
        Dim inputData As String = MyRequestString(a)
        Dim postData As String = "firstone" + ChrW(61) + inputData
        Dim encoding As New System.Text.ASCIIEncoding()
        Dim byteData As Byte() = encoding.GetBytes(postData)
        httpWebReq.ContentLength = byteData.Length
        Dim newStream As IO.Stream = httpWebReq.GetRequestStream()
        newStream.Write(byteData, 0, byteData.Length)
        newStream.Flush()
        newStream.Dispose()
        Dim Response As Net.WebResponse = httpWebReq.GetResponse()
        Dim ResponseStream As Io.Stream = Response.GetResponseStream()
        Dim Content = New Io.MemoryStream()
        ResponseStream.CopyTo(Content)
        Response.Close()
        Response.Dispose()
        ResponseStream.Flush()
        ResponseStream.Dispose()
        ContentList.Add(System.Text.Encoding.UTF8.GetString(Content.ToArray))
        Content = Nothing
    Next
End Sub

When I run the code, the first time I get the correct response, but when I try to reuse the HttpWebRequest, an Exception it's thrown at this line:

httpWebReq.ContentLength = byteData.Length

the Exception is This property cannot be set after writing has started

Searching, I've found this topic:
Am I able to reuse a HttpWebRequest?

Where it's explained that to reuse a HttpWebRequest, the Stream and WebResponse must be closed, and I did it, releasing the resources.

Also in this topic it's explained the same thing:
Reusing HttpWebRequest Object

But in this other topic:
This property cannot be set after writing has started! on a C# WebRequest Object

A member says that it's not possible to reuse the HttpWebRequest.
I'm in confusion between reuse and create a new one and I need to understand what KeepAlive it's referred to: to the Connection, or to the Request?

I suppose that when I execute this instruction:

Dim httpWebReq = CType(Net.WebRequest.Create("http://www.contoso.com/"), Net.HttpWebRequest)

I should create an instance of HttpWebRequest class, but I should establish the connection with this instruction:

Dim newStream As IO.Stream = httpWebReq.GetRequestStream()

Am I correct?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

This is what I think needs some clarifications, because this statement may be considered misleading, the way it's phrased:

1 WebRequest => 1 WebResponse. You can't change anything in a WebRequest once it has been initialized.

This remains valid, in principle, but the initialized term could be confusing. A better definition, would be:

You can't change any parameter of a WebRequest after the request has been issued and a WebResponse has been returned, until after the WebResponse is closed (disposed).

After a WebResponse has returned a result, it can be closed - explicitly or implicitly (in a Using block) - and you can request another, modifying as necessary the WebRequest parameters (e.g. changing the Method from POST to GET).
More, a WebRequest must be re-initialized before requesting a new WebResponse. If you don't, it just falls back to it's defaults.

The code I posted below, is an example of a classic context (a Web LogIn request) when a WebRequest must be re-initialized multiple times in the same procedure, to receive an undetermined number of WebResponses until a destination address (or landing page) is reached.

This is, more or less, the schema:

                  --------------
(GET or POST)     | WebRequest |       (Method is POST)
      |---------> | GET/(POST) | <-----------| <-------------- |
      |           --------------             |                 |
      |                 |                    |                 |
--------------    ---------------    ------------------   --------------
|    New     |    | WebResponse |--> | LogIn Required |-->|   LogIn    |
|  Location  |    ---------------    ------------------   |   Address  |
| (Referer   |          |                                 --------------
|    Set)    |          |
--------------     (Set Cookies)
      |                 |
      |           ---------------
      |           |    LogIn    |
 Redirection <----|     OK      |---NO---|
                  ---------------        |
                        |                |
                       YES               |
                   (Set Cookies)         |
                        |             Request
                  ---------------     Denied
                  |  Response   |        |
                  |    URI      |        |
                  ---------------        |
                        |                |
                       EXIT <------------|
                        |

Note that, issuing a WebRequest, if accessing the requested resource URI requires authentication, the server may NOT answer with a StatusCode 302 (Found), 301 (Moved) or 303 (Redirected), it might just set the StatusCode to 200 (OK). The redirection is implicit because the "Location" Header is set or, if it's a WebForm Login, the Html page retrieved contains the redirection.

Anyway, after a redirection has been detected, the new redirected location must be followed to destination. A redirection may consist of one or more Hops, which often must be addressed manually (to verify that we're sent where we actually want to go).


About the keep-alive Header.
A keep-alive header is set by the Client and/or the Server, to hint the counterpart that the established connection should be maintained open, at least for some time, because the chance that other resources, linked to the current transaction, will be exchanged is high. This prevents the creation of a possibly high number of costly connections.
This setup is common in Http and Ftp requests and it's the standard in Http 1.1.

Hypertext Transfer Protocol (HTTP) Keep-Alive Header (IETF)
Compatibility with HTTP/1.0 Persistent Connections (IETF)

It needs to be remembered that the keep-alive header is referring to the Connection that has been established with a WebRequest, not to the WebRequest itself. When a connection is created specifying that it should be kept open, subsequent requests should maintain the connection: keep-alive Header to conform with the protocol.
In a .NET HttpWebRequest, setting the KeepAlive property to False, is equivalent to setting the connection: close Header.

However, the logic which governs a Connection and the Connection-Pool which a process is granted access to, is managed by the ServicePointManager, using a ServicePoint as a reference for every connection request.

Thus, a WebRequest may specify that the Connection it is asking to create should be kept open (because it needs to be reused more times), but the real logic behind a Connection, how it is established, maintained and managed, lies somewhere else.

In HTTP 2.0 (StackOverflow/StackExchange use this protocol), the keep-alive setting is completely ignored, for this exact reason. A Connection logic is managed at a higher, independent, level.

(...) when you call 1 new HttpWebRequest every 3 seconds, every 10 seconds, or every 60 seconds? What's the difference when i send those requests with True or False?

You set the KeepAlive property to hint the Connection Manager that the Connection established should be kept open, because you know it will be re-used. However, both the ServicePointManager and the remote Server will comply to the request if the protocol in use provides for this setting and within the limits imposed by the internal logic that governs the complex of the Connection-Pools.
It should be set in HTTP 1.0, it's the default setting in HTTP 1.1, it's ignored in HTTP 2.0.

Since you can't know which protocol will be used until a Connection is established, it's usually set to keep-alive, because some devices (Proxies, specifically) in the route to the requested resource, might need this setting to be explicit (read the IETF documents about Proxies and their behavior).


In this example, a WebRequest is repeatedly initialized in a Loop, and the underlying WebResponse is disposed each time, until a StatusCode 200 (OK) is received or the request is denied or we have been redirected too many times (a Cancellation Token may also be useful here).

In the example, the main method is meant to be called this way:

Public Async Sub SomeMethodAsync()
    LoginParameters = New LoginObject() With {
        .CookieJar = New CookieContainer,
        .LogInUrl = "[Some IP Address]",
        .Credentials = New Dictionary(Of String, String)
    }
    LoginParameters.Credentials.Add("UserName", "[Username]")
    LoginParameters.Credentials.Add("Email", "[email]")
    LoginParameters.Credentials.Add("Password", "[Password]")

    LoginParameters = Await HttpLogIn(LoginParameters)
7End Sub

The LoginParameters object must be preserved, because references a CookieContainer, which contains the Cookies received after the authentication. These Cookies are passed to the server when a new WebRequest is initialized, as a "proof" that the request's credentials are already authenticated. Note that these Cookies expire after a while (these are "refreshed" when a new WebRequest is issued, unless the Session has a time limit). If this is the case, the Login procedure is automatically repeated.

Imports System.Net
Imports System.Net.Security
Imports System.IO
Imports System.Security
Imports System.Security.Cryptography
Imports System.Security.Cryptography.X509Certificates
Imports System.Text

Public LoginParameters As LoginObject

Public Class LoginObject
    Public Property LogInUrl As String
    Public Property ResponseUrl As String
    Public Property Credentials As Dictionary(Of String, String)
    Public Property StatusCode As HttpStatusCode
    Public Property CookieJar As New CookieContainer()
End Class

Public Async Function HttpLogIn(LogInParameters As LoginObject) As Task(Of LoginObject)
    Dim httpRequest As HttpWebRequest
    Dim StatusCode As HttpStatusCode
    Dim MaxHops As Integer = 20

    ' Windows 7 (.Net 4.5.1+ required):     
    'ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12

    ' Windows 10 (.Net 4.5.1+ required):    
    ServicePointManager.SecurityProtocol = SecurityProtocolType.SystemDefault

    'If needed or for testing
    'ServicePointManager.ServerCertificateValidationCallback = AddressOf CertificateValidation

    httpRequest = WebRequest.CreateHttp(LogInParameters.LogInUrl)

    Try
        HTTP_RequestHeadersInit(httpRequest, String.Empty, LogInParameters.CookieJar)
        Using httpResponse As HttpWebResponse = CType(Await httpRequest.GetResponseAsync(), HttpWebResponse)
            StatusCode = httpResponse.StatusCode
        End Using

        If StatusCode = HttpStatusCode.OK OrElse StatusCode = HttpStatusCode.NoContent Then
            'POST Parameters are URLEncoded and the encoded strings converted to a Byte array of UTF8 chars
            Dim EncodedParameters As Byte() = HTTP_EncodePOSTParameters(LogInParameters.Credentials)

            httpRequest = WebRequest.CreateHttp(LogInParameters.LogInUrl)
            httpRequest.Method = WebRequestMethods.Http.Post
            httpRequest.ContentType = "application/x-www-form-urlencoded"
            httpRequest.ContentLength = EncodedParameters.Length
            HTTP_RequestHeadersInit(httpRequest, String.Empty, LogInParameters.CookieJar)

            Using stream As Stream = Await httpRequest.GetRequestStreamAsync()
                stream.Write(EncodedParameters, 0, EncodedParameters.Length)
            End Using

            Dim Hops As Integer = 0
            Dim Referer As String = LogInParameters.LogInUrl
            Dim LastHttpMethod As String = httpRequest.Method

            Do
                'Evaluate Authentication redirect or page moved
                Using httpResponse As HttpWebResponse = CType(Await httpRequest.GetResponseAsync(), HttpWebResponse)
                    StatusCode = httpResponse.StatusCode
                    LogInParameters.ResponseUrl = URIFromResponseLocation(httpResponse).ToString()
                End Using

                If (StatusCode = HttpStatusCode.Moved) OrElse
                   (StatusCode = HttpStatusCode.Found) OrElse
                   (StatusCode = HttpStatusCode.RedirectMethod) OrElse
                   (StatusCode = HttpStatusCode.RedirectKeepVerb) Then

                    httpRequest = WebR

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...