HTTP协议是比较简单的协议,纯文本的,它的命令及选项都比较少,特别是常用的选项就那么几个。现在HTTP协议有1.0和1.1两个版本,文档分别为RFC1945和RFC2068。前者有中文版,后者似乎没有看到中文版,有时间了也许可以翻译一下。
一个典型的HTTP会话的请求与响应如下:
请求头:
GET /news/0601/10/2.jpg HTTP/1.1
Accept: */*
Referer: http://news.163.com
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; MyIE2; (R1 1.5))
Host: cimg.163.com
Connection: Keep-Alive
Cookie: **************************
响应头:
HTTP/1.0 200 OK
Date: Tue, 10 Jan 2006 09:54:32 GMT
Server: Apache/1.3.34 (Unix) PHP/4.4.1
Cache-Control: max-age=3600
Expires: Tue, 10 Jan 2006 10:54:32 GMT
Last-Modified: Tue, 10 Jan 2006 09:46:14 GMT
ETag: "320156-5132-43c38266"
Accept-Ranges: bytes
Content-Length: 20786
Content-Type: image/jpeg
Age: 2006
X-Cache: HIT from cimg.163.com
Connection: keep-alive
在Windows下写HTTP程序有几个选择:直接使用winsock编程、使用wininet库、使用MFC的CHttpConnection等相关类。MFC的网络库我从来没有用过,效率不高不说,我还是喜欢比较低层次的面向过程一点的,因为可以自己掌握整个过程。
我最先开始写HTTP程序的时候是直接用的winsock,如我的“BBS图片同步浏览工具”的最早版本。使用winsock的一个优点是你可以完全自定义你所发送的请求数据,比如向上面的请求头,也许你只需要发送一个GET命令加一个Host字段就可以胜任了。然而如此自由地使用最低层的接口时,你就必须能对整个HTTP协议了如指掌,你有一个功能不能实现你的程序就有了巨大的缺陷。比如上面我的“BBS图片同步浏览工具”,当在全国大多数教育网内的BBS内都能够用得很好的时候,发现在武汉白云黄鹤根本不能用。究其原因原来是它使用了Chunked传输编码,于是我必须能识别并能够接收这种编码的文件。虽然后来我实现了chuncked编码的解码(在我的另一篇文章中我会把关于它的中文翻译贴出来的),但是我不能等我的程序出来问题再来补救,天晓得什么时候某个服务器又有什么特殊的选项。后来逐渐接触到wininet库,它是在windows平台下写HTTP程序的最佳选择。有时候折衷的也许是最好的。wininet库介于winsock与MFC库之间,它的灵活性仍然非常大,你可以掌控HTTP一个会话的每一个过程,同时它又封装了HTTP协议,免去了自己实现HTTP所有选项的麻烦,况且,这个库就是微软Internet Explorer所使用的核心库,对于它的强大性应该不庸质疑了。我后来的几个HTTP相关的软件都是在WinInet库上做的,比如前面说的BBS图片浏览器的后来版本,BBS的文件上传及转载工具,万方论文下载、软件在线升级、突破下载限制的下载工具等。
下面描述一下使用WinInet库写HTTP程序的一般过程:
InternetOpen 打开一个Internet会话句柄,WinInet不仅实现了HTTP,还实现了FTP,GOPHER等,因此这个句柄也就不限于HTTP程序。对于这个句柄,还可以设置一些选项,比如连接超时、重试次数等
InternetConnect 指定INTERNET_SERVICE_HTTP类型,它向指定的服务器及端口发起连接
HttpOpenRequest 打开一个HTTP请求,此时并不会向服务器发送数据,它需要与下面的几个函数结合使用,HTTP请求有GET、POST、HEAD等。通常可用HEAD命令获取服务器上文件的信息,比如说大小等。GET命令就是下载文件了,POST通常用于发送额外的数据给服务器,常用的就是对有用户登录要求的服务器发送登录信息、发送表单、上传文件等。
HttpSendRequest 正式向服务器发送请求命令,在这个函数里还可以自定义你的请求头内容了,比如自定义User-Agent字段,文件分段下载所使用的Range字段等。除了使用HttpSendRequest定义请求头外,还可以使用HttpAddRequestHeaders逐个添加字段。另外还有一个HttpSendRequestEx函数,这个函数可以用于发送大数据量的POST请求,比如文件上传等,后面会有一个上传文件的例子。
InternetReadFile 开始接收服务器的响应,服务器的响应通常是一个网页(更准确地说是文件)
HttpQueryInfo 可以查询当前HTTP请求及响应的状态,它可以查询服务器响应头的几乎每一个字段,比如文件大小、时间、服务器的状态码等。也可以用它得到整个响应的原始字符串。
InternetCloseHandle HTTP会话结束后就可以使用这个函数关闭打开的各个句柄了,包括Internet会话句柄、HTTP连接句柄、HTTP请求句柄。当然最好是按句柄打开的先后顺序的逆序关闭。
WinInet库除了直接与HTTP、FTP等相关的函数外,还有许多实用函数,比如与URL处理相关的函数,本地缓存操作的函数等。
HTTP协议程序的调试:
要评测你的HTTP程序写得对不对,除了看结果以外,最好还是要查看它的过程。要查看HTTP会话的过程,那就要用到网络数据包监视软件了。比如CommView、Ethereal、Iris等,这些都是全局的网络监视软件,支持对各种网络协议的解析,但讲到要监视HTTP协议的软件,用EffeTech HTTP Sniffer就比较好了,它能够直接解析HTTP协议,界面也符合HTTP协议的特殊要求。有时候你会抱怨,明明我的代码是对的,为什么服务器就不给你你想要的结果呢?而用IE等客户端就能得到正确的结果。这个时候你就可以用网络监视软件监视你的程序发送的数据包与IE等发送的数据包有什么不同,如果你能把与IE完全一样的数据包发送给服务器,它能做到的,你的程序也能做到。我的另一个软件URLDownloader就是一个可以突破某些网站下载限制的下载工具。因为它的一个最大特点就是可以完全自定义请求头,你可以把请求头定义成与IE的完全一致,这样IE中能看到的,能听到的、能下载的,它也都能下载。因为网站限制下载通常都是利用HTTP请求头中的特殊的字段,比如Referer字段、User-Agent字段、Cookie字段等。
使用HttpSendRequestEx向服务器上传文件:
说实在的,做了一些HTTP相关的软件,但我始终没有去完全解读HTTP协议,基本上都上按照我上面的理解加上参考IE等客户端的网络包做出来的。关于向服务器上传文件,我不知道在HTTP协议中是否有明确的规定,或者只是某些客户端/服务器软件对HTTP协议的扩展。不过我见过一些文件上传功能,基本上都是使用MIME封装(RFC1341/1521/1522/1523等)的,先给出Content-type,boundary,接下来就是要上传的文件的数据流了,文件完了之后还要加一些辅助数据(都通过边界隔开)。下面是上传的过程:
......
INTERNET_BUFFERS BufferIn;
BufferIn.dwStructSize = sizeof( INTERNET_BUFFERS ); // Must be set or error will occur
BufferIn.Next = NULL;
BufferIn.lpcszHeader = strRequest; // 请求头
BufferIn.dwHeadersLength = strRequest.GetLength();
BufferIn.dwHeadersTotal = 0;
BufferIn.lpvBuffer = NULL;
BufferIn.dwBufferLength = 0;
BufferIn.dwBufferTotal = dwContentLen; // This is the only member used other than dwStructSize
BufferIn.dwOffsetLow = 0;
BufferIn.dwOffsetHigh = 0;
HttpSendRequestEx(hHttpRequest, &BufferIn, NULL, 0, 0);
InternetWriteFile(hHttpRequest, (LPVOID)(LPCTSTR)strPostData, strPostData.GetLength(), &dwBytesWritten); // Post数据
InternetWriteFile(hHttpRequest, buf, dwBytesRead, &dwBytesWritten); // 文件数据
InternetWriteFile(hHttpRequest, (LPVOID)(LPCTSTR)strPostData2, strPostData2.GetLength(), &dwBytesWritten); // 剩下的Post数据
......
|