由于知乎目前限制单人仅能开通单个专栏,所以关于文章主题的所有文字都会写在该单篇文章中(避免污染专栏),目前处于长篇连载且停滞状态,待续。。

Github Repo: nekocode/tornaREST · GitHub

Preface

我是一名 Android 开发工程师,我在用 Kotlin 和 Java 写着 Android 应用,可是我也很喜欢 Python,我用它来写一些网页应用、工具。这次,我打算使用 Python 实现一个提供 RESTful Service 的后端,它能为各种前端提供 API。

Python 是一门很强大、很容易入门的语言,但是它不是一门简单的语言,它在语法上有很多可以玩的 trick(虽然很多情况下你并不需要掌握这些 trick)。它很强大,你能通过它用更少的时间来做更多的事情,你能花更多时间关注更高层的东西(业务逻辑)。『人生苦短,我用 Python』,当然我也相信,大多数对某种语言有信仰的人,都是用着该门语言在做合适的事情。Java、Ruby、C/C++ 也是如此。

Framework

Tornado + MongoDB + Redis

Tornado

它实现了一个高效的非阻塞异步 IO 的网络模型,另外一些明星级别的工业产品(Fackbook & 知乎)也选择了它。它不像 Django 一样庞大,但是它提供了构建一个基础 Web Server 所需的大多数工具,并且得力于异步 IO 的支持,它很适合作为一个 RESTful API Server 的中间件。

MongoDB

它是新兴 NoSQL 领域的一批黑马,我对于 SQL 数据库并没有太多的经验,所以我更愿意尝试新领域的产品。我认为 MongoDB 比起 MySQL 有更吸引我的点:更高的性能。

宽松的结构,更容易拓展。

NoSQL 是一场技术革命运动,已经有很多 Startup 选择使用 MongoDB。Why not have a try?

Redis

它是一个高性能的 Key-Value 类型内存数据库(也支持对数据进行持久化)。它很适合对一些使用量高,实时性要求高数据进行缓存。我将用它来储存 Token,消息队列以及 Feeds。

API Doc

一份详细的 API 文档是必须的。我使用 apidoc 来为代码自动生成 API 文档,另外我自己维护了一个分支 nekocode/apidoc,它修复了一些问题(issue #394),以及把接口测试器改为使用 urlencode 进行 POST 和 PUT(默认是使用 JSON)。

详细的效果可以查看 Slate 来生成文档,它更加漂亮,但是不像 apidoc 一样提供接口测试器。

Collections

MongoDB 使用 Collection 来储存同一类别的数据,类似于 SQL 中的 Table。现在,我们首先为我们的 Backend Service 添加一系列最基础的用户操作接口。

我使用了 MotorEngine 来作为 MongoDB 的 ORM 框架,他是针对 在 Tornado 使用 MongoEngine 的一个 Port,使其能够异步访问 MongoDB。我首先通过它来定义一些需要的数据库对象:

class BaseDocument(Document):
def to_dict(self):
data = super(Document, self).to_son()
data['id'] = self._id
return data
class User(BaseDocument):
mobile = StringField(required=True)
password = StringField(required=True)
followers = ListField(ReferenceField(reference_document_type='collections.User'), default=[])
create_time = DateTimeField(required=True, auto_now_on_insert=True, auto_now_on_update=False)
# ...
def to_dict(self):
data = super(User, self).to_dict()
del data['password']
return data

注意,我们首先定义了一个 BaseDocument,并添加了一个 to_dict 方法,它将 Document 对象转换为一个 dict,并将隐藏的 ObjectId 添加入字典,方便向客户端返回 JSNO 对象时的序列化。

这里的获取到的 dict 不能直接用 json 的 dumps 进行序列化,可以借助 bson.json_util 进行序列化,但是它把一些 BSON Object 包裹起来序列化,这样输出的数据很丑,我们还是模仿着 bson.json_util 自己实现一个 json_util 来进行序列化吧(因为篇幅我隐藏了一些代码):

def _json_convert(obj):
if hasattr(obj, 'iteritems') or hasattr(obj, 'items'): # PY3 support
return SON(((k, _json_convert(v)) for k, v in obj.iteritems()))
elif hasattr(obj, '__iter__') and not isinstance(obj, string_types):
return list((_json_convert(v) for v in obj))
try:
return default(obj)
except TypeError:
return obj
def default(obj):
if isinstance(obj, ObjectId):
return str(obj)
if isinstance(obj, datetime.datetime):
if obj.utcoffset() is not None:
obj = obj - obj.utcoffset()
millis = int(calendar.timegm(obj.timetuple()) * 1000 +
obj.microsecond / 1000)
return millis
if bson.has_uuid() and isinstance(obj, bson.uuid.UUID):
return obj.hex
raise TypeError("%ris not JSON serializable" % obj)
def dumps(obj, *args, **kwargs):
return json.dumps(_json_convert(obj), *args, **kwargs)
主要对 ObjectId/ datatime/ UUID 对象进行处理。
Router & BaseHandler

我们需要一个 BaseHandler 来封装一些基础的操作(例如格式化输出)。为了保持输出的一致性,我们还需要捕获一些异常,我们需要在路由设置上使用一些小技巧:

url = [
(r'/api/user/login', LoginHandler),
# ...
(r'.*', APINotFoundHandler),
]

我们需要定义一个 APINotFoundHandler 用来捕获一些未定义路径的请求,并通过 JSON 以及 Http Status Code 返回 404 错误信息给用户。

而 BaseHandler 的部分代码应该是这样的:

class BaseHandler(RequestHandler):
def __init__(self, application, request, **kwargs):
RequestHandler.__init__(self, application, request, **kwargs)
# TODO: REMOVE?
self.access_control_allow()
def access_control_allow(self):
self.set_header('Content-Type', 'text/json')
# 允许 JS 跨域调用
self.set_header("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE,OPTIONS")
self.set_header("Access-Control-Allow-Headers", "Content-Type, Depth, User-Agent, Token")
self.set_header('Access-Control-Allow-Origin', '*')
def get(self, *args, **kwargs):
raise HTTPError(**errors.status_0)
# 这里因为篇幅省略了复写 post/put/options/delete
def options(self, *args, **kwargs):
# TODO: REMOVE?
self.write("")
def write_error(self, _status_code, **kwargs):
# TODO: REMOVE?
self.access_control_allow()
if self.settings.get("serve_traceback") and "exc_info" in kwargs:
# in debug mode, try to send a traceback
lines = []
for line in traceback.format_exception(*kwargs["exc_info"]):
lines.append(line)
self.finish(dumps({
'reason': self._reason,
'traceback': ''.join(lines)
}))
else:
self.finish(dumps({
'reason': self._reason,
}))
def write_json(self, data):
self.finish(json_util.dumps(data))
def is_logined(self):
if 'Token' in self.request.headers:
token = self.request.headers['Token']
logined, uid = validate_token(token)
if logined:
# 已经登陆
return uid
# 尚未登陆
raise HTTPError(**errors.status_2)
@staticmethod
def vaildate_id(id):
if id is None or not ObjectId.is_valid(id):
raise HTTPError(**errors.status_3)
class APINotFoundHandler(BaseHandler):
def data_received(self, chunk):
pass
def get(self, *args, **kwargs):
raise HTTPError(**errors.status_1)
# 这里因为篇幅省略了复写 post/put/options/delete
def options(self, *args, **kwargs):
# TODO: REMOVE?
self.access_control_allow()
self.write("")
另外我使用了一个 errors 文件来储存所有的错误码以及对于的 Reason:
status_0 = dict(status_code=405, reason='Method not allowed.')
status_1 = dict(status_code=404, reason='API not found.')
# ...

还有些细节日后再聊,晚安~