您的位置:首页 > 理论基础 > 计算机网络

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

2013-03-11 09:32 465 查看
返回目录

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

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


先看ObtainRequest() 方法

public bool ObtainRequest()
{
if (!this.Request.ReadRequest())
{
......
}
......
}


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

  所以上面可以变成

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


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

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

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

Do
{
// 全局变量,用来存读取到的请求流
this.m_requestData = new MemoryStream(0x1000);
byte[] arrBuffer = new byte[_cbClientReadBuffer];
try
{
iMaxByteCount = this.ClientPipe.Receive(arrBuffer);
}
catch (Exception exception)
{
flag = true;
}
if (iMaxByteCount <= 0)
{
flag2 = true;
}
else
{
if (this.m_requestData.Length == 0L)
{
this.m_session.Timers.ClientBeginRequest = DateTime.Now;
int index = 0;
while ((index < iMaxByteCount) && ((arrBuffer[index] == 13) || (arrBuffer[index] == 10)))
{
index++;
}
this.m_requestData.Write(arrBuffer, index, iMaxByteCount - index);
}
else
{
this.m_requestData.Write(arrBuffer, 0, iMaxByteCount);
}
}
}
while ((!flag2 && !flag) && !this.isRequestComplete());


  还记得前面的讲解吗,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方法。里面有段代码

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


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

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

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


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

  另外在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 进行调用 。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: