构造 HttpClient 三部曲之二:GET 方法实现
09 May 2011继续 HttpClient 构造的博文,第二篇:GET 方法的实现。HTTP 协议定义了和服务器交互的不同方法,包括 GET,POST,PUT,DELETE,CONNECT 等等,其中最常用的两个方法就是 GET 和 POST。这篇先讲讲 GET 方法的一些细节。
HTTP 协议的交互主要由请求和响应组成:客户端发起请求,服务端返回响应。而一个简单的 HTTP 请求又可以分成信息头和信息体。但对于 GET 来说,它的请求只有 HTTP 消息头而已。
HTTP 请求之 GET
一个最简单的 HTTP GET 请求可以写成:
而复杂的请求往往会加入很多的请求头域,如:
GET /logos/2011/hargreaves11-hp-15.jpg HTTP/1.1 Host: www.google.com.hk Connection: keep-alive Referer: http://www.google.com.hk/ User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.60 Safari/534.24 Accept: / Accept-Encoding: gzip,deflate,sdch Accept-Language: zh-CN,zh;q=0.8 Accept-Charset: GBK,utf-8;q=0.7,*;q=0.3
每一个头域都由一个域名,冒号 (:) 和域值组成。域名是大小写无关,而域值前面可以添加数个空格。之所以将前面那段字特地高亮是因为碰到很多 “不太规范” 的 HTTP Server 返回的域名经常是不 “正规” 的:比如将 Content-Type 写成 Content-type—- 这只是从视觉上不太美观,但是绝对是符合规范的,所以客户端在解析的时候特别要注意忽略大小写的影响。
一些典型的头域有:
- Host:指定请求资源的主机和端口
- Connection:指定连接状态,HTTP1.1 和 1.0 协议的一个很大区别就是 HTTP1.1 可以保持连接,HTTP1.1 默认是持久连接,一次连接可以发起多个请求。
- User-Agent:包含发出请求的用户信息,多用于系统和浏览器检测。关于它的八卦可以参考这里。
- Referer:请求 URI 的源地址。一般可用于反盗链。
- Accept:可接受的文件类型 当然我们也可以往 HTTP 头中塞入一些自定义的头域,这样的效果和在 URL 添加请求参数的效果是一样的。
HTTP 响应
无论是 GET 方法还是 POST 方法,HTTP 的响应都是一致的:一个 HTTP 消息头和一个 HTTP 消息体。在 HTTP 消息头的第一行指定了:HTTP 版本号,HTTP 响应码和详细消息。而接下来就是一个个头域,直到接受到两个 \ r\n 为止。一个典型的 HTTP 相应的消息头如下:
HTTP/1.1 200 OK Cache-Control: private, max-age=30 Content-Type: text/html; charset=utf-8 Content-Encoding: gzip Expires: Mon, 25 May 2009 03:20:33 GMT Last-Modified: Mon, 25 May 2009 03:20:03 GMT Vary: Accept-Encoding Server: Microsoft-IIS/7.0 X-AspNet-Version: 2.0.50727 X-Powered-By: ASP.NET Date: Mon, 25 May 2009 03:20:02 GMT Content-Length: 12173
对于客户端来说需要着重关注的头域一般只有 Content-Length 和 Transfer-Encoding 而已。如果返回的 HTTP 消息头中有 Content-Length 这个头域则说明 HTTP 体是定长的,客户端只需要继续接受 Content-Length 指定的数据长度的内容即可。而如果服务器在返回数据给客户端时是一边生成数据一边发送,那么很可能就会采用 chunked 的形式,将 Transfer-Encoding 指定为 chunked。这样客户端对 HTTP 消息体的处理就会稍显复杂。 在 HTTP 协议的 RFC 中对 chunk 传输的定义如下:
Chunked-Body = *chunk
last-chunk
trailer
CRLF
chunk = chunk-size [ chunk-extension ] CRLF
chunk-data CRLF
chunk-size = 1*HEX
last-chunk = 1*("0") [ chunk-extension ] CRLF
chunk-extension= *( ";" chunk-ext-name [ "=" chunk-ext-val ] )
chunk-ext-name = token
chunk-ext-val = token | quoted-string
chunk-data = chunk-size(OCTET)
trailer = *(entity-header CRLF)
简单来说就是:一个 chunked 编码的 body 可以分为四个部分:一系列的 chunk 段,last chunk,trailer 和 CRLF。一个 chunk 又可以分为 chunk-size 和 chunk-data 两部分。通过解析 chunk-size(16 进制)可以获取到真正的 chunk-data 长度并进行拼接得到最后的 HTTP 体内容。RFC 中关于 chunk 的解析伪代码如下:
length := 0
read chunk-size, chunk-ext (if any) and CRLF
while (chunk-size > 0) {
read chunk-data and CRLF
append chunk-data to entity-body
length := length + chunk-size
read chunk-size and CRLF
}
read entity-header
while (entity-header not empty) {
append entity-header to existing header fields
read entity-header
}
Content-Length := length
Remove "chunked" from Transfer-Encoding
为了构造这个 HTTPClient 我也相应写了 C++ 版的处理方法,不过没有考虑 chunk-extension 和 entity-header 的处理(在我测试的几个网站来看都没有这两个值,所以即使写了也无法验证其正确性)。