前言:应 Python-CN 社区之邀,我们这里公开一些技术上的小秘密,描述搜狐邮件中心是怎样基于 webpy 进行 webmail 的开发和部署的。要理解我们自己山寨出来的这个框架——姑且命名为 webpy+ 吧——您首先得熟悉 webpy 程序的开发模式。
本文档采用协议发布
Q: 搜狐邮件中心为什么选择 web.py?
A: 轻便,这意味可以更容易的去定制它。这对于我们这种需要部署几百台服务器的应用非常重要。
==================================
搜狐邮件中心webpy项目的目录结构
proj\runctl
proj\run.py
proj\app1\
proj\app2\
...
我们有一个 /opt/sohumc 的目录,为便于理解你可以把它认为是 /usr/local.
该目录就是我们的运行环境。编译一次,到处部署
python 以及 webpy 等都安装在 /opt/sohumc 里。
run.py和runctl是我们这个webpy+ framework的核心,下面只介绍这两个文件
==================================
runctl 是从 shell 启动应用的入口
实质上它也是一个 python 脚本,但由于我们架构师的趣味,去掉了 .py 后缀,可能是为了让它看起来更像一个可执行程序。
它的开头是这样:
#!/opt/sohumc/bin/python
import sys, os, os.path, signal, zdaemon.zdctl, zdaemon.zdrun,shutil
CONF_FILE = '''<runner>
program /opt/sohumc/bin/python run.py %(run_as)s %(port)d
socket-name /opt/work/log/run.%(port)d.sock
%(log_files)s
forever true
user postfix
umask 022
default-to-interactive false
</runner>
'''
1. 我们利用了 zope 里的 zdaemon 组件来控制进程. 这样停止应用不需要使用 killall 或者额外写什么脚本了.
2. 因为我们主要是文件系统的操作,所以要保证正确的 user 和 mask. zdaemon 确实很合用.
runctl 最终调用 zdaemon.zdctl.main() 来启动 run.py
==================================
run.py 是启动 webpy 的入口,格式为 run.py app_dir port
换言之就是把某个应用启动在某端口上(然后前端nginx反向代理至该端口)
我们一个应用就是一个目录
最开始webpy之旅的时候,首要解决的问题就是如何把app分割到多个文件里,然后把 url/class 的映射关系自动组装起来。
1. 我们遍历 app_dir 目录,里面的 .py 文件都是需要导入的模块
modules = [(x[:-3], os.path.join(app_dir, x)) for x in os.listdir(app_dir) if x.endswith('.py')]
得到一个 list,每个元素是一个 tuple,(模块名, 文件路径名)
2. 然后我们去获得 webpy 所需的 url/class 映射关系并生成 app
urls = import_modules(modules)
app = web.application(urls, globals(), False)
这里得先介绍一下 app_dir 目录下每个 .py 文件的规范:
a. 必须有一个名为 urls 的 tuple,里面声明这个 py 文件想处理哪些 url
b. 必须有一个名为 handler 的 class,就是 webpy 里的 GET/POST 请求处理单元
比如写个 demo.py:
urls = ('/demo', '/hello')
class handler():
def GET(self):
return "Hello, From Demo!"
3. 重头戏来了,看看 demo.py 如何被集成
def import_modules(modules):
urls = []
g = globals()
from imp import load_source
def import_module(name, file):
m = load_source(name, file) g[name] = m
#寻找文件中是否有handler和urls的声明
v = dir(m)
h, u = 'handler' in v, 'urls' in v
if h or u:
if not (h and u):
#handler和urls必须成对出现,handler是一个类,而urls是一个元素是字符串的tuple或列表
raise ImportError('module %s doesn\'t have matched handler and urls' % name)
#有handler和urls的声明,就记下来加入全局的urls表中
urls.extend(reduce(list.__add__, [[x, '%s.handler' % name] for x in m.urls]))
for (name, file) in modules:
if name not in g:
import_module(name, file)
return urls
现在还比较山寨,所有的名字都在 globals() 空间里. 总之这样执行完以后,返回的 urls 是
('/demo', 'demo.handler', '/hello', 'demo.handler')
同时 'demo.handler' 是可以被访问的类资源了
run.py 的最后是启动 FastCGI
app.add_processor(web.loadhook(prototype_patch))
# 我们使用了 loadhook 完成一些入口清理工作
func = app.wsgifunc(access_log, )
# access_log 我们是利用 WSGI 中间件来做的
from flup.server.fcgi_fork import WSGIServer
# fork 是 python 王道
WSGIServer(func, .../*flup提供了一堆参数, 值得了解*/...).run()
或是邮件反馈可也:
askdama[AT]googlegroups.com
订阅 substack 体验古早写作:
关注公众号, 持续获得相关各种嗯哼: