epollpython
㈠ 怎麼通俗理解python epoll
首先我們來定義流的概念,一個流可以是文件,socket,pipe等等可以進行I/O操作的內核對象。不管是文件,還是套接字,還是管道,我們都可以把他們看作流。
現在我們來討論I/O的操作,通過read,我們可以從流中讀入數據;通過write,我們可以往流寫入數據。現在假定一個情形,我們需要從流中讀數據,但是流中還沒有數據,(典型的例子為,客戶端要從socket讀如數據,但是伺服器還沒有把數據傳回來),這時候該怎麼辦?
阻塞:阻塞是個什麼概念呢?比如某個時候你在等快遞,但是你不知道快遞什麼時候過來,而且你沒有別的事可以干(或者說接下來的事要等快遞來了才能做);那麼你可以去睡覺了,因為你知道快遞把貨送來時一定會給你打個電話(假定一定能叫醒你)。
非阻塞忙輪詢:接著上面等快遞的例子,如果用忙輪詢的方法,那麼你需要知道快遞員的手機號,然後每分鍾給他掛個電話:「你到了沒?」
很明顯一般人不會用第二種做法,不僅顯很無腦,浪費話費不說,還佔用了快遞員大量的時間。
大部分程序也不會用第二種做法,因為第一種方法經濟而簡單,經濟是指消耗很少的CPU時間,如果線程睡眠了,就掉出了系統的調度隊列,暫時不會去瓜分CPU寶貴的時間片了。
為了了解阻塞是如何進行的,我們來討論緩沖區,以及內核緩沖區,最終把I/O事件解釋清楚。緩沖區的引入是為了減少頻繁I/O操作而引起頻繁的系統調用(你知道它很慢的),當你操作一個流時,更多的是以緩沖區為單位進行操作,這是相對於用戶空間而言。對於內核來說,也需要緩沖區。
假設有一個管道,進程A為管道的寫入方,B為管道的讀出方。一開始內核緩沖區是空的,B作為讀出方,被阻塞著。然後首先A往管道寫入,這時候內核緩沖區由空的狀態變到非空狀態,內核就會產生一個事件告訴B該醒來了,這個事件姑且稱之為「緩沖區非空」。但是「,緩沖區非空」事件通知B後,B卻還沒有讀出數據;且內核許諾了不能把寫入管道中的數據丟掉這個時候,A寫入的數據會滯留在內核緩沖區中,如果內核也緩沖區滿了,B仍未開始讀數據,最終內核緩沖區會被填滿,這個時候會產生一個I/O事件,告訴進程A,你該等等(阻塞)了,我們把這個事件定義為「緩沖區滿」。後來B終於開始讀數據了,於是內核的緩沖區空了出來,這時候內核會告訴A,內核緩沖區有空位了,你可以從長眠中醒來了,繼續寫數據了,我們把這個事件叫做「緩沖區非滿」。也許事件「緩沖區非滿「已經通知了A,但是A也沒有數據寫入了,而B繼續讀出數據,知道內核緩沖區空了。這個時候內核就告訴B,你需要阻塞了!,我們把這個時間定為「緩沖區空」。
這四個情形涵蓋了四個I/O事件,緩沖區滿,緩沖區空,緩沖區非空,緩沖區非滿(注都是說的內核緩沖區,且這四個術語都是我生造的,僅為解釋其原理而造)。這四個I/O事件是進行阻塞同步的根本。(如果不能理解「同步」是什麼概念,請學習操作系統的鎖,信號量,條件變數等任務同步方面的相關知識)。
然後我們來說說阻塞I/O的缺點:阻塞I/O模式下,一個線程只能處理一個流的I/O事件。如果想要同時處理多個流,要麼多進程(fork),要麼多線程(pthread_create),很不幸這兩種方法效率都不高。
現在我們再來考慮一下」非阻塞忙輪詢「的I/O方式,我們發現我們可以同時處理多個流了(把一個流從阻塞模式切換到非阻塞模式再此不予討論):
[java]view plain
whiletrue{
active_stream[]=epoll_wait(epollfd)
foriinactive_stream[]{
readorwritetill
}
}
[java]view plain
whiletrue{
active_stream[]=epoll_wait(epollfd)
foriinactive_stream[]{
readorwritetill
}
}
㈡ mac os x 下的python 為什麼沒有epoll
介紹
從2.6版本開始, python 提供了使用linux epoll 的功能. 這篇文章通過3個例子來大致介紹如何使用它. 歡迎提問和反饋.
阻塞式socket通訊
第一個例子是一個簡單的python3.0版本的伺服器代碼, 監聽8080埠的http請求, 列印結果到命令行, 回應http response給客戶端.
行 9: 建立伺服器的socket
行 10: 允許11行的bind()操作, 即使其他程序也在監聽同樣的埠. 不然的話, 這個程序只能在其他程序停止使用這個埠之後的1到2分鍾後才能執行.
行 11: 綁定socket到這台機器上所有IPv4地址上的8080埠.
行 12: 告訴伺服器開始響應從客戶端過來的連接請求.
行 14: 程序會一直停在這里, 直到建立了一個連接. 這個時候, 伺服器socket會建立一個新的socket, 用來和客戶端通訊. 這個新的socket是accept()的返回值, address對象標示了客戶端的IP地址和埠.
行 15-17: 接收數據, 直到一個完整的http請求被接收完畢. 這是一個簡單的http伺服器實現.
行 18: 為了方便驗證, 列印客戶端過來的請求到命令行.
行 19: 發送回應.
行 20-22: 關閉連接, 以及伺服器的監聽socket.
python官方 HOWTO 裡面有具體如何使用socket編程的描述.
1 import socket
2
3 EOL1 = b'\n\n'
4 EOL2 = b'\n\r\n'
5 response = b'HTTP/1.0 200 OK\r\nDate: Mon, 1 Jan 1996 01:01:01 GMT\r\n'
6 response += b'Content-Type: text/plain\r\nContent-Length: 13\r\n\r\n'
7 response += b'Hello, world!'
8
9 serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
10 serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
11 serversocket.bind(('0.0.0.0', 8080))
12 serversocket.listen(1)
13
14 connectiontoclient, address = serversocket.accept()
15 request = b''
16 while EOL1 not in request and EOL2 not in request:
17 request += connectiontoclient.recv(1024)
18 print(request.decode())
19 connectiontoclient.send(response)
20 connectiontoclient.close()
21
22 serversocket.close()
第2個例子, 我們在15行加上了一個循環, 用來循環處理客戶端請求, 直到我們中斷這個過程(在命令行下面輸入鍵盤中斷, 比如Ctrl-C). 這個例子更明顯地表示出來了, 伺服器socket並沒有用來做數據處理, 而是接受伺服器過來的連接, 然後建立一個新的socket, 用來和客戶端通訊.
最後的23-24行確保伺服器的監聽socket最後總是close掉, 即使出現了異常.
1 import socket
2
3 EOL1 = b'\n\n'
4 EOL2 = b'\n\r\n'
5 response = b'HTTP/1.0 200 OK\r\nDate: Mon, 1 Jan 1996 01:01:01 GMT\r\n'
6 response += b'Content-Type: text/plain\r\nContent-Length: 13\r\n\r\n'
7 response += b'Hello, world!'
8
9 serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
10 serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
11 serversocket.bind(('0.0.0.0', 8080))
12 serversocket.listen(1)
13
14 try:
15 while True:
16 connectiontoclient, address = serversocket.accept()
17 request = b''
18 while EOL1 not in request and EOL2 not in request:
19 request += connectiontoclient.recv(1024)
20 print('-'*40 + '\n' + request.decode()[:-2])
21 connectiontoclient.send(response)
22 connectiontoclient.close()
23 finally:
24 serversocket.close()
非同步socket和linux epoll的優勢
第2個例子裡面的socket採用的是阻塞方式, 因為python解釋器在出現事件之前都處在停止狀態. 16行的accept()一直阻塞, 直到新的連接進來. 19行的recv()也是一直阻塞, 直到從客戶端收到數據(或者直到沒有數據可以接收). 21行的send()也一直阻塞, 直到所有需要發送給客戶端的數據都交給了linux內核的發送隊列.
當一個程序採用阻塞socket的時候, 它經常採用一個線程(甚至一個進程)一個socket通訊的模式. 主線程保留伺服器監聽socket, 接受進來的連接, 一次接受一個連接, 然後把生成的socket交給一個分離的線程去做交互. 因為一個線程只和一個客戶端通訊, 在任何位置的阻塞都不會造成問題. 阻塞本身不會影響其他線程的工作.
多線程阻塞socket模式代碼清晰, 但是有幾個缺陷, 可能很難確保線程間資源共享工作正常, 可能在只有一個CPU的機器上效率低下.
C10K(單機1萬連接問題!) 探討了其他處理並行socket通訊的模式. 一種是採用非同步socket. socket不會阻塞, 直到特定事件發生. 程序在非同步socket上面進行一個特定操作, 並且立即得到一個結果, 不管執行成功或者失敗. 然後讓程序決定下一步怎麼做. 因為非同步socket是非阻塞的, 我們可以不採用多線程. 所有的事情都可以在一個線程裡面完成. 雖然這種模式有它需要面對的問題, 它對於特定程序來說還是不錯的選擇. 也可以和多線程合起來使用: 單線程的非同步socket可以當作伺服器上面處理網路的一個模塊, 而線程可以用來訪問阻塞式的資源, 比如資料庫.
Linux 2.6有一些方式來管理非同步socket, python API能夠用的有3種: select, poll和epoll. epoll和poll比select性能更好, 因為python程序不需要為了特定的事件去查詢單獨的socket, 而是依賴操作系統來告訴你什麼socket產生了什麼事件. epoll比poll性能更好, 因為它不需要每次python程序查詢的時候, 操作系統都去檢查所有的socket, 在事件產生的時候, linux跟蹤他們, 然後在python程序調用的時候, 返回具體的列表. 所以epoll在大量(上千)並行連接下, 是一種更有效率, 伸縮性更強的機制. 圖示.
採用epoll的非同步socket編程示例
採用epoll的程序一般這樣操作:
建立一個epoll對象
告訴epoll對象, 對於一些socket監控一些事件.
問epoll, 從上次查詢以來什麼socket產生了什麼事件.
針對這些socket做特定操作.
告訴epoll, 修改監控socket和/或監控事件.
重復第3步到第5步, 直到結束.
銷毀epoll對象.
採用非同步socket的時候第3步重復了第2步的事情. 這里的程序更復雜, 因為一個線程需要和多個客戶端交互.
行 1: select模塊帶有epoll功能
行 13: 因為socket默認是阻塞的, 我們需要設置成非阻塞(非同步)模式.
行 15: 建立一個epoll對象.
行 16: 注冊伺服器socket, 監聽讀取事件. 伺服器socket接收一個連接的時候, 產生一個讀取事件.
行 19: connections表映射文件描述符(file descriptors, 整型)到對應的網路連接對象上面.
行 21: epoll對象查詢一下是否有感興趣的事件發生, 參數1說明我們最多等待1秒的時間. 如果有對應事件發生, 立刻會返回一個事件列表.
行 22: 返回的events是一個(fileno, event code)tuple列表. fileno是文件描述符, 是一個整型數.
行 23: 如果是伺服器socket的事件, 那麼需要針對新的連接建立一個socket.
行 25: 設置socket為非阻塞模式.
行 26: 注冊socket的read(EPOLLIN)事件.
行 31: 如果讀取事件發生, 從客戶端讀取新數據.
行 33: 一旦完整的http請求接收到, 取消注冊讀取事件, 注冊寫入事件(EPOLLOUT), 寫入事件在能夠發送數據回客戶端的時候產生.
行 34: 列印完整的http請求, 展示即使通訊是交錯的, 數據本身是作為一個完整的信息組合和處理的.
行 35: 如果寫入事件發生在一個客戶端socket上面, 我們就可以發送新數據到客戶端了.
行s 36-38: 一次發送一部分返回數據, 直到所有數據都交給操作系統的發送隊列.
行 39: 一旦所有的返回數據都發送完, 取消監聽讀取和寫入事件.
行 40: 如果連接被明確關閉掉, 這一步是可選的. 這個例子採用這個方法是為了讓客戶端首先斷開, 告訴客戶端沒有數據需要發送和接收了, 然後讓客戶端斷開連接.
行 41: HUP(hang-up)事件表示客戶端斷開了連接(比如 closed), 所以伺服器這端也會斷開. 不需要注冊HUP事件, 因為它們都會標示到注冊在epoll的socket.
行 42: 取消注冊.
行 43: 斷開連接.
行s 18-45: 在這里的異常捕捉的作用是, 我們的例子總是採用鍵盤中斷來停止程序執行.
行s 46-48: 雖然開啟的socket不需要手動關閉, 程序退出的時候會自動關閉, 明確寫出來這樣的代碼, 是更好的編碼風格.
㈢ python2.7怎麼實現非同步
改進之前
之前,我的查詢步驟很簡單,就是:
前端提交查詢請求 --> 建立資料庫連接 --> 新建游標 --> 執行命令 --> 接受結果 --> 關閉游標、連接
這幾大步驟的順序執行。
這裡面當然問題很大:
建立資料庫連接實際上就是新建一個套接字。這是進程間通信的幾種方法里,開銷最大的了。
在「執行命令」和「接受結果」兩個步驟中,線程在阻塞在資料庫內部的運行過程中,資料庫連接和游標都處於閑置狀態。
這樣一來,每一次查詢都要順序的新建資料庫連接,都要阻塞在資料庫返回結果的過程中。當前端提交大量查詢請求時,查詢效率肯定是很低的。
第一次改進
之前的模塊里,問題最大的就是第一步——建立資料庫連接套接字了。如果能夠一次性建立連接,之後查詢能夠反復服用這個連接就好了。
所以,首先應該把資料庫查詢模塊作為一個單獨的守護進程去執行,而前端app作為主進程響應用戶的點擊操作。那麼兩條進程怎麼傳遞消息呢?翻了幾天Python文檔,終於構思出來:用隊列queue作為生產者(web前端)向消費者(資料庫後端)傳遞任務的渠道。生產者,會與SQL命令一起,同時傳遞一個管道pipe的連接對象,作為任務完成後,回傳結果的渠道。確保,任務的接收方與發送方保持一致。
作為第二個問題的解決方法,可以使用線程池來並發獲取任務隊列中的task,然後執行命令並回傳結果。
第二次改進
第一次改進的效果還是很明顯的,不用任何測試手段。直接點擊頁面鏈接,可以很直觀地感覺到反應速度有很明顯的加快。
但是對於第二個問題,使用線程池還是有些欠妥當。因為,CPython解釋器存在GIL問題,所有線程實際上都在一個解釋器進程里調度。線程稍微開多一點,解釋器進程就會頻繁的切換線程,而線程切換的開銷也不小。線程多一點,甚至會出現「抖動」問題(也就是剛剛喚醒一個線程,就進入掛起狀態,剛剛換到棧幀或內存的上下文,又被換回內存或者磁碟),效率大大降低。也就是說,線程池的並發量很有限。
試過了多進程、多線程,只能在單個線程里做文章了。
Python中的asyncio庫
Python里有大量的協程庫可以實現單線程內的並發操作,比如Twisted、Gevent等等。Python官方在3.5版本里提供了asyncio庫同樣可以實現協程並發。asyncio庫大大降低了Python中協程的實現難度,就像定義普通函數那樣就可以了,只是要在def前面多加一個async關鍵詞。async def函數中,需要阻塞在其他async def函數的位置前面可以加上await關鍵詞。
import asyncio
async def wait():
await asyncio.sleep(2)
async def execute(task):
process_task(task)
await wait()
continue_job()
async def函數的執行稍微麻煩點。需要首先獲取一個loop對象,然後由這個對象代為執行async def函數。
loop = asyncio.get_event_loop()
loop.run_until_complete(execute(task))
loop.close()
loop在執行execute(task)函數時,如果遇到await關鍵字,就會暫時掛起當前協程,轉而去執行其他阻塞在await關鍵詞的協程,從而實現協程並發。
不過需要注意的是,run_until_complete()函數本身是一個阻塞函數。也就是說,當前線程會等候一個run_until_complete()函數執行完畢之後,才會繼續執行下一部函數。所以下面這段代碼並不能並發執行。
for task in task_list:
loop.run_until_complete(task)
對與這個問題,asyncio庫也有相應的解決方案:gather函數。
loop = asyncio.get_event_loop()
tasks = [asyncio.ensure_future(execute(task))
for task in task_list]
loop.run_until_complete(asyncio.gather(*tasks))
loop.close()
當然了,async def函數的執行並不只有這兩種解決方案,還有call_soon與run_forever的配合執行等等,更多內容還請參考官方文檔。
Python下的I/O多路復用
協程,實際上,也存在上下文切換,只不過開銷很輕微。而I/O多路復用則完全不存在這個問題。
目前,Linux上比較火的I/O多路復用API要算epoll了。Tornado,就是通過調用C語言封裝的epoll庫,成功解決了C10K問題(當然還有Pypy的功勞)。
在Linux里查文檔,可以看到epoll只有三類函數,調用起來比較方便易懂。
創建epoll對象,並返回其對應的文件描述符(file descriptor)。
int epoll_create(int size);
int epoll_create1(int flags);
控制監聽事件。第一個參數epfd就對應於前面命令創建的epoll對象的文件描述符;第二個參數表示該命令要執行的動作:監聽事件的新增、修改或者刪除;第三個參數,是要監聽的文件對應的描述符;第四個,代表要監聽的事件。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
等候。這是一個阻塞函數,調用者會等候內核通知所注冊的事件被觸發。
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
int epoll_pwait(int epfd, struct epoll_event *events,
int maxevents, int timeout,
const sigset_t *sigmask);
在Python的select庫里:
select.epoll()對應於第一類創建函數;
epoll.register(),epoll.unregister(),epoll.modify()均是對控制函數epoll_ctl的封裝;
epoll.poll()則是對等候函數epoll_wait的封裝。
Python里epoll相關API的最大問題應該是在epoll.poll()。相比於其所封裝的epoll_wait,用戶無法手動指定要等候的事件,也就是後者的第二個參數struct epoll_event *events。沒法實現精確控制。因此只能使用替代方案:select.select()函數。
根據Python官方文檔,select.select(rlist, wlist, xlist[, timeout])是對Unix系統中select函數的直接調用,與C語言API的傳參很接近。前三個參數都是列表,其中的元素都是要注冊到內核的文件描述符。如果想用自定義類,就要確保實現了fileno()方法。
其分別對應於:
rlist: 等候直到可讀
wlist: 等候直到可寫
xlist: 等候直到異常。這個異常的定義,要查看系統文檔。
select.select(),類似於epoll.poll(),先注冊文件和事件,然後保持等候內核通知,是阻塞函數。
實際應用
Psycopg2庫支持對非同步和協程,但和一般情況下的用法略有區別。普通資料庫連接支持不同線程中的不同游標並發查詢;而非同步連接則不支持不同游標的同時查詢。所以非同步連接的不同游標之間必須使用I/O復用方法來協調調度。
所以,我的大致實現思路是這樣的:首先並發執行大量協程,從任務隊列中提取任務,再向連接池請求連接,創建游標,然後執行命令,並返回結果。在獲取游標和接受查詢結果之前,均要阻塞等候內核通知連接可用。
其中,連接池返回連接時,會根據引用連接的協程數量,返回負載最輕的連接。這也是自己定義AsyncConnectionPool類的目的。
我的代碼位於:bottle-blog/dbservice.py
存在問題
當然了,這個流程目前還一些問題。
首先就是每次輪詢拿到任務之後,都會走這么一個流程。
獲取連接 --> 新建游標 --> 執行任務 --> 關閉游標 --> 取消連接引用
本來,最好的情況應該是:在輪詢之前,就建好游標;在輪詢時,直接等候內核通知,執行相應任務。這樣可以減少輪詢時的任務量。但是如果協程提前對應好連接,那就不能保證在獲取任務時,保持各連接負載均衡了。
所以這一塊,還有工作要做。
還有就是epoll沒能用上,有些遺憾。
以後打算寫點C語言的內容,或者用Python/C API,或者用Ctypes包裝共享庫,來實現epoll的調用。
最後,請允許我吐槽一下Python的epoll相關文檔:簡直太弱了!!!必須看源碼才能弄清楚功能。
㈣ python http怎麼加入epoll
介紹
從2.6版本開始, python 提供了使用linux epoll 的功能. 這篇文章通過3個例子來大致介紹如何使用它. 歡迎提問和反饋.
阻塞式socket通訊
第一個例子是一個簡單的python3.0版本的伺服器代碼, 監聽8080埠的http請求, 列印結果到命令行, 回應http response給客戶端.
行 9: 建立伺服器的socket
行 10: 允許11行的bind()操作, 即使其他程序也在監聽同樣的埠. 不然的話, 這個程序只能在其他程序停止使用這個埠之後的1到2分鍾後才能執行.
行 11: 綁定socket到這台機器上所有IPv4地址上的8080埠.
行 12: 告訴伺服器開始響應從客戶端過來的連接請求.
行 14: 程序會一直停在這里, 直到建立了一個連接. 這個時候, 伺服器socket會建立一個新的socket, 用來和客戶端通訊. 這個新的socket是accept()的返回值, address對象標示了客戶端的IP地址和埠.
行 15-17: 接收數據, 直到一個完整的http請求被接收完畢. 這是一個簡單的http伺服器實現.
行 18: 為了方便驗證, 列印客戶端過來的請求到命令行.
行 19: 發送回應.
行 20-22: 關閉連接, 以及伺服器的監聽socket.
Example1:
import socketEOL1 = b'\n\n'EOL2 = b'\n\r\n'response = b'HTTP/1.0 200 OK\r\nDate: Mon, 1 Jan 1996 01:01:01 GMT\r\n'response += b'Content-Type: text/plain\r\nContent-Length: 13\r\n\r\n'response += b'Hello, world!'serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
serversocket.bind(('0.0.0.0', 8080))
serversocket.listen(1)
connectiontoclient, address = serversocket.accept()
request = b''while EOL1 not in request and EOL2 not in request:
request += connectiontoclient.recv(1024)print(request.decode())
connectiontoclient.send(response)
connectiontoclient.close()
serversocket.close()
第2個例子, 我們在15行加上了一個循環, 用來循環處理客戶端請求, 直到我們中斷這個過程(在命令行下面輸入鍵盤中斷, 比如Ctrl-C). 這個例子更明顯地表示出來了, 伺服器socket並沒有用來做數據處理, 而是接受伺服器過來的連接, 然後建立一個新的socket, 用來和客戶端通訊.
最後的23-24行確保伺服器的監聽socket最後總是close掉, 即使出現了異常.
Example 2:
import socketEOL1 = b'\n\n'EOL2 = b'\n\r\n'response = b'HTTP/1.0 200 OK\r\nDate: Mon, 1 Jan 1996 01:01:01 GMT\r\n'response += b'Content-Type: text/plain\r\nContent-Length: 13\r\n\r\n'response += b'Hello, world!'serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
serversocket.bind(('0.0.0.0', 8080))
serversocket.listen(1)
try: while True:
connectiontoclient, address = serversocket.accept()
request = b''
while EOL1 not in request and EOL2 not in request:
request += connectiontoclient.recv(1024) print('-'*40 + '\n' + request.decode()[:-2])
connectiontoclient.send(response)
connectiontoclient.close()
finally:
serversocket.close()24
非同步socket和linux epoll的優勢
第2個例子裡面的socket採用的是阻塞方式, 因為python解釋器在出現事件之前都處在停止狀態. 16行的accept()一直阻塞, 直到新的連接進來. 19行的recv()也是一直阻塞, 直到從客戶端收到數據(或者直到沒有數據可以接收). 21行的send()也一直阻塞, 直到所有需要發送給客戶端的數據都交給了linux內核的發送隊列.
當一個程序採用阻塞socket的時候, 它經常採用一個線程(甚至一個進程)一個socket通訊的模式. 主線程保留伺服器監聽socket, 接受進來的連接, 一次接受一個連接, 然後把生成的socket交給一個分離的線程去做交互. 因為一個線程只和一個客戶端通訊, 在任何位置的阻塞都不會造成問題. 阻塞本身不會影響其他線程的工作.
多線程阻塞socket模式代碼清晰, 但是有幾個缺陷, 可能很難確保線程間資源共享工作正常, 可能在只有一個CPU的機器上效率低下.
C10K(單機1萬連接問題!) 探討了其他處理並行socket通訊的模式. 一種是採用非同步socket. socket不會阻塞, 直到特定事件發生. 程序在非同步socket上面進行一個特定操作, 並且立即得到一個結果, 不管執行成功或者失敗. 然後讓程序決定下一步怎麼做. 因為非同步socket是非阻塞的, 我們可以不採用多線程. 所有的事情都可以在一個線程裡面完成. 雖然這種模式有它需要面對的問題, 它對於特定程序來說還是不錯的選擇. 也可以和多線程合起來使用: 單線程的非同步socket可以當作伺服器上面處理網路的一個模塊, 而線程可以用來訪問阻塞式的資源, 比如資料庫.
Linux 2.6有一些方式來管理非同步socket, python API能夠用的有3種: select, poll和epoll. epoll和poll比select性能更好, 因為python程序不需要為了特定的事件去查詢單獨的socket, 而是依賴操作系統來告訴你什麼socket產生了什麼事件. epoll比poll性能更好, 因為它不需要每次python程序查詢的時候, 操作系統都去檢查所有的socket, 在事件產生的時候, linux跟蹤他們, 然後在python程序調用的時候, 返回具體的列表. 所以epoll在大量(上千)並行連接下, 是一種更有效率, 伸縮性更強的機制.
採用epoll的非同步socket編程示例
採用epoll的程序一般這樣操作:
建立一個epoll對象
告訴epoll對象, 對於一些socket監控一些事件.
問epoll, 從上次查詢以來什麼socket產生了什麼事件.
針對這些socket做特定操作.
告訴epoll, 修改監控socket和/或監控事件.
重復第3步到第5步, 直到結束.
銷毀epoll對象.
採用非同步socket的時候第3步重復了第2步的事情. 這里的程序更復雜, 因為一個線程需要和多個客戶端交互.
行 1: select模塊帶有epoll功能
行 13: 因為socket默認是阻塞的, 我們需要設置成非阻塞(非同步)模式.
行 15: 建立一個epoll對象.
行 16: 注冊伺服器socket, 監聽讀取事件. 伺服器socket接收一個連接的時候, 產生一個讀取事件.
行 19: connections表映射文件描述符(file descriptors, 整型)到對應的網路連接對象上面.
行 21: epoll對象查詢一下是否有感興趣的事件發生, 參數1說明我們最多等待1秒的時間. 如果有對應事件發生, 立刻會返回一個事件列表.
行 22: 返回的events是一個(fileno, event code)tuple列表. fileno是文件描述符, 是一個整型數.
行 23: 如果是伺服器socket的事件, 那麼需要針對新的連接建立一個socket.
行 25: 設置socket為非阻塞模式.
行 26: 注冊socket的read(EPOLLIN)事件.
行 31: 如果讀取事件發生, 從客戶端讀取新數據.
行 33: 一旦完整的http請求接收到, 取消注冊讀取事件, 注冊寫入事件(EPOLLOUT), 寫入事件在能夠發送數據回客戶端的時候產生.
行 34: 列印完整的http請求, 展示即使通訊是交錯的, 數據本身是作為一個完整的信息組合和處理的.
行 35: 如果寫入事件發生在一個客戶端socket上面, 我們就可以發送新數據到客戶端了.
行s 36-38: 一次發送一部分返回數據, 直到所有數據都交給操作系統的發送隊列.
行 39: 一旦所有的返回數據都發送完, 取消監聽讀取和寫入事件.
行 40: 如果連接被明確關閉掉, 這一步是可選的. 這個例子採用這個方法是為了讓客戶端首先斷開, 告訴客戶端沒有數據需要發送和接收了, 然後讓客戶端斷開連接.
行 41: HUP(hang-up)事件表示客戶端斷開了連接(比如 closed), 所以伺服器這端也會斷開. 不需要注冊HUP事件, 因為它們都會標示到注冊在epoll的socket.
行 42: 取消注冊.
行 43: 斷開連接.
行s 18-45: 在這里的異常捕捉的作用是, 我們的例子總是採用鍵盤中斷來停止程序執行.
行s 46-48: 雖然開啟的socket不需要手動關閉, 程序退出的時候會自動關閉, 明確寫出來這樣的代碼, 是更好的編碼風格.
Example 3:
import socket, selectEOL1 = b'\n\n'EOL2 = b'\n\r\n'response = b'HTTP/1.0 200 OK\r\nDate: Mon, 1 Jan 1996 01:01:01 GMT\r\n'response += b'Content-Type: text/plain\r\nContent-Length: 13\r\n\r\n'response += b'Hello, world!'serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
serversocket.bind(('0.0.0.0', 8080))
serversocket.listen(1)
serversocket.setblocking(0)
epoll = select.epoll()
epoll.register(serversocket.fileno(), select.EPOLLIN)
try:
connections = {}; requests = {}; responses = {} while True:
events = epoll.poll(1) for fileno, event in events: if fileno == serversocket.fileno():
connection, address = serversocket.accept()
connection.setblocking(0)
epoll.register(connection.fileno(), select.EPOLLIN)
connections[connection.fileno()] = connection
requests[connection.fileno()] = b''
responses[connection.fileno()] = response
elif event & select.EPOLLIN:
requests[fileno] += connections[fileno].recv(1024) if EOL1 in requests[fileno] or EOL2 in requests[fileno]:
epoll.modify(fileno, select.EPOLLOUT) print('-'*40 + '\n' + requests[fileno].decode()[:-2])
elif event & select.EPOLLOUT:
byteswritten = connections[fileno].send(responses[fileno])
responses[fileno] = responses[fileno][byteswritten:] if len(responses[fileno]) == 0:
epoll.modify(fileno, 0)
connections[fileno].shutdown(socket.SHUT_RDWR)
elif event & select.EPOLLHUP:
epoll.unregister(fileno)
connections[fileno].close()
del connections[fileno]
finally:
epoll.unregister(serversocket.fileno())
epoll.close()
serversocket.close()
epoll有2種模式, 邊沿觸發(edge-triggered)和狀態觸發(level-triggered). 邊沿觸發模式下, epoll.poll()在讀取/寫入事件發生的時候只返回一次, 程序必須在後續調用epoll.poll()之前處理完對應事件的所有的數據. 當從一個事件中獲取的數據被用完了, 更多在socket上的處理會產生異常. 相反, 在狀態觸發模式下面, 重復調用epoll.poll()只會返回重復的事件, 直到所有對應的數據都處理完成. 一般情況下不產生異常.
比如, 一個伺服器socket注冊了讀取事件, 邊沿觸發程序需要調用accept建立新的socket連接直到一個socket.error錯誤產生, 然後狀態觸發下只需要處理一個單獨的accept(), 然後繼續epoll查詢新的事件來判斷是否有新的accept需要操作.
例子3採用默認的狀態觸發模式, 例子4展示如何用邊沿觸發模式. 例子4中的25, 36和45行引入了循環, 直到錯誤產生(或者所有的數據都處理完了), 32, 38 和48行捕捉socket異常. 最後16, 28, 41 和51行添加EPOLLET mask用來設置邊沿觸發.
Example 4:
import socket, selectEOL1 = b'\n\n'EOL2 = b'\n\r\n'response = b'HTTP/1.0 200 OK\r\nDate: Mon, 1 Jan 1996 01:01:01 GMT\r\n'response += b'Content-Type: text/plain\r\nContent-Length: 13\r\n\r\n'response += b'Hello, world!'serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
serversocket.bind(('0.0.0.0', 8080))
serversocket.listen(1)
serversocket.setblocking(0)
epoll = select.epoll()
epoll.register(serversocket.fileno(), select.EPOLLIN | select.EPOLLET)
try:
connections = {}; requests = {}; responses = {} while True:
events = epoll.poll(1) for fileno, event in events: if fileno == serversocket.fileno():
try: while True:
connection, address = serversocket.accept()
connection.setblocking(0)
epoll.register(connection.fileno(), select.EPOLLIN | select.EPOLLET)
connections[connection.fileno()] = connection
requests[connection.fileno()] = b''
responses[connection.fileno()] = response
except socket.error:
pass
elif event & select.EPOLLIN:
try: while True:
requests[fileno] += connections[fileno].recv(1024)
except socket.error:
pass if EOL1 in requests[fileno] or EOL2 in requests[fileno]:
epoll.modify(fileno, select.EPOLLOUT | select.EPOLLET) print('-'*40 + '\n' + requests[fileno].decode()[:-2])
elif event & select.EPOLLOUT:
try: while len(responses[fileno]) > 0:
byteswritten = connections[fileno].send(responses[fileno])
responses[fileno] = responses[fileno][byteswritten:]
except socket.error:
pass if len(responses[fileno]) == 0:
epoll.modify(fileno, select.EPOLLET)
connections[fileno].shutdown(socket.SHUT_RDWR)
elif event & select.EPOLLHUP:
epoll.unregister(fileno)
connections[fileno].close()
del connections[fileno]
finally:
epoll.unregister(serversocket.fileno())
epoll.close()
serversocket.close()
因為比較類似, 狀態觸發經常用在轉換採用select/poll模式的程序上面, 邊沿觸發用在程序員不需要或者不希望操作系統來管理事件狀態的場合上面.
除了這兩種模式以外, socket經常注冊為EPOLLONESHOT event mask, 當用到這個選項的時候, 事件只有效一次, 之後會自動從監控的注冊列表中移除.
㈤ Python三大web框架分別是什麼 哪個更好
【導讀】目前,Python比較火的三大web框架有Django、Flask和Tornado,要論這三個Web框架哪個更好的話,建議一點,Django幫我們事先搭建了好多,上手會快一些,學習的話可以先從Django學起,然後再學習Flask和Tornado,下面我們就來具體了解一下Python三大web框架的詳情。
1、Django
Django是一個開放源代碼的Web應用框架,由Python寫成。採用了MTV的框架模式,即模型M,模板T和視圖V。它最初是被開發來用於管理勞倫斯出版集團旗下的一些以新聞內容為主的網站的,即是CMS(內容管理系統)軟體。
2、Flask
Flask是一個使用Python編寫的輕量級Web應用框架。其 WSGI工具箱採用Werkzeug ,模板引擎則使用 Jinja2
。Flask使用BSD授權。
Flask也被稱為 「microframework」 ,因為它使用簡單的核心,用 extension
增加其他功能。Flask沒有默認使用的資料庫、窗體驗證工具。
Flask 很輕,花很少的成本就能夠開發一個簡單的網站。非常適合初學者學習。Flask 框架學會以後,可以考慮學習插件的使用。例如使用 WTForm +
Flask-WTForm 來驗證表單數據,用 SQLAlchemy + Flask-SQLAlchemy 來對你的資料庫進行控制。
3、Tornado
Tornado是一種 Web 伺服器軟體的開源版本。Tornado 和現在的主流 Web 伺服器框架(包括大多數 Python
的框架)有著明顯的區別:它是非阻塞式伺服器,而且速度相當快。
得利於其 非阻塞的方式和對epoll的運用,Tornado 每秒可以處理數以千計的連接,因此 Tornado 是實時 Web 服務的一個
理想框架。
關於Python三大web框架的簡單介紹,就給大家分享到這里了,當然學習是永無止境的,學習一項技能更是受益終身,所以,只要肯努力學,什麼時候開始都不晚,希望大家抓緊時間進行學習吧。
㈥ 干貨分享!Python三大web框架簡單介紹
1、Django
Django是一個開放源代碼的Web應用框架,由Python寫成。採用了MTV的框架模式,即模型M,模板T和視圖V。它最初是被開發來用於管理勞倫斯出版集團旗下的一些以新聞內容為主的網站的,即是CMS(內容管理系統)軟體。
2、Flask
Flask是一個使用 Python 編寫的輕量級 Web 應用框架。其 WSGI 工具箱採用 Werkzeug ,模板引擎則使用 Jinja2 。Flask使用 BSD 授權。
Flask也被稱為 “microframework” ,因為它使用簡單的核心,用 extension 增加其他功能。Flask沒有默認使用的資料庫、窗體驗證工具。
Flask 很輕,花很少的成本就能夠開發一個簡單的網站。非常適合初學者學習。Flask 框架學會以後,可以考慮學習插件的使用。例如使用 WTForm + Flask-WTForm 來驗證表單數據,用 SQLAlchemy + Flask-SQLAlchemy 來對你的資料庫進行控制。
3、Tornado
Tornado是一種 Web 伺服器軟體的開源版本。Tornado 和現在的主流 Web 伺服器框架(包括大多數 Python 的框架)有著明顯的區別:它是非阻塞式伺服器,而且速度相當快。
得利於其 非阻塞的方式和對epoll的運用,Tornado 每秒可以處理數以千計的連接,因此 Tornado 是實時 Web 服務的一個 理想框架。
關於干貨分享!Python三大web框架簡單介紹,環球青藤小編就和大家分享到這里了,學習是永無止境的,學習一項技能更是受益終身,所以,只要肯努力學,什麼時候開始都不晚。如果您還想繼續了解關於python編程的學習方法及素材等內容,可以點擊本站其他文章學習。