開發一個http代理服務器

參考連接:html

http://www.cnblogs.com/jivi/archive/2013/03/10/2952860.html數組

https://www.2cto.com/kf/201405/297926.html瀏覽器

http://www.jb51.net/article/116591.htm緩存

http://www.jb51.net/article/79893.htm服務器

http://blog.csdn.net/u012734441/article/details/44801523session

http://blog.csdn.net/turkeyzhou/article/details/5512348dom

http://blog.csdn.net/u013087513/article/details/53560827異步

1、產品原型

一、配置使用代理服務器

參照上圖設置,HTTPS和FTP等不要設置(本代理服務器未對這兩種協議進行處理), IP是 127.0.0.1 表示代理服務器是在本機,監聽端口設成8888。   post

在IE裏設置完後,咱們會發現其它瀏覽器也自動開始使用代理服務器了,這是由於設置代理服務器是系統的功能,每一個瀏覽器打開的都是同一個設置代理服務器的程序。網站

其實,咱們能夠實現一個自動設置代理服務器的功能,這樣,當咱們的代理服務器啓動的時候,就自動將本機的代理服務器設置成本身,退出的時候,再恢復成原樣,這樣就再也不須要向上面同樣手動設置了。

二、使用代理服務器

配置完畢後,在瀏覽器裏輸入網址: http://www.baidu.com 

咱們能夠清楚看到,全部的請求和響應信息都已經被監聽到了。

2、理論儲備

一、http請求處理過程

客戶端先創建一個和服務端的TCP鏈接,而後利用這個TCP鏈接將一份象上面同樣的HTTP請求報文發到服務端,

服務端監聽到這個請求,而後利用Accept創建一條和這個客戶端的專門鏈接,而後利用這個專門鏈接讀取這一段請求報文,而後再分析這段報文,當他看到有connection:keep-alive的首部時,服務端就知道,客戶端要求創建持久鏈接,服務端根據實際狀況對這個請求進行處理。      

a.  若是服務端不一樣意創建持久鏈接,那麼會在響應報文里加上一個首部 connection:close 。而後再利用這個專門鏈接將這個響應報文發回給客戶端,接着服務端就會關閉這條鏈接,

     客戶端會收到服務器剛纔的應答信息,看到了connection:close,這時候客戶端就知道服務端拒絕了他的持久鏈接,那麼,客戶端在完成此次響應報文的解析後會關閉這條鏈接,當下次再有請求發送到這個服務器的時候,會從新建一個鏈接。

b.  若是服務端贊成創建持久鏈接,那麼會在響應報文里加上一個首部connection:keep-alive。而後利用這個專門鏈接,將這個響應報文發回給客戶端, 但不關閉這條鏈接,而是阻塞在那裏,直到監視到有新的請求從這個鏈接傳來,再接着處理。

     客戶端收到剛纔的響應報名,看到了connection:keep-alive,因而客戶端知道服務端贊成了他的持久鏈接請求,那麼客戶端也不會關閉這個鏈接,當有新的向此服務器發送的請求時,客戶端就會經過這個已經打開的鏈接進行傳輸,這樣就能夠節省不少時間(鏈接創建的時間是很耗時的)。

二、http請求報文

 

http請求報文包含以下三部分:

http請求報文例子分析:

關於請求頭host補充一下:請求頭host冒號後面的部分除了採用 域名:端口 的方法,還能夠採用 IP:端口的方法。

例如 host:192.168.1.12:8080 。這種狀況下,第二個冒號前面的就是IP了。

若是是域名,host:www.domain.com 的狀況。這種狀況,就須要從域名獲得IP了。那麼從域名--IP,須要什麼技術呢,天然就是DNS(Domain Name System)了。

在本地的DNS緩存裏找域名對應的IP,而後封裝成IPAddress類型的數組進行返回。爲何返回的是數組? 

由於有IPV4和IPV6兩種類型的地址,但在一些不支持IPV6的機器上,IPV6項會被篩選掉,因此這種狀況下,這個數組的大小就是1。

三、http響應報文

 

四、若是設置了代理

當沒有設置代理的時候,正常的HTTP請求報文格式應該是這樣的

可是若是設置了代理,那麼瀏覽器的請求報文都會被髮送到代理服務器,在這種狀況下,瀏覽器會對請求報文作些簡單的變化,主要在兩個方面。

4.一、區別一:post / http/1.1 ----》post http://www.domain.com/ http/1.1

咱們看到在直接發送到目標服務器的狀況下,通常都是相對的地址(前面的例子默認都是這種狀況下的):

post / http/1.1

而在發送到代理服務器的狀況下,就會變成:

post http://www.domain.com/ http/1.1

<request-url> 部分被替換成了完整的URL地址。爲何要這樣作呢,是由於早期的HTTP協議,是沒有HOST首部的,這樣當直接鏈接到服務器時,是沒有問題的,由於服務器IP在發送報文前確定是已知的,因此這種狀況只要傳個相對路徑給服務器,服務器就能夠經過這個相對路徑找到資源並返回了。

可是若是是發送到代理的話,這個請求報文就出問題了,由於代理沒法從這個請求報文裏分析出來目標服務器的地址,地址都不知道代理又如何將這個請求轉發呢。

因此,在HTTP協議裏將發送到代理服務器狀況下的請求報文裏的<request-url>設計成了完整地址,這樣代理服務器就能夠經過分析這個完整的址址的主機部分獲取目標服務器的地址,如此就可能順利的創建到目標服務器的鏈接並實現轉發了。

可是後來隨着虛擬主機的出現,同一個服務器能夠映射多個站點,沒有HOST首部的HTTP協議,已經沒辦法處理這種狀況了,因此後來HTTP協議裏引入了HOST這個首部,

理論上在引入了HOST首部後,不只虛擬主機的狀況能夠解決,就連代理服務器也能夠一併解決了,就不用再在<request-url>裏寫完整地址了,可是爲了保持協議的兼容性,當發送到代理服務器時<request-url>爲完整地址的規則仍是被繼承了下來。 

4.二、區別二: connection:keep-alive/close  ----》proxy-conection:keep-alive/close

那就是當直接發送到目標服務器時,有個首部是
connection:keep-alive/close
當發送到代理服務器時,這個首部會被替換成
proxy-conection:keep-alive/close

這段代碼在 ServerChatter 類裏的 ResendRequest方法體裏。這裏又出現了this._bWasForwarded.  意思是 若是繼續轉發請求給代理服務器或網關,並且不是HTTPS的狀況下,就把proxy-connection 替換成 connection .

然而程序自己是沒有辦法判斷出要轉發到的目標服務器到底是不是代理服務器或者網關的,因此這個變量,不在人工干預的狀況下,百分之百是FALSE的 。在咱們的代碼裏,目前並無實現人工干預的功能,因此這個變量永遠都是FALSE。保留他下來只是爲了之後的擴展。

事實上代理服務器確實不必定會把請求直接轉發到最終的目標服務器,而是有可能先轉發到另一個代理服務器或者網關上,而後再由他們轉發到最終的目標服務器或者他也是再轉發至下一個代理服務器或者網關。

由於咱們頗有可能會遇到下面的這種狀況, 在本身的內網建了一個過濾代理,這個過濾代理的功能就是把除了公司想讓咱們訪問的網址之外的全部網址所有屏蔽,可是如今出問題了,公司想讓咱們訪問的網站,所有都是被天朝封鎖的國外網站,這可怎麼辦呢?

其實不難辦,這時候只要過濾代理再把這個請求轉發給另一個代理服務器(這個服務器要求在國內能訪問,另外它也能訪問國外的網站就能夠了),而後由它來獲取網頁後響應給咱們的過濾代理,再由咱們的過濾代理髮回給客戶端 。 

3、關鍵點設計

代理服務器程序啓動時,new 一個代理(Proxy)類的實例,而後調用這個實例的Start方法,在Start方法裏不停的異步監聽代理服務器8888端口, 

若是監聽到了,就從線程池裏取出來一個線程,並在這個線程裏構造一個Session對象。一個Session對象表明客戶端與服務器的一次會話,在有代理服務器狀況下的一次會話(Session) 過程以下:

  • 1.從客戶端讀請求
  • 2.從新包裝客戶端的請求,轉發至目標服務器. 
  • 3.從目標服務器讀取響應信息 
  • 4.包裝接收到的響應信息並返回給客戶端。

故而在Session類裏,封裝一個ClientChatter類型的名爲Request的對象,用來實現和客戶端的通信, 另外又封裝了一個ServiceChatter類型的名爲Response的對象,用來實現和目標服務器的通信。 

一、ObtainRequest()

這裏就是不停的讀取請求信息,直到讀取完成爲止。讀取的同時將這些請求信息存在this.m_requestData(MemoryStream類型)這個全局變量裏。有一點要注意一下,那就是判斷接收結束的方法。也就是while裏面的那三個條件:

  • 一個是 flag2 = true , 從上面的代碼能夠看出,就是iMaxByteCount = 0 ,客戶端關閉了連接 
  • 另一個條件是 flag = true,也就是出意外了,出意外了天然結束
  • 還有一個就是 isRequestComplete(), 封裝了兩種結束方式:content-length結束和transfer-encoding:chunked方式結束 

iMaxByteCount = 0 了,不就表明已經讀取完客戶端發過來的請求數據了嗎,固然不是,iMaxByteCount實際上是客戶端關閉了連接。

這個iMaxByteCount 實際上是Socket.Receive的返回值, Socket.Receive(byte[] buffer)  從綁定的 Socket 套接字接收數據,將數據存入接收緩衝區。  

當讀不到數據的時候,Receive方法會阻塞在那裏,直到有數據到達,或者超時爲止,而不是象咱們想象的那樣返回0,返回0只有一種狀況,就是Socket.Shutdown(),也就是鏈接的那個Socket關閉了他的鏈接,在這裏也就是客戶端關閉了鏈接。    

因此說通常狀況下,咱們是不可能經過iMaxByteCount=0(iMaxByteCount= Socket.receive())來判斷是否已經讀取完了客戶端的請求報文(用戶在請求過程上,關閉了瀏覽器可能會發生這種狀況)。 

請求接收結束時機 isRequestComplete()->content-length方式結束

當客戶端將請求報文發送到服務器後,鏈接是不會關閉的,客戶端是否關閉鏈接,要等到服務器響應後才決定。 

也就是說通常狀況下,咱們是不可能經過iMaxByteCount=0(iMaxByteCount= Socket.receive())來判斷是否已經讀取完了客戶端的請求報文(用戶在請求過程上,關閉了瀏覽器可能會發生這種狀況)。 那麼咱們又怎麼來判斷請求報文已經所有接收完成了呢? 

 

請求頭:content-length:8, 表示實體 <entity-body>(在上面的例子裏就是a=b&b=cd)的長度,那麼<head>頭部解析完後再讀取content-length個字符,不就表示這次的請求已經所有讀取完成了嗎? 

實際的處理以下: 

請求接收結束時機 isRequestComplete()->transfer-encoding:chunked方式結束  

固然content-length並不能判斷全部的狀況,只有確切的知道entity-body長度的狀況下,content-length纔是有意義的。 

可是事實上entity-body的長度並不老是能夠預知的,尤爲在傳一些大文件的時候,爲了節省資源和時間,通常會採用分塊傳輸的方式,採用分塊傳輸的時候,會在報文裏增長一個首部transfer-encoding:chunked,另外在entity-body裏也要遵循必定的格式, 

這種狀況在請求報文裏不多見,由於請求報文在不選擇文件進行提交的時候,通常報文都很小,這種狀況主要出如今響應報文裏。這種狀況通常會有以下處理方式: 

同時,isRequestComplete()解析了請求報文頭,ParseRequestForHeaders()

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方法終於分析完成! 

調用完Session的ObtainRequest方法後,程序會變成什麼樣呢,通過剛纔的分析其實已經很清楚了。

這時在Session類裏,只要使用this.Request.Headers就能夠得到全部的報頭信息了。報體部分entity-body 則是經過this.RequestBodyBytes 進行調用 。 

二、response.ResendRequest

 

核心邏輯分析以下:

其中,

a、this.ConnectToHost();  

// 構造一個ServerPipe,並在裏面封裝一個和服務端通信的Socket

// 獲取ip(能夠經過dns解析域名,從在本地的DNS緩存裏找域名對應的IP獲得)

// 獲取端口

// 建立連接

this.ConnectToHost() 展開以下:

 

b、this.ServerPipe.Send( this.m_session.Request.Headers.ToByteArray( true, true, this._bWasForwarded && !this.m_session.IsHTTPS ) );

// 經過這個ServerPipe的Send方法將使用this.m_session.Request.Headers.ToByteArray方法從新包裝的請求報頭髮送給服務器 。

 

c、this.ServerPipe.Send(this.m_session.RequestBodyBytes);

 

// 發送原始請求報文的報體到服務器

 

三、response.ReadRequest

 響應報文:

ReadResponse(ServerChatter.cs裏)方法和讀取請求報文時(ClientChatter的ReadRequest方法)基本上是同樣的,都是不停的使用Receive方法接收數據,直到已經接收完成爲止(也要判斷三種狀況)。
而後再將接收到的響應信息映射到一個HTTPResponseHeaders 類型的對象裏,這個對象名叫_inHeaders。一樣的也有一個public的Headers屬性來訪問它。
和讀取請求報文時同樣,這個方法裏,也會記錄<entity-body>的偏移位置,以備 TakeEntity方法調用時,用來讀取entito-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,或者二進制的圖片,視頻等數據都在這一部分

 

 

四、returnResponse

相關文章
相關標籤/搜索