Python3 協程

協程

定義:協程,又稱微線程,纖程。英文名Coroutine。一句話說明什麼是線程:協程是一種用戶態的輕量級線程。協程的標準定義:

  •     必須在只有一個單線程裏實現併發
  •     修改共享數據不需加鎖
  •     用戶程序裏自己保存多個控制流的上下文棧
  •     一個協程遇到IO操作自動切換到其它協程

特點:協程擁有自己的寄存器上下文和棧。協程調度切換時,將寄存器上下文和棧保存到其他地方,在切回來的時候,恢復先前保存的寄存器上下文和棧。因此,協程能保留上一次調用時的狀態(即所有局部狀態的一個特定組合),每次過程重入時,就相當於進入上一次調用的狀態,換種說法:進入上一次離開時所處邏輯流的位置。線程是CPU控制的,而協程是程序自身控制的

優點:

              ​​​​​​​​​​​​​​1、由於自身帶有上下文和棧,無需線程上下文切換的開銷;

              2、無需原子操作的鎖定及同步的開銷;

              3、方便切換控制流,簡化編程模型

              4、可以輕鬆實現高併發,且可擴展性高,成本低

缺點:

              1、無法利用多核資源:協程的本質是個單線程,它不能同時將 單個CPU 的多個核用上,協程需要和進程配合才能運行在多CPU上.當然我們日常所編寫的絕大部分應用都沒有這個必要,除非是cpu密集型應用。

              2、當進行阻塞(Blocking)操作時,會阻塞整個程序。

Greenlet模塊

greenlet是一個用C實現的協程模塊,通過設置.switch()可以實現任意函數之間的切換,但這種切換屬於手動切換,當遇到IO操作時,程序會阻塞,而不能自動進行切換。舉例如下:

from greenlet import greenlet
import time
def test1():
    print(" running test1")
    gr2.switch() # 切換到test2
    print(" running test1 again ")
    time.sleep(2)
    gr2.switch()

def test2():
    print("\033[31;1m running test2 \033[0m")
    gr1.switch()
    print("\033[32;1m running test2 again\033[0m")

gr1 = greenlet(test1) # 實例化一個協程
gr2 = greenlet(test2) # 實例化另一個協程
gr1.switch() # 執行gr1,切換到grl1執行test1
運行結果:
 running test1
 running test2 
 running test1 again 
 running test2 again

Gevent模塊

Gevent 是一個第三方庫,可以輕鬆通過gevent實現併發同步或異步編程,在gevent中用到的主要模式是Greenlet, 它是以C擴展模塊形式接入Python的輕量級協程。 Greenlet全部運行在主程序操作系統進程的內部,但它們被協作式地調度。Gevnet遇到IO操作時,會進行自動切話,屬於主動式切換。例如:

import gevent,time
def func1():
    print(' 主人來電話啦...')
    gevent.sleep(3)
    print(' 主人那傢伙又來電話啦...')

def func2():
    print('\033[32;1m 打個電話...\033[0m')
    gevent.sleep(2)
    print('\033[32;1m 咦,電話給掛了,接着打...\033[0m')

def func3():
    print("\033[31;1m 哈哈哈哈 \033[0m")
    gevent.sleep(0)
    print("\033[31;1m 嘿嘿嘿。。。。\033[0m")

start_time = time.time()
gevent.joinall([
    gevent.spawn(func2), # 生成一個協程
    gevent.spawn(func1),
    gevent.spawn(func3),
])
print("\033[32;1m running time:", (time.time() - start_time))
運行結果:
 打個電話...
 主人來電話啦...
 哈哈哈哈 
 嘿嘿嘿。。。。
 咦,電話給掛了,接着打...
 主人那傢伙又來電話啦...
 running time: 3.006500244140625
# 可以看出協程併發的效果,遇到IO操作時會自動進行切換,當IO操作沒有完成時,會不停的進行循環切換,看哪個IO操作完成了。

事件驅動模型

將所有發生的事件按照先後順序存放在事件隊列中(先進先出,與隊列相似), 事件(消息)一般都各自保存各自的處理函數指針,這樣每一種事件對應相應的處理函數,再循環事件隊列進行處理。

爲什麼用到事件驅動模型?

       通常在寫服務器處理模型的程序時,對於接收到請求,有三種常用的處理模型:新建一個進程、新建一個線程或者事件驅動模型。新建一個進程實現比較簡單,但當請求較多時,會導致服務器的開銷變大,從而降低服務器的性能;新建一個線程,會涉及到線程的同步,可能會面臨死鎖的問題;事件驅動模型,可以很好地解決前兩個問題,但實現邏輯比較複雜。
           目前大部分的UI編程都是事件驅動模型,如很多UI平臺都會提供onClick()事件,這個事件就代表鼠標按下事件。事件驅動模型大體思路如下:

  1. 有一個事件(消息)隊列;
  2. 鼠標按下時,往這個隊列中增加一個點擊事件(消息);
  3. 有個循環,不斷從隊列取出事件,根據不同的事件,調用不同的函數,如onClick()、onKeyDown()(按下鍵盤)等;
  4. 事件(消息)一般都各自保存各自的處理函數指針,這樣,每個消息都有獨立的處理函數;

image_1c849phf6ui095eu351afapgpm.png-30.1kB
        圖中事件1假如是鼠標點擊,那麼鼠標點擊一下就將該事件放入這個事件隊列中;假如事件2是按下鍵盤事件,也將該事件放入事件隊列中; 線程會循環的去處理事件隊列中的事件。 將事件加入到事件列表,和提取事件處理相互是不影響的,事件的處理速度,並不影響事件的產生速度,這就是典型的生產者消費者模型。比如我每秒點10次鼠標,但是你的處理速度是每秒8次,雖然你處理的慢,但是並不影響我繼續點擊鼠標。事件驅動模型就是根據事件做出相應的反應,比如點下文檔的'X'就關閉文檔,點擊 '-' 就最小化文檔。

常見的IO操作

  • CPU

中央處理器(CPU,Central Processing Unit)是一塊超大規模的集成電路,是一臺計算機的運算核心(Core)和控制核心( Control Unit)。它的功能主要是解釋計算機指令以及處理計算機軟件中的數據。物理結構主要包括:
   邏輯部件:運算邏輯部件。主要是進行定點或浮點算數運算操作、移位操作以及邏輯操作,還可以進行地址運算和轉換;
   寄存器部件:保存指令執行過程中臨時存放的寄存器操作數和中間(或最終)的操作結果;
   控制部件:主要是負責對指令譯碼,並且發出爲完成每條指令所要執行的各個操作的控制信號;
   CPU具有以下4個方面的基本功能:數據通信,資源共享,分佈式處理,提供系統可靠性。運作原理可基本分爲四個階段:提取(Fetch)、解碼(Decode)、執行(Execute)和寫回(Writeback)。

  • 用戶空間與內核空間

現在操作系統都是採用虛擬存儲器,那麼對32位操作系統而言,它的尋址空間(虛擬存儲空間)爲4G(2的32次方)。操作系統的核心是內核(操作系統需要使用部分內存空間來運行,這就是內核空間),獨立於普通的應用程序,可以訪問受保護的內存空間,也有訪問底層硬件設備的所有權限(比如訪問網卡、音響聲卡都是通過內核訪問的,而不是用戶程序)。爲了保證用戶進程不能直接操作內核(kernel),保證內核的安全,操心繫統將虛擬空間劃分爲兩部分,一部分爲內核空間,一部分爲用戶空間。針對linux操作系統而言,將最高的1G字節(從虛擬地址0xC0000000到0xFFFFFFFF),供內核使用,稱爲內核空間,而將較低的3G字節(從虛擬地址0x00000000到0xBFFFFFFF),供各個進程使用,稱爲用戶空間。

  • 進程切換
    進程切換就是上下文的切換

  • 進程阻塞
    正在執行的進程,由於期待的某些事件未發生,如請求系統資源失敗、等待某種操作的完成、新數據尚未到達或無新工作做等,則由系統自動執行阻塞原語(Block),使自己由運行狀態變爲阻塞狀態(比如socket server等不到client的數據就會阻塞)。可見,進程的阻塞是進程自身的一種主動行爲,也因此只有處於運行態的進程(獲得CPU),纔可能將其轉爲阻塞狀態。當進程進入阻塞狀態,是不佔用CPU資源的。

  • 文件描述符id

文件描述符(File descriptor)是計算機科學中的一個術語,是一個用於表述指向文件的引用的抽象化概念。

文件描述符在形式上是一個非負整數。實際上,它是一個索引值,指向內核爲每一個進程所維護的該進程打開文件的記錄表。當程序打開一個現有文件或者創建一個新文件時,內核向進程返回一個文件描述符。在程序設計中,一些涉及底層的程序編寫往往會圍繞着文件描述符展開。但是文件描述符這一概念往往只適用於UNIX、Linux這樣的操作系統。

文件描述符相當於一個索引,通過索引打開真正的內容。

  • 緩存I/O

緩存 I/O 又被稱作標準 I/O,大多數文件系統的默認 I/O 操作都是緩存 I/O。在 Linux 的緩存 I/O 機制中,操作系統會將 I/O 的數據緩存在文件系統的頁緩存( page cache )中,也就是說,數據會先被拷貝到操作系統內核的緩衝區中,然後纔會從操作系統內核的緩衝區拷貝到應用程序的地址空間。

緩存 I/O 的缺點:
            數據在傳輸過程中需要在應用程序地址空間和內核進行多次數據拷貝操作,這些數據拷貝操作所帶來的 CPU 以及內存開銷是非常大的。

打開一個文件默認不是在用戶的內存空間,而是放入了內核的緩存中,然後在從內核的緩存拷貝到用戶的內存空間; 傳數據也是一樣,先是到內核緩存中,然後纔會拷貝到用戶的內存空間; 使用內核是很耗CPU的,耗CPU是指拷貝到內存的這個指令,如果有大量數據需要從內核緩存拷貝到用戶內存空間,那麼就會有大量的指令會消耗CPU資源。訪問網卡、聲卡等只能通過內核實現,而用戶空間是無法直接訪問內核空間的,所以需要通過內核緩存的空間將內容拷貝到用戶的內存空間,然後用戶纔可以使用。

IO模式

剛纔說了,對於一次IO訪問(以read舉例),數據會先被拷貝到操作系統內核的緩衝區中,然後纔會從操作系統內核的緩衝區拷貝到應用程序的地址空間。所以說,當一個read操作發生時,它會經歷兩個階段:

  1. 等待數據準備 (Waiting for the data to be ready)(就是將數據放到內核緩存中)
  2. 將數據從內核拷貝到進程中 (Copying the data from the kernel to the process)

正是因爲這兩個階段,linux系統產生了下面五種網絡模式的方案。

  • 阻塞 I/O(blocking IO)
  • 非阻塞 I/O(nonblocking IO)
  • I/O 多路複用( IO multiplexing)
  • 信號驅動 I/O( signal driven IO)
  • 異步 I/O(asynchronous IO)

注:由於signal driven IO在實際中並不常用,所以我這隻提及剩下的四種IO Model。

  • 阻塞 I/O(blocking IO)

在linux中,默認情況下所有的socket都是blocking,一個典型的讀操作流程大概是這樣:

image_1c850bb5d1vmik5lsf112n2163f1j.png-55.3kB
recve時接收端會阻塞,直到系統接收到數據,系統接收到數據後此時也是阻塞的,會從內核緩存copy到用戶內存,然後返回一個OK纔是用戶真正接收到了數據。

當用戶進程調用了recvfrom這個系統調用,kernel就開始了IO的第一個階段:準備數據(對於網絡IO來說,很多時候數據在一開始還沒有到達。比如,還沒有收到一個完整的UDP包。這個時候kernel就要等待足夠的數據到來)。這個過程需要等待,也就是說數據被拷貝到操作系統內核的緩衝區中是需要一個過程的。而在用戶進程這邊,整個進程會被阻塞(當然,是進程自己選擇的阻塞)。當kernel一直等到數據準備好了,它就會將數據從kernel中拷貝到用戶內存,然後kernel返回結果,用戶進程才解除block的狀態,重新運行起來。

所以,blocking IO的特點就是在IO執行的兩個階段都被block了(等數據的階段和從內核拷貝給用戶的階段)。
  • 非阻塞 I/O(nonblocking IO)

linux下,可以通過設置socket使其變爲non-blocking。當對一個non-blocking socket執行讀操作時,流程是這個樣子:
image_1c854ikmt52118hhooe14531kq920.png-75.3kB

當用戶進程發出read操作時,如果kernel中的數據還沒有準備好,那麼它並不會block用戶進程,而是立刻返回一個error。從用戶進程角度講 ,它發起一個read操作後,並不需要等待,而是馬上就得到了一個結果。用戶進程判斷結果是一個error時,它就知道數據還沒有準備好,於是它可以再次發送read操作。一旦kernel中的數據準備好了,並且又再次收到了用戶進程的system call(receive的動作),那麼它馬上就將數據拷貝到了用戶內存,然後返回。

所以,nonblocking IO的特點是用戶進程需要不斷的主動詢問kernel數據好了沒有。
  • IO多路複用

IO multiplexing就是我們說的select,poll,epoll,有些地方也稱這種IO方式爲event driven IO。select/epoll的好處就在於單個process就可以同時處理多個網絡連接的IO。它的基本原理就是select,poll,epoll這個function會不斷的輪詢所負責的所有socket,當某個socket有數據到達了,就通知用戶進程。

在單線程且又是阻塞模式下,是沒法實現多個IO一起執行的,因爲當接收數據時,一直沒有接收到的話就會一直卡住。

在單線程下非阻塞模式下,假如此時有10個IO,對這10個IO進行for循環來接收數據,先接收其中2個IO的數據,如果這2個IO沒有接收到數據就會返回err,此時就不會在阻塞了,然後繼續進行for循環,此時其他IO如果有數據就會將數據接收過來,然後就這樣不斷的receive,發現err就不阻塞,有數據則接收。使用非阻塞模式就可以處理多個socket,對於用戶來說就已經是併發了。但是要注意的是第一階段不卡了,但是此時第二階段依然會卡,如果從內核copy到用戶內存的數據不大,則很快會copy完成,但是如果數據很大的話,第二階段就會一直在copy數據,直到數據copy完成,但相應的在第二階段卡的時間也會很久。

當用戶進程調用了select,那麼整個進程會被block,假如此時有100個socket的IO,那麼kernel會監視所有select負責的socket,當任何一個socket中的數據準備好了(kernel的數據準備好),select就會返回。這個時候用戶進程在調用read操作,將數據kernelcopy到用戶進程。所以,I/O多路複用的特點是通過一種機制 一個進程能同時等待多個文件描述符,而這些文件描述符(socket連接)其中的任意一個進入讀就緒狀態,select()函數就可以返回。

image_1c8h2jhepdd8g77705ug01eh39.png-69.6kB
多路複用和阻塞模式的區別就是,阻塞模式監視一個socket,有數據則接收;而多路複用就是可以通過select監視N個socket,只要其中任何一個有數據,則進行select返回,然後receive數據(第二階段數據過大的話,依然會有阻塞)。

假如此時有10000個socket連接,監視到有數據後kernel就會告訴返回給用戶進程,但kernel不會告訴用戶進程具體是哪個socket連接,所以用戶就會循環着10000個socket連接,但是即使其中只有2個socket有數據,用戶程序也會去循環着10000個socket連接,這就造成了大量的多餘循環操作。

select 
select最早於1983年出現在4.2BSD中,它通過一個select()系統調用來監視多個文件描述符的數組,當select()返回後,該數組中就緒的文件描述符便會被內核修改標誌位,使得進程可以獲得這些文件描述符從而進行後續的讀寫操作。

select目前幾乎在所有的平臺上支持,其良好跨平臺支持也是它的一個優點,事實上從現在看來,這也是它所剩不多的優點之一。

select的一個缺點在於單個進程能夠監視的文件描述符的數量存在最大限制,在Linux上一般爲1024,不過可以通過修改宏定義甚至重新編譯內核的方式提升這一限制。

另外,select()所維護的存儲大量文件描述符的數據結構,隨着文件描述符數量的增大,其複製的開銷也線性增長。同時,由於網絡響應時間的延遲使得大量TCP連接處於非活躍狀態,但調用select()會對所有socket進行一次線性掃描,所以這也浪費了一定的開銷。

poll 
poll在1986年誕生於System V Release 3,它和select在本質上沒有多大差別,但是poll沒有最大文件描述符數量的限制。

poll和select同樣存在一個缺點就是,包含大量文件描述符的數組被整體複製於用戶態和內核的地址空間之間,而不論這些文件描述符是否就緒,它的開銷隨着文件描述符數量的增加而線性增大。

另外,select()和poll()將就緒的文件描述符告訴進程後,如果進程沒有對其進行IO操作,那麼下次調用select()和poll()的時候將再次報告這些文件描述符,所以它們一般不會丟失就緒的消息,這種方式稱爲水平觸發(Level Triggered)。

epoll 
直到Linux2.6纔出現了由內核直接支持的實現方法,那就是epoll,它幾乎具備了之前所說的一切優點,被公認爲Linux2.6下性能最好的多路I/O就緒通知方法。

epoll可以同時支持水平觸發和邊緣觸發(Edge Triggered,只告訴進程哪些文件描述符剛剛變爲就緒狀態,它只說一遍,如果我們沒有採取行動,那麼它將不會再次告知,這種方式稱爲邊緣觸發),理論上邊緣觸發的性能要更高一些,但是代碼實現相當複雜。

epoll同樣只告知那些就緒的文件描述符,而且當我們調用epoll_wait()獲得就緒文件描述符時,返回的不是實際的描述符,而是一個代表就緒描述符數量的值,你只需要去epoll指定的一個數組中依次取得相應數量的文件描述符即可,這裏也使用了內存映射(mmap)技術,這樣便徹底省掉了這些文件描述符在系統調用時複製的開銷。

另一個本質的改進在於epoll採用基於事件的就緒通知方式。在select/poll中,進程只有在調用一定的方法後,內核纔對所有監視的文件描述符進行掃描,而epoll事先通過epoll_ctl()來註冊一個文件描述符,一旦基於某個文件描述符就緒時,內核會採用類似callback的回調機制,迅速激活這個文件描述符,當進程調用epoll_wait()時便得到通知。

epool會告訴用戶進程具體哪個socket連接有數據了,所以用戶進程不需要在將所有socket 連接全都循環一次才發現具體哪個有數據。

Windows不支持epool,支持select

  • 異步I/O

inux下的asynchronous IO其實用得很少。先看一下它的流程:

image_1c8h2pbkgpv51ghd82psdamvim.png-59.9kB
用戶進程發起read操作之後,立刻就可以開始去做其它的事(不需要等待kernel拷貝數據到用戶)。而另一方面,從kernel的角度,當它受到一個asynchronous read之後,首先它會立刻返回,所以不會對用戶進程產生任何block。然後,kernel會等待數據準備完成,然後kernel主動將數據拷貝到用戶內存,當這一切都完成之後,kernel會給用戶進程發送一個signal,告訴它read操作完成了(沒有任何阻塞)。

  • 小結
    同步IO:阻塞、非阻塞、多路複用都屬於同步IO,因爲他們都需要等待kernel到用戶的數據copy。同步IO都是需要用戶進程去kernel 接收數據。
    異步IO:異步I/O不需要等待kernel到用戶的數據copy。異步是kernel主動將數據copy到用戶內存。

異步因爲實現比較複雜,所以使用的較少,使用較多的還是epool多路複用。

阻塞模式:

原理:內核阻塞,直到接收到完整的數據後,在將數據copy給用戶內存,copy完成後會返回一個OK給當前server的進程,然後進程接觸阻塞繼續向下執行(執行代碼)。

server等待接收數據時,會處於阻塞的狀態,當前進程不會再往下執行,除非接收到數據以後纔會;假如當前有3個IO,目前第1個IO處於接收數據狀態,除非第1個IO內核接收完成,否則第2和第3個內核IO都不會開始數據的接收;內核準備好數據還要講數據copy給用戶內存。

非阻塞模式:

原理:內核不阻塞,用戶內存接收數據會阻塞(直到接收完成);多個IO時,內核會同時接收多個IO數據,用戶進程會輪詢的方式read內核是否準備好數據,如果沒有準備好的話內核返回err給用戶進程,此時用戶進程會問內核其他IO是否準備好數據,沒準備好內核返回err給用戶進程,準備的話就將數據copy給用戶內存。

server等待接收數據,假如當前有3個IO,內核的第1個IO數據沒有準備好,用戶進程向進程詢問第1個IO數據是否準備好,內核返回err給用戶進程,用戶進程去問內核第2個IO數據是否準備好,準備好了就會將數據copy到用戶內存,用戶進程以此類推的循環去詢問內核;  需要不斷的read和返回err,開銷較大。

IO多路複用:
select:

    有多個socket IO時,通過select來負責所有socket IO,然後內核會監視所有select負責的socket,用戶進程會循環所有IO,當任何一個socket IO中的數據準備好了(相當於內核的數據準備好了)且剛好被用戶進程循環到,select就會返回datagram ready給用戶進程(此時用戶內存處於阻塞狀態),用戶進程將該IO數據copy到用戶內存,copy完成後返回OK給用戶進程,告知解除用戶內存,讓其他IO在內核內存準備好的數據可以copy到用戶內存(如果當前從內核copy到用戶內存的數據較大的話,只能等待數據copy完成,也就是該階段依然是阻塞,除非數據copy完成後,纔會爲其他IO的數據進行copy操作)。
使用select不需要內核返回大量的err,但是用戶進程依然需要循環所有IO,假如10000個IO,其中只有2個IO數據準備好了,那麼用戶進程依然需要去循環這10000個IO來發現這兩個已經準備好數據的IO。
select還是存在大量循環的操作。

poll:

    poll和select基本相似,select支持的文件描述符(相當於每個IO的索引地址,用戶進程需要根據具體的地址來進行制定的數據操作)爲1024,poll則沒有文件描述符的限制。

epoll:

    不需要用戶進程去循環,當內核數據準備好後會立即告知用戶進程具體的哪個socket IO數據準備好了,且只會說一遍,不會再次告知,用戶進程不需要循環所有socket IO ,不過epoll的代碼實現相當複雜。

異步IO:

用戶進程發起read後就立刻開始做其他事情,不需要等內核將數據copy到用戶;而內核接到read後會返回信息給用戶進程,不會讓用戶進程產生阻塞(這樣第二階段不會阻塞),然後當內核準備好數據後,內核會主動將數據copy給用戶內存(其他模式是用戶主動從內核copy數據),當數據copy完成後內核會發送一個signal給用戶進程,告訴用戶進程read操作完成了(沒有任何阻塞)。
異步因爲實現比較複雜,所以使用的較少,使用較多的還是epool多路複用。

select IO多路複用

server端

# 服務器端
import select
import socket
import sys
import queue


server = socket.socket()
server.setblocking(0)

server_addr = ('localhost',10000)

print('starting up on %s port %s' % server_addr)
server.bind(server_addr)

server.listen(5)


inputs = [server, ] #自己也要監測呀,因爲server本身也是個fd
outputs = []

message_queues = {}

while True:
    print("waiting for next event...")

    readable, writeable, exeptional = select.select(inputs,outputs,inputs) #如果沒有任何fd就緒,那程序就會一直阻塞在這裏

    for s in readable: #每個s就是一個socket

        if s is server: #別忘記,上面我們server自己也當做一個fd放在了inputs列表裏,傳給了select,如果這個s是server,代表server這個fd就緒了,
            #就是有活動了, 什麼情況下它纔有活動? 當然 是有新連接進來的時候 呀
            #新連接進來了,接受這個連接
            conn, client_addr = s.accept()
            print("new connection from",client_addr)
            conn.setblocking(0)
            inputs.append(conn) #爲了不阻塞整個程序,我們不會立刻在這裏開始接收客戶端發來的數據, 把它放到inputs裏, 下一次loop時,這個新連接
            #就會被交給select去監聽,如果這個連接的客戶端發來了數據 ,那這個連接的fd在server端就會變成就續的,select就會把這個連接返回,返回到
            #readable 列表裏,然後你就可以loop readable列表,取出這個連接,開始接收數據了, 下面就是這麼幹 的

            message_queues[conn] = queue.Queue() #接收到客戶端的數據後,不立刻返回 ,暫存在隊列裏,以後發送

        else: #s不是server的話,那就只能是一個 與客戶端建立的連接的fd了
            #客戶端的數據過來了,在這接收
            data = s.recv(1024)
            if data:
                print("收到來自[%s]的數據:" % s.getpeername()[0], data)
                message_queues[s].put(data) #收到的數據先放到queue裏,一會返回給客戶端
                if s not  in outputs:
                    outputs.append(s) #爲了不影響處理與其它客戶端的連接 , 這裏不立刻返回數據給客戶端


            else:#如果收不到data代表什麼呢? 代表客戶端斷開了呀
                print("客戶端斷開了",s)

                if s in outputs:
                    outputs.remove(s) #清理已斷開的連接

                inputs.remove(s) #清理已斷開的連接

                del message_queues[s] ##清理已斷開的連接


    for s in writeable:
        try :
            next_msg = message_queues[s].get_nowait()

        except queue.Empty:
            print("client [%s]" %s.getpeername()[0], "queue is empty..")
            outputs.remove(s)

        else:
            print("sending msg to [%s]"%s.getpeername()[0], next_msg)
            s.send(next_msg.upper())


    for s in exeptional:
        print("handling exception for ",s.getpeername())
        inputs.remove(s)
        if s in outputs:
            outputs.remove(s)
        s.close()

        del message_queues[s]

client端

import socket
import sys

messages = [ b'This is the message. ',
             b'It will be sent ',
             b'in parts.',
             ]
server_address = ('localhost', 10000)

# 創建一個 TCP/IP socket
socks = [ socket.socket(socket.AF_INET, socket.SOCK_STREAM) for i in range(50)]  # 同時開啓50個socket

print('connecting to %s port %s' % server_address)
for s in socks:
    s.connect(server_address)

for message in messages:

    # 發送消息
    for s in socks:
        print('%s: sending "%s"' % (s.getsockname(), message) )
        s.send(message)

    # 接收消息
    for s in socks:
        data = s.recv(1024)
        print( '%s: received "%s"' % (s.getsockname(), data) )
        if not data:
            print(sys.stderr, 'closing socket', s.getsockname() )

selectors模塊

該模塊允許基於選擇模塊原語的高級和高效I / O複用。 建議用戶使用此模塊,除非他們希望精確控制所使用的操作系統級基元。

import selectors
import socket
 
sel = selectors.DefaultSelector()
 
def accept(sock, mask):
    conn, addr = sock.accept()  
    print('accepted', conn, 'from', addr)
    conn.setblocking(False)
    sel.register(conn, selectors.EVENT_READ, read)
 
def read(conn, mask):
    data = conn.recv(10000)  
    if data:
        print('echoing', repr(data), 'to', conn)
        conn.send(data)  # Hope it won't block
    else:
        print('closing', conn)
        sel.unregister(conn)
        conn.close()
 
sock = socket.socket()
sock.bind(('localhost', 10000))
sock.listen(100)
sock.setblocking(False)
sel.register(sock, selectors.EVENT_READ, accept)
 
while True:
    events = sel.select()
    for key, mask in events:
        callback = key.data
        callback(key.fileobj, mask)

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章