1. 程式人生 > >編寫了一個HTTP高匿代理

編寫了一個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本來是打不開的,我用了我這個代理後就能打開了。

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