還記得前面提到的一次會話的四個過程嗎,這次講第一個
從客戶端讀取請求報文並封裝
- HTTP代理實現請求報文的攔截與篡改1--開篇
- HTTP代理實現請求報文的攔截與篡改2--功能介紹+源碼下載
- HTTP代理實現請求報文的攔截與篡改3--代碼分析開始
- HTTP代理實現請求報文的攔截與篡改4--從客戶端讀取請求報文並封裝
- HTTP代理實現請求報文的攔截與篡改5--將請求報文轉發至目標服務器
- HTTP代理實現請求報文的攔截與篡改6--從目標服務器接收響應報文並封裝
- HTTP代理實現請求報文的攔截與篡改7--將接收到的響應報文返回給客戶端
- HTTP代理實現請求報文的攔截與篡改8--自動設置及取消代理+源碼下載
- HTTP代理實現請求報文的攔截與篡改8補--自動設置及取消ADSL拔號連接代理+源碼下載
- HTTP代理實現請求報文的攔截與篡改9--實現篡改功能后的演示+源碼下載
- HTTP代理實現請求報文的攔截與篡改10--大結局 篡改部分的代碼分析
先看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 進行調用 。