從目標服務器讀取響應並封裝
至此,請求已經重新轉發到目標服務器了,那么下一步,自然就是要接收從服務器返回的響應信息了。 還記得請求是怎么發送的嗎
- 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編碼的格式介紹完了,至於如何判定其結束的代碼,各位可以自行參考代碼,或者自己去實現 。 所謂十年修得同船渡,百年修得共枕眠,自已動手,才能豐衣足食的。