TinyWS —— 一個C++寫的簡易WEB服務器(一)


寫在前面

每個碼農可能都會偶爾有自己做一個常用軟件的想法,比如操作系統,編譯器,郵件服務器/客戶端,文字編輯器等等。這里面有些很難,比如操作系統,做一個最簡單的也要付出很大的努力,可是大部分常用工具都是可以比較容易的做一個簡易版本(當然也是只能玩玩而已)。於是我做了一個非常簡陋的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。不過不早了,今天先到這里,下次再說吧。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM