zoukankan      html  css  js  c++  java
  • 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 进行调用 。  

     

  • 相关阅读:
    封装成帧、帧定界、帧同步、透明传输(字符计数法、字符串的首尾填充法、零比特填充的首尾标志法、违规编码法)
    计算机网络之数据链路层的基本概念和功能概述
    物理层设备(中继器、集线器)
    计算机网络之传输介质(双绞线、同轴电缆、光纤、无线电缆、微波、激光、红外线)
    计算机网络之编码与调制
    0953. Verifying an Alien Dictionary (E)
    1704. Determine if String Halves Are Alike (E)
    1551. Minimum Operations to Make Array Equal (M)
    0775. Global and Local Inversions (M)
    0622. Design Circular Queue (M)
  • 原文地址:https://www.cnblogs.com/jivi/p/2953415.html
Copyright © 2011-2022 走看看