編寫了一個HTTP高匿代理

本以爲編寫http代理和上一篇的端口轉發差不多的,結果實際一編寫起來發現要複雜的多。怎麼回事呢,就在於要手動解析http協議。

說簡單點吧,如果直接用ie上一個網站,用sniffe一看http請求頭是這樣的。

 

GET / HTTP/1.1
Accept: application/x-ms-application, image/jpeg, application/xaml+xml, image/gif, image/pjpeg, application/x-ms-xbap, application/x-shockwave-flash, application/QVOD, application/QVOD, */*
Accept-Language: zh-CN
User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E)
Accept-Encoding: gzip, deflate
Host: www.microsoft.com
Connection: Keep-Alive
Cookie: xxxxxxxxx

 

 

但是如果用代理就變成了這樣

 

GET http://www.microsoft.com/ HTTP/1.1
Accept: application/x-ms-application, image/jpeg, application/xaml+xml, image/gif, image/pjpeg, application/x-ms-xbap, application/x-shockwave-flash, application/QVOD, application/QVOD, */*
Accept-Language: zh-CN
User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E)
Accept-Encoding: gzip, deflate
Host: www.microsoft.com
Proxy-Connection: Keep-Alive
Cookie: xxxxxxxxx

 

區別就在這裏,用代理get那地方會把完整url寫上,而且Connection加上了proxy標誌,其他一樣。所以用TcpListener和TcpClient每接受一個連接,就要首先把提交的http請求的頭部分改寫,就是把下面的改成上面的。

這是GET方法,只有請求的頭部分沒有實體部分。

還有一種POST方法,是包含實體部分的,比如上傳圖片了什麼的,都是用的POST方法。post方法緊跟在頭部分後面。

怎麼判斷哪是頭那是實體呢?

http協議規定頭必然有2個連續的"/r/n",就像上面Cookie後面就跟了2個/r/n,所以讀取請求頭的時候只要讀到/r/n/r/n,那麼前面就是頭,後面就是實體。實體大小在上面有一個Content-Length標記。所以從/r/n/r/n後面讀Content-Length大小後就結束了

 

還有一種是CONNECT方法,凡是用connect的就是ssl加密通信,當收到 CONNECT urs.microsoft.com:443 HTTP /1.0之類的請求後,代理服務器要給客戶(如IE)返回一個"HTTP/1.1 200 Connection established/r/n/r/n",然後就tcpclinet一個服務器的443,後只負責客戶和服務器的轉發就可以了,就像上一篇的轉發一樣,什麼都不用管了。這種反而最簡單。

就以上3種最常用。

 

其他的請求方法還有put option什麼的,因爲實在是沒見過,也不知道去哪裏試所以都按照get post的方法處理了。

 

服務器返回更麻煩,麻煩就在於http協議過於寬鬆,如果每個迴應或者請求都包括Content-Length或者chunked之類表明實體大小的東西那麼就好判斷了,http協議規定判斷實體大小的方法有好幾種,當然最準確的就是有Content-Length和chunked,還有以服務器斷開連接來判斷的,有的迴應中沒有Content-Length或者chunked,以什麼時候斷開來判斷,疑似那些網絡上下壞文件的就是這麼造成的,客戶根本不知道有多大,如果讀取完了服務器斷開那麼沒問題,如果讀着讀着網絡中斷了了,客戶還以爲是服務器斷開了是吧。

 

所以讀取服務器迴應的時候就要判斷好幾個值

1、判斷狀態碼,http協議規定1xx 204 304肯定不包括實體,所以讀到/r/n/r/n就不用再讀了

2、判斷沒有Content-Length

3、判斷有沒有chunked

 

如果有Content-Length,那麼讀取和上面請求頭一樣,/r/n/r/n後面讀Content-Length個返回給客戶。

還有一種是chunked編碼,這種編碼一般是gzip壓縮的,微軟論壇就是用的這種,當你請求頁面的時候,服務器一邊把頁面gzip壓縮一點傳給你再壓縮一點傳給你,所以開始沒法得到Content-Length,但是每chunked卻有標記的大小

 

 

HTTP/1.1 200 OK
Cache-Control: private
Content-Type: text/html; charset=utf-8
Server: Microsoft-IIS/7.5
X-AspNetMvc-Version: 2.0
X-AspNet-Version: 4.0.30319
Set-Cookie: Set-Cookie: X-Powered-By: ASP.NET
P3P: CP=ALL IND DSP COR ADM CONo CUR CUSo IVAo IVDo PSA PSD TAI TELo OUR SAMo CNT COM INT NAV ONL PHY PRE PUR UNI
Server: CO1VB06
Date: Fri, 24 Sep 2010 09:33:28 GMT
ntCoent-Length: 166137
Content-Encoding: gzip
Transfer-Encoding: chunked

 

2D23
...........}.s.G.......j....*u......y....%...;QO.M.[....3..,...
.!..O....H-."v..>.............=YY

 

像上面chunked/r/n/r/n後面是實體,第一行2D23就是一chunk的大小所以在2D23/r/n後面開始讀2D23個然後會緊跟着/r/n,然後後面就是下一chunk的大小,直到最後一chunk是0大小。實體結束,最後再來一個/r/n。也就是說chunked的最後7個一定是/r/n0/r/n/r/n,本來判斷讀到/r/n0/r/n/r/n就結束應該沒問題,但是爲了保險起見,還是一次一次的讀大小再讀大小。

 

 

最討厭的就是既沒有content-length也沒有chunked,如果返回的是conntion: close還好點,讀着讀着發現那邊斷開了就行了,如果返回的是keep-alive,networkstream.read那裏就卡住了,表現在ie就是看似頁面都加載完了,但是進度條還是在慢慢地走着,所以只能加個讀取超時,比如3秒鐘還讀不出來就斷開連接。反而ie那裏卻顯示“完成”,

 

而且如果再分析keep-alive那就太麻煩了,我是從服務器那裏一旦讀取完,不管是不是keep-alive一律關閉連接,也就說ie每一個請求都單獨的tcpclient一次服務器完後關閉。

 

但是處理ie就不能這樣了,ie每開一的端口和代理服務器連接發送的請求是一個或多個,所以tcplistener每進來一個ie的tcpclient(即ip+端口),處理完這個tcpclient的請求後不能像斷開服務器那樣斷開,否則ie就什麼都不顯示一直走那個進度條或者找不到服務器。所以處理完一次請求後要循環再讀這個tcpclient的下一個請求,如果發現這個請求斷開了就徹底關閉這個tcpclient。所以整個流程是這樣的

 

 

1、tcplistener監聽

2、循環tcplistener.accepttcpclient()

3、進來一個tcpclient()後,啓動一個線程處理,上面繼續循環等待

 

4、同時3的那個tcpclient開始處理,讀取他的http請求頭,改寫http請求頭,然後把改寫的請求頭和下面實體部分發送到請求的服務器,這裏要注意必須是隨讀隨改隨發送,不能等到全讀取完了再發送,否則就超時了。

5、發送完畢,開始從服務器接收

6、和第4差不多,也是從服務器隨讀隨往ie發送,也是不能讀取完再發否則就超時

7、讀取完畢,斷開和服務器的連接,不管是不是keep-alive

8、重複到第4步開始,再從ie讀取下一個請求,如果有那麼再執行5/6/7/8,直到發現ie的這個tcpclient斷開了,就徹底結束掉這個線程。

 

 

大體就是這個樣子了,所以把上面的條件用代碼寫出來就是http代理服務器了,把上面這些條件用代碼寫出來是很麻煩的,所以寫出來的代碼是非常醜陋的,而且我本來寫的代碼就很難看,這樣一來就更沒法看了,所以我就不獻醜了,關鍵是解釋這個大體過程比代碼要重要,當時我找這過程別人的文章解釋的都不太清楚,看了幾頁http協議文檔,應該是機器翻譯的,很難看懂,總共100幾十頁,看全了不值當的,有一篇介紹c#2003做代理的文章,一看根本就是端口轉發沒改請求頭都,一試果然不行,還有一個外國人的,很長不願看了,那種風格就像反編譯.net類庫看到的那種感覺,坐一塊右一塊的,而且運行後發現也不大行,所以只好一點一點的摳,但是好在編譯後運行效果還是挺好的,測試了一下午,cpu佔用率沒超過3%的時候,內存佔用10兆左右,下載什麼的ssl都可以,只是上網偶爾出現進度條等待的情況,就是上面說的因爲服務器那邊沒有實體長度信息等待超時的情況,但是這無關大雅了。大部分網站都是和直接用ie一樣刷的就出來了,感覺不到慢。下載更沒問題了,和直接用ie下載速度一樣的。

 

最後就是爲什麼說是高匿呢,本來想查查原理的代理部分,又想先試試是什麼結果,本以爲是透明代理,結果試了好幾個檢查代理匿名性的網站,結果全都說是“高匿”,關於匿名性,代碼體現的僅僅是把ie發送的proxy-connection改成了connection,難道這樣就“高匿”了?那就高匿吧。對了,picasaweb本來是打不開的,我用了我這個代理後就能打開了。

 

大家有興趣的可以下載來測試一下

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章