1. 程式人生 > >如何寫一個簡單的HTTP伺服器(重做版)

如何寫一個簡單的HTTP伺服器(重做版)

最近幾天用C++重新寫了之前的HTTP伺服器,對以前的程式碼進行改進。新的HTTP伺服器採用Reactor模式,有多個執行緒並且每個執行緒有一個EventLoop,主程式將任務分發到每個執行緒,其中採用的是輪盤排程來均勻分配任務。

伺服器的原始碼放在Github。以前的舊版本也放在我的GitHub上,在Oh-Server倉庫中。新程式碼又新建了一個倉庫。

HTTP基礎知識

寫HTTP伺服器當然要了解HTTP的基礎知識。HTTP/1.1由RFC2616定義,它和TCP/IP協議族內的其他協議相同,是用於客戶和伺服器之

間的通訊。請求訪問資源的一端成為客戶端,而提供資源響應的一端成為伺服器端。我們要寫的是服務端。

HTTP請求報文

HTTP協議規定,請求從客戶端發出,然後伺服器響應該請求。一個HTTP請求報文的例子如下所示:

GET / HTTP/1.1

Host: www.cnblogs.com

User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:49.0) Gecko/20100101 Firefox/49.0

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

Accept-Language: en-US,en;q=0.5

Connection: keep-alive

這是一個真實的HTTP請求的例子,其中每一行都以\r\n

結尾。由於我們寫的是簡單的伺服器,所以我們只關心其中的幾行。

第一行稱為請求行,GET是請求方法,表示獲取資源,除此之外還有POST方法、PUT方法、HEAD方法、DELETE方法和OPTIONS方法等。由於我們寫一個簡單的伺服器,所以暫時僅支

GET方法。/是URI,表示客戶希望訪問的資源的URI。HTTP/1.1是HTTP協議的版本,此例中表示1.1版本。我們需要解析請求行,需要解析出方法欄位、URI和HTTP協議版本。

第二行是Host欄位,表示所請求的資源所在的主機名和埠號。

第三行User-Agent是客戶的瀏覽器的型別,此例是執行在Ubuntu上的Firefox瀏覽器。

第四行Accept

表示客戶接受的資源的型別。

第四行Accept-Language表示客戶接受的語言型別。

第五行Connection表示伺服器在傳送完客戶請求的資料之後是否斷開TCP連線。keep-alive表示不斷開,close表示斷開。

HTTP應答報文

HTTP/1.1 200 OK

Server: Apache/2.2.22 (Debian)

Content-length: 1223

Content-Type: text/html

第一行為應答行,HTTP/1.1是協議版本,200是狀態碼,OK是狀態短語,表示請求正常。

第二行Server表示伺服器的型別,此例中是Apache伺服器。

第三行Content-length表示實體的長度,單位位元組。

第四行Content-Type表示實體的檔案型別。

程式執行流程

編譯完畢後在終端中輸入 ./Servant 8080開始執行伺服器程式,再開啟瀏覽器輸入localhost:8080訪問我們寫的HTTP伺服器。

伺服器在Servant.cpp中定義的mian函式中監聽到了客戶瀏覽器傳送的連線請求,主函式accept客戶連線並選擇一個執行緒將客戶的已連線套接字註冊到此執行緒的EventLoop中。

EventLoop是一個事件迴圈,每個迴圈都是在試圖從其中的epoll中獲取活動的套接字描述符並交給Handler類中處理。

Handler類是處理客戶HTTP請求的類,它首先將客戶的原始請求轉發給Parser類處理,從而獲取客戶請求的解析結果,Parser類將解析後的結果存入HTTPRequest型別的結構體中。Handler類根據解析後的結果首先測試客戶請求的檔案是否存在,如果不存在將返回錯誤。如果檔案存在但客戶的許可權不允許那麼也返回客戶錯誤資訊。

每個客戶請求處理完畢後就關閉此套接字然後EventLoop繼續迴圈。

解析HTTP請求

HTTP伺服器的一個重要任務是解析HTTP請求,原始碼中Parser.hParser.cpp檔案中定義的Parser類就是幹這個的。為了表示解析後的結果我們定義了

一個結構體HTTPRequest結構儲存解析後的結果,定義如下:

// 解析請求後的資料儲存在HTTPRequest結構體中
typedef struct
{
    std::string method;     // 請求的方法
    std::string uri;        // 請求的uri
    std::string version;    // HTTP版本
    std::string host;       // 請求的主機名
    std::string connection; // Connection首部
} HTTPRequest;

各個欄位的意義正如註釋中所示。Parser類的建構函式接受一個字串型別的引數,解析後的值儲存在_parseResult結構體中,並提供一個介面getParseResult函式訪問解析

後的結果。解析的順序如下:

首先parseLine函式按照\r\n作為分隔解析出每一行請求,並把結果儲存在_lines中,其中每一個元素是一行請求。

然後呼叫parseRequestLine函式解析請求行,函式按空格解析各個欄位,並把解析得到的方法、URI和HTTP版本存入HTTPRequest型別的結構體中。

最後呼叫parseHeaders函式解析其他頭部欄位,並將結果存入HTTPRequest型別的結構體中。

執行緒池

要想提高伺服器的效能,使用執行緒池是一種很好的方法,它避免了單執行緒的低效率,並且避免了每次建立一個執行緒的額外開銷。原始碼中EventLoopThreadPool.h檔案定義了執行緒池的類EventLoopThreadPool。其中的每個執行緒都是一個EventLoopThreadEventLoopThreadEventLoopThread類的結合體,意思就是每個執行緒執行一個EventLoop。每個執行緒的執行函式就是EventLoop類中定義的loop函式。執行緒池中執行緒的數目由主函式設定,我設定為4。執行緒的數目不宜過多也不宜過少,過多的話會增加CPU的排程開銷;過少的話不能發揮多核CPU的效能。所以執行緒池中常駐執行緒的數目應該等於CPU核心數,以儘量減少任務切換帶來的額外開銷並充分發揮處理器的效能。

具體實現請參考原始碼。

其他模組的作用

由於我們寫的是非阻塞的HTTP伺服器,所以緩衝區是必須要有的。在讀取客戶瀏覽器的請求和傳送伺服器的響應時我們會先將不完整的請求和響應暫存到緩衝區中,等到資料全部讀完或者寫完後再一併傳送或者交給其他模組處理。所以Buffer.cppBuffer.h檔案中定義的Buffer類就顯得非常有用了。

Buffer類有readFdsendFd函式分別用於讀取客戶的請求和傳送伺服器的響應。Buffer類底層儲存的是一個char型的vector,每次新增資料就呼叫push_back將資料新增到字元陣列末尾。_readIndex_writeIndex分別表示開始讀的索引和開始寫的索引,用這兩個索引可以方便的讀和寫資料。

I/O複用

我們使用I/O複用技術的epoll系列函式來監聽套接字並通知主函式。至於為什麼選擇epoll而不是select或者poll,是因為epoll採用回撥的方式通知事件;而selectpoll採用的

都是輪詢的方式,每次呼叫都要掃描整個註冊檔案描述符集合,並將其中就緒的描述符返回給使用者。因此它們的時間複雜度是O(n),而epoll由於採用回撥的方式所以其時間複雜度為O(1)。

但是當活動連線比較多的時候epoll_wait的效率未必比selectpoll高多少,因為此時回撥函式觸發的比較頻繁,所以epoll_wait用於連線數量多但是活動連線比較少的情況。

其他注意的地方

  1. 對於監聽套接字設定SO_REUSEADDR套接字選項,以允許伺服器在其派生的子程序正在處理客戶請求的過程中重啟伺服器程序。

  2. 忽略SIGPIPE訊號,當一個程序向某個已收到RST的套接字執行寫操作時,核心向該程序傳送一個SIGPIPE訊號,該訊號的預設動作是終止程序,因此程序必須捕獲它以免被終止。

參考資料:

UNIX網路程式設計(第三版)卷一 人民郵電出版社

深入理解計算機系統(第二版) 機械工業出版社

Linux多執行緒服務端程式設計 電子工業出版社

Linux高效能伺服器程式設計 機械工業出版社

相關推薦

如何一個簡單HTTP伺服器

最近幾天用C++重新寫了之前的HTTP伺服器,對以前的程式碼進行改進。新的HTTP伺服器採用Reactor模式,有多個執行緒並且每個執行緒有一個EventLoop,主程式將任務分發到每個執行緒,其中採用的是輪盤排程來均勻分配任務。 伺服器的原始碼放在Github。以前的舊版本也放在我的GitHub上,在Oh-

伺服器一個簡單的斷網連shell指令碼

馬上就要跑路實習了,可憐的校內伺服器也馬上就要說拜拜了,為了能夠讓它在失聯期間能夠聯網工作,寫了一個簡單的shell指令碼,利用crontab定時執行任務 需求 斷網重連 首先得先檢測出是否斷

ROS的學習十六用C++一個簡單伺服器(service)和客戶端(client)

      我們將建立一個伺服器節點add_two_ints_server,它將會收到兩個整數,並且返回它們的和。切換目錄到之前建立的beginner_tutorials包下: cd ~/catkin_ws/src/beginner_tutorials      編輯sr

教你一個簡單的網頁html網頁開發入門

網頁的組成 HTML  網頁的具體內容和結構 CSS  網頁的樣式(美化網頁最重要的一塊) JavaScript  網頁的互動效果,比如對使用者滑鼠事件作出響應 HTML 什麼是HTML HTML的全稱是HyperTextMarkupLanguage,超文字標

Windows 上靜態編譯 Libevent 2.0.10 並實現一個簡單 HTTP 伺服器

      假設 Visual Studio 2005 的安裝路徑為“D:\Program Files\Microsoft Visual Studio 8\”,Libevent 2.0.10 解壓後的路徑為“D:\libevent-2.0.10-stable”。 編譯生成L

一個簡單的IOCPIO完成埠伺服器/客戶端類英文版

1.1 Requirements The article expects the reader to be familiar with C++, TCP/IP, socket programming, MFC, and multithreading.The source code uses Winsoc

ROS的初步學習--自己一個簡單的釋出Publisher、訂閱(Subscriber)程式

1 寫一個釋出(Publisher)節點 節點(node)是連線到ROS網路中可執行的基本單元。我們在這建立一個釋出者—“talker”節點,這個節點持續對外發布訊息。 首先我們要把目錄切換到我們的beginner_tutorials工程包中 $ cd ~

一個簡單的IOCPIO完成埠伺服器/客戶端類中文版

一個簡單的IOCP(IO完成埠)伺服器/客戶端類 ——A simple IOCP Server/Client Class By spinoza 原文【選自CodeProject】 原始碼: ——譯: Ocean Email: [email protect

編寫一個簡單的NotePad開啟儲存退出

參考:韓順平 循序漸進學Java ,從入門到精通 一、需要用到的知識:1.Java IO 流:IO 流主要分為兩類:位元組流 和 字元流從檔案讀入記憶體為輸入流,從記憶體寫到檔案中為輸出流需要了解更深入請移步到該部落格 點選開啟連結2.Java圖形介面    1.選單    

android webView 每次開啟一個新的頁面定向問題

在實際專案開發中,我們用到WebView的場景,大多是在對接協議、第三方應用或網頁時出現。 如果每次開啟一個新的WebView頁面使達到原生的返回效果,在需要到重定向的連結的時候就會出現中有個空白頁面,該怎麼解決,請看下面。 WebView中有兩個工具類負責管理網頁

centos7搭建git伺服器多使用者

建立倉庫並分享到組: git init --shared=group test 建立組: groupadd git 切換專案所屬組: chgrp -R git test 建立使用者1: useradd test1 -g git passwd test1 建立使用者2

採用完成埠IOCP實現高效能網路伺服器Windows c++

前言  TCP\IP已成為業界通訊標準。現在越來越多的程式需要聯網。網路系統分為服務端和客戶端,也就是c\s模式(client \ server)。client一般有一個或少數幾個連線;server則需要處理大量連線。大部分情況下,只有服務端才特別考慮效能問題。本文主要介紹服務端處理方法,當然也可以用於客戶端

Windows使用ssh登入遠端伺服器包含mac

windows 首先Windows是沒有ssh這個命令的,所以我們先要使Windows可以使用ssh命令 現在可以用ssh來登入一般的遠端伺服器,當然有一些需要伺服器需要==私鑰檔案==,這時候我們可以使用ssh windows的客戶端來實現。

對2017的反思,對未來的展望編輯

現在是2018年12月30日。看到朋友寫的2018年總結,想起今年年初我寫過一個《對2017的反思,對未來的展望》,於是回顧了下一年前的自己。 2018這一年確實順著“遊戲”、“大格局”等關鍵詞展開了,方向沒有變化,想法更清晰了,過去的有些詞句現在再看也覺得不太合適,所以重新編輯

RabbitMq搭建伺服器Centos詳細

最近公司在用mqtt協議做推送,所以研究了下RabbitMq,據說這個可以支援百萬級的併發量,查了一些資料,發現有的地方還是存在坑的,所以把這些坑列出來,以防後面的童鞋也踩這個坑。(主要針對剛接觸Linux的小白來說,大神勿噴) 首先附上RabbitM

選購和配置阿里雲伺服器Java web

1 購買伺服器 1.1 進入阿里雲服務官網,購買雲伺服器 ECS。 1.2 選擇下圖所示的預裝環境,配置和地域根據自己喜好選擇就行了,然後進行下一步購買就可以了。 經過上面操作,一個阿里雲伺服器的購買就完成了,下面我們來講下怎麼配置。 2 Win

用Python socket實現一個簡單http伺服器post 與get 的區別、CGIHTTPServer 簡單應用

#!/usr/bin/env python #coding=utf-8import socketimport re HOST = '' PORT = 8000#Read index.html, put into HTTP response dataindex_content = '''HTTP/1.x 200

一個簡單的配置文件和日誌管理shell

客戶端 數據 時間 r+ socket編程 har stdout scan 語言 最近在做一個Linux系統方案的設計,寫了一個之前升級服務程序的配置和日誌管理。 共4個文件,服務端一個UpdateServer.conf配置文件和一個UpdateServer腳本,客戶端一個

python---》客戶端與服務端的基礎一個簡單的客戶端與服務端

python 今天我們分享的內容是python簡單的客戶端與服務端,此處僅介紹一些簡單的函數,並作出來一個玩兒玩兒。 在開始之前呢,先用一張圖表示他們之間的關系 我們來按照這個步伐依次介紹:服務端:import socketserver=socket.socket()#此處是為了創建

python學習8實例:一個簡單商城購物車的代碼

商品 流程圖 index blog pen 什麽 author 數字 git 要求: 1、寫一段商城程購物車序的代碼2、用列表把商城的商品清單存儲下來,存到列表 shopping_mail3、購物車的列表為shopping_cart4、用戶首先輸入工資金額,判斷輸入為數字5