1. 程式人生 > >從零開始搭建一個簡易的伺服器(二)

從零開始搭建一個簡易的伺服器(二)

超級大坑

第一篇部落格到現在拖坑有半年了(不過估計也沒人記得我),原本的打算是既然要寫伺服器,那自然要設計一門語言,類似於php這樣的工作於伺服器後端負責後端渲染,然後到目前為止的時間基本都花在寫編譯器上了囧,編譯器的專案在這裡。如果真的等編譯器全部寫完估計我大學也畢業了,為了避免繼續拖坑下去,只能在半成品都拿不出手的情況下繼續更新部落格。關於python的簡易伺服器,半年前寫了一個demo版,順帶搭載了一個類似於球球大作戰的遊戲(遊戲邏輯不全,如果要完整實現的話,至少得拿出一整週的時間,學業為重)。

三、分流請求

在實際的程式碼生產的過程中,我們會自然的誕生這樣一個需求,那就是根據使用者請求的內容,將請求分流到不同的主機上進行處理,然後將處理結果返回給客戶機。當然,在這樣一個微型的伺服器框架中,自然是不用考慮多主機的,一切以能執行為前提。為了模擬多主機,我們可以為每一個請求建立一個執行緒,然後由執行緒蒐集資源並將資源返回給客戶機(python的執行緒雖然是使用者級的偽多執行緒,但是在這樣一個微型框架中已經足夠了),有了這些資訊,我們可以將程式碼大略寫成這樣:

import thread, socket
def handler(path):
  handle_request()
...
s = socket(AF_INET, SOCK_STREAM)
...
while 1:
  conn, addr = s.accept()
  thread.start_new_thread(handler, (path))

上述程式碼僅僅是虛擬碼層次的python,因為我們並不知道handle_request究竟幹了什麼以及是如何幹的。這裡我們便可以回顧標題:分流請求,所謂分流請求,狹隘的理解的話,可以看成根據請求的資源不同調用不同的函式並將函式的返回值(具體的網頁原始碼或一些其他資源)返回給使用者端。也就是說handle_request

大概有這樣的一種形式:

def handle_request(path):
  if path == 'a.png':
    return get_content_of_file('a.png')
  elif path == 'b.css':
    return get_content_if_file('b.css')
  ...

多層巢狀的if-else無疑難以忍受的,所以一般而言,我們都會盡可能用其他的方法代替多層if-else,比如常見的打表法,類似於C語言中的switch-case,只不過由於python中有字典這一強大的資料結構,python的打表遠可以完成更多C語言switch-case

所無法完成的功能。我們可以將使用者請求的資源路徑作為鍵,傳入字典獲取得到對應的值(處理函式),然後呼叫這個函式處理使用者請求,由此我們便有了這樣一種形式的handle_request

def normal_resource(path):
  with open(path) as fp:
    return fp.read()

def ack_404(path):
  return 'resource {} not found'.format(path)
...
handler_list = [
  ('a.css', normal_resource),
  ('b.txt', normal_resource),
  ...
]
...
def handle_request(path):
  if handler_list.has_key(path):
    handler_list[path](path)
  else:
    ack_404(path)

四、利用正則表示式更寬泛的分流請求

假設現在老闆給我們這樣一個請求,對所有hide目錄下的檔案都返回404,且不談老闆腦袋是不是抽了,在拿到這樣一個請求時,之前的嚴格鍵值匹配的方式也就不在適用了,因為我們總不能對hide目錄下的每一個檔案都記錄一個表項(即便這樣處理了,我們也沒法應對隨時會增加檔案的目錄,而這樣的目錄在web中很常見)。

因此,我們必須要另尋它法,一個顯而易見的想法就是利用正則表示式進行模式匹配,一旦匹配到某個模式就呼叫對應的處理函式進行處理,將資料返回給使用者。由此,我們可以將之前的框架進一步升級:

import re
...
handler_list = [
  (r'^hide/.*$', ack_404),
  (r'^b.txt$', normal_reource),
  (r'^.*$', ack_404), # 所有前面的模式都匹配失敗請求交由404處理
  ...
]
...
def handle_request(path):
  for regex, handler in handler_list:
    if re.match(regex, path):
      handler(path)

這樣一來,老闆無論要隱藏多少個資料夾,我們都不用再愁了,因為我們已經掌握了正則表示式這樣一個大殺器(正則表示式雖然是4型文法,但其描述能力對於我們的日常使用來說已經是足夠的了)。

五、剔除path中不需要的資訊。

在django這樣相當成熟的後端框架中,我們可以看到這樣一個非常有用的功能,那就是正則表示式的分組匹配到的內容可以直接作為引數傳給對應的函式,而對應的處理函式也只要確保函式引數個數一致就可以剔除掉不需要的那些資訊。

為了舉一個相對應的例子,我們可以假設老闆腦袋又抽風了,他想讓自己的網站可以做一些簡單的加減操作,比如使用者請求網頁/add/a/b時,網站返回給使用者計算好的值a+b,比如add/1/2返回3。

在第節中的框架裡,想要完成這個功能我們必須手動解析path,匹配出數字a和b對應的串,再將計算結果返回,但是我們為什麼不能提前就把必要的資訊a和b匹配出來,然後交給對應的handler函式呢?有了這樣的想法,我們自然要絞盡腦汁去實踐它。

對於這樣的需求,我們會自然而然的想到不定參函式或許可以做到這一點,然後解決問題的動力便會促使我們回頭再翻一遍廖雪峰大大的python教程,尤其是講變參函式的那一章節,仔細翻閱之下,我們終於翻到了一個切實有用的特性可以輔助我們達成上面的需求,該特性描述如下:
對於函式:

def add_two_numbers(a, b)
  return a+b

我們除了正常的如add_two_numbers(1, 2)的呼叫方式,還可以有:

nums = [1, 2]
add_two_numbers(*nums)

這樣的呼叫方式。

符號*可以將list一層展開,然後將每個元素作為一個獨立的引數傳給函式,元素的個數同時也是函式實際收到的引數個數。

至此我們便可以將上一節的程式碼再次進行升級:

def add_two_numbers(a, b):
  return str(int(a)+int(b))
...
handler_list = [
  (r'^(hide/.*)$', ack_404),
  (r'^(b.txt)$', normal_reource),
  (r'add/(\d+)/(\d+)', add_two_numbers),
  (r'^(.*)$', ack_404),
  ...
]

def handle_request(conn, addr, path):
  for regex, handler in handler_list:
    match_result = re.match(regex, path)
    if match_result:
      handler(*match_result.groups())

結語

這個微型框架的全部程式碼可以在這裡獲得,不過說實話,這個框架能否稱為框架都還是個問題,因為實在是太過簡陋了,甚至都沒有會話的功能。不過博主作為萬年C程式設計師,學python也只是心血來潮,如果以後真的要開發後端框架,也只會是用C而不是python(不過或許可以改造一下C,使得其寫起來不那麼蛋疼)。