2010年10月1日星期五

Python的Web服务器

Python的Web服务器

* Socket

基本的概念见 http://en.wikipedia.org/wiki/Berkeley_sockets

先拿Netcat做点试验:
输入“nc g.cn 80”命令,然后输入“GET / HTTP/1.0”,然后敲几个回车。
这样会通过使用TCP协议得到HTTP协议下网页返回的内容。
然后输入“nc -l 8080 | nc g.cn 80”,实现了一个简单的转发,-l是listen的意思。

常用到的API是:
socket() 创建Socket对象
bind() 服务器端与地址绑定
listen() 服务器端开始监听TCP连接
connect() 客户端建立TCP连接
accept() 服务器端接受并建立TCP连接
send() recv() 用于数据IO
close() 关闭Socket对象
select() 用于实现非阻塞Socket
poll()
这里Socket的协议通常是PF_INET表示IPv4,类型有:
SOCK_STREAM如TCP
SOCK_DGRAM如UDP
以及基本的SOCK_RAW

用Python代码表示就是:
#+BEGIN_SRC python
#服务端
import socket            
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('', 8080))
s.listen(1)
conn, addr = s.accept()
print 'Connected by', addr
while 1:
    data = conn.recv(1024)
    if not data: break
    conn.send(data)
conn.close()
#+END_SRC
#+BEGIN_SRC python
#客户端
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('localhost', 8080))
s.send('Hello, world')
data = s.recv(1024)
s.close()
print 'Received', repr(data)
#+END_SRC
UDP客户端则没有connect而使用sendto来发送数据。

使用“infds,outfds,errfds = select.select([s,],[],[],5)”会阻塞,直到s有数据传入,以实现非阻塞的网络服务器。


* 内置

使用cgi模块和CGIHTTPServer模块便实现了简单的网络应用及服务器了,不过这不是这里的话题。
感觉Java有界面这个东西会使得一些概念变得清晰,在实现接口知道了要提供的东西,Python的模块提供使用方式似乎有很大的多样化。
这里要说的是SocketServer模块里内容,涉及TCPServer和UDPServer通过实现自己的Handler类来处理数据。
在实现异步服务器上有ForkingMixIn和ThreadingMixIn用于实现多进程。
或者用asyncore和基于它的asynchat模块来使用Socket,通过实现handle_*对Socket的不同阶段进行处理,不过没有实现更抽象的东西。
SocketServer.TCPServer有子类叫HTTPServer,实现有handle()方法将HTTP请求的消息发送发到do_*()方法上。
通过Python模块自身就是用Python编写的,仅在最底层上或出于性能考虑才会和Python的实现直接接触。
#+BEGIN_SRC python
import SocketServer, BaseHTTPServer
class server(SocketServer.ThreadingMixIn,BaseHTTPServer.HTTPServer):
    pass
class handler(BaseHTTPServer.BaseHTTPRequestHandler):
    def do_GET(self):
        print self.path
        self.wfile.write("Hello World")
httpd = server(('',8000), handler)
while 1:
    httpd.handle_request()
#+END_SRC

不过对于Web框架来说通常的做法是实现WSGI这个接口。

叫底层的SocketServer是,直接贴Py文档中的一段代码了:
#+BEGIN_SRC python
import SocketServer

class MyTCPHandler(SocketServer.BaseRequestHandler):
    """
    The RequestHandler class for our server.

    It is instantiated once per connection to the server, and must
    override the handle() method to implement communication to the
    client.
    """

    def handle(self):
        # self.request is the TCP socket connected to the client
        self.data = self.request.recv(1024).strip()
        print "%s wrote:" % self.client_address[0]
        print self.data
        # just send back the same data, but upper-cased
        self.request.send(self.data.upper())

if __name__ == "__main__":
    HOST, PORT = "localhost", 9999

    # Create the server, binding to localhost on port 9999
    server = SocketServer.TCPServer((HOST, PORT), MyTCPHandler)

    # Activate the server; this will keep running until you
    # interrupt the program with Ctrl-C
    server.serve_forever()
#+END_SRC


* CherryPy

CherryPy是一个Python的面向对象的Http框架(这里使用了CherryPy3)。
先来看一段代码,最常见的HelloWorld程序:
#+BEGIN_SRC python
import cherrypy
class Main(object):
    @cherrypy.expose
    def index(self):
        return "Hello World!"
cherrypy.quickstart(Main())
#+END_SRC
看起来和Web.py的HelloWorld很像,而且它们都是用于Web站点的开发。
不过之间的区别还是明显的:Web.py涵盖了模板引擎和数据库绑定(虽然都功能比较基本)需要部署到服务器上;而Cherrypy虽然可以按照TurboGears样子整合其他组件成为一个完整的Web开发框架,但是其自身是一个多线程的Web服务器。

对于Cherrypy有两个值得关注的地方,一是URI与Callable对象的映射规则,一是Configuration文件的使用。

这里的Callable对象是一个"exposed = True"的函数或者有__call__方法的实例,以对象属性的形式构成树形结构。Get和Post的内容以命名参数的形式传入,特殊名称index匹配"/"而default则将URL的剩余部分作为调用的参数。
以下是一个简单的示例:
#+BEGIN_SRC python
import cherrypy
class Main(object):
    @cherrypy.expose
    def index(self):
        return "Hello World!"
    @cherrypy.expose
    def about(self):
        return "Page About<hr />"
class Page(object):
    @cherrypy.expose
    def index(self,q=None):
        return "Page <index>"+("<br />%s"%q if q else "")
    @cherrypy.expose
    def default(self,page_id):
        return "Page %s"%page_id
main = Main()
main.page=Page()
cherrypy.quickstart(main)
#+END_SRC

Cherrypy的配置文件使用了ini的格式(以dict形式写在Py代码中也是可以的),包括针对全局"global"的属性以及针对路径如"/"|"/static"等等的。配置可以作为调用cherrypy.quickstart时的参数,或者用cherrypy.config.update更新全局配置cherrypy.tree.mount为路径添加配置。
例如存在config.ini:
#+BEGIN_SRC ini
[global]
server.socket_port = 8000
server.thread_pool = 10
tools.sessions.on = True
tools.staticdir.dir = "/path/to/app"
[/static]
tools.staticdir.on = True
tools.staticdir.dir = "static"
#+END_SRC
然后是修改很小的HelloWorld程序:
#+BEGIN_SRC python
import cherrypy
class Main(object):
    @cherrypy.expose
    def index(self):
        return "Hello World!"
cherrypy.quickstart(Main(),"/","config.ini")
#+END_SRC

关于CherryPy基本的信息就是这样了。其他信息如自定义URL调度,Sessions机制,Request/Response对象,Tools模块,Plugins机制以及其他更多的Web开发中会用到内容在这里将不再详述。
现在的TurboGears1.1分支依然以CherryPy作为其Controller的部件,模板用的是Genshi,Model部分是SQLAlchemy,还有一个Py味道的MochiKit作为Javascript库。


* Tornado

Tornado是FriendFeed所使用的非阻塞Web服务器,具有优异的特性,可用于构建实时服务。
在接口提供上有对web.py和Django的参照,所以使用起来会有些许似曾相识的感觉。
其自带一份说明文档和若干示例代码,API的信息在其GIT仓库中查询。
首先依旧是HelloWorld,代码有Tornado首页提供:
#+BEGIN_SRC python
import tornado.httpserver
import tornado.ioloop
import tornado.web
class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello, world")
application = tornado.web.Application([
    (r"/", MainHandler),
])
if __name__ == "__main__":
    http_server = tornado.httpserver.HTTPServer(application)
    http_server.listen(8888)
    tornado.ioloop.IOLoop.instance().start()
#+END_SRC
从正则匹配后调用对应提供get或post方法的类,这正是Web.py的方式。
而模板引擎和Web.py一样是转化render为Py代码(而没有以解释的方式),不过标签是使用Django中{{}}和{%%}来嵌入。
Django的自定义标签这里叫UIModule,在通过RequestHandler或其子类派生的控制器(可有initialize方法用于初始化)中Tornado提供方法通过self调用,返回text/html/json或其他数据流,Post的内容由get_argument和self.request访问,用户状态设置cookie_secret后由Secure cookies存放,应用的配置通过Application构造器传入,有类似的CSRF的XSRF模块,可以提供静态文件,有和Django的Authentication功能相关的用户验证模块(有authenticated修饰器和login_url设置),翻译的文本由csv格式存放,部署的话多实例结合nginx服务器使用。
以上内容先暂且大略说这么些,因为以上内容并不是使用Tornado的有所特别的地方。

下面来说Tornado中特色的功能了——Asynchronous机制。
添加了"@tornado.web.asynchronous"的RequestHandler子类的get方法在结束时请求会依然保持打开,直到调用了self.finish而不是return的时候才会像客户端返回相应。
这样的作用是例如在读取远程文件(包括运程调用等待返回结果)这种需要一定时间的操作时,可以传入一个回调函数来进行之后需要的操作,而不必在自身过程中等待可以去继续执行之后的语句。
这和浏览器中JQuery包装的Ajax函数在原理和接口上有类似的地方。
在调用异步执行的方法(例如由AsyncHTTPClient提供的)时,回调函数在RequestHandler中可以以callback=self.async_callback(self.on_response)形式的参数(这里的async_callback不是必须的,是一个wrapper以添加调用参数)传入,这里的on_response是自行编写的用于处理异步方法返回结果的方法。

一样明显的使用的例子的聊天室程序:
其工作方式是存在一个以async方式的poll方法,创建async_callback对象添加到waiterList中,并在客户端push时去处理这个List中残留的项目。
客户端可以在发送消息时执行push操作,并让异步执行的回调函数之间构成一个循环。
可以的优化是客户端保存并发送一个cursor让服务端只返回需要的数据,还有在poll的时候如果有可以返回的数据就直接返回而不再往waiterList添加新的callback对象。
因为XSRF的使用,在客户端get或post时要传入用作验证的值。
Tornado自带的示例还考虑的不使用Ajax时伪静态的页面也能够正确的通过form和render来正确显示,示例中Ajax使用JQuery库。
这个试验代码写起来有点头晕,效果会比用setInterTime实现的感觉要好,而且最好还是去放到Linux下运行,先保证it just work。
测试用代码:
#+BEGIN_SRC python
见Tornado自带示例
#+END_SRC


* Twisted

一个事件驱动的Python网络框架,可以用来构建高效的Game和ZOPE服务器。
因为用法(去实现事件)是抽象后暴露出来的,和之前的Socket的时候有不同的关注层面。
可以把写Qt代码的感觉找出来一点的,就像去实现自己的Widget一样,虽然说没用抽象的信号机制而是只用到了override的方式事件循环。同时,和Tornado一样,是构建了一个异步非阻塞网络服务器。

最简单的使用是:
通过派生Protocol来实现事件代码,有self.transport用于传输数据,类似于Python库中SocketServer.StreamRequestHandler的作用。
Factory是用来制造的Protocol的子类,只需要一个。
reactor使用Factory的,进行事件循环,负责整个程序的执行,类似于Application对象的作用。
首先启用作为Web服务器。
然后来实现自己的一段简单的代码,不过先乱如一段WebServer代码,这和Cherrypy是同样的作用:
#+BEGIN_SRC python
from twisted.web import server, resource
from twisted.internet import reactor

class Root(resource.Resource):
    isLeaf = True
    def render_GET(self, request):
        return "Hello World"
resource = Root()
site = server.Site(resource)
reactor.listenTCP(8080, site)
reactor.run()
#+END_SRC
接下来是和SocketServer同样原理的一段代码:
#+BEGIN_SRC python
from twisted.internet.protocol import Protocol, Factory
from twisted.internet import reactor
class QOTD(Protocol):
    def connectionMade(self):
        self.transport.write("Hello World")
        self.transport.loseConnection()
factory = Factory()
factory.protocol = QOTD
reactor.listenTCP(8000, factory)
reactor.run()
#+END_SRC
此外还有reactor.callLater添加回调,也可以把回调添加到Deferred对象中,实现Tornado中的那种异步调用。

由于Twisted具有较大的规模和复杂度,所以这里简短的介绍就暂且到这里不再往下了。


* 其他

org-mode中贴源码是用
#+BEGIN_SRC python
#+END_SRC
其实还需要`C-c C-e t'
#+OPTIONS:   H:3 num:t toc:t \n:t @:t ::t |:t ^:nil -:t f:t *:nil <:t
来让导出的显示符合预期

在Web开发中方便调试会对Py模块使用reload机制

貌似在Web开发中涉及模型后直接生成CURD的URL和View会是一个很好的原型。

State Machine是状态机的意思。

没有评论: