使用Socket控制前後端的數據交換和Web應用的資源消耗

# 首發於CSDN

自從學會了使用Socket搭建一個簡單的伺服器並實現它同客戶端通信,我就一直很想在實際的工作中用上這個我很喜歡的功能,而其中最讓我容易想到的就是利用它來管理進程開始和停止的時機。但在之前的好幾次嘗試中,我要麼在過程中就發現有比Socket簡單得多而且也有效的方法,要麼就是最後發現即使用Socket也實現不了我的想法,只得另尋他路。這也算是成了我工作中一個不大不小的執念。

直到最近,我使用Django在後台構建了一個Web應用,由於其功能需要需要在後台運行,且會長時間消耗伺服器的大量(5%-10%)計算資源,但用戶可能不一定需要等這個程序全部運行完成就能得到他想要的結果。所以出於對降低伺服器負載的考慮,我需要讓它儘可能達到這樣一種情況——一旦用戶不再需要這個程序繼續工作,程序就能馬上停止。

如果這個程序是運行在前端的,那麼很簡單——一旦用戶關閉了網頁或跳轉到別的頁面,程序的運行就會自然終止了。然而我這個程序是運行在後端的。大家都知道http協議是一種無狀態的協議,也就是說,就算你在網頁上設計了一個關閉的開關,如果用戶能記得按它還好。如果用戶忘了按它(事實上我相信大部分時候用戶都會忘記,讓我來用,我覺得我也很可能忘記),你就得想其他辦法了。

當然,你可以在用戶嘗試退出頁面的時候彈一個提示框,提示用戶是否確定要離開該頁面?如果用戶選擇是,網頁就知道這個程序該停了,可是問題是,此時一切的變化都還只發生在前端,如何讓後端知道這個變化已經發生呢,從另一方面來說,後端在程序運行輸出結果之後,又如何告知前端呢?

由於我對Socket的執念,我幾乎第一時間就決定,要用Socket來做這個功能。經過不懈的嘗試,我終於成功用Socket完成了這個功能,現在就跟大家分享一下完成的過程。

功能規劃

其實對於一個單一頁面的web應用來說,用戶的使用邏輯還算是比較好預測的了。開始程序的方法,自然是只有按下」開始「按鈕一種。而當用戶不再需要這個程序繼續運行的時候,一共也就只有三種行為:一是按下我們提供的」結束搜索「按鈕(不太可能),二是通過各種手段(包括不限於點擊網頁標籤頁的關閉鍵、關閉瀏覽器、直接關機等)關閉網頁,三是直接去到其他頁面。後兩種本質是一樣的。而且都可以用彈窗提示是否離開來確定發送停止信號的時機。但如果用戶通過比較強行的手段關閉瀏覽器(忽略了提示),則仍然可能會導致前端沒機會向後端發出停止信號。而考慮到只要解決了網頁在被強行關閉的時候也能讓後端程序停止,就自然能讓網頁在正常關閉時的後端能夠停止,因此其實可以視為只有兩種情況。

1:用戶點擊停止按鈕

2:用戶強行關閉了網頁,網頁沒來得及送出停止信號

架構計劃:

一個首要的問題是:既然決定了要用Socket來解決,那麼必然會有一個伺服器端和一個或數個客戶端。那麼,後端的工作程序到底是用作客戶端還是伺服器端呢?我的第一反應是用作服務端。但是很快就能想到,對簡單的Socket應用來說,每次伺服器端在等待來自客戶端的消息時,進程都會處於掛起狀態。在這種設定下,客戶端,也就是前端不可能以非常高的頻率向服務端發送信息,那樣既浪費計算資源又會對網路造成很大的壓力。因此後端程序對來自前端的信號的等待必然會造成程序運行的延遲,由於整個程序運行過程很長。所以即使這個延遲很短,從整個程序的運行過程來看也會造成大量的時間浪費。所以,伺服器端只能用一個可以負擔的起等待代價的進程來擔任。

在決定了這點之後,我決定建立一個額外的線程來擔任伺服器端的角色。這裡可能要稍微解釋一下Django後端的運行原理:在一個完成度較高的頁面中,頁面的最終樣式應該是由模板和view.py中你位這個頁面寫的函數共同決定的,而模板中可以使用Django規定的語法在HTML代碼間的各處預留變數的位置,在view.py運算出需要的結果後,再用結果的變數對預留的位置逐一替換。我想過使用Ajax來傳送請求給後端。可Django中並沒有原生集成對Ajax的支持。因此最後我實際上是用了一種使用JS代碼定時提交POST請求的方法,曲線救國的完成了頁面的定時刷新。誠然這樣的方式需要刷新整個頁面,會比Ajax消耗更多資源,但好在這些消耗都是發生在用戶的計算機上的,這種實現方式對於這種較小規模的並發請求已經足夠了。

那既然是定時刷新,每次POST也就意味著view.py中相對應的函數會被運行一次,因此view.py中的函數也不太適合做伺服器端,但是如果有一個獨立於view.py和後端服務程序之外的伺服器端函數,那麼,view.py和後端服務程序都可以作為客戶端。定時向這個函數報告情況,當這個函數發現從客戶端報告上來的狀態發生了變化,就可以做出相應的調整了。

最後,程序的結構用圖片可以表示成這樣。

代碼實現:

1:view.py:

view.py最先啟動,當Django第一次收到用戶提交的POST請求時,首先決定好本進程要使用的地址和埠,(地址決定都用localhost,如何防止多個用戶被分配到同一個埠就是另外一個話題了,如何判斷是否是第一次提交POST請求也是)使用threading里的Thread單獨建立一個線程,也就是我們的Server,建立一個Socket實例sock,並傳給Server。

from threading import Threadnndef methodinview():n # ...其他功能...n sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)n sock.bind((127.0.0.1, port))n t = Thread(target=server, args=(sock,)) # sock作為參數傳入n t.start()n

view.py需要做的還不止這些,如果不是第一次提交POST請求,也就意味著如果不出意外,Server已經建立好並在等待信號了(至少是曾經建立過),因此這時既然view.py還在正常運行,也就是說用戶還停留在網頁上並且沒有點擊停止按鈕。既然在網頁被關閉時可能並沒有機會向Server報告這一變化,不如就反過來,在每次正常運行時告訴Sever自己仍在正常運行。這種信號一般都被稱之為心跳(heartbeat)信號。

import socketnnsockhb = socket.socket(socket.AF_INET, socket.SOCK_STREAM)nsockhb.connect((127.0.0.1, port))nsockhb.send(bkeepalive)n

如果收到了用戶的停止指令,則view.py還需要向Server報告程序需要停止。

if stopbutton: # stopbutton代表了用戶發送的停止指令n requeststop = socket.socket(socket.AF_INET, socket.SOCK_STREAM)n requeststop.connect((127.0.0.1, port))n requeststop.send(btimetostop)n

2:後端程序:

後端程序會不停的向Server詢問是否可以停止,由於Server在常規狀態下處理完每個請求並做出回復後都會馬上待命等待處理下一個請求,因此這種詢問幾乎不會造成延時。而如果後端程序自己已經完成,則會向Server發送一條告知自己已經完成的信息,由Server做出處理。由於要多次調用,我把向Server發信的語句寫成了一個函數。

import socketnn@staticmethodndef sendmessage(port, message):n sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)n sock.connect((127.0.0.1, port))n time.sleep(0.1)n sock.send(message)n msgrecieved = sock.recv(1024)n print(Email Parser recieved Messeage back from view:, msgrecieved)n if msgrecieved == byes or msgrecieved == bgotit:n sock.close()n return Truen elif msgrecieved == bno:n return Falsen else:n return Falsen

如果多次發送失敗,則認為程序已經可以停止。

while True:n # ...功能代碼...n print(Send ifstop Message)n try:n answer = self.sendmessage(port, bifstop)n except:n ifstopfailnum += 1n print(ifstopfailnum:, ifstopfailnum)n if ifstopfailnum > 7:n returnn time.sleep(3)n continuen if answer is True:n returnn #...功能代碼...n

3:Server:

Server要考慮的事情就多得多。它需要接收從兩個客戶端傳來的信號,並分別作出回應。在前面已經有提到guo:後端程序會不斷詢問Server是否需要結束。而view.py在運行時則會定時向Server報告自己正在運行。後端程序大概0.8秒左右會詢問一次Server是否需要停止,而view.py則是6秒一次(這些數值都可以根據實際需要自由調整)。因此我設定了一個25次的閾值。也就是說,在正常情況下無論後端程序如何詢問Server是否停止,Server都會回答"no",但是在3次本該收到view.py的心跳信號(18秒)卻沒有收到後。Server就會認為用戶已經關閉了網頁,此時,Server會直接關閉。而如果接到了view.py的"timetostop"信號,Server在之後則會回復後端"yes"。

def server(sock):n sock.listen(5)n notkeepalive = 0 # 設定沒有收到心跳信號的計數器為0n while notkeepalive < 25:n try:n print(Waiting for signal)n connection, address = sock.accept()n recv = connection.recv(1024)n print(signal recieved:, recv)n if recv == bifstop: # 只要收到的不是心跳信號(keepalive信號),計數器就+1n notkeepalive += 1n print(Notkeepalive value:, notkeepalive)n connection.send(bno)n elif recv == bend: # 如果收到後端程序運行的結束信號,則回復收到n try:n connection.send(bgotit)n sock.close()n print(No pages anymore, Heartbeat server stopped)n returnn except Exception as e:n passn elif recv == bkeepalive:n notkeepalive = 0n elif recv == btimetostop: # 收到用戶發送的停止指令,接下來再收到後端程序的詢問就回復"yes"n print(Waiting for stop signal)n connection, address = sock.accept()n recv = connection.recv(1024)n if recv == bifstop:n connection.send(byes)n sock.close()n print(Time to stop, Heartbeat server stopped)n returnn elif recv == bnoanymore:n connection.send(bgotit)n sock.close()n print(No pages anymore, Heartbeat server stopped)n returnn else:n notkeepalive += 1n print(Notkeepalive value:, notkeepalive)n connection.send(bno)n except:n print(heartbeat error:n, traceback.print_exc())n time.sleep(10)n sock.close() # 如果長時間未收到心跳信號,則停止Server,後端程序在連續多次發送心跳信號失敗後,也會自行停止。n print(No keepalive signal, stop heartbeat service)n returnn

在經過無數次微調和嘗試之後,這個由一個伺服器端和兩個客戶端組成的Socket小網路終於正常運作了起來。再加上後端程序本身的性能優化,這個程序可以說是終於初步的做好了應對一定數量的並發請求的準備。另外,也算是了了我一樁心愿。


推薦閱讀:

mongodb+django 怎麼配置?
國內用 Django 開發的知名站點有哪些?
用Django學習設計網站後台有什麼好書可以入門和深入學習?
django優秀的github項目推薦?
為什麼感覺django很難呢?

TAG:Python | Web开发 | Django框架 |