python中常用的几个web框架有django, tornado, flask等,今天来总结一下django和tornado的不同。
工作中django和tornado都用过,使用django相对更多一些。个人感觉django虽然好用,有搭建项目快、自带ORM、自动生成路由、自带管理后台等优势;但若实际工作中选择,我还是会偏向于使用tornado框架,因为torndo使用更加灵活,并且支持websocket,tcp等通信协议,最重要的是tornado是异步非阻塞的web框架;而在django中要实现websocket、异步非阻塞等功能则需要引入dwebsocket、celery等第三方模块。
本文使用的环境是python3.6, django2.0, tornado5.1。
下面主要从以下几个方面介绍一下这两个框架的不同: 1.创建项目的方式 2.数据库连接 3.异步非阻塞请求 4.websocket的使用
项目创建方式 django django主要是通过下面两个命令创建项目:
1 2 django-admin startproject Test django-admin startpapp Test01
执行完成后,会生成如下的目录结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 D:. │ manage.py │ test.txt │ ├─.idea │ │ misc.xml │ │ modules.xml │ │ Test.iml │ │ workspace.xml │ │ │ └─inspectionProfiles │ profiles_settings.xml │ ├─Test │ settings.py │ urls.py │ wsgi.py │ __init__.py │ └─Test01 │ admin.py │ apps.py │ models.py │ tests.py │ views.py │ __init__.py │ └─migrations __init__.py
主要是manage.py,Test,Test01这几个文件和文件夹, manage.py是管理项目的文件,通过它运行django的一些内置命令,如模型迁移、启动项目等; Test/settings.py是配置文件,项目配置存放在这里 Test/urls.py是路由文件,负责分发http请求 Test01/models.py是模型文件,Test01下创建的模型就放在这里,模型负责将表结构映射到数据库中 Test01/views.py是视图文件,django中的视图在这里定义 Test01/migrations目录中存放迁移后生成的迁移文件。 django项目的基本结构就是这样。
tornado tornado项目的创建比较灵活,没有什么项目名称和app的概念,全靠自己组织项目,就是创建一个个python文件和python package。可以像下面一样来组织tornado项目:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 ├── App │ ├── __init__.py │ ├── Shop │ │ ├── __init__.py │ │ └── views.py │ └── User │ ├── __init__.py │ └── views.py ├── application.py ├── Config │ ├── config_base.py │ ├── config_db.conf │ ├── config_db_get.py │ ├── config_engine.py │ ├── __init__.py ├── Models │ ├── __init__.py │ ├── Shop │ │ └── __init__.py │ └── User │ ├── BaseClass.py │ ├── __init__.py │ └── UserModel.py ├── server.py ├── static │ └── __init__.py ├── templates │ └── __init__.py ├── test.py └── Urls ├── __init__.py ├── Shop.py └── User.py
这里有几个主要文件App, Config, Models, Urls, static, templates, application.py, server.py。 项目的app可以集中放在App目录中,与数据库对应的模型文件可以放在Models中,http路由可以放在Urls中,项目配置信息可以放在Config目录中,静态文件和模板分别放在static和templates中。application.py文件可以加载路由信息和项目配置信息,server.py文件负责启动项目。 项目的基本配置信息可以放在Config/config_base.py中,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import osBASE_DIR = os.path.dirname(__file__) options = { "port" : 8001 , } settings = { "debug" : True , "static_path" : os.path.join(BASE_DIR, "static" ), "template_path" : os.path.join(BASE_DIR, "templates" ) }
路由信息可以放在Urls/User.py中,如下:
1 2 3 4 5 6 7 8 9 from App.UserInfo import viewsuser_urls = [ (r'/user/' , views.IndexHandler), ]
application.py中加载路由信息和配置信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from tornado import webfrom Config.config_base import settingsfrom Urls.UserInfo import user_urlsfrom Urls.Shop import shop_urls""" 路由配置 """ class Application (web.Application) : def __init__ (self) : urls = user_urls + shop_urls super(Application, self).__init__(urls, **settings)
最后在server.py中启动项目:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from tornado import ioloop, httpserverfrom application import Applicationfrom Config import config_baseif __name__ == '__main__' : app = Application() http_server = httpserver.HTTPServer(app) http_server.listen(config_base.options.get("port" )) ioloop.IOLoop.current().start()
数据库连接 django django中使用数据库时,首先要在settings.py中配置数据库信息:
1 2 3 4 5 6 7 8 9 10 DATABASES = { 'default' : { 'ENGINE' : 'django.db.backends.mysql' , 'NAME' : 'django_test' , 'USER' : 'root' , 'PASSWORD' : 'test' , 'HOST' : 'localhost' , 'PORT' : '3306' , } }
然后在每个app下编写完models.py后,执行以下两个命令后,就可以使用数据库了:
1 2 python manage.py makemigrations python manage.py migrate
可以调用模型管理器对象objects的相应方法,执行增删改查等操作。
tornado 这里说一下在tornado中使用sqlalchemy连接数据库,需要安装sqlalchemy和pymysql。
首先在Config/config_db.conf中配置数据库信息 1 2 3 4 5 6 7 [db_user] name = db_tornado03port = 3306 user = roothost = 127.0 .0.1 pass = testpool_size = 3
然后在Config/config_engine.py中配置engine 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 from sqlalchemy import create_enginefrom Config.config_db_get import ConfigDBUserdb_user = ConfigDBUser("db_user" ) engine_user = create_engine( "mysql+pymysql://%s:%s@%s:%d/%s" % ( db_user.get_db_user(), db_user.get_db_pass(), db_user.get_db_host(), db_user.get_db_port(), db_user.get_db_database() ), encoding='utf-8' , echo=True , pool_size=20 , pool_recycle=100 , connect_args={"charset" : 'utf8mb4' } )
create_engine用来初始化数据库连接。
在Models/UserInfo/BaseClass.py中配置连接数据库的session信息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from sqlalchemy.orm import scoped_session, sessionmakerfrom Config.config_engine import engine_userclass BaseClass : def __init__ (self) : self.engine_user = scoped_session( sessionmaker( bind=engine_user, autocommit=False , autoflush=True , expire_on_commit=False ) )
在Models/UserInfo/UserModel.py中配置模型信息,用于映射到数据库中对应的表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from sqlalchemy import Table, MetaDatafrom sqlalchemy.ext.declarative import declarative_basefrom Config.config_engine import engine_userBaseModel = declarative_base() def user_model (table_name) : class UserModel (BaseModel) : __tablename__ = table_name metadata = MetaData(engine_user) Table(__tablename__, metadata, autoload=True ) return UserModel
配置模型信息前,需要在数据库中把表创建好,这是就需要写sql语句创建表了。对于熟练sql的同学,写sql语句应该不算什么;对应不熟悉sql的同学,可能更习惯于django中那种创建表的方式。
在视图中使用 App/UserInfo/views.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from tornado import webfrom Models.UserInfo.BaseClass import BaseClassfrom Models.UserInfo.UserModel import user_modelclass UserInfoHandler (web.RequestHandler, BaseClass) : def get (self) : """ 获取用户信息 :return: """ user_info = user_model("user_info" ) user_id = self.get_query_argument("id" ) user_info_obj = self.engine_user.query(user_info).filter(user_info.id==user_id).first() self.write(user_info_obj.name) self.finish()
最后配置路由 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 Urls/UserInfo.py: from App.UserInfo import viewsuser_urls = [ (r'/userinfo' , views.UserInfoHandler), ] application.py: from tornado import webfrom Config.config_base import settingsfrom Urls.UserInfo import user_urlsfrom Urls.Shop import shop_urls""" 路由配置 """ class Application (web.Application) : def __init__ (self) : urls = user_urls + shop_urls super(Application, self).__init__(urls, **settings)
启动服务后,就可以访问了。
异步非阻塞请求 django django中可以通过celery来实现异步任务,也可以使用asyncio和aiohttp实现异步。下面讲一下celery的使用: 首先需要安装 celery和 django-celery,使用pip安装就行了; 然后在zsettings.py中进行如下配置:
1 2 3 4 5 6 7 8 9 在INSTALLED_APPS中加入djcelery。 import djcelerydjcelery.setup_loader() BROKER_URL = 'redis://127.0.0.1:6379/2' CELERY_RESULT_BACKEND = 'redis://127.0.0.1:6379/3' BROKER_URL = 'amqp://test:test@192.168.173.1:5672/testhost' CELERY_RESULT_BACKEND = 'amqp://test:test@192.168.173.1:5672/testhost'
在需要使用异步的app中创建tasks.py文件,然后编辑该文件:
1 2 3 4 5 6 7 8 9 10 11 12 import timefrom celery import task@task def test (data) : """ 预处理 :param data: :return: """ time.sleep(3 ) return data
耗时的任务就可以放在使用@task修饰的函数中 在views.py中调用tasks.py中的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 from rest_framework.response import Responsefrom .tasks import testclass CeleryTrainView (APIView) : def get (self, request) : try : for i in range(0 , 5 ): ret = test.delay(str(i)) print("ret:" , ret) except Exception as e: return Response(dict(msg=str(e), code=10001 )) return Response(dict(msg="OK" , code=10000 ))
上面的结果ret是一个AsyncResult对象,可以通过这个对象拿到保存在CELERY_RESULT_BACKEND中的结果。如果想立即得到结果,可以直接调用get()方法,但是这样就会阻塞其他请求,直到结果返回:
1 2 3 4 5 6 7 8 9 10 11 12 13 from rest_framework.response import Responsefrom .tasks import testclass CeleryTrainView (APIView) : def get (self, request) : try : for i in range(0 , 5 ): ret = test.delay(str(i)) print("ret:" , ret.get()) except Exception as e: return Response(dict(msg=str(e), code=10001 )) return Response(dict(msg="OK" , code=10000 ))
最后启动celery:
1 2 3 4 python manage.py runserver python manage.py celery worker
tornado tornado中实现异步有回调和协程这两种方式,这里只举一个协程实现异步的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from tornado import webfrom tornado import genfrom tornado.httpclient import AsyncHTTPClientclass AsyncHandler (web.RequestHandler) : @gen.coroutine def get (self, *args, **kwargs) : client = AsyncHTTPClient() url = 'http://ip.taobao.com/service/getIpInfo.php?ip=14.130.112.24' resp = yield client.fetch(url) data = str(resp.body, encoding="utf-8" ) print("data:" , data) self.write(data) self.finish()
或者像下面这样,把获取ip信息的部分封装成一个函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from tornado import webfrom tornado import genfrom tornado.httpclient import AsyncHTTPClientclass AsyncHandler (web.RequestHandler) : @gen.coroutine def get (self, *args, **kwargs) : ip_info = yield self.get_ip_info() self.write(ip_info) self.finish() @gen.coroutine def get_ip_info (self) : client = AsyncHTTPClient() url = 'http://ip.taobao.com/service/getIpInfo.php?ip=14.130.112.24' resp = yield client.fetch(url) data = str(resp.body, encoding="utf-8" ) return data
也可以同时发起多个异步请求:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 from tornado import webfrom tornado import genfrom tornado.httpclient import AsyncHTTPClientclass AsyncHandler (web.RequestHandler) : @gen.coroutine def get (self, *args, **kwargs) : ips = [ "14.130.112.24" , "14.130.112.23" , "14.130.112.22" ] info1, info2, info3 = yield [self.get_ip_info(ips[0 ]), self.get_ip_info(ips[1 ]), self.get_ip_info(ips[2 ])] self.write(info1) self.write(info2) self.write(info3) self.finish() @gen.coroutine def get_ip_info (self, ip) : client = AsyncHTTPClient() url = 'http://ip.taobao.com/service/getIpInfo.php?ip=' + ip resp = yield client.fetch(url) data = str(resp.body, encoding="utf-8" ) return data
AsyncHTTPClient的fetch()方法有两种调用方式,一种是像上面那样只传入一个url的字符串,另一种是接收一个HTTPRequest对象作为参数,像下面这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @gen.coroutine def get_ip_info (self, ip) : client = AsyncHTTPClient() url = 'http://ip.taobao.com/service/getIpInfo.php?ip=' + ip header = {'Accept' : 'application/json;charset=utf-8' , 'Content-Type' : 'application/x-www-form-urlencoded;charset=utf-8' } param1 = 'test' http_request = HTTPRequest(url=url, method='POST' , headers=header, body=urlencode({'param1' : param1})) resp = yield client.fetch(http_request) data = str(resp.body, encoding="utf-8" ) return data
websocket的使用 django django中使用websocket需要安装第三方包dwebsocket。
tornado tornado中实现websocket功能需要用到tornado.websocket模块,主要有以下几个方法:open(), write_message(), on_message(), on_close()
1 2 3 4 open(): 当websocket客户端连接时所做的操作 write_message(): 使用这个方法向客户端发送消息 on_message(): 接收并处理客户端的消息 on_close(): websocket关闭连接时所作的操作
下面看一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 views.py: from tornado import websocketclass IndexHandler (web.RequestHandler) : def get (self, *args, **kwargs) : self.render("chat.html" ) class ChatHandler (websocket.WebSocketHandler) : clients = set() def open (self, *args, **kwargs) : self.clients.add(self) for client in self.clients: client.write_message("%s上线了" % self.request.remote_ip) def on_message (self, message) : for client in self.clients: client.write_message("%s: %s" % (self.request.remote_ip, message)) def on_close (self) : self.clients.remove(self) for client in self.clients: client.write_message("%s下线了" % self.request.remote_ip) def check_origin (self, origin) : """ 用于处理跨域问题 :param origin: :return: """ return True
路由:
1 2 3 4 5 6 7 8 9 10 from App.UserInfo import viewsuser_urls = [ (r'/index' , views.IndexHandler), (r'/chat' , views.ChatHandler), ]
chat.html:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > 聊天室</title > </head > <body > <div id ="content" style ="height: 500px;overflow: auto;" > </div > <div > <textarea id ="msg" > </textarea > <a href ="javascript:;" onclick ="sendMsg()" > 发送</a > </div > <script src ="{{ static_url('js/jquery.min.js') }}" > </script > <script type ="text/javascript" > var ws = new WebSocket("ws://192.168.1.104:8001/chat" ); ws.onmessage = function (data) { $("#content" ).append("<p>" + data.data +"</p>" ) }; function sendMsg () { var msg = $("#msg" ).val(); if (msg) { ws.send(msg); } } </script > </body > </html >
上面一个例子通过websocket实现了简单的聊天室功能。
以上就简单的比较了django和tornado几个方面的不同,它们各有优缺点,实际工作中可以根据不同的需求选择不同的框架进行开发。