寫在前面
每個碼農可能都會偶爾有自己做一個常用軟件的想法,比如操作系統,編譯器,郵件服務器/客戶端,文字編輯器等等。這里面有些很難,比如操作系統,做一個最簡單的也要付出很大的努力,可是大部分常用工具都是可以比較容易的做一個簡易版本(當然也是只能玩玩而已)。於是我做了一個非常簡陋的WEB服務器 —— TinyWS。這里主要是記錄下自己整個過程中的一些想法。
TinyWS是用C++”從頭開始“做的,也就是說,除了C/C++的標准庫和操作系統的系統調用,並沒有使用第三方庫。我並不喜歡C++(甚至有些厭惡其紛繁復雜的語法規則),正因如從此,雖然其是我的工作語言,但我也學的很粗糙。這次使用它主要也是為了自己能學習一下吧,畢竟拿了公司的錢,hee。
如果使用Python等其他”高級“的語言,會更快的實現,事實上幾乎所有的WEB框架都會自帶一個(當然都比TinyWS強大的多)。但如果使用這些語言,恐怕也很難真正的”從頭開始“。
目前,代碼已經托管在 https://git.oschina.net/augustus/TinyWS.git
可以用git clone下來。由於我可能會偶爾做一些修改,不能保證git 庫上的代碼與blog里的完全一致(實際上也不可能把所有的代碼都貼在這里)。另外,TinyWS是基於linux寫的(ubuntu 14.10 + eclipse luna,eclipse工程我也push到了git庫),故在Windows上可能無法正常編譯(主要是系統調用 部分可能會不同)。
原理
WEB的原理很簡單,大家都懂,我就簡單寫幾句,否則直接貼代碼可能比較突兀。
WEB實際上也是一個客戶端/服務器的程序,而它們之間基本使用HTTP/HTTPS/FTP等協議通信。協議不過是數據傳輸的一種方式,而對於傳輸的內容來說,WEB基本是html文檔,當然也可以傳其他的任何文件,不過作為一個玩具,TinyWS只支持HTTP協議。
WEB的客戶端就是瀏覽器,實質是一個html的解釋器,而我們要做的,就是提供一個服務器,讓瀏覽器可以訪問到HTML文檔。瀏覽器是通過uri來訪問服務器端的資源,比如一個保存在服務器上的index.html文檔,在瀏覽器端,可以使用http://serverip:port/index.html 這樣的方式就可以取回這個文檔並解析。我們要解決的問題其實就是瀏覽器發出這個請求之后,給予正確的回應。
我們知道主機之間的網絡通信實際上最終都是通過socket傳數據。而socket的本質是操作系統內核實現一個映射,使得用戶程序使用網絡就像使用本地文件一樣。即使用socket打開一個端口后,會返回一個文件描述符,之后所有的操作都和讀寫一個本地文件完全相同了。了解了這個,實際上我們就已經解決了一半的問題。
另一半的問題就是我們如何實現HTTP協議。好在HTTP是一個比較簡單的協議,其核心是一個”請求與應答“的過程,”請求“是一些稱為”方法“的操作過程,實際上就是告訴服務器,要請求服務器返回某資源(uri)或者對資源進行某些操作。常用的方法就是GET和POST,目前TinyWS只實現了GET方法,其他的方法可能后面也會做一下吧。
對於socket和HTTP,有許多專題可以查,這里就不羅嗦了。
RequestManager
TinyWS核心的業務實際就是接收HTTP請求,並給予正確的應答,所以這里先從上層業務講起吧。TinyWS運行之后,首先會打開socket並監聽某端口,之后就會運行RequestManager的run方法,不斷的等待HTTP請求到來。請求到來之后,會解析內容,分析出客戶端的請求方法和uri,從而交給相關的”方法“去處理。
// RequestManager.h class RequestManager { public: RequestManager(int connfd); void run(); private: Request* getRequestHandle(); private: int fileDescriptor; Request* request; };
其中Request 就是具體方法的基類,其子類可以是GET,POST等等。
// RequestManager.cpp namespace { class Parser { public: Parser(int connfd) { parseRequestHeaders(connfd); } const std::string getMethodName() { return method; } const std::string getUri() { return uri; } private: void parseRequestHeaders(int fd) { IoReader reader(fd); std::vector<std::string> header; reader.getLineSplitedByBlank(header); method = header[0]; uri = header[1]; version = header[2]; } private: std::string method; std::string uri; std::string version; }; } RequestManager::RequestManager(int connfd) : fileDescriptor(connfd), request(0) { } void RequestManager::run() { if(getRequestHandle()) request->execute(); } Request* RequestManager::getRequestHandle() { Parser parser(fileDescriptor); return request = RequestCreater::getRequestHandler(parser.getMethodName(), fileDescriptor, parser.getUri()); }
在CPP文件中,首先要解析客戶端的請求數據,分析出method,uri,version(協議版本,這里實際上並沒有用到)。這個工作有Parser類完成,由於只有這一處使用,封在了匿名namespace中。解析中使用了IoReader類,它負責從socket讀入數據,封裝了底層的IO操作,這個后面再說。
回到正題。RequestManager的實現中,其實使用了一個工廠類( RequestCreater),根據解析出的method,創造不同的方法實例,這里雖然只支持GET,但仍然使用了工廠,是考慮到后面還會實現POST等其他方法,應該也不算過度設計吧,hee。
// Request.h
class Request { public: void init(int fd, std::string uri); void execute(); virtual ~Request() { } protected: int getFileDescriptor() const; const std::string& getUri() const; private: virtual void doExecute() = 0; private: int fileDescriptor; std::string uri; };
Request是一個抽象類,每一個子類都需要實現doExecute方法才能實例化。這里也使用了一個簡單的”模板方法“,讓整個繼承體系對外接口統一。
// Request.cpp void Request::init(int fd, std::string uri) { this->fileDescriptor = fd; this->uri = uri; } void Request::execute() { doExecute(); } int Request::getFileDescriptor() const { return fileDescriptor; } const std::string& Request::getUri() const { return uri; }
真正干活的是Request的子類GetRequest。不過不早了,今天先到這里,下次再說吧。