Its quite common for people to ask whether they can send data to the Internet (or Intranet) from, say, Excel, or Word. To which I frequently answer yes they can do it. But the problem is that there are a variety of ways this can be done, and that whats possibly the best-supported way (using WinInet) is by no means trivial. Hence this note.
Other ways? If you understand http and Winsock (not as tricky as it sounds) and are familiar with converting Windows API calls for use in VB, then you can drive the server directly. But WinInet overcomes that need. So lets dive into some detail in that area.
To use Windows API calls in any VB (or VBA) program, they have to be included in the declarations section at the top of the program. The declaration looks something like this.
Private Declare Function FuncName Lib "libname" Alias "AliasName" (ByVal variable)
FuncName is the name you use to refer to the function, libname is the windows dll (which will be WinInet in our example) and AliasName is the name of the function in the Windows API.
Theres a standard set of calls that every access to the Internet must make: start with InternetOpen, then call InternetConnect, and then HttpOpenRequest. Lets look at each in turn. InternetOpen is declared as
Private Declare Function InternetOpen Lib "wininet.dll" Alias
"InternetOpenA" ( _
ByVal lpszAgent As String, _
ByVal dwAccessType As Long, _
ByVal lpszProxyName As String, _
ByVal lpszProxyBypass As String, _
ByVal dwFlags As Long) As Long
Im not going to go into all of the options in too much detail (read the help files if you want to understand the lot) but Id just note that we need to use the Visual Basic variable vbNullString where wed normally use a C NULL. My standard use of InternetOpen is:
Dim hInternet As Long
hInternet = InternetOpen(App.Title, INTERNET_OPEN_TYPE_DIRECT, vbNullString, vbNullString, 0)
and we put Const INTERNET_OPEN_TYPE_DIRECT = 1 in the declarations section. Now InternetConnect:
Private Declare Function InternetConnect Lib "wininet.dll" Alias
"InternetConnectA" ( _
ByVal hInternetSession As Long, _
ByVal lpszServerName As String, _
ByVal nServerPort As Integer, _
ByVal lpszUsername As String, _
ByVal lpszPassword As String, _
ByVal dwService As Long, _
ByVal dwFlags As Long, _
ByVal dwContext As Long) As Long
Key things to know about this. hInternetSession is the return value of InternetOpen. lpszServerName is the name of the web server youre trying to access (for instance, it could be "www.bt.com"). nServerPort is the TCP port you want to access, and would usually be 80 for the internet. I use Const INTERNET_SERVICE_HTTP = 3 as dwService. A standard call:
Dim hConnect As Long
hConnect = InternetConnect(hInternet, strServer, iPort, "", "", INTERNET_SERVICE_HTTP, 0, 0)
Private Declare Function HttpOpenRequest Lib "wininet.dll" Alias
"HttpOpenRequestA" ( _
ByVal hHttpSession As Long, _
ByVal lpszVerb As String, _
ByVal lpszObjectName As String, _
ByVal lpszVersion As String, _
ByVal lpszReferer As String, _
ByVal lpszAcceptTypes As String, _
ByVal dwFlags As Long, _
ByVal dwContext As Long) As Long
Much of this needs a quick explanation. hHttpSession is the value returned from InternetConnect. lpszVerb is either "GET" or "POST", depending (roughly) on whether youre getting information from the internet, or sending it. If its the latter, its a bit more complex and well cover it later. For now, assume GET. lpszObjectName is the URL youre wanting to download (e.g. "\index.htm"), lpszVersion is the version of http you want to use no reason for not using the value below. Be a bit careful with lpszAcceptTypes this is a list of the mime types youre willing to accept. However, its defined as a set of null terminated strings followed by a further null at the end. I dont know how to do this in VB. Just putting, say, "text/html" for this value will lead to incorrect data being sent, since there will be no final null. As below, I set it to a null string. dwFlags also needs careful treatment. Im not going to go into more detail here, but if you want to send cookies (using HttpAddRequestHeaders) you MUST set the flag equal to INTERNET_FLAG_NO_COOKIES (weird, huh). There are also "gotchas" around the way that WinInet caches returned values. For most applications, you should OR that value with INTERNET_FLAG_NO_CACHE_WRITE. So in the declares we have:
Const INTERNET_FLAG_NO_COOKIES = &H80000
Const INTERNET_FLAG_NO_CACHE_WRITE = &H4000000
In the body of the program, wed see something like:
Dim lFlags As Long
Dim hRequest As Long
lFlags = INTERNET_FLAG_NO_COOKIES
lFlags = lFlags Or INTERNET_FLAG_NO_CACHE_WRITE
hRequest = HttpOpenRequest(hConnect, "GET", strURL, "HTTP/1.0", vbNullString, vbNullString, lFlags, 0)
OK now weve opened a path to the server. All we now need to do is send the request and read what the server sends back. Sending is done with HttpSendRequest.
Private Declare Function HttpSendRequest Lib "wininet.dll" Alias
"HttpSendRequestA" ( _
ByVal hHttpRequest As Long, _
ByVal lpszHeaders As String, _
ByVal dwHeadersLength As Long, _
ByVal lpOptional As String, _
ByVal dwOptionalLength As Long) As Boolean
Youll get used to this soon. hHttpRequest is the return from HttpOpenRequest. Well talk about the rest in a bit, but for now they can be set to NULL or zero.
Dim bRes As Boolean
bRes = HttpSendRequest(hRequest, vbNullString, 0, vbNullString, 0)
Reading the result is done with InternetReadFile.
Private Declare Function InternetReadFile Lib "wininet.dll" ( _
ByVal hFile As Long, _
ByVal lpBuffer As String, _
ByVal dwNumberOfBytesToRead As Long, _
ByRef lpNumberOfBytesRead As Long) As Boolean
This puts the returned data into lpBuffer, a string buffer. Theres obviously lots of ways of saving this data, but I tend to limit the buffer to 1 byte (poor for performance, but good to ensure that you accurately capture all the returned data) and write this to file. Another VB gotcha is the need to delete the file before opening it for writing, otherwise it just overwrites the data at the start of the file, leaving the rest as was. Sample code (variables declared only where theyre not obvious).
Dim strBuffer As String * 1
strDir = Dir(App.Path & "\" & strFile)
If Len(strDir) > 0 Then
Kill App.Path & "\" & strFile
iFile = FreeFile()
Open App.Path & "\" & strFile For Binary Access Write As iFile
bRes = InternetReadFile(hRequest, strBuffer, Len(strBuffer), lBytesRead)
If lBytesRead > 0 Then
Put iFile, , strBuffer
Loop While lBytesRead > 0
Thats about it for reading a Web page. What about sending data to one?
There are actually 2 ways of sending data to a webserver extending the URL in a GET, or using POST. The former is generally OK for small amounts of data (less than a few hundred bytes), but is unreliable for more data. (Ive spent some time looking at this, and concluded that part of the problem lies in the size of packet that is sent by the TCP protocol which means that, even if you can use GET to send, say, 2kBytes of data on a LAN, it wont always work over dial up!).
Anyway to send data using GET, all you need to do is add a question mark to the URL and then the form data (with each field separated by an &) e.g. the URL would become index.asp?FormField=value&Field2=value2. (Theres a gotcha here with asps theyll only accept form data where the mime type is set to "Content-Type: application/x-www-form-urlencoded", which causes some problems with the mime type stuff I put above but Ill just flag the issue here, not solve it!).
So, its better to use POST. What differences do we need to make to what we said above? Firstly, HttpOpenRequest obviously uses POST, not GET, so it becomes:
hRequest = HttpOpenRequest(hConnect, "POST", sURL, "HTTP/1.0", vbNullString, vbNullString, 0, 0)
The POST data is created as above, with field name/value pairs, separated by & and is put into a query string which is sent as part of the HttpSendRequest, as follows:
bRes = HttpSendRequest(hRequest, vbNullString, 0, strPostData, Len(strPostData))
Theres quite a bit of other stuff I could go into about cookies, headers, returned status codes, etc. but Ill leave that for another day.
Its not clear from the M$ documentation, but WinInet isnt a standard part of Windows 95 it was actually delivered as part of Internet Explorer. Youve therefore got to be prepared to send out WinInet.dll with your program, if you distribute it. But unfortunately, Wininet depends on 2 other dlls shlwapi.dll and advapi32.dll, so you need to be ready to send them out too. Problems here are that you cant just copy those dlls to windows/system, because they may already be there and in use. Also, advapi32 comes in NT and 95 specific versions, and if you install the wrong one, it will crash Windows and implies a reinstall. My advice would be to suggest users install IE5, unless you are using a professional installer and know what youre doing!
© Phil Holmes, 2000