1. 程式人生 > >HTTP 代理伺服器的設計與實現

HTTP 代理伺服器的設計與實現

寫在前面

花了好幾天才把計算機網路的實驗一搞定,在此記錄一下這個實驗的流程。

本實驗的要求也是比較簡單明瞭的:

(1) 設計並實現一個基本 HTTP 代理伺服器。要求在指定埠(例如 8080)接收來自客戶的 HTTP 請求並且根據其中的 URL 地址訪問該地址 所指向的 HTTP 伺服器(原伺服器),接收 HTTP 伺服器的響應報文,並 將響應報文轉發給對應的客戶進行瀏覽。
(2) 設計並實現一個支援 Cache 功能的 HTTP 代理伺服器。要求能緩 存原伺服器響應的物件,並能夠通過修改請求報文(新增 if-modified-since 頭行),向原伺服器確認快取物件是否是最新版本。(選作內容,加分項 目,可以當堂完成或課下完成)
(3) 擴充套件 HTTP 代理伺服器,支援如下功能: (選作內容,加分專案, 可以當堂完成或課下完成)
a) 網站過濾:允許/不允許訪問某些網站;
b) 使用者過濾:支援/不支援某些使用者訪問外部網站;
c) 網站引導:將使用者對某個網站的訪問引導至一個模擬網站(釣 魚)。

而且實驗指導書上還給出了 200 來行的程式碼作為參考,可以說是很貼心了。但關鍵問題不是不知道原理,而是對 socket 程式設計是相當地陌生,還好程式碼大部分都能看懂,看不懂的查詢一下也能搞定。本實驗基本功能還是很好做的,主要就是 cache 的實現,我大部分時間就在搞這個,最後東拼西湊的,也算是搞出來了(雖然外部儲存有時會亂碼)。

實驗程式碼在我 github 上,想參考的話,可以點選這裡 ,歡迎來提各種建議,雖然我也不一定會去改,但還是希望這個程式碼會越來越好。

實驗配置問題

首先要說的就是環境問題,由於我是使用的 CodeBlocks 進行編譯的,因此,有時候會出現一些莫名的問題。這裡簡單介紹一下。

  1. 最大的問題就是靜態連結問題,也就是這段程式碼 #pragma comment(lib,"Ws2_32.lib") ,在 VS 裡可以很好地執行,但是在 CodeBlocks 中就失去作用了。這段程式碼也很簡單,就是說要連結一個庫,但是Codeblocks 使用的是 MingGW 來編譯,MingGW不支援 #pragma comment(lib,"Ws2_32.lib") 的寫法
    解決方法也是很簡單,由於該命令是靜態連結 Ws2_32.lib 庫,因此可以在設定裡,加上 -lws2_32 或 -lwsock32,具體怎麼加,這裡就不講了。

  2. 第二個問題也是編譯器的問題,由於版本問題,這裡並不支援 int _tmain(int argc, _TCHAR* argv[])

    的寫法,需要改成 int main(int argc, char* argv[]) 或者直接寫成 int main() ,其實沒有什麼區別。具體原因,參考 main()和_tmain(int argc, _TCHAR* argv[]) 的詳細區別c/c++ int _tmain(int argc, _TCHAR* argv[])

  3. 再就是 goto 語句的問題了,程式碼一直報 goto 語句的問題,不常用這個,我也是很懵啊,不過,還好前輩們有經驗分享,具體原因可以參考這個: g++編譯goto語句出現:[error:jump to label XXX],簡單地說,就是你的 goto 語句之後不能再定義新的變數

  4. 再就是關於strtok_s的問題了,可以參考這篇 stackoverflow :關於strtok_s的問題,就是說,只要改成 strtok() 這個函式就可以了。再去掉最後一個引數,因為這個函式只需要倆引數。雖然這個函式並不安全,但它可以用啊。

  5. 大點的問題就這些,還有一些小的問題,比如 VS 裡專用的 #include "stdafx.h" ,要去掉,可以參考 為什麼要加#include “stdafx.h” ,剩下的,大都沒有詳細說的必要了

好了,bug 就算是修復完了,現在就可以正常訪問網站了:

執行程式 --> 開啟瀏覽器 --> 設定代理 --> 設定 127.0.0.1 和埠號 1240

這樣就實現了一個基本的代理伺服器,其實現在就已經完成第一個要求了。但你還不知道它的原理是什麼,所以,下面看一下它的原理。

實現一個基本的代理伺服器

在繼續往下看之前,你最好對這幾個函式有一定的瞭解:

  • bind() : 將一本地地址與一套接字捆綁,在 connect() 或 listen() 呼叫前使用
  • listen() : 監聽套接字的連線請求,將套接字設為監聽模式
  • connect() : 用於建立與指定 socket 的連線
  • accept() : 在一個套接字處,接受一個連線
  • send() : 傳送資料(客戶端向伺服器傳送請求,伺服器端向客戶端傳送應答)
  • recv() : 接收資料

更詳細的可以自行去百度查詢,這裡就不多介紹了。先來看代理伺服器的原理

  1. 首先初始化一個套接字,利用 blind() 函式將該套接字與伺服器 host 地址繫結,地址設為 “127.0.0.1”;同時,也要繫結埠號,這裡就按照指導書上的要求設定為 “10240”。然後,利用 listen() 函式對該埠進行監聽。
  2. 通過設定 accept() 函式,對每個到來的請求進行接收和相應,為了提供效率,對每個請求都建立一個新的執行緒來處理。
  3. 利用 recv() 和 send() 函式,接收來自客戶端的 HTTP 請求,並通過這個代理伺服器將該請求轉發給伺服器;同時,伺服器也將獲得的響應發給代理伺服器,然後代理伺服器再將該響應傳送給客戶端。在這裡,代理伺服器相當於一箇中介,提供一個代理的服務,所有的請求和響應都經過它
  4. 處理完成後,等待 200 ms 後,關閉該執行緒,並清理快取,然後繼續接收並處理下一個請求。對於客戶端而言,它只要將正常傳送的請求發給代理伺服器,就可以接收到對應的響應。

流程圖可以表示為:

代理伺服器的流程圖

我個人覺得,這張流程圖非常容易理解,基本上就是這段程式碼的邏輯,對於理解這段程式碼很有幫助。

擴充套件功能

對於這三個擴充套件功能,只要看懂了程式碼是如何解析並存儲的 HTTP 頭部資訊,寫這三個功能還是很簡單的。不需要增加多少程式碼,只需進行 if 判斷即可。

遮蔽網站

對請求過來的 HTTP 報文頭部進行檢測,提取出其中的訪問地址 url ,檢測其是否為要被遮蔽的網址,如果是,則直接跳轉到程式碼中的 erro 部分,即關閉套接字,斷開此次連線。程式碼片段如下:

if (strcmp (httpHeader->url, INVILID_WEBSITE) == 0) {
    printf("\n=====================================\n\n");
    printf("-------------Sorry!!!該網站已被遮蔽----------------\n");
    goto error;
}

遮蔽使用者

更改套接字繫結的主機地址,這樣的話,只要不是從該地址訪問代理伺服器的客戶端,都會被該代理伺服器遮蔽,部分程式碼如下:

//遮蔽使用者
//ProxyServerAddr.sin_addr.S_un.S_addr = INADDR_ANY;
ProxyServerAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//僅本機使用者可訪問伺服器
//ProxyServerAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.2");  //遮蔽使用者

其實,就是更改套接字繫結的代理伺服器的 IP 地址,這樣的話,就會遮蔽掉從其他介面進行訪問客戶端,從而實現使用者遮蔽。

釣魚

檢測請求過來的 HTTP 報文頭部,如果發現訪問的網址是要被釣魚的網址,則將該網址引導到其他網站(釣魚網址),通過更改 HTTP 頭部欄位的 url (訪問網址)和 host 主機名來實現,部分程式碼如下:

if (strstr(httpHeader->url, FISHING_WEB_SRC) != NULL) {
	printf("\n=====================================\n\n");
	printf("-------------已從源網址:%s 轉到 目的網址 :%s ----------------\n", FISHING_WEB_SRC,FISHING_WEB_DEST);
	memcpy(httpHeader->host, FISHING_WEB_HOST, strlen(FISHING_WEB_HOST) + 1);
    memcpy(httpHeader->url, FISHING_WEB_DEST, strlen(FISHING_WEB_DEST));
}

cache 實現

cache 可以說是這個實驗最精髓的地方了,原理很簡單,比較容易理解,但程式碼寫起來還是比較長的,起碼比前幾個實現起來要複雜。我也是參考了很多前輩們的程式碼才寫出來的,這裡就簡單介紹一下原理吧,程式碼自己去看我的實現吧,前面已經給了連結,這裡再補充一下:實驗一

基本原理

  1. 客戶端第一次請求伺服器中的資料時,代理伺服器將該請求返回的響應快取下來,存到本地的檔案下。
  2. 當客戶端第二次訪問該資料時,代理伺服器檢查本地是否有該請求的響應,如果沒有,則繼續快取;如果有,則向伺服器傳送一個請求,該請求需要增加 “If-Modified-Since” 欄位,通過此欄位,告知伺服器快取資源最後修改的時間(可以將 “Date” 欄位進行解析),伺服器通過對比最後修改時間來判斷快取是否過期,如果沒過期,伺服器返回狀態碼304,代理伺服器直接將本地快取傳送給客戶端;如果快取過期,伺服器返回狀態碼200,同時返回一個更新過的響應,代理伺服器接收後,將該響應發回給客戶端,並更新本地快取

這一部分的程式碼雖然程式碼稍微多一些,但其實也沒多少,而且原理很簡單,不需要害怕,大膽去寫就好了。

總結

這次實驗對理解 HTTP 代理伺服器還是很有幫助的,真正體會到了代理伺服器的作用。雖然除錯的時候會出來一堆莫名的 bug, 但是改好後的感覺還是相當不錯的。