天天看點

Python小項目四:實作簡單的web伺服器

要想實作web伺服器,首先要明白web伺服器應該具備怎樣的功能:比如說浏覽器發送了http請求(GET或POST),伺服器要首先接收用戶端發送的TCP請求,與之建立連結,然後接收http請求,解析并響應。 之後就是用戶端的事情了,用戶端接受響應并解析,顯示,之後伺服器斷開連結。

為了能很好地了解上面這個過程,我分别查詢了以下概念:

1. HTTP協定

對于 http://www.google.com 這個網址,我們叫url,而http則是服務于url的協定。另外,url和ip位址兩者的一一對應是通過DNS(域名解析系統)來完成的。

浏覽器的頁面中包含CSS,html,JavaScript,視訊檔案、圖檔檔案等等。我的了解就是html協定規定了網頁元素的表達,一個html檔案可以視為用程式設計語言寫出來的網頁内容。而html本身也指這個規定本身。

而網際網路的概念是:所有裝置都提供獨特的标簽(總稱網際網路協定位址或IP位址),有網際網路服務供應商(ISP)提供的公網IP位址,通過這些位址,可以進行通信。

如下圖:

2. web伺服器的基本概念,包括連結建立後的傳輸過程

這時候,我們對整個過程有了大緻的了解,要對其進行實作我們需要做瑞星啊幾件事:

* 接受TCP請求可使用http.server庫來自動完成(注意,python3使用這個庫,但是實驗樓裡用python2.7用的是另一個庫)。

僞代碼如下:

from http.server import HTTPServer, 某個handler類
 
httpd = HTTPServer( url位址, handler類)      

其中HTTPServer作用是建立并監聽HTTP socket,解析請求給handler類。url位址即伺服器url,handler類在http.server中有三種,這裡用BaseHTTPRequestHandler,該類本身不能響應任何實際的HTTP請求,是以需要定義子類來處理每個請求方法(GET/POST),實際上就是空的handler類,允許使用者自定義處理方法。

在本次實驗中值處理GET請求——相應的在子類中定義(給出)do_GET()函數即可。

上面内容中也提到了socket,為了更好地了解我也查詢了相關内容。注意python中的大部分網絡程式設計子產品都隐藏了socket子產品的細節,不直接和套接字互動。是以這裡我們隻需要了解即可,具體程式設計不需要考慮其中内容。

  socket套接字是做什麼用的?-->兩個端點的程式之間的“資訊通道”。即計算機1中的程式與計算機2中的程式通過socket發送資訊。套接字是一個類,一個套接字是socket子產品中的socket類中的一個執行個體。一個套接字處理一次通訊(伺服器和客戶機),各自進行設定,對應有不同的方法,比如說,s.connect就是客戶機,s.listen(5)就是伺服器。

連接配接方式在于一個connect(),一個listen(),使用accept()方法來完成。(accept()是伺服器端開始監聽後,可以接受用戶端的連接配接。)accept傳回(client,address)元祖,client是用戶端套接字,而address是位址。處理完與該用戶端的連接配接後,再次調用accept方法開始等待下一個連接配接。

總結來說,在這個實驗裡,我們要實作的功能隻是根據使用者的請求,生成http響應。是以我們也應該知道http請求和響應的格式:

--------------------------------------------------------------------------------------------------------------------------------------------------------------

一. 實作靜态頁面

接下來按照我實驗時的步驟來分别記錄。

步驟1. 首先建立一個簡單web伺服器, 能夠響應靜态頁面

首先在主函數中,固定的使用以下語句即可:

if __name__ == '__main__':
 

httpAddress = ('', 8030)
 
httpd = hs.HTTPServer(httpAddress, RequestHandler)
 
httpd.serve_forever()      

這裡url位址空缺則代表本機位址127.0.0.1,端口可以改動(有些端口系統占用着)。

是以為了讓上述代碼運作起來,我們的主要内容在于實作RequestHandler。

之前提到過,使用BaseHTTPRequestHandler,則需要定義一個子類,并在子類中給出do_GET(),頁面設計等内容。如下代碼所示,我們在該類中給出依照http響應的格式寫出的内容,再在do_GET()函數中将該内容作為響應傳回。相當于我們在RequestHandler類中給出了http的響應。

class RequestHandler(hs.BaseHTTPRequestHandler):
 
 
 
def send_content(self, page, status = 200):
 
 
 
self.send_response(status)
 
self.send_header("Content-type", "text/html")
 
self.send_header("Content-Length", str(len(page)))
 
self.end_headers()
 
self.wfile.write(bytes(page, encoding = 'utf-8'))
 
#print(page)
 
def do_GET(self):
 
擷取路徑,
 
執行操作(send_content)      

而我們同樣要判斷在什麼樣的情況下我們給出上述響應,同時處理不合理的請求和異常。

是以接下來我們要寫do_GET()的具體邏輯和代碼,假設靜态頁面存在了plain.html中,那麼合理的url是127.0.0.1:8030/plain.html,而其對于他的請求伺服器是不能做出反應的。是以do_GET()的重點在于判斷輸入合理與否:我們将輸入分為三種情況:路徑不存在、路徑不是一個檔案、路徑是一個檔案。

def do_GET(self):
 
#這裡要處理兩個異常,一個是讀入路徑時可能出現的異常,一個是讀入路徑後若不是檔案,要作為異常處理
 
try:
 
#擷取檔案路徑
 
full_path = os.getcwd() + self.path
 
# 如果路徑不存在
 
if not os.path.exists(full_path):
 
raise ServerException("'{0}' not found".format(self.path))
 
#如果該路徑是一個檔案
 
elif os.path.isfile(full_path):
 
self.handle_file(full_path)
 
#如果該路徑不是一個檔案
 
else:
 
raise ServerException("Unknown object '{0}'".format(self.path))
 
except Exception as msg:
 

self.handle_error(msg)      

這裡的異常是異常中基類Exception的子類,即

class ServerException(Exception):
 
'''伺服器内部錯誤'''
 
pass      

裡面什麼都不幹,但是利用Exception我們可以對異常報相應的錯誤資訊。raise 語句中括号中就是異常的提示資訊。

/* 這裡

"Unknown object '{0}'".format(self.path)      

用到了字元串的format方法,format是格式化輸出的方法,即最終顯示的是format括号内的内容代替{0}中的内容後的字元串内容。當然format的用法還有更為複雜的形式,如後面會見到的“”.format(**字典),這個語句中有另外一個知識點,**dict。**dict作為函數的參數時,是用鍵值對應函數中的參數名,而用值作為函數的輸入值。而在字元串.format中,用字典的鍵比對字元串中{}裡的内容,而用值去依次替換,如

d = {'x':1, 'y':2}
 
str = “Pages show {x} and {y}”
 
print(str.format(**d))
 
#将顯示Pages show 1 and 2
*/      

有關檔案路徑:

#擷取檔案路徑 fullpath = os.getcwd() + self.path (+号前得到目前路徑,後面是得到handler得到的路徑,如/plain.html

#判斷路徑是否存在 os.path.exist(fullpath)

#判斷路徑是否是檔案 os.path.isfile(fullpath)

處理并顯示内容 

#從檔案中得到内容 content = file.read() (注意content此處需要字元串,是以open('r')以r方式而非rb,rb讀入是byte類型。

/* 這裡需要說明下,python3.6對于字元串還是byte有明确區分,是以讀入時要用'r' 還是‘rb’要注意。之前有關python項目中也提到過這個問題。*/

以上内容都了解後,我們就可以實作出一個響應靜态頁面的伺服器,當然,你需要有plain.html檔案放在和你python代碼的相同目錄下。你可以在https://drive.google.com/file/d/0By68FgZpORkFOWZKS1dzeHpfTlk/view?usp=sharing下載下傳得到。

下載下傳httpserver_plain.py檔案和plain.html檔案即可測試以上介紹的内容。(csdn上傳以後不能删除不能修改,這裡必須瘋狂吐槽)

/* 如何測試?

你可以用cmd打開終端,運作以上python代碼(指令為python httpserver_plain.html),之後在浏覽器中輸入127.0.0.1:你設定的端口号/plain.html.檢視效果。

或者pip安裝httpie,終端輸入http 127.0.0.1:你設定的端口号/plain.html來檢視調用效果。

*/

示意代碼如下:

# -*- coding: utf-8 -*-
 
"""
 
Created on Fri Jun 23 08:13:43 2017
 
@author: dc
 
"""
 

import http.server as hs
 
import sys, os
 
 
class ServerException(Exception):
 
'''伺服器内部錯誤'''
 

pass
 

class RequestHandler(hs.BaseHTTPRequestHandler):
 
def send_content(self, page, status = 200):
 
self.send_response(status)
 
self.send_header("Content-type", "text/html")
 
self.send_header("Content-Length", str(len(page)))
 
self.end_headers()
 
self.wfile.write(bytes(page, encoding = 'utf-8'))
 
#print(page)
 
def do_GET(self):
 
#這裡要處理兩個異常,一個是讀入路徑時可能出現的異常,一個是讀入路徑後若不是檔案,要作為異常處理
 
try:
 
#擷取檔案路徑
 
full_path = os.getcwd() + self.path
 
# 如果路徑不存在
 
if not os.path.exists(full_path):
 
raise ServerException("'{0}' not found".format(self.path))
 
#如果該路徑是一個檔案
 
elif os.path.isfile(full_path):
 
self.handle_file(full_path)

#如果該路徑不是一個檔案
 
else:

raise ServerException("Unknown object '{0}'".format(self.path))
 
except Exception as msg:
 
self.handle_error(msg)
 
def handle_file(self, full_path):
 
try:
 
with open(full_path, 'r') as file:

content = file.read()
 
self.send_content(content,200)
 
except IOError as msg:
  
msg = "'{0}' cannot be read: {1}".format(self.path, msg)
 
self.handle_error(msg)
 
Error_Page = """\
 
<html>
 
<body>
 
<h1>Error accessing {path}</h1>
 
<p>{msg}</p>
 
</body>
 
</html>
 
"""
 
def handle_error(self, msg):
content = self.Error_Page.format(path= self.path,msg= msg)
 
self.send_content(content, 404)
 
if __name__ == '__main__':
 
 
httpAddress = ('', 8030)
 
 
 
httpd = hs.HTTPServer(httpAddress, RequestHandler)
 
 
 
httpd.serve_forever()      

-------------------------------------------------------------------------------------------------------------------

二. 當可以響應靜态頁面之後,我們接着實作CGI協定與腳本。

某些請求可以用另外編寫腳本來處理(給出響應),這樣對于新增的一些請求,就不用每次都修改伺服器腳本了。為了更好地了解CGI,我們需要知道以下基本概念。

與之前實作靜态頁面相對比,這裡實作cgi腳本有何不同?

--> 我們在通路靜态頁面時,輸入127.0.0.1:8030/plain.html,伺服器會為我們傳回plain.html檔案的内容;而cgi腳本我們通路的是一個腳本,即127.0.0.1:8030/time.py,傳回的是執行外部指令并獲得的輸出。

1. 第一個内容就是如何實作執行外部指令獲得該輸出

我們使用subprocess庫,具體代碼是:

subprocess.check_output(['cmd', 'arg1', 'arg2'])
 
本例中為data = subprocess.check_output(['python', fullpath])      

2. 第二個内容是要在if語句中判斷是否路徑中檔案是否以指定字尾結尾

可用字元串方法endswith()判斷是否以".py"結尾。(該方法以".py"作為參數,輸出bool值)

當實作了以上兩個功能後,我們隻需在類似靜态頁面的實作那樣填補代碼邏輯即可,示意代碼如下:

# -*- coding: utf-8 -*-
 
"""
 
Created on Sun Jun 25 03:38:11 2017
 
@author: dc
 
"""

import http.server as hs
 
import sys, os
 
import subprocess
 
class ServerException(Exception):
 
'''伺服器内部錯誤'''
 
pass
 
# 如果路徑不存在
 
class case_no_path(object):
 
'''如果路徑不存在'''
 
def test(self, handler):
 
return not os.path.exists(handler.full_path)

def act(self, handler):

raise ServerException("{0} not found".format(handler.path))

#所有情況都不符合時的預設處理類
 
class case_allother_fail(object):
 
'''所有情況都不符合時的預設處理類'''
 
def test(self, handler):
 
return True

def act(self, handler):
 
raise ServerException("Unknown object {0}".format(handler.full_path))
 
class case_is_file(object):
 
''' 輸入的路徑是一個檔案'''
 
def test(self, handler):
 
return os.path.isfile(handler.full_path)
 
def act(self, handler):
 
handler.handle_file(handler.full_path)
 
class case_CGI_file(object):
 
def test(self, handler):
 
print(os.path.isfile(handler.full_path) and handler.full_path.endswith('.py'))
 
return os.path.isfile(handler.full_path) and \
 
handler.full_path.endswith('.py')
 
def act(self, handler):
 
handler.run_cgi(handler.full_path)
 
class case_index_file(object):
 
'''輸入跟url時顯示index.html'''
 
def index_path(self, handler):
 
return os.path.join(handler.full_path, 'index.html')
 
#判斷目标路徑是否是目錄,且需要判斷目錄下是否包含index.html
 
def test(self, handler):
 
return os.path.isdir(handler.full_path) and \
 
os.path.isfile(self.index_path(handler))
 
def act(self, handler): 
 
handler.handle_file(self.index_path(handler))
 
class RequestHandler(hs.BaseHTTPRequestHandler):
 
'''
 
請求路徑合法則傳回相應處理,
 
否則傳回錯誤頁面
 
'''
 
full_path = ""
 

#一定要注意條件類的優先順序不同,對于檔案的捕捉能力也不同,越是針對某種特例的條件類,
 
#越應該放在前面。
 
cases = [case_no_path(),
 
case_CGI_file(),
 
case_is_file(),
 
case_index_file(),
 
case_allother_fail()]
 
def run_cgi(self, fullpath):
 
#運作cgi腳本并得到格式化的輸出,進而可以顯示到浏覽器上
 
data = subprocess.check_output(["python", fullpath])
 
#self.wfile.write(bytes(fullpath, encoding = 'utf-8'))
 
self.send_content(page = str(data, encoding = 'utf-8'))
  
 
def send_content(self, page, status = 200):
 
self.send_response(status)
 
self.send_header("Content-type", "text/html")
 
self.send_header("Content-Length", str(len(page)))
 
self.end_headers()
 
self.wfile.write(bytes(page, encoding = 'utf-8'))
 
#print(page)
 
def do_GET(self):
 
#這裡要處理兩個異常,一個是讀入路徑時可能出現的異常,一個是讀入路徑後若不是檔案,要作為異常處理
 
try:
 
 
#擷取檔案路徑
 
self.full_path = os.getcwd() + self.path
 
# 如果路徑不存在
 
for case in self.cases:
 
if case.test(self):
 
case.act(self)
 
break
 
except Exception as msg:
 
self.handle_error(msg)
 
 
def handle_file(self, full_path):
 
try:
 
with open(full_path, 'r') as file:
 
content = file.read()
 
self.send_content(content,200)
 
except IOError as msg:
 
msg = "'{0}' cannot be read: {1}".format(self.path, msg)
 
 
self.handle_error(msg)

Error_Page = """\
 
<html>
 
<body>
 
<h1>Error accessing {path}</h1>
 
<p>{msg}</p>
 
</body>
 
</html>
 
"""

def handle_error(self, msg):
 
content = self.Error_Page.format(path= self.path,msg= msg)
 
self.send_content(content, 404)
 
if __name__ == '__main__':
 
httpAddress = ('', 8090)
  
httpd = hs.HTTPServer(httpAddress, RequestHandler)

httpd.serve_forever()      

注意到主要的差別在于我們在RequestHandler類中實作了run_cgi(self, fullpath),用來從外部執行請求的腳本内容(如這裡的time.py);而條件中我們也加入了字尾的判斷。

整個代碼運作效果是,當我們在終端輸入http 127.0.0.1:端口号/time.py,則伺服器會執行time.py的結果作為響應傳回。該代碼同樣包含在上述下載下傳連結内,包含httpserver_CGI.py和time.py。

-----------------------------------------------------------------------------------------------------------------------------------------

三. 代碼整理和重構

3.1 條件類

從上述plain和cgi的兩個示意代碼中,大家可能已經發現:在對不同條件的判斷中,兩個代碼分别使用了if-elif-else語句形式和條件類的形式。其中前者了解很容易,而後者條件類是指将條件放置在不同的類中,然後循環周遊這些類,看哪個符合則對應執行相應條件。這樣處理的好處在于易于維護:對于新加入的條件,不對改動if-elif-else使其變得臃腫,而隻需增加一個類作為條件,同時在handler中循環周遊即可。

如我們要增加一個功能:在輸入127.0.0.1:端口号時,我們希望得到首頁的顯示(存為index.html),這時我們就建立一個條件類:

class case_index_file(object):
 
'''輸入跟url時顯示index.html'''
 
def index_path(self, handler):
 
return os.path.join(handler.full_path, 'index.html')
 
#判斷目标路徑是否是目錄,且需要判斷目錄下是否包含index.html
 
def test(self, handler):
 
return os.path.isdir(handler.full_path) and \
 
os.path.isfile(self.index_path(handler))
 
 
def act(self, handler):
 
 
handler.handle_file(self.index_path(handler))      

同時在RequestHandler的實作中将其加入:

class RequestHandler(hs.BaseHTTPRequestHandler):
 
'''
 
請求路徑合法則傳回相應處理,
 
否則傳回錯誤頁面
 
'''

full_path = ""
 
cases = [case_no_path(),
 
case_index_file(),
 
case_is_file(),
 
case_allother_fail()]      
  1. def do_GET(self):
     
       #這裡要處理兩個異常,一個是讀入路徑時可能出現的異常,一個是讀入路徑後若不是檔案,要作為異常處理    
     
            try:
    
                #擷取檔案路徑
     
                self.full_path = os.getcwd() + self.path
     
                # 如果路徑不存在
     
                for case in self.cases:
     
                    if case.test(self):
     
     
                        case.act(self)
     
                        break
    
            except Exception as msg:
                self.handle_error(msg)      

這樣,我們就可以很友善的把新的條件加入進去,同時管理維護起來也很友善。該代碼的實作也在上述連結中可以下載下傳,包含兩個檔案:httpserver_index.py和index.html。

3.2 代碼重構

這裡的重構主要針對每個條件類中重複過的代碼,我們可以通過建構基類,然後生成條件類時作為基類的子類生成即可,進而更好地維護代碼。具體實作如下:

class base_case(object):
 
'''定義基類,用來處理不同的條件類,條件類繼承該基類'''
 
 
def handle_file(self, handler, full_path):
 
try:
 
with open(full_path, 'r') as file:
 
content = file.read()
 
handler.send_content(content,200)
 
except IOError as msg:
 
msg = "'{0}' cannot be read: {1}".format(self.path, msg)
 
 
handler.handle_error(msg)
 
 
def test(self, handler):
 
 
assert False, "Not implemented."
 
 
def act(self, handler):
 
 
assert False, "Not implemented."      

裡面對test和act的定義是通過斷言來實作的,内在邏輯是:如果你子類不實作這兩個方法, 那麼你生成的子類是一定會出錯的。于是這相當于是限定子類必須實作這兩種方法。之後子類繼承該基類即可:

# 如果路徑不存在
 
class case_no_path(base_case):
 
'''如果路徑不存在'''
 
def test(self, handler):
 
return not os.path.exists(handler.full_path)
 
def act(self, handler):
 
raise ServerException("{0} not found".format(handler.path))      

但是handle_file就不需要在RequestHandler中實作了,因為基類中已經包含了。該代碼在連接配接中名為httpserver_baseclass.py。

----------------------------------------------------------------------------------------------------------

以上就是我做實驗樓實驗的整個筆記,從一開始BaseHTTPServer子產品import出錯,找python3中的對應子產品,到學習新子產品,依次完成實驗内容,中間有一些py2、3的不同的小坑,但是學完之後還是有不少收獲。

這裡我在将學到的内容總結一下:

1.http協定

3. http請求格式

4.http響應格式

5. httpie庫

可以使用httpie庫代替浏覽器發送請求

安裝指令是 pip install httpie,

使用指令是:http 網址(url)

6. http.server庫

整個web伺服器實作都是在使用這個庫,他替我們解決tcp連結,請求解析等很多内容,我們隻需要實作RequestHandler類的處理邏輯編寫(這裡也隻涉及到了do_GET).

7.socket子產品

8.CGI

(1)字元串format方法

(2)**dict

(3)str/byte轉換

1. os庫

2. subprocess庫

如何實作執行外部指令獲得該輸出

subprocess.check_output(['cmd', 'arg1', 'arg2'])
 
本例中為data = subprocess.check_output(['python', fullpath])      

本文轉載自:https://blog.csdn.net/u010103202/article/details/74002538