HTTP代理实现请求报文的拦截与篡改6--从目标服务器接收响应报文并封装

返回目录

  从目标服务器读取响应并封装      

  至此,请求已经重新转发到目标服务器了,那么下一步,自然就是要接收从服务器返回的响应信息了。 还记得请求是怎么发送的吗

 

this.Response.ResendRequest() // 将请求报文重新包装后转发给目标服务器  

  那么接收呢  

this.Response.ReadResponse () // 读取从目标服务器返回的信息 

  进入ReadResponse(ServerChatter.cs里)方法看一看后,我们会发现,他其实和读取请求报文时(ClientChatter的ReadRequest方法)基本上是一样的,都是不停的使用Receive方法接收数据,直到已经接收完成为止(要判断三种情况,不记得的,可以翻回去看一看请求报文的读取那部分)。然后再将接收到的响应信息映射到一个HTTPResponseHeaders 类型的对象里,这个对象名叫_inHeaders。同样的也有一个public的Headers属性来访问它。和读取请求报文时一样,这个方法里,也会记录<entity-body>的偏移位置,以备 TakeEntity方法调用时,用来读取entito-body 部分  。      

  响应信息是以响应报文的方式返回回来的, 响应报文的格式如下: 

<version><status><reason-phrase>
<header>
<entity-body>

<version> :  HTTP的版本号,例如 HTTP/1.1 or HTTP/1.0

<status> : 状态码 常见的  200 成功  404 没有找到文件 500 服务器错误等 

<reason-phrase> : 对于状态码的简短解释,这个解析不同的服务器内容可能不尽相同。例如 当 <status>为200时,这里可以是 OK

<head> : 首部,可以有0个或者多个,每个首部都是key:value的形式,然后以CRLF(回车换行)结束  例如: content-type:text/html   

<entity-body> 主体内容,返回的HTML,或者二进制的图片,视频等数据都在这一部分   

  这里不作详细说明,其它的可参考HTTP协议的报文部分说明,只举个简单例子  

HTTP/1.1 200 OK                     <version><status><reason-phrase>CRLF      
content-type:text/html              <head>CRLF
content-length:1196                 CRLF
                                    CRLF
<html>                              <entity-body>
<head>...(省略N个字)</head>
<body>...(省略N个字)</body>
</html>

  其它具体的代码就不分析了,因为和读取请求报文区别不是很大,对照着理解和阅读就可以了,不过有个小技巧要在这里讲一讲。 

  我们知道,在读取请求报文的时候,我们没办法确定客户端的连接是否关闭,但在读取服务端响应的时候就完全不同了,因为我们可以控制让服务器发送完数据后自动关闭,那么如何控制呢,其实很简单,还记得前面提到的connection首部吗,在讲连接管理的时候主要讲的就是它,还记得他的用法吗,不记得的,可以翻回去再看一看,不过上面只讲了请求报文中connection:keep-alive的情况,还少讲了一种情况,那就是请求报文中,connection:close的情况(http1.0版本,默认就是这种情况)  

  当我们的请求报文里有connection:close首部时,服务端收到这个报文的时候,就知道,我们并不想建立持久连接,那么他也会在他的响应报文里加一个connection:close首部,然后等发送完所有数据后关闭他的连接。   

  所以如果你嫌判断接收完毕要三种情况太麻烦了。这里就有一个小技巧了,就是在ResendRequest的时候,简单的把connection:keep-alive,改成connection:close就行了。这样就可以只判断 iMaxByteCount = 0 (iMaxByteCount=Socket.Receive()) 来判断接收 是否完毕了   。  

  到这里,从服务器接收响应数据应该就算讲完了,但前面还遗留了一个transfer-encoding:chuncked的问题,transfer-encoding首部和connection首部一样,都是请求报文和响应报文中都有的首部,这种首部叫做通用首部。前面也讲过了,在请求的时候,如果不上传文件的情况下,报文是非常小的,基本上用到chunked的机率很小,而接收的时候相对的chunked出现的频率就要高一些了。

  他主要用于结合jsp,.net,asp等动态技术进行输出的时候。当然大部分情况下这些程序输出的时候标识长度使用的仍然是content-length。但是当输出内容需要压缩输出的时候,例如gzip。因为需要额外的在服务器申请一个很大的内存来存完整的压缩内容,然后再计算长度,这样是很耗空间和时间的,这时候,就需要使用chuncked了。 

  Chunck 从字面意思翻译就是“块”的意思。 简单说起来就是数据发送方把一个大的包分割成多个小块,便于在网络上传输。使用chunked机制的时候,需要在报文里加一个transder-encoding:chunked首部。然后在<entity-body>部分遵循一定的格式

  这个格式如下所示:  

chunked-body = *chunk
"0" CRLF
footer
CRLF
chunk = chunk-size [ chunk-ext ] CRLF
chunk-data CRLF
hex-no-zero = <HEX excluding "0">
chunk-size = hex-no-zero *HEX
chunk-ext = *( ";" chunk-ext-name [ "=" chunk-ext-value ] )
chunk-ext-name = token
chunk-ext-val = token | quoted-string
chunk-data = chunk-size(OCTET)
footer = *entity-header 

  这个是正则文法的范式表示,基本上属于普通人类看着比较郁闷的范围, 所以我们给他加上语法图,让其更容易理解一点(antlr不支持中间杠,所以我把中间杠全变成下划杠了)

chunked-body = *chunk
"0" CRLF
Footer
CRLF

chunk = chunk-size [ chunk-ext ] CRLF
chunk-data CRLF

hex-no-zero = <HEX excluding "0"> 

  

       

chunk-size = hex-no-zero *HEX    

chunk-ext = *( ";" chunk-ext-name [ "=" chunk-ext-value ] )

   

chunk-ext-name = token
chunk-ext-val = token | quoted-string
chunk-data = chunk-size(OCTET)     
footer = *entity-header 

  Chunked-Body表示经过chunked编码后的报文体。报文体可以分为chunk, ‘0’CRLF,footer和结束符(CRLF)四部分。chunk的数量在报文体中最少可以为0,无上限;每个chunk的长度是自指定的,即,起始的数据必然是16进制数字的字符串,代表后面chunk-data的长度(字节数)。这个16进制的字符串第一个字符如果是“0”,则表示chunk-size为0,该chunk为最后一个chunck,无chunk-data部分。可选的chunk-ext由通信双方自行确定,如果接收者不理解它的意义,可以忽略。

  footer是附加的在尾部的额外头域,通常包含一些元数据,这些头域可以在解码后附加在现有头域之,这个额外头域可以为空。

  Footer之后紧跟一个结束符CRLF表示整个文件已经传输完毕了 。    

  好的,chuncked编码的格式介绍完了,至于如何判定其结束的代码,各位可以自行参考代码,或者自己去实现 。 所谓十年修得同船渡,百年修得共枕眠,自已动手,才能丰衣足食的。    

posted @ 2013-03-12 09:08  乔伟2024  阅读(3366)  评论(0编辑  收藏  举报