django源碼分析——本地runserver分析
本文環境python3.5.2,django1.10.x系列 1.根據上一篇文章分析了,django-admin startproject與startapp的分析流程後,根據django的官方實例此時編寫好了基本的路由和相應的處理函數,此時需要調試我們寫的接口此時本地調試,django框架提供了python manage.py runserver 命令來本地調試。 2.runserver的特點是啟用多線程處理請求,並可以監控當文件修改後自動重啟服務,以達到服務重啟極大方便了本地的調試,根據官方文檔該命令只用於本地調試,不簡易部署到生成環境。 3.當生成項目後,找到manage.py文件,其實代碼與django-admin代碼一樣,都是調用django註冊的方法。 我們直接分析runserver的命令位於django/core/management/commands/runserver.py,由於上一篇博文已經分析過具體的調用過程,我們直接就看其中的Command定義:
class Command(BaseCommand):
help = "Starts a lightweight Web server for development."
# Validation is called explicitly each time the server is reloaded.
requires_system_checks = False
leave_locale_alone = True
default_port = ‘8000‘ # 默認啟動服務監聽的端口
def add_arguments(self, parser): # 創建幫助信息
parser.add_argument(
‘addrport‘, nargs=‘?‘,
help=‘Optional port number, or ipaddr:port‘
)
parser.add_argument(
‘--ipv6‘, ‘-6‘, action=‘store_true‘, dest=‘use_ipv6‘, default=False,
help=‘Tells Django to use an IPv6 address.‘,
)
parser.add_argument(
‘--nothreading‘, action=‘store_false‘, dest=‘use_threading‘, default=True,
help=‘Tells Django to NOT use threading.‘,
)
parser.add_argument(
‘--noreload‘, action=‘store_false‘, dest=‘use_reloader‘, default=True,
help=‘Tells Django to NOT use the auto-reloader.‘,
)
def execute(self, *args, **options): # 調用處理方法
if options[‘no_color‘]:
# We rely on the environment because it‘s currently the only
# way to reach WSGIRequestHandler. This seems an acceptable
# compromise considering `runserver` runs indefinitely.
os.environ[str("DJANGO_COLORS")] = str("nocolor")
super(Command, self).execute(*args, **options) # 調用父類的執行方法
def get_handler(self, *args, **options):
"""
Returns the default WSGI handler for the runner.
"""
return get_internal_wsgi_application()
def handle(self, *args, **options): # 調用處理方法
from django.conf import settings # 導入配置文件
if not settings.DEBUG and not settings.ALLOWED_HOSTS: # 檢查是否是debug模式,如果不是則ALLOWED_HOSTS不能為空
raise CommandError(‘You must set settings.ALLOWED_HOSTS if DEBUG is False.‘)
self.use_ipv6 = options[‘use_ipv6‘]
if self.use_ipv6 and not socket.has_ipv6: # 檢查輸入參數中是否是ipv6格式,檢查當前python是否支持ipv6
raise CommandError(‘Your Python does not support IPv6.‘)
self._raw_ipv6 = False
if not options[‘addrport‘]: # 如果輸入參數中沒有輸入端口則使用默認的端口
self.addr = ‘‘
self.port = self.default_port
else:
m = re.match(naiveip_re, options[‘addrport‘]) # 檢查匹配的ip格式
if m is None:
raise CommandError(‘"%s" is not a valid port number ‘
‘or address:port pair.‘ % options[‘addrport‘])
self.addr, _ipv4, _ipv6, _fqdn, self.port = m.groups() # 找出匹配的數據
if not self.port.isdigit(): # 檢查端口是否為數字
raise CommandError("%r is not a valid port number." % self.port)
if self.addr: # 檢查解析出的地址是否合法的ipv6地址
if _ipv6:
self.addr = self.addr[1:-1]
self.use_ipv6 = True
self._raw_ipv6 = True
elif self.use_ipv6 and not _fqdn:
raise CommandError(‘"%s" is not a valid IPv6 address.‘ % self.addr)
if not self.addr: # 如果沒有輸入ip地址則使用默認的地址
self.addr = ‘::1‘ if self.use_ipv6 else ‘127.0.0.1‘
self._raw_ipv6 = self.use_ipv6
self.run(**options) # 運行
def run(self, **options):
"""
Runs the server, using the autoreloader if needed
"""
use_reloader = options[‘use_reloader‘] # 根據配置是否自動加載,如果沒有輸入則default=True
if use_reloader:
autoreload.main(self.inner_run, None, options) # 當開啟了自動加載時,則調用自動啟動運行
else:
self.inner_run(None, **options) # 如果沒有開啟文件更新自動重啟服務功能則直接運行
def inner_run(self, *args, **options):
# If an exception was silenced in ManagementUtility.execute in order
# to be raised in the child process, raise it now.
autoreload.raise_last_exception()
threading = options[‘use_threading‘] # 是否開啟多線程模式,當不傳入時則默認為多線程模式運行
# ‘shutdown_message‘ is a stealth option.
shutdown_message = options.get(‘shutdown_message‘, ‘‘)
quit_command = ‘CTRL-BREAK‘ if sys.platform == ‘win32‘ else ‘CONTROL-C‘ # 打印停止服務信息
self.stdout.write("Performing system checks...\n\n") # 想標準輸出輸出數據
self.check(display_num_errors=True) # 檢查
# Need to check migrations here, so can‘t use the
# requires_migrations_check attribute.
self.check_migrations() # 檢查是否migrations是否與數據庫一致
now = datetime.now().strftime(‘%B %d, %Y - %X‘) # 獲取當前時間
if six.PY2:
now = now.decode(get_system_encoding()) # 解析當前時間
self.stdout.write(now) # 打印時間等信息
self.stdout.write((
"Django version %(version)s, using settings %(settings)r\n"
"Starting development server at http://%(addr)s:%(port)s/\n"
"Quit the server with %(quit_command)s.\n"
) % {
"version": self.get_version(),
"settings": settings.SETTINGS_MODULE,
"addr": ‘[%s]‘ % self.addr if self._raw_ipv6 else self.addr,
"port": self.port,
"quit_command": quit_command,
})
try:
handler = self.get_handler(*args, **options) # 獲取信息處理的handler,默認返回wsgi
run(self.addr, int(self.port), handler,
ipv6=self.use_ipv6, threading=threading) # 調用運行函數
except socket.error as e:
# Use helpful error messages instead of ugly tracebacks.
ERRORS = {
errno.EACCES: "You don‘t have permission to access that port.",
errno.EADDRINUSE: "That port is already in use.",
errno.EADDRNOTAVAIL: "That IP address can‘t be assigned to.",
}
try:
error_text = ERRORS[e.errno]
except KeyError:
error_text = force_text(e)
self.stderr.write("Error: %s" % error_text)
# Need to use an OS exit because sys.exit doesn‘t work in a thread
os._exit(1)
except KeyboardInterrupt:
if shutdown_message:
self.stdout.write(shutdown_message)
sys.exit(0)
該command主要進行了檢查端口是否正確,是否多線程開啟,是否開啟了文件監控自動重啟功能,如果開啟了自動重啟功能則,
autoreload.main(self.inner_run, None, options)
調用django/utils/autoreload.py中的mian函數處理,如下所示:
def main(main_func, args=None, kwargs=None):
if args is None:
args = ()
if kwargs is None:
kwargs = {}
if sys.platform.startswith(‘java‘): # 獲取當前reloader的處理函數
reloader = jython_reloader
else:
reloader = python_reloader
wrapped_main_func = check_errors(main_func) # 添加對man_func的出錯處理方法
reloader(wrapped_main_func, args, kwargs) # 運行重啟導入函數
目前電腦運行的環境reloader為python_reloader,查看代碼為:
def python_reloader(main_func, args, kwargs):
if os.environ.get("RUN_MAIN") == "true": # 獲取環境變量是RUN_MAIN是否為"true"
thread.start_new_thread(main_func, args, kwargs) # 開啟子線程運行服務程序
try:
reloader_thread() # 調用監控函數
except KeyboardInterrupt:
pass
else:
try:
exit_code = restart_with_reloader() # 調用重啟函數
if exit_code < 0:
os.kill(os.getpid(), -exit_code)
else:
sys.exit(exit_code)
except KeyboardInterrupt:
pass
第一次運行時,RUN_MAIN沒有設置,此時會運行restart_with_reloader函數
def restart_with_reloader():
while True:
args = [sys.executable] + [‘-W%s‘ % o for o in sys.warnoptions] + sys.argv # 獲取當前運行程序的執行文件
if sys.platform == "win32":
args = [‘"%s"‘ % arg for arg in args]
new_environ = os.environ.copy() # 拷貝當前的系統環境變量
new_environ["RUN_MAIN"] = ‘true‘ # 設置RUN_MAIN為true
exit_code = os.spawnve(os.P_WAIT, sys.executable, args, new_environ) # 啟動新進程執行當前代碼,如果進程主動退出返回退出狀態嗎
if exit_code != 3: # 如果返回狀態碼等於3則是因為監控到文件有變化退出,否則是其他錯誤,就結束循環退出
return exit_code
該函數主要是對當前執行的可執行文件進行重啟,並且設置環境變量RUN_MAIN為true,此時再重新運行該程序時,此時再python_reloader中執行:
if os.environ.get("RUN_MAIN") == "true": # 獲取環境變量是RUN_MAIN是否為"true"
thread.start_new_thread(main_func, args, kwargs) # 開啟子線程運行服務程序
try:
reloader_thread() # 調用監控函數
except KeyboardInterrupt:
pass
開啟一個子線程執行運行服務的主函數,然後重啟進程執行reloader_thread函數:
def reloader_thread():
ensure_echo_on()
if USE_INOTIFY: # 如果能夠導入pyinotify模塊
fn = inotify_code_changed # 使用基於pyinotify的文件監控機制
else:
fn = code_changed # 使用基於對所有文件修改時間的判斷來判斷是否進行文件的更新
while RUN_RELOADER:
change = fn() # 獲取監控返回值
if change == FILE_MODIFIED: # 如果監控到文件修改則重啟服務運行進程
sys.exit(3) # force reload
elif change == I18N_MODIFIED: # 監控是否是本地字符集的修改
reset_translations()
time.sleep(1) # 休眠1秒鐘
此時主進程會一直循環執行該函數,間隔一秒調用代碼變化監控函數執行,如果安裝了pyinotify,則使用該模塊監控代碼是否變化,否則就使用django自己實現的文件監控,在這裏就分析一下django自己實現的代碼是否變化檢測函數:
def code_changed():
global _mtimes, _win
for filename in gen_filenames():
stat = os.stat(filename) # 獲取每個文件的狀態屬性
mtime = stat.st_mtime # 獲取數據最後的修改時間
if _win:
mtime -= stat.st_ctime
if filename not in _mtimes:
_mtimes[filename] = mtime # 如果全局變量中沒有改文件則存入,該文件的最後修改時間
continue
if mtime != _mtimes[filename]: # 如果已經存入的文件的最後修改時間與當前獲取文件的最後修改時間不一致則重置保存最後修改時間變量
_mtimes = {}
try:
del _error_files[_error_files.index(filename)]
except ValueError:
pass
return I18N_MODIFIED if filename.endswith(‘.mo‘) else FILE_MODIFIED # 如果修改的文件是.mo結尾則是local模塊更改,否則就是項目文件修改需要重啟服務
return False
首先,調用gen_filenames函數,該函數主要是獲取要監控項目的所有文件,然後將所有文件的最後編輯時間保存起來,當第二次檢查時比較是否有新文件添加,舊文件的最後編輯時間是否已經改變,如果改變則重新加載:
def gen_filenames(only_new=False):
"""
Returns a list of filenames referenced in sys.modules and translation
files.
"""
# N.B. ``list(...)`` is needed, because this runs in parallel with
# application code which might be mutating ``sys.modules``, and this will
# fail with RuntimeError: cannot mutate dictionary while iterating
global _cached_modules, _cached_filenames
module_values = set(sys.modules.values()) # 獲取模塊的所有文件路徑
_cached_filenames = clean_files(_cached_filenames) # 檢查緩存的文件列表
if _cached_modules == module_values: # 判斷所有模塊是否與緩存一致
# No changes in module list, short-circuit the function
if only_new:
return []
else:
return _cached_filenames + clean_files(_error_files)
new_modules = module_values - _cached_modules
new_filenames = clean_files(
[filename.__file__ for filename in new_modules
if hasattr(filename, ‘__file__‘)]) # 檢查獲取的文件是否存在,如果存在就添加到文件中
if not _cached_filenames and settings.USE_I18N:
# Add the names of the .mo files that can be generated
# by compilemessages management command to the list of files watched.
basedirs = [os.path.join(os.path.dirname(os.path.dirname(__file__)),
‘conf‘, ‘locale‘),
‘locale‘]
for app_config in reversed(list(apps.get_app_configs())):
basedirs.append(os.path.join(npath(app_config.path), ‘locale‘)) # 添加項目目錄下的locale
basedirs.extend(settings.LOCALE_PATHS)
basedirs = [os.path.abspath(basedir) for basedir in basedirs
if os.path.isdir(basedir)] # 如果現在的文件都是文件夾,將文件夾添加到路徑中去
for basedir in basedirs:
for dirpath, dirnames, locale_filenames in os.walk(basedir):
for filename in locale_filenames:
if filename.endswith(‘.mo‘):
new_filenames.append(os.path.join(dirpath, filename))
_cached_modules = _cached_modules.union(new_modules) # 添加新增的模塊文件
_cached_filenames += new_filenames # 將新增的文件添加到緩存文件中
if only_new:
return new_filenames + clean_files(_error_files)
else:
return _cached_filenames + clean_files(_error_files)
def clean_files(filelist):
filenames = []
for filename in filelist: # 所有文件的全路徑集合
if not filename:
continue
if filename.endswith(".pyc") or filename.endswith(".pyo"): # 監控新的文件名
filename = filename[:-1]
if filename.endswith("$py.class"):
filename = filename[:-9] + ".py"
if os.path.exists(filename): # 檢查文件是否存在,如果存在就添加到列表中
filenames.append(filename)
return filenames
至此,django框架的自動加載功能介紹完成,主要實現思路是,
1.第一次啟動時,執行到restart_with_reloader時,自動重頭執行加載
2.第二次執行時,會執行python_reloader中的RUN_MAIN為true的代碼
3.此時開啟一個子線程執行服務運行程序,主進程進行循環,監控文件是否發生變化,如果發生變化則sys.exit(3),此時循環程序會繼續重啟,依次重復步驟2
4.如果進程的退出code不為3,則終止整個循環
當監控運行完成後繼續執行self.inner_run函數,當執行到
handler = self.get_handler(*args, **options) # 獲取信息處理的handler,默認返回wsgi
run(self.addr, int(self.port), handler,
ipv6=self.use_ipv6, threading=threading) # 調用運行函數
獲取wsgi處理handler,然後調用django/core/servers/basshttp.py中的run方法
def run(addr, port, wsgi_handler, ipv6=False, threading=False):
server_address = (addr, port) # 服務監聽的地址和端口
if threading: # 如果是多線程運行
httpd_cls = type(str(‘WSGIServer‘), (socketserver.ThreadingMixIn, WSGIServer), {} ) # 生成一個繼承自socketserver.ThreadingMixIn, WSGIServer的類
else:
httpd_cls = WSGIServer
httpd = httpd_cls(server_address, WSGIRequestHandler, ipv6=ipv6) # 實例化該類
if threading:
# ThreadingMixIn.daemon_threads indicates how threads will behave on an
# abrupt shutdown; like quitting the server by the user or restarting
# by the auto-reloader. True means the server will not wait for thread
# termination before it quits. This will make auto-reloader faster
# and will prevent the need to kill the server manually if a thread
# isn‘t terminating correctly.
httpd.daemon_threads = True # 等到子線程執行完成
httpd.set_app(wsgi_handler) # 設置服務類的處理handler
httpd.serve_forever()
調用標準庫中的wsgi處理,django標準庫的wsgi處理再次就不再贅述(在gunicorn源碼分析中已經分析過),所以本地調試的server依賴於標準庫,django並沒有提供高性能的server端來處理連接,所以不建議使用該命令部署到生產環境中。
django源碼分析——本地runserver分析