HTTP代理实现请求报文的拦截与篡改4--从客户端读取请求报文并封装

返回目录  

  还记得前面提到的一次会话的四个过程吗,这次讲第一个

  从客户端读取请求报文并封装

先看ObtainRequest() 方法     

1 public bool ObtainRequest()
2 {
3     if (!this.Request.ReadRequest())
4     {
5         ......
6     }
7     ......
8 } 

  ObtainRequest就是调用了this.Request.ReadRequest()方法

  所以上面可以变成

1 this.Request.ReadRequest()   // 获取请求信息 
2 this.Response.ResendRequest() // 将请求报文重新包装后转发给目标服务器     
3 this.Response.ReadResponse () // 读取从目标服务器返回的信息 
4 this.ReturnResponse() // 将从目标服务器读取的信息返回给客户端

  后面的代码比较复杂,我们将不再详细的列出代码,只对其中的关键知识点进行讲解,只要能打通整个环节就行了。 

  在this.Request(ClientChatter类型).ReadRequest() 里 调用 this.ClientPipe(ClientPipe类型).Receive 来从客户端读取信息

  下面我们来看下代码(ClientChatter类的ReadRequest方法)  

 1 Do
 2 {
 3     // 全局变量,用来存读取到的请求流  
 4     this.m_requestData = new MemoryStream(0x1000);
 5     byte[] arrBuffer = new byte[_cbClientReadBuffer];
 6     try
 7     {
 8          iMaxByteCount = this.ClientPipe.Receive(arrBuffer);
 9     }
10     catch (Exception exception)
11     {
12          flag = true;
13     }
14     if (iMaxByteCount <= 0)
15     {
16          flag2 = true;
17     }
18     else
19     {
20         if (this.m_requestData.Length == 0L)
21         {
22            this.m_session.Timers.ClientBeginRequest = DateTime.Now;
23            int index = 0;
24            while ((index < iMaxByteCount) && ((arrBuffer[index] == 13) || (arrBuffer[index] == 10)))
25            {
26                index++;
27            }
28            this.m_requestData.Write(arrBuffer, index, iMaxByteCount - index);
29         }
30         else
31         {
32            this.m_requestData.Write(arrBuffer, 0, iMaxByteCount);
33         }
34     }
35 }
36 while ((!flag2 && !flag) && !this.isRequestComplete()); 
37 

  还记得前面的讲解吗,this.ClientPipe.Receive  其实就是对Socket.Receive的简单封装,this.ClientPipe里封装的那个Socket就是和客户端进行通讯的那个Socket,如果不记得了,可以翻回去看一看 :)  

  这里没什么太难理解的,就是不停的读取请求信息,直到读取完成为止。读取的同时将这些请求信息存在this.m_requestData(MemoryStream类型)这个全局变量里。

  不过有一点要注意一下,那就是判断接收结束的方法。 也就是while里面的那三个条件。 一个是 flag2 = true , 从上面的代码可以看出,就是iMaxByteCount = 0,另外一个条件是 flag = true,也就是出意外了,还有一个就是  isRequestComplete() 。   

  出意外了自然结束,这个不难理解,但为什么有了 iMaxByteCount = 0 了,还要再多加个isRequestComplete()的判断呢? iMaxByteCount = 0 了,不就代表,已经读取完客户端发过来的请求数据了吗,当然不是,这和iMaxByteCount什么时候为0有关,那么iMaxByteCount什么时候为0呢,这个我们先要来看看他的定义,我们知道这个iMaxByteCount 其实就是  Socket.Receive(this.ClientPipe.Receive就是他的封装,又讲一遍了)的返回值, 那么Socket.Receive是怎么定义的呢。  

http://technet.microsoft.com/zh-cn/library/8s4y8aff(v=vs.90) 

Socket.Receive(byte[] buffer) 
从绑定的 Socket 套接字接收数据,将数据存入接收缓冲区。  
参数
buffer
类型:System.Byte()
Byte 类型的数组,它是存储接收到的数据的位置。
返回值
类型:System.Int32
接收到的字节数。

  从上面的定义我们可以看到这个iMaxByteCount其实就是指Socket.Receive每次从客户端读取的数据长度。这不就结了,搞了半天还不是当读取到0的时候就代表再也读不到数据了吗,做人要有耐心,我们再往下看看他后面的备注      

  如果没有可读取的数据,则 Receive 方法将一直处于阻止状态,直到数据可用,除非使用 Socket.ReceiveTimeout 设置了超时值。如果超过超时值,Receive 调用将引发 SocketException。如果您处于非阻止模式,并且协议堆栈缓冲区中没有可用的数据,则 Receive 方法将立即完成并引发 SocketException。您可以使用Available 属性确定是否有数据可以读取。如果 Available 为非零,请重试接收操作。

  如果当前使用的是面向连接的 Socket,那么 Receive 方法将会读取所有可用的数据,直到达到缓冲区的大小为止。如果远程主机使用 Shutdown 方法关闭了 Socket连接,并且所有可用数据均已收到,则 Receive 方法将立即完成并返回零字节。

  备注里已经讲的很清楚了,当读不到数据的时候,Receive方法,会阻塞在那里,直到有数据到达,或者超时为止,而不是象我们想象的那样返回0,返回0只有一种情况,就是Socket.Shutdown(),也就是连接的那个Socket关闭了他的连接,在这里也就是客户端关闭了连接。    

  好的Socket的一些相关知识已经储备完了,但是要想明白刚才的问题,还需要一些其它知识的储备,那就是HTTP的报文和连接管理         

  众所周知(一般都是这样写的),HTTP协议是依托TCP协议的,客户端以HTTP请求报文的形式利用TCP将请求发送给服务端,服务端接收到来自客户端的请求报文,然后解析请求报文,再进行相应的处理,最后将处理结果以响应报文的形式发送回给客户端。 

  从上面的描述中,我们知道,HTTP的报文分为两种,请求报文和响应报文,这里先讲请求报文  

HTTP请求报文的形式如下: 

<method><request-url><version>                              
<header>                                                    
<entity-body>   

<method>: get/post/put/delete/trace等。一般搞WEB开发的对GET和POST会比较熟悉。

<request-url> :也就是要请求的资源的URL。例如 /a.jpg 表示根目录下的a.jpg     

<version>: 所用HTTP协议的版本 。例如HTTP1.0 或HTTP1.1 

以上三个部分也被合起来称为<request-line>请求行 

<header>:首部,可以有0个或者多个,每个首部都是key:value的形式,然后以CRLF(回车换行)结束  例如: host:www.domain.com  

<entity-body>: 任意数据组成的数据块,例如POST时提交的数据,上传文件时文件的内容都放在这里。  

<header>和<entity-body>通过两个CRLF分隔  

  具体的就不再详细的说明了,可以自行查HTTP的协议说明。这里我们只简单的举个例子,让大家有个直观的认识。

post / http/1.1        <method><request-url><version> CRLF
host:www.domain.com    <header> CRLF
content-length:8       CRLF
connection:keep-alive  CRLF  
                       CRLF    
a=b&b=cd               <entity-body>         

  上面就是一个简单的请求报文(红字部分是结构说明,不属于报文的内容),在这个报文里,请求方法是POST,使用的协议是HTTP1.1,发送到的主机是www.domain.com,内容长度是8。内容是a=b&b=cd 。 

  在我们的源码里时使用了一个类:HTTPRequestHeaders来封装(映射)这些报文里的报头信息,也就是除entity-body以外的部分。映射后的情形是这样的,这里假设有一个变量reqHeaders它就是HTTPRequestHeaders的实例,我们把刚才的示例报文分析完后然后映射到这个实例,那么这时使用

reqHeaders.HTTPMethod   得到的就是 post ;
reqHeaders.HTTPVersion  得到的就是http/1.1 
reqHeaders[“host”]  就是  www.domain.com
reqHeaders[“content-length”] 就是 8 

  其它以此类推    

  报文讲完了,下完再简单讲讲HTTP连接管理。 

  开篇的时候,我们用了一张简单的图来说明HTTP的一次会话(没有代理服务器的情况)情况,但这张图过于简单,反映不了HTTP协议的通讯细节,现在我们已经有了足够的知识储备,为了更好的理解HTTP的连接管理,我们有必要在程序的层面,再将客户端与服务端的一次会话说明一遍。

  客户端先建立一个和服务端的TCP连接,然后利用这个TCP连接将一份象上面一样的HTTP请求报文发到服务端,服务端监听到这一个请求,然后利用Accept建立一条和这个客户端的专门连接,然后利用这个专门连接读取这一段请求报文,然后再分析这段报文,当他看到有connection:keep-alive的首部时,服务端就知道,客户端要求建立持久连接,服务端根据实际情况对这个请求进行处理。      

  1.  如果服务端不同意建立持久连接,那么会在响应报文里加上一个首部 connection:close 。然后再利用这个专门连接将这个响应报文发回给客户端,接着服务端就会关闭这条连接,最后,客户端会收到服务器刚才的应答信息,看到了connection:close,这时候客户端就知道服务端拒绝了他的持久连接,那么,客户端在完成这次响应报文的解析后会关闭这条连接,当下次再有请求发送到这个服务器的时候,会重新建一个连接。

  2. 如果服务端同意建立持久连接,那么会在响应报文里加上一个首部connection:keep-alive。然后利用这个专门连接,将这个响应报文发回给客户端,但不关闭这条连接,而是阻塞在那里,直到监视到有新的请求从这个连接传来,再接着处理。客户端收到刚才的响应报名,看到了connection:keep-alive,于是客户端知道服务端同意了他的持久连接请求,那么客户端也不会关闭这个连接,当有新的向此服务器发送的请求时,客户端就会通过这个已经打开的连接进行传输,这样就可以节省很多时间(连接建立的时间是很耗时的)。

 

  好了,所有的相关知识都已经储备完了,可以接着上面讲了。 

 

  从上面我们知道,当客户端将请求报文发送到服务器后,连接是不会关闭的,客户端是否关闭连接,要等到服务器响应后才决定。那也就是说一般情况下,我们是不可能通过iMaxByteCount=0(iMaxByteCount= Socket.receive())来判断是否已经读取完了客户端的请求报文(用户在请求过程上,关闭了浏览器可能会发生这种情况)。 那么我们又怎么来判断请求报文已经全部接收完成了呢。

   答案就是利用content-length首部。 在刚才的例子报文里就有这个头部,我们再把刚才的例子复制过来看一看。    

post / http/1.1        <method><request-url><version> CRLF
host:www.domain.com    <header> CRLF
content-length:8       CRLF
connection:keep-alive  CRLF  
                       CRLF    
a=b&b=cd               <entity-body>

  看到上面的content-length:8  这句了吧,这就是content-length首部了。这个首部就是告诉你<entity-body>(在上面的例子里就是a=b&b=cd)的长度,那么<head>头部解析完后再读取content-length个字符,不就表示此次的请求已经全部读取完成了吗。 

   

  我们来看一下 ClientChatter.cs里的isRequestComplete方法。里面有段代码   

1 if (this.m_headers.Exists("Content-Length")) 
2 {
3     // 处理代码  
4 } 

  这一段就是处理这种情况的。    

  当然content-length并不能判断所有的情况,只有确切的知道entity-body长度的情况下,content-length才是有意义的。但是事实上entity-body的长度并不总是可以预知的,尤其在传一些大文件的时候,为了节省资源和时间,一般会采用分块传输的方式,采用分块传输的时候,会在报文里增加一个首部transfer-encoding:chunked,另外在entity-body里也要遵循一定的格式,这种情况在请求报文里很少见,因为请求报文在不选择文件进行提交的时候,一般报文都很小,这种情况主要出现在响应报文里,后面讲响应报文的时候,会详细讲一下,这里只要提一下,因为Session.isRequestComplete 有处理这种情况的代码       

1 if (this.m_headers.ExistsAndEquals("Transfer-encoding", "chunked"))
2 {
3     // 处理代码   
4 }

  上面两段代码没有帖出来具体的内容,各位可以自行去看一看,其实原理知道了,完全可以自己去写实现的代码,只要上面三种情况全部考虑到就可以了。 

   另外在ClientChatter.cs里的isRequestComplete方法里还有一句要注意下

if (!this.ParseRequestForHeaders()) 

  这个就是分析报头的代码了,前面提到过,会将原始报头映射到一个HTTPRequestHeaders类型的对象里,那么这个方法就是做那个的了,此方法执行完成后,会把原始的请求报文流中的报头部分(除entity-body以外的部分)分析到一个HTTPRequestHeaders类型的私有属性(m_headers)里。 然后在ClientChatter里又暴露了一个Public的属性Headers来访问这个属性。当然这个方法里还会记录entity-body的起始位置,这样,在后面的TakeEntity方法就可以通过这个位置读取entity-body的内容了。而TakeEntity会在 Session类的ObtainRequest里被调用

this.RequestBodyBytes = this.Request.TakeEntity();

  Session类的ObtainRequest方法终于分析完成了,调用套调用,是不是已经晕了,没关系,现在我们再来理一下刚才的调用过程   

  Okay,现在是不是又有点清晰了,那么调用完Session的ObtainRequest方法后,程序会变成什么样呢,经过刚才的分析其实已经很清楚了。

  这时在Session类里,只要使用this.Request.Headers就可以获得所有的报头信息了。

  而报体部分entity-body 则是通过this.RequestBodyBytes 进行调用 。  

 

posted @ 2013-03-11 09:32  乔伟2024  阅读(5224)  评论(0编辑  收藏  举报