HTTP代理实现请求报文的拦截与篡改6--从目标服务器接收响应报文并封装
从目标服务器读取响应并封装
至此,请求已经重新转发到目标服务器了,那么下一步,自然就是要接收从服务器返回的响应信息了。 还记得请求是怎么发送的吗
- HTTP代理实现请求报文的拦截与篡改1--开篇
- HTTP代理实现请求报文的拦截与篡改2--功能介绍+源码下载
- HTTP代理实现请求报文的拦截与篡改3--代码分析开始
- HTTP代理实现请求报文的拦截与篡改4--从客户端读取请求报文并封装
- HTTP代理实现请求报文的拦截与篡改5--将请求报文转发至目标服务器
- HTTP代理实现请求报文的拦截与篡改6--从目标服务器接收响应报文并封装
- HTTP代理实现请求报文的拦截与篡改7--将接收到的响应报文返回给客户端
- HTTP代理实现请求报文的拦截与篡改8--自动设置及取消代理+源码下载
- HTTP代理实现请求报文的拦截与篡改8补--自动设置及取消ADSL拔号连接代理+源码下载
- HTTP代理实现请求报文的拦截与篡改9--实现篡改功能后的演示+源码下载
- HTTP代理实现请求报文的拦截与篡改10--大结局 篡改部分的代码分析
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编码的格式介绍完了,至于如何判定其结束的代码,各位可以自行参考代码,或者自己去实现 。 所谓十年修得同船渡,百年修得共枕眠,自已动手,才能丰衣足食的。