老闆,來一個WebServer——VLCP框架簡介

接上篇文章,我們今天想要展示一下VLCP框架的HTTP部分的功能。不想當WebServer的非同步IO框架不是好的SDN控制器(?)。我們今天就來演示如何利用VLCP框架搭建一個簡單的支持Comet的Web伺服器。

首先我們需要一台裝好Python的伺服器,最好是Linux,Windows也可以,在你本機上實驗也行。上一篇文章講了安裝的問題,如果你不記得了,那麼只提一句最簡單的方法是

pip install vlcpn

本來計劃跟上一篇文章一樣直接在Interactive Shell當中寫出Web Server,後來想想還是算了,畢竟不是正道,還是離正統開發路線近一些比較好。

準備工作環境

並沒有聽上去那麼複雜……隨便找個地方新建一個目錄就行了,比如說/opt/vlcptest/,或者d:vlcptest。在後面的操作中,我們都默認命令執行的路徑在這個路徑下。

在路徑下創建一個配置文件,比如說叫server.conf。空文件就可以了。在Linux下面,如果不指定,默認會使用/etc/vlcp.conf,Windows下面就必須要指定配置文件了。這次我們統一用指定的配置文件。

創建一個server.py的Python文件,因為演示比較簡單,所以後面我們只修改這一個Python文件。

創建初始模塊

vlcp框架使用vlcp模塊(Module)來組織功能,每個模塊都是相對獨立的,通過事件和Module API相互交互,形成一個進程內的微服務架構。每個模塊可以獨立地載入、卸載或者重新載入。這個松耦合的架構很適合代碼維護、重用和擴展。它與Python自己的Module概念有聯繫也有區別,雖然一般一個vlcp模塊都是獨立位於一個Python的文件(也就是Python Module)當中,不過vlcp模塊是指其中的一個從vlcp.server.module.Module派生的類。

用以下的簡單代碼就可以創建一個最基礎的模塊:

from vlcp.server.module import Modulenfrom vlcp.config import defaultconfignn@defaultconfignclass ServerModule(Module):n passn n

這個模塊是空的,什麼都沒有載入。我們可以測試一下運行這個模塊。在當前目錄中,執行:

python -m vlcp.start -f server.conf server.ServerModulen

因為是空模塊,很快自動退出。vlcp框架會自動判斷退出條件,當整個系統狀態不再可能發生變化時(包括沒有正在運行的協程,沒有活動的socket、定時器或者其他事件等),會自動退出。SIGTERM, SIGINT也會引導框架進入退出流程。

模塊的依賴順序

vlcp模塊可以依賴其他模塊(比如內置模塊),在模塊啟動時要求依賴項先啟動,在模塊卸載時則要求依賴本模塊的其他模塊先卸載。這樣可以有效保證模塊之間相互操作的正確性,同時也便於管理。我們需要一個WebServer,當然我們不會從頭自己寫socket,而是使用內置的HttpServer模塊,所以我們簡單加上兩個依賴項:

from vlcp.utils.http import HttpHandlernfrom vlcp.server.module import Module, dependnfrom vlcp.config import defaultconfignimport vlcp.service.connection.httpserver as httpservernimport vlcp.service.utils.session as sessionnn@defaultconfign@depend(httpserver.HttpServer, session.Session)nclass ServerModule(Module):n passn

HttpServer是內置的HTTP伺服器模塊,Session則是為Web提供Session服務的模塊。如果不需要Session可以不載入。

vlcp中間所有的HTTP都被寫成了Http(首字母大寫),跟系統庫和PEP8的要求不同,這是歷史原因造成的。請習慣它。

為了防止跟你正在運行的網站衝突搞出大新聞,我們來先修改一下配置。將server.conf文件做如下修改:

module.httpserver.url=tcp://localhost:8080/nserver.startup=(server.ServerModule,)n

前一個配置是HttpServer的監聽地址,綁定到localhost,如果想要對外服務也可以不寫或者寫0.0.0.0;如果8080也衝突,可以改到其他埠。用IPv6也是可以的。後一個跟我們在命令行里寫啟動模塊列表是一樣的,修改過之後就可以不用寫在命令行里了。接下來我們只要這樣啟動就好:

python -m vlcp.start -f server.confn

這次啟動之後沒有立即退出,我們可以在瀏覽器當中(或者在另一個終端里使用curl)查看一下localhost:8080/,會看到404 Not Found幾個大字

這當然是因為我們還沒有相應的處理程序。接下來就來編寫處理相關頁面的代碼。按Ctrl+C結束服務運行,如果是Windows的話,Ctrl+C可能會顯得沒有響應,請直接關閉命令行窗口。

簡單易懂的HttpHandler

最簡單快捷的處理HTTP請求的方法是使用HttpHandler類,從它派生出子類,使用裝飾器將子類的方法綁定到路徑。這種風格和Flask很像,但要記住,這個方法是個協程,因此yield返回的並不是WSGI中的字元串,而是vlcp EventMacher的元組(詳見上一篇文章)。這個方法除了self以外會有一個額外的參數env,類似於WSGI中的Environment,不過它的大部分方法都是協程。可以參考vlcp.utils.http中Environment類的代碼。使用起來是比較簡單的,我們將server.py修改如下:

from vlcp.utils.http import HttpHandlernfrom vlcp.server.module import Module, dependnfrom vlcp.config import defaultconfignimport vlcp.service.connection.httpserver as httpservernimport vlcp.service.utils.session as sessionnn@defaultconfign@depend(httpserver.HttpServer, session.Session)nclass ServerModule(Module):n def __init__(self, server):n Module.__init__(self, server)n self.routines.append(MyHandler(self.scheduler))n nclass MyHandler(HttpHandler):n @HttpHandler.route(/)n def index(self, env):n for m in env.write(b<html><head><title>Index Page</title></head><body>Hello world!</body></html>):n yield mn

首先看到我們創建了一個MyHandler類,它有一個方法index,被綁定到了/路徑上,向外輸出了一段HTML。其次,我們重寫了ServerModule的構造函數,創建了MyHandler的實例,並加入到routines變數,這樣在模塊真正啟動的時候,這個RoutineContainer會被啟動,在模塊卸載時隨之被卸載。同一個模塊中可以加入多個RoutineContainer,啟動許多協程進行協作。HttpHandler也是RoutineContainer的子類,因此可以這樣啟動。注意我們並不需要Flask裡面那樣的Application全局變數,不同模塊中可以載入不同的HttpHandler,它們是完全獨立的,只要它們綁定的路徑相互不同就不會衝突。

實際應用中,將HTML直接寫進代碼中當然是非常不明智的。vlcp並沒有內置的模板組件,因為開源的模板比如jinja2已經足夠好了,將jinja2集成進來是很容易的事情,這裡不再演示了。如果要顯示的內容是靜態的,則有另一種更加簡單的方法即使用rewrite機制,後面會提到,我們先講點別的。

自動重新載入

每次修改完代碼都要中止服務然後重新載入太麻煩了。我們可以配置自動重新載入來簡化開發測試過程,只需要修改一下server.conf:

module.httpserver.url=tcp://localhost:8080/nmodule.manager.autoreload=Truenserver.startup=(server.ServerModule,n vlcp.service.manage.modulemanager.Manager)n

配置文件中每行都是以配置名開頭,等號分隔,一個Python值表達式結尾的。如果下一行開頭為空格字元,會自動合併到上一行。

重新啟動服務,之後每次修改過server.py,vlcp框架會在5秒之內檢測文件修改時間,發現有改動,然後重新載入,就會立即看到效果改變了。

參數處理

Web伺服器框架最重要的功能之一就是從QueryString或者POST數據中接收請求數據,對vlcp來說自然也不例外。VLCP中對請求數據的處理可以用類似於其他框架的方法,即使用args, form, files, cookies等等的內置變數,但也有遠遠來得簡單的方法:

from vlcp.utils.http import HttpHandlernfrom vlcp.server.module import Module, dependnfrom vlcp.config import defaultconfignimport vlcp.service.connection.httpserver as httpservernimport vlcp.service.utils.session as sessionnn@defaultconfign@depend(httpserver.HttpServer, session.Session)nclass ServerModule(Module):n def __init__(self, server):n Module.__init__(self, server)n self.routines.append(MyHandler(self.scheduler))n nclass MyHandler(HttpHandler):n @HttpHandler.route(/)n def index(self, env):n for m in env.write(b<html><head><title>Index Page</title></head><body>Hello world!</body></html>):n yield mnn @HttpHandler.route(r/args(?:/(.*))?, method = [bGET, bHEAD, bPOST])n def test_args(self, env):n env.header(content-type, text/plain)n for m in env.parseform():n yield mn for m in env.write(bnargs = %rnform = %rnfiles = %rncookies = %rnheaders = %rnheaderdict = %rnpath_match.groups() = %rn % (env.args, env.form, env.files, env.cookies, env.headers, env.headerdict, env.path_match.groups())):n yield mn @HttpHandler.routeargs(/args2, method = [bGET, bHEAD, bPOST])n def test_args2(self, env, a, b, c = None, **kwargs):n env.header(content-type, text/plain)n for m in env.write(a = %r, b = %r, c = %r, kwargs = %r % (a,b,c,kwargs)):n yield mn

訪問/args?a=1&b=2,以及/args2?a=1&b=2進行測試。/args使用了和其他框架中差不多的方法,而/args2使用了vlcp獨有的routeargs方法,可以看到test_args2方法多了一些額外的參數,這些參數會自動從輸入的參數(POST時默認從POST數據,GET時默認從QueryString)中獲取並賦給相應的值,如果必選參數缺失會直接返回HTTP 400錯誤。如果可選參數缺失,則會使用默認值。加上**kwargs可以接收指定參數以外的額外參數,如果不加,出現額外參數時也會返回HTTP 400錯誤。

vlcp的參數解析支持PHP風格的數組,即/args2?a=1&b[]=2&b[]=3會返回a = 1, b = [2,3]。用於提交列表很方便。如果不加[]後綴則只保留一個唯一的值。這樣Web開發者可以不用擔心設計為單個參數的地方被傳入數組。

除了處理QueryString和POST數據以外,也可以處理path中的參數。route或routeargs可以綁定一個正則表達式作為路徑,正則表達式中可以用括弧匹配需要的參數,匹配到的內容可以通過env.path_match來獲取。測試/args/test?a=1&b=2來觀察這個功能。

非同步功能

不要忘了vlcp是一個基於協程的非同步框架,由於每個處理流程都是個協程,上一篇文章中所有的協程功能都可以自然運用。我們來做一個簡單的帶延遲的頁面:

from vlcp.utils.http import HttpHandlernfrom vlcp.server.module import Module, dependnfrom vlcp.config import defaultconfignimport vlcp.service.connection.httpserver as httpservernimport vlcp.service.utils.session as sessionnn@defaultconfign@depend(httpserver.HttpServer, session.Session)nclass ServerModule(Module):n def __init__(self, server):n Module.__init__(self, server)n self.routines.append(MyHandler(self.scheduler))n nclass MyHandler(HttpHandler):n @HttpHandler.route(/)n def index(self, env):n for m in env.write(b<html><head><title>Index Page</title></head><body>Hello world!</body></html>):n yield mn @HttpHandler.route(/counter)n def counter(self, env):n for m in env.write(b<html><head><title>Counter</title></head><body>, buffering = False):n yield mn for i in range(0,10):n for m in self.waitWithTimeout(1):n yield mn for m in env.write(str(i) + <br/>, buffering = False):n yield mn for m in env.write(b</body></html>):n yield mn

訪問/counter,可以看到它會逐漸顯示出新的數字。這個過程是不會阻塞其他頁面的,可以看到數字刷出來的同時仍然可以訪問/

URL重寫/重定向

URL重寫和重定向是Web服務的重要功能。重寫是指從Web伺服器內部將請求修改為另一個路徑,由另一段程序來處理;重定向是指返回HTTP 302(或其他重定向狀態碼),使用戶瀏覽器轉向新的地址。在vlcp中,既可以使用env.rewrite、env.redirect的方法在處理頁面中重定向,也可以用預先定義的重寫/重定向規則在進入處理程序之前進行相應的操作。

from vlcp.utils.http import HttpHandlernfrom vlcp.server.module import Module, dependnfrom vlcp.config import defaultconfignimport vlcp.service.connection.httpserver as httpservernimport vlcp.service.utils.session as sessionnn@defaultconfign@depend(httpserver.HttpServer, session.Session)nclass ServerModule(Module):n def __init__(self, server):n Module.__init__(self, server)n self.routines.append(MyHandler(self.scheduler))n nclass MyHandler(HttpHandler):n rewrites = ((/index.html, /),n (/index, /),n (/args3, /args),n (r/args4/([^/]*)/([^/]*), r/args?a=1&b=2))n redirects = ((/index2, /),)n @HttpHandler.route(/)n def index(self, env):n for m in env.write(b<html><head><title>Index Page</title></head><body>Hello world!</body></html>):n yield mnn @HttpHandler.route(r/args(?:/(.*))?, method = [bGET, bHEAD, bPOST])n def test_args(self, env):n env.header(content-type, text/plain)n for m in env.parseform():n yield mn for m in env.write(bnargs = %rnform = %rnfiles = %rncookies = %rnheaders = %rnheaderdict = %rnpath_match.groups() = %rnpath = %rnfullpath = %rnoriginalpath = %rn % (env.args, env.form, env.files, env.cookies, env.headers, env.headerdict, env.path_match.groups(),n env.path, env.fullpath, env.originalpath)):n yield mnn @HttpHandler.routeargs(/args2, method = [bGET, bHEAD, bPOST])n def test_args2(self, env, a, b, c = None, **kwargs):n env.header(content-type, text/plain)n for m in env.write(a = %r, b = %r, c = %r, kwargs = %r % (a,b,c,kwargs)):n yield mn n @HttpHandler.route(/counter)n def counter(self, env):n for m in env.write(b<html><head><title>Counter</title></head><body>, buffering = False):n yield mn for i in range(0,10):n for m in self.waitWithTimeout(1):n yield mn for m in env.write(str(i) + <br/>, buffering = False):n yield mn for m in env.write(b</body></html>):n yield mn @HttpHandler.routeargs(/go, method = [bGET, bHEAD, bPOST])n def test_go(self, env, path):n for m in env.redirect(path):n yield mn n

測試/args4/1/2?c=12,會發現顯示的其實是args的頁面。

可以使用env.path, env.fullpath, env.originalpath分別獲取路徑、含QueryString的路徑、重寫前的路徑(含QueryString)。

訪問/go?path=/counter 則會被瀏覽器重定向到/counter。

會話(Session)

在vlcp中使用session是比較簡單的。首先要載入Session模塊,然後在處理頁面的協程中使用env.sessionstart即可,注意這是一個協程過程,需要使用for...yield循環來調用。

from vlcp.utils.http import HttpHandlernfrom vlcp.server.module import Module, dependnfrom vlcp.config import defaultconfignimport vlcp.service.connection.httpserver as httpservernimport vlcp.service.utils.session as sessionnn@defaultconfign@depend(httpserver.HttpServer, session.Session)nclass ServerModule(Module):n def __init__(self, server):n Module.__init__(self, server)n self.routines.append(MyHandler(self.scheduler))n nclass MyHandler(HttpHandler):n @HttpHandler.route(/)n def index(self, env):n for m in env.write(b<html><head><title>Index Page</title></head><body>Hello world!</body></html>):n yield mn n @HttpHandler.route(/session)n def test_session(self, env):n for m in env.sessionstart():n yield mn try:n for m in env.session.lock():n yield mn count = env.session.vars.get(count, 0) + 1n env.session.vars[count] = countn for m in env.write(Session locked, id = + str(env.session.id) + , count = + str(count)):n yield mn finally:n env.session.unlock()n

訪問/session,可以發現count的值不斷遞增。

對session的lock並不是必須的,由於協程實際上是單線程運行,讀寫session.vars一般不會有競爭冒險的情況出現,但是在經過yield之後,vars中的值可能被其他協程修改。使用lock可以保證同一個會話的不同請求之間,lock與unlock之間的區域不會同時執行。

靜態內容

顯然對於一般的網站來說,大部分資源是靜態的,並不應該從代碼輸出。對這些靜態資源專門部署伺服器甚至CDN自然是極好的,但對於小伺服器來說就略複雜了。如同大多數web框架一樣,vlcp也有內置的靜態資源處理模塊。靜態資源可以配合重寫功能發揮更大的作用。

為了演示靜態資源處理的功能,我們在當前目錄下創建三個子目錄:static, error, rewrite。這三個目錄分別對應靜態資源處理常用的三個功能:靜態文件,自定義錯誤,重寫到靜態頁面

創建以下幾個文件:

/static/default.css:

.mainpage{ntbox-shadow:1px 1px 3px #2769aa;ntclear:both;ntmargin:35px auto 0 auto;ntmin-height:400px;ntpadding:20px 15px;ntwidth:960px;ntborder-radius:3px;ntbackground-color:#F9FAFC;ntcolor: #2779aa;n}n

/rewrite/index.html:

<!DOCTYPE html >n<html>n<head>n<meta charset="UTF-8">n<title>首頁</title>n<link rel="stylesheet" type="text/css"nthref="/static/default.css" />n</head>n<body>nt<div class="mainpage"><p>我是新版的首頁</p><h1>Hello world!</h1></div>n</body>n</html>n

/error/404-notfound.html

<!DOCTYPE html >n<html>n<head>n<meta charset="UTF-8">n<title>404 - Not found</title>n<link rel="stylesheet" type="text/css"nthref="/static/default.css" />n</head>n<body>n<div class="mainpage">您訪問的路徑不存在,請與管理員確認。</div>n</body>n</html>n

我們要修改配置文件,先停止服務。修改配置文件如下:

module.httpserver.url=tcp://localhost:8080/nmodule.manager.autoreload=Truenmodule.static.vdir.error.dir=errornmodule.static.vdir.error.errorpage=Truenmodule.static.vdir.error.rewriteonly=Truenmodule.static.vdir.rewrite.dir=rewritenmodule.static.vdir.rewrite.rewriteonly=Truenmodule.static.vdir.rewrite.checkreferer=Falsenmodule.static.checkreferer=Falsenmodule.static.relativemodule=servernprotocol.http.errorrewrite={404:b/error/404-notfound.html}nserver.startup=(server.ServerModule,n vlcp.service.manage.modulemanager.Manager)n

將服務簡單修改如下:

from vlcp.utils.http import HttpHandlernfrom vlcp.server.module import Module, dependnfrom vlcp.config import defaultconfignimport vlcp.service.connection.httpserver as httpservernimport vlcp.service.utils.session as sessionnimport vlcp.service.web.static as staticnn@defaultconfign@depend(httpserver.HttpServer, session.Session, static.Static)nclass ServerModule(Module):n def __init__(self, server):n Module.__init__(self, server)n self.routines.append(MyHandler(self.scheduler))n nclass MyHandler(HttpHandler):n rewrites = ((/,/rewrite/index.html),)n n n

訪問新首頁就會顯示出新的頁面,其中引用了/static/default.css。訪問不存在的地址比如說/notfound則會顯示出自定義的錯誤頁面。

我們通過添加依賴項的方式載入了Static模塊,它會自動響應相應的靜態資源的載入請求,處理緩存等機制。簡單解釋一下配置,errorpage = True的目錄會在展示頁面時,自動使用文件開頭的三個數字作為返回的狀態碼,所以文件名應該命名為404-notfound.html之類。對於rewriteonly = True的目錄,只有通過rewrite機制訪問才能正確顯示,直接訪問/rewrite/index.html會顯示HTTP 404。對於checkreferer = True的目錄,會檢測Referer是否來自於同一站點,這是一種防盜鏈的機制。relativemodule用於指定目錄應當相對於哪個模塊的路徑。

綜合 —— WebIM示例

這個例子放在了github的example目錄中

github.com/hubo1016/vlc

這是一個經典的WebChat的vlcp實現,它的Python代碼部分只有100行左右,其中運用了AJAX long-poll技術來實現comet。默認綁定在0.0.0.0:8888/上。下載整個目錄的源代碼之後,使用python main.py的方法來執行。在瀏覽器中打開兩個頁面,可以看到在一個頁面中提交的內容可以立即在另一個頁面中顯示出來。

可以試一下左手和右手聊天,體驗一下孤獨的感受

小結

雖然不是主要設計功能,但vlcp有較強的搭建Web服務的能力,尤其適合規模不大且需要使用comet的情況。實際上現在公司內的一個Web操作平台的項目就是我使用這個框架建立的,一直在穩定運行,證明了框架的可靠性。


推薦閱讀:

一個非同步函數是不是內核會單獨開一個線程來運行?
angularjs如何在循環裡面請求數據並正確賦值?

TAG:Python | Web开发 | 异步IO |