James Braum
Microsoft Developer Network Technology Group
September 1996
Click to open or copy the files in the VBHTTP sample application for this technical article.
Visual Basic® lets you leverage the power of the Microsoft® Win32® Internet (WinInet for short) Software Development Kit (SDK) in a number of useful ways. The WinInet general Internet functions allow you to easily read files from the Internet. This article is a successor to "Visual Basic and the WinInet SDK," which shows you the basics of these general functions. This technical article lets you dig a little deeper into the WinInet bag of tricks, showing you HTTP functions that will allow to do such things as determine a file size before reading it, examine and modify HTTP request headers, and look at HTTP response headers. A sample Visual Basic application (called VBHTTP) shows request and response headers that are exchanged between a client and an HTTP server during a transaction. You can do exciting things with WinInet, so I encourage you to read on for some ideas and examples of what can be done.
The Microsoft Win32 Internet (WinInet) application programming interface (API) allows you to quickly create Internet-aware applications using a wealth of functions for working with HTTP, FTP, and Gopher protocols. Without the API, you would be overwhelmed with having to know the specifics of HTTP, TCP/IP, FTP, Windows® Sockets, and so on. Fortunately, the WinInet API makes life significantly easier. With Visual Basic, you can use the functionality that WinInet exposes in a number of useful ways. This article sheds some light on HTTP request and response headers, and how you can use them. A small sample (VBHTTP) illustrates the headers and shows how you can use this information to customize your HTTP server requests.
The WinInet API also provides FTP and Gopher functions; however, the WinInet SDK documentation indicates that these functions are best suited to asynchronous use, and Visual Basic requires third-party tools to use asynchronous functions and callbacks. Therefore, this article focuses on HTTP functions, which can safely be called synchronously. (For a look at how you would use the FTP functions asynchronously using Visual C++®, please refer to Robert Coleridge's article titled "Advanced FTP: Teaching Fido to Phetch."
This article begins with some background on the Hypertext Transfer Protocol (HTTP) and moves into HTTP specifics and the use of request and response headers.
The Hypertext Transfer Protocol (HTTP) has been in use since 1990. HTTP is an application-level protocol that is fast and non–resource-intensive. It is the protocol used to transmit Hypertext Markup Language (HTML) files. It can be considered a generic protocol that can be used in a variety of ways.
This article is concerned with HTTP version 1.0, which is in common use today. The next version of HTTP, referred to as HTTP/1.1, is currently being developed by the World Wide Web Consortium (W3C) and the HTTP working group of the Internet Engineering Task Force (IETF). Their Web site at http://www.w3.org/ contains current drafts of specifications and more information about HTTP.
HTTP uses a request-and-response model to transfer information. In a simple case, the user agent (client application) sends a request to the server. The server responds with a message that contains a success or failure code, protocol version, server information, and body content, depending on the request. The user agent can modify the request to provide greater control over the server. The section of this article titled "WinInet HTTP Functions" looks at a WinInet function that will allow us to make such modifications.
The WinInet HTTP functions work with HTTP servers that recognize requests in HTTP/1.0. HTTP servers can actually be less complex than a typical user agent because they have an easier job: they just respond to requests. The client, on the other hand, must process the information that it has received in response to a request. In the case of an HTML file, this involves a significant amount of processing. To display an HTML page, for example, multiple threads may be spawned to simultaneously load and display inline .GIF and .JPG files.
WinInet is useful for creating HTTP clients. (The client is the application that sends a request to a server.) You can build your own Web browser, create Web-traversing robots, or write your own Internet-aware application for solving some particular problem.
Request headers are sent to the server as part of a request message. You can modify them to develop client applications that have detailed control over the server. If your client application needs caching capabilities, security, and so forth, you can append or modify request headers to do this.
Commonly used HTTP/1.0 request headers are:
Refer to the Internet Draft (http://www.w3.org/pub/WWW/Protocols/HTTP/1.0/spec.txt), published by the Internet Engineering Task Force in August 1996, for detailed descriptions of request headers.
HTTP servers respond to client requests in the form of a response message. This message contains a status line, response headers, and entity-header meta-information about the resource (that is, information about the resource itself, not about the information that is contained in the resource) identified in the request message sent by the client. The status line contains the HTTP version number, a status code, and a reason phrase. For instance, "HTTP/1.0 200 OK" is a typical status line returned in a response message from an HTTP server. Table 1 contains status codes and reason phrases.
Table 1. Status Codes and Their Meaning
Status Code | Meaning |
200 | OK |
201 | Created |
202 | Accepted |
204 | No Content |
301 | Moved Permanently |
302 | Moved Temporarily |
304 | Not Modified |
400 | Bad Request |
401 | Unauthorized |
403 | Forbidden |
404 | Not Found |
500 | Internal Server Error |
501 | Not Implemented |
502 | Bad Gateway |
503 | Service Unavailable |
Two common response headers are:
Entity-header fields that contain meta-information about the file (resource) specified in the request are also returned with the response message. Common entity-header fields are:
The VBHTTP sample application groups the entity-header fields under the "Response Headers" tab to make it clearer to read. Instead of differentiating between response headers and entity-header fields, you can think in terms of headers returned in response to a request. This oversimplifies the issue somewhat, but this article is concerned with making things easy.
OK, enough background theory. Let's look at some actual Visual Basic code that lets us both view and modify our requests.
We will dive in with some sample code that requests a page, then looks at the content length of that page:
Dim hInternetSession As Long
Dim hInternetConnect As Long
Dim hHttpOpenRequest As Long
Dim sBuffer As String * 1024
Dim lBufferLength As Long
lBufferLength = Len(sBuffer)
hInternetSession = InternetOpen(scUserAgent, _
INTERNET_OPEN_TYPE_PRECONFIG, vbNullString, vbNullString, 0)
hInternetConnect = InternetConnect(hInternetSession, _
"www.microsoft.com", INTERNET_DEFAULT_HTTP_PORT, _
vbNullString, vbNullString, INTERNET_SERVICE_HTTP, 0, 0)
hHttpOpenRequest = HttpOpenRequest(hInternetConnect, "GET", _
vbNullString,"HTTP/1.0", vbNullString, 0, _
INTERNET_FLAG_RELOAD, 0)
HttpSendRequest hHttpOpenRequest, vbNullString, 0, 0, 0
HttpQueryInfo hHttpOpenRequest, HTTP_QUERY_CONTENT_LENGTH, _
ByVal sBuffer, lBufferLength, 0
Debug.Print sBuffer
InternetCloseHandle (hInternetSession)
It doesn't take much code to examine the HTTP response headers before taking further action in your code. This example opens "www.microsoft.com" using the access parameters contained in the registry, then sends a request to retrieve the file. After sending the request, we examine the response from the server. The buffer contains the length of the file. (To keep things clear, the code does not check for errors.) Not all servers will provide file length for you. After examining the response header for content length, you can decide if you want to retrieve the file, using the InternetReadFile function.
Notice that there is only one call to InternetCloseHandle. This function allows you to close entire handle subtrees. Closing the root handle will close all subsequent handles you may have opened up.
This is the type of functionality that the WinInet HTTP functions provide that you can't get with the Internet functions. You will still use some Internet functions to open the connection and actually read the file.
The following sections examine these WinInet functions: InternetOpen, InternetConnect, HttpOpenRequest, HttpAddRequestHeaders, HttpSendRequest, HttpQueryInfo, InternetReadFile, and InternetCloseHandle.
InternetOpen initializes our application's use of WinInet functions. A long handle is returned. Below are the two constants you can use to call the function, as well as the declaration you will need:
Public Const scUserAgent = "my wininet app"
Public Const INTERNET_OPEN_TYPE_PRECONFIG = 0
Public Declare Function InternetOpen Lib "wininet.dll" Alias _
"InternetOpenA" (ByVal sAgent As String, ByVal lAccessType _
As Long, ByVal sProxyName As String, ByVal sProxyBypass As _
String, ByVal lFlags As Long) As Long
The Win32 Internet functions do not currently provide support for Unicode. However, support will be provided in future versions. Therefore, all the functions are aliased to call the ANSI version.
InternetConnect returns a handle to an HTTP session. The two constants tell the function to listen on port 80, the default port that HTTP servers listen to, before establishing the connection. Here are the declaration and two constants that you will need:
Public Declare Function InternetConnect Lib "wininet.dll" Alias _
"InternetConnectA" (ByVal InternetSession As Long, _
ByVal sServerName As String, ByVal nServerPort As Integer, _
ByVal sUsername As String, ByVal sPassword As String, _
ByVal lService As Long, ByVal lFlags As Long, _
ByVal lContext As Long) As Long
Public Const INTERNET_DEFAULT_HTTP_PORT = 80
Public Const INTERNET_SERVICE_HTTP = 3
HttpOpenRequest returns an HTTP request handle. Here are the declaration and a constant you will need:
Public Declare Function HttpOpenRequest Lib "wininet.dll" Alias _
"HttpOpenRequestA" (ByVal hHttpSession As Long, ByVal sVerb As _
String, ByVal sObjectName As String, ByVal sVersion As String, _
ByVal sReferer As String, ByVal something As Long, ByVal lFlags _
As Long, ByVal lContext As Long) As Long
Public Const INTERNET_FLAG_RELOAD = &H80000000
HttpOpenRequest takes eight parameters (described below) and returns the HTTP request handle, if successful. The handle holds the request until you send it with HttpSendRequest, which stores the HTTP headers to be sent as part of the request.
If the If-Modified-Since header field is used, the GET becomes a conditional get. It will only retrieve the file if the file has been modified since a particular date specified in the header field. You can structure your requests to only retrieve the file if it has been changed since some predetermined point in time. An example of this will be illustrated below.
The HttpAddRequestHeaders function allows you to add or modify headers before sending them to the server. Here are the declaration and three constants you need:
Public Declare Function HttpAddRequestHeaders Lib "wininet.dll" Alias _
"HttpAddRequestHeadersA" (ByVal hHttpRequest As Long, _
ByVal sHeaders As String, ByVal lHeadersLength As Long, ByVal _
lModifiers As Long) As Integer
Public Const HTTP_ADDREQ_FLAG_ADD_IF_NEW = &H10000000
Public Const HTTP_ADDREQ_FLAG_ADD = &H20000000
Public Const HTTP_ADDREQ_FLAG_REPLACE = &H80000000
Note All HTTP/1.0 dates are represented as Greenwich Mean Time (GMT). An HTTP Date in augmented Backus-Naur Form (BNF) is:
HTTP-date = rfc1123-date | rfc850-date | asctime-date
rfc1123-date = wkday "," SP date1 SP time SP "GMT"
rfc850-date = weekday "," SP date2 SP time SP "GMT"
asctime-date = wkday SP date3 SP time SP 4DIGIT
date1 = 2DIGIT SP month SP 4DIGIT
; day month year (e.g., 02 Jun 1982)
date2 = 2DIGIT "-" month "-" 2DIGIT
; day-month-year (e.g., 02-Jun-82)
date3 = month SP ( 2DIGIT | ( SP 1DIGIT ))
; month day (e.g., Jun 2)
time = 2DIGIT ":" 2DIGIT ":" 2DIGIT
; 00:00:00 - 23:59:59
wkday = "Mon" | "Tue" | "Wed"
| "Thu" | "Fri" | "Sat" | "Sun"
weekday = "Monday" | "Tuesday" | "Wednesday"
| "Thursday" | "Friday" | "Saturday" | "Sunday"
month = "Jan" | "Feb" | "Mar" | "Apr"
| "May" | "Jun" | "Jul" | "Aug"
| "Sep" | "Oct" | "Nov" | "Dec"
(See also: http://www.w3.org/)
There are three other flags that can be used if you are interested in coalescing headers. Refer to the WinInet SDK documentation (http://www.microsoft.com/intdev/sdk/docs/wininet) if this interests you.
After you have opened your request and added request headers, if any, you are ready to send your request to the server. The HttpSendRequest function does exactly what you would expect: It sends your request. It also lets you append additional request headers before you send it, so you do not have to make an additional call to HttpAddRequestHeaders. (I would, however, recommend using HttpAddRequestHeaders so you can explicitly check the return value to ensure that the request headers were successfully appended or modified.) After the request is sent, the status line, response headers, and any entity header meta-information are read. You can interrogate these with the HttpQueryInfo function, which is discussed below.
The declaration for HttpSendRequest is:
Public Declare Function HttpSendRequest Lib "wininet.dll" Alias _
"HttpSendRequestA" (ByVal hHttpRequest As Long, ByVal sHeaders _
As String, ByVal lHeadersLength As Long, sOptional As Any, _
ByVal lOptionalLength As Long) As Integer
The function will return TRUE if the request was successfully sent.
This function lets you look at the status line, response headers, and entity header meta-information returned from the request sent by HttpSendRequest.
The declaration is:
Public Declare Function HttpQueryInfo Lib "wininet.dll" _
Alias "HttpQueryInfoA" (ByVal hHttpRequest As Long, _
ByVal lInfoLevel As Long, ByRef sBuffer As Any, _
ByRef lBufferLength As Long, ByRef lIndex As Long) As Integer
Public Const HTTP_QUERY_FLAG_REQUEST_HEADERS = &H80000000
The function returns TRUE if successful.
InternetReadFile is the function that will start reading the file once you have sent the request and decided to actually retrieve the body content.
Public Declare Function InternetReadFile Lib "wininet.dll" _
(ByVal hFile As Long, ByVal sBuffer As String, _
ByVal lNumberOfBytesToRead As Long, lNumberOfBytesRead As Long) _
As Integer
The InternetCloseHandle function closes handles and frees resources associated with WinInet functions. The function returns TRUE if the handle is successfully closed. This is the declaration:
Public Declare Function InternetCloseHandle Lib "wininet.dll" _
(ByVal hInet As Long) As Integer
This sample application lets you examine the request and response headers that are exchanged between the client and the HTTP server during a typical transaction. The following screen shot, from our sample, is what you will see when you run it (Figure 1).
Figure 1. Response view
The screen contains results sent back from the request sent to www.microsoft.com. We are looking at the status message, response headers, and entity header meta-information about the resource content.
From this we can determine that the content length is 10,447 bytes, the status code is 200 (which means "OK"), and the content expires on Wednesday, September 11, 1996, at 16:19:02 GMT. With this, your Internet-aware application can determine if the content is still valid, verify that the content type is correct, and set up a progress meter so that when the resource is read the progress can be displayed.
Let's take a look at the request headers that were sent to the server (Figure 2).
Figure 2. Request view
We can see that GET is the request method, "http sample" is the user agent (this is typically more useful product information), and the Pragma is "No-Cache," which tells the server to send the page regardless of whether or not the client has it cached. You can also see that we sent a cookie—"MC1=GUID=3986c31df6ce11cfbcee0000f84a13db"—to the server.
These are just to give you an idea of what information is available with simple calls to the WinInet functions.
You can add a request header instructing the server to send the content of the resource only if it was updated after a certain time. This way the application only retrieves the content if it has changed since the last time it was retrieved.
For instance, a call to the WinInet HttpAddRequestHeaders function adds a request header that indicates to the server that we are only interested in seeing the content of the resource specified in the HttpOpenRequest call if that resource has been modified since August 24, 1996. The following example shows how this request header is added. You would insert the following code just after you open your HTTP request using the handle returned from that call.
Dim sHeaders As String
Dim lLength As Long
Dim iRetVal As Integer
sHeaders = "If-Modified-Since: Sat, 24 Aug 1996 17:38:52 GMT" & vbCrLf
lLength = Len(sHeaders)
iRetVal = HttpAddRequestHeaders(hInternetConnect, sHeaders, lLength, _
HTTP_ADDREQ_FLAG_ADD Or HTTP_ADDREQ_FLAG_REPLACE)
Check the return value to make sure that it successfully added the request.
If the file has not been modified since the date specified, the status code in the response message will be 304, "Not Modified." You can check for this, then opt to not call the InternetReadFile function to retrieve the contents.
It is easy to check which version of the WinInet DLL you are using. Add these declarations to your project:
Public Declare Function InternetQueryOption Lib "wininet.dll" _
Alias "InternetQueryOptionA" (ByVal hInternet As Long, _
ByVal lOption As Long, ByRef sBuffer As Any, ByRef lBufferLength _
As Long) As Integer
Public Const INTERNET_OPTION_VERSION = 40
Public Type tWinInetDLLVersion
lMajorVersion As Long
lMinorVersion As Long
End Type
InternetQueryOption is a WinInet function that lets you check option settings on a specified handle. INTERNET_OPTION_VERSION will return the version of the DLL that you are currently using.
This is how you call the function:
dim vDllVersion As tWinInetDLLVersion
dim iRetVal As Integer
iRetVal = InternetQueryOption (hInternetSession, _
INTERNET_OPTION_VERSION, vDllVersion, Len(vDllVersion))
Debug.Print vDllVersion.lMajorVersion
Debug.Print vDllVersion.lMinorVersion
You can use this function to check for the length of time before a connection attempt will time out, or to return the parent handle of the connection, and so forth.
As you can see, the WinInet HTTP functions provide you with more flexibility than the general WinInet Internet functions. Using these functions will allow you to build slick Internet-aware applications that have more sophisticated control over the requests they send to the server.