1. 程式人生 > >高併發伺服器---基礎----IO模式和IO多路複用

高併發伺服器---基礎----IO模式和IO多路複用

轉自:https://www.cnblogs.com/zingp/p/6863170.html

 

閱讀目錄

  網路程式設計裡常聽到阻塞IO、非阻塞IO、同步IO、非同步IO等概念,總聽別人裝13不如自己下來鑽研一下。不過,搞清楚這些概念之前,還得先回顧一些基礎的概念。

回到頂部

1 基礎知識回顧

注意:咱們下面說的都是Linux環境下,跟Windows不一樣哈~~~

1.1 使用者空間和核心空間

  現在作業系統都採用虛擬定址,處理器先產生一個虛擬地址

,通過地址翻譯成實體地址(記憶體的地址),再通過匯流排的傳遞,最後處理器拿到某個實體地址返回的位元組。

  對32位作業系統而言,它的定址空間(虛擬儲存空間)為4G(2的32次方)。作業系統的核心是核心,獨立於普通的應用程式,可以訪問受保護的記憶體空間,也有訪問底層硬體裝置的所有許可權。為了保證使用者程序不能直接操作核心(kernel),保證核心的安全,操心繫統將虛擬空間劃分為兩部分,一部分為核心空間,一部分為使用者空間。針對linux作業系統而言,將最高的1G位元組(從虛擬地址0xC0000000到0xFFFFFFFF),供核心使用,稱為核心空間,而將較低的3G位元組(從虛擬地址0x00000000到0xBFFFFFFF),供各個程序使用,稱為使用者空間。

補充:地址空間就是一個非負整數地址的有序集合。如{0,1,2...}。

1.2 程序上下文切換(程序切換)

  為了控制程序的執行,核心必須有能力掛起正在CPU上執行的程序,並恢復以前掛起的某個程序的執行。這種行為被稱為程序切換(也叫排程)。因此可以說,任何程序都是在作業系統核心的支援下執行的,是與核心緊密相關的。

  從一個程序的執行轉到另一個程序上執行,這個過程中經過下面這些變化
  1. 儲存當前程序A的上下文

  上下文就是核心再次喚醒當前程序時所需要的狀態,由一些物件(程式計數器、狀態暫存器、使用者棧等各種核心資料結構)的值組成。

  這些值包括描繪地址空間的頁表、包含程序相關資訊的程序表、檔案表等。
  2. 切換頁全域性目錄以安裝一個新的地址空間

    ...
  3. 恢復程序B的上下文

  可以理解成一個比較耗資源的過程。

1.3 程序的阻塞

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

1.4 檔案描述符

  檔案描述符(File descriptor)是電腦科學中的一個術語,是一個用於表述指向檔案的引用的抽象化概念。

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

1.5 直接I/O和快取I/O

  快取 I/O 又被稱作標準 I/O,大多數檔案系統的預設 I/O 操作都是快取 I/O。在 Linux 的快取 I/O 機制中,以write為例,資料會先被拷貝程序緩衝區,在拷貝到作業系統核心的緩衝區中,然後才會寫到儲存裝置中。

快取I/O的write:

直接I/O的write:(少了拷貝到程序緩衝區這一步)

 

write過程中會有很多次拷貝,知道資料全部寫到磁碟。好了,準備知識概略複習了一下,開始探討IO模式。

 

回到頂部

2 I/O模式

  對於一次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 模型。

2.1 block I/O模型(阻塞I/O)

阻塞I/O模型示意圖:

read為例:

(1)程序發起read,進行recvfrom系統呼叫;

(2)核心開始第一階段,準備資料(從磁碟拷貝到緩衝區),程序請求的資料並不是一下就能準備好;準備資料是要消耗時間的;

(3)與此同時,程序阻塞(程序是自己選擇阻塞與否),等待資料ing;

(4)直到資料從核心拷貝到了使用者空間,核心返回結果,程序解除阻塞。

也就是說,核心準備資料資料從核心拷貝到程序記憶體地址這兩個過程都是阻塞的。

 

2.2 non-block(非阻塞I/O模型)

可以通過設定socket使其變為non-blocking。當對一個non-blocking socket執行讀操作時,流程是這個樣子:

 

  (1)當用戶程序發出read操作時,如果kernel中的資料還沒有準備好;

  (2)那麼它並不會block使用者程序,而是立刻返回一個error,從使用者程序角度講 ,它發起一個read操作後,並不需要等待,而是馬上就得到了一個結果;

  (3)使用者程序判斷結果是一個error時,它就知道資料還沒有準備好,於是它可以再次傳送read操作。一旦kernel中的資料準備好了,並且又再次收到了使用者程序的system call;

  (4)那麼它馬上就將資料拷貝到了使用者記憶體,然後返回。

  所以,nonblocking IO的特點是使用者程序核心準備資料的階段需要不斷的主動詢問資料好了沒有

 

2.3 I/O多路複用

    I/O多路複用實際上就是用select, poll, epoll監聽多個io物件,當io物件有變化(有資料)的時候就通知使用者程序。好處就是單個程序可以處理多個socket。當然具體區別我們後面再討論,現在先來看下I/O多路複用的流程:

  (1)當用戶程序呼叫了select,那麼整個程序會被block;

      (2)而同時,kernel會“監視”所有select負責的socket;

  (3)當任何一個socket中的資料準備好了,select就會返回;

  (4)這個時候使用者程序再呼叫read操作,將資料從kernel拷貝到使用者程序。

  所以,I/O 多路複用的特點是通過一種機制一個程序能同時等待多個檔案描述符,而這些檔案描述符(套接字描述符)其中的任意一個進入讀就緒狀態,select()函式就可以返回

  這個圖和blocking IO的圖其實並沒有太大的不同,事實上,還更差一些。因為這裡需要使用兩個system call (select 和 recvfrom),而blocking IO只調用了一個system call (recvfrom)。但是,用select的優勢在於它可以同時處理多個connection。

  所以,如果處理的連線數不是很高的話,使用select/epoll的web server不一定比使用多執行緒 + 阻塞 IO的web server效能更好,可能延遲還更大。

  select/epoll的優勢並不是對於單個連線能處理得更快,而是在於能處理更多的連線。)

  在IO multiplexing Model中,實際中,對於每一個socket,一般都設定成為non-blocking,但是,如上圖所示,整個使用者的process其實是一直被block的。只不過process是被select這個函式block,而不是被socket IO給block。

 

 2.4 asynchronous I/O(非同步 I/O)

  真正的非同步I/O很牛逼,流程大概如下:

(1)使用者程序發起read操作之後,立刻就可以開始去做其它的事。

(2)而另一方面,從kernel的角度,當它受到一個asynchronous read之後,首先它會立刻返回,所以不會對使用者程序產生任何block。

(3)然後,kernel會等待資料準備完成,然後將資料拷貝到使用者記憶體,當這一切都完成之後,kernel會給使用者程序傳送一個signal,告訴它read操作完成了。

 

2.5 小結

(1)blocking和non-blocking的區別

  呼叫blocking IO會一直block住對應的程序直到操作完成,而non-blocking IO在kernel還準備資料的情況下會立刻返回。

(2)synchronous IO和asynchronous IO的區別

  在說明synchronous IO和asynchronous IO的區別之前,需要先給出兩者的定義。POSIX的定義是這樣子的:
    - A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
    - An asynchronous I/O operation does not cause the requesting process to be blocked;

  兩者的區別就在於synchronous IO做”IO operation”的時候會將process阻塞。按照這個定義,之前所述的blocking IO,non-blocking IO,IO multiplexing都屬於synchronous IO。

  有人會說,non-blocking IO並沒有被block啊。這裡有個非常“狡猾”的地方,定義中所指的”IO operation”是指真實的IO操作,就是例子中的recvfrom這個system call。non-blocking IO在執行recvfrom這個system call的時候,如果kernel的資料沒有準備好,這時候不會block程序。但是,當kernel中資料準備好的時候,recvfrom會將資料從kernel拷貝到使用者記憶體中,這個時候程序是被block了,在這段時間內,程序是被block的。

  而asynchronous IO則不一樣,當程序發起IO 操作之後,就直接返回再也不理睬了,直到kernel傳送一個訊號,告訴程序說IO完成。在這整個過程中,程序完全沒有被block。

(3)non-blocking IO和asynchronous IO的區別

  可以發現non-blocking IO和asynchronous IO的區別還是很明顯的。

  --在non-blocking IO中,雖然程序大部分時間都不會被block,但是它仍然要求程序去主動的check,並且當資料準備完成以後,也需要程序主動的再次呼叫recvfrom來將資料拷貝到使用者記憶體

  --而asynchronous IO則完全不同。它就像是使用者程序將整個IO操作交給了他人(kernel)完成,然後他人做完後發訊號通知。在此期間,使用者程序不需要去檢查IO操作的狀態,也不需要主動的去拷貝資料。

 

回到頂部

3 事件驅動程式設計模型

3.1論事件驅動

  通常,我們寫 伺服器處理模型的程式時,有以下幾種模型:     (1)每收到一個請求,建立一個新的程序,來處理該請求;     (2)每收到一個請求,建立一個新的執行緒,來處理該請求;     (3)每收到一個請求,放入一個事件列表,讓主程序通過非阻塞I/O方式來處理請求   上面的幾種方式,各有千秋:     第(1)中方法,由於建立新的程序:實現比較簡單,但開銷比較大,導致伺服器效能比較差。     第(2)種方式,由於要涉及到執行緒的同步,有可能會面臨死鎖等問題。     第(3)種方式,在寫應用程式程式碼時,邏輯比前面兩種都複雜。   綜合考慮各方面因素,一般普遍認為 第(3)種方式是大多數網路伺服器採用的方式。  

3.2 看圖說話講事件驅動模型

  在UI程式設計中,常常要對滑鼠點選進行相應,首先如何獲得滑鼠點選呢?
  方式一:建立一個執行緒,該執行緒一直迴圈檢測是否有滑鼠點選,那麼這個方式有以下幾個缺點
    1. CPU資源浪費,可能滑鼠點選的頻率非常小,但是掃描執行緒還是會一直迴圈檢測,這會造成很多的CPU資源浪費;如果掃描滑鼠點選的介面是阻塞的呢?
    2. 如果是堵塞的,又會出現下面這樣的問題,如果我們不但要掃描滑鼠點選,還要掃描鍵盤是否按下,由於掃描滑鼠時被堵塞了,那麼可能永遠不會去掃描鍵盤;
    3. 如果一個迴圈需要掃描的裝置非常多,這又會引來響應時間的問題;
  所以,該方式是非常不好的。

方式二:就是事件驅動模型
  目前大部分的UI程式設計都是事件驅動模型,如很多UI平臺都會提供onClick()事件,這個事件就代表滑鼠按下事件。事件驅動模型大體思路如下:
    1. 有一個事件(訊息)佇列;
    2. 滑鼠按下時,往這個佇列中增加一個點選事件(訊息);
    3. 有個迴圈,不斷從佇列取出事件,根據不同的事件,呼叫不同的函式,如onClick()、onKeyDown()等;
    4. 事件(訊息)一般都各自儲存各自的處理函式指標,這樣,每個訊息都有獨立的處理函式;

  事件驅動程式設計是一種網路程式設計正規化,這裡程式的執行流由外部事件來決定。它的特點是包含一個事件迴圈,當外部事件發生時使用回撥機制來觸發相應的處理。另外兩種常見的程式設計正規化是(單執行緒)同步以及多執行緒程式設計。

  讓我們用例子來比較和對比一下單執行緒、多執行緒以及事件驅動程式設計模型。下圖展示了隨著時間的推移,這三種模式下程式所做的工作。這個程式有3個任務需要完成,每個任務都在等待I/O操作時阻塞自身。阻塞在I/O操作上所花費的時間已經用灰色框標示出來了。

  在單執行緒同步模型中,任務按照順序執行。如果某個任務因為I/O而阻塞,其他所有的任務都必須等待,直到它完成之後它們才能依次執行。這種明確的執行順序和序列化處理的行為是很容易推斷得出的。如果任務之間並沒有互相依賴的關係,但仍然需要互相等待的話這就使得程式不必要的降低了執行速度。

  在多執行緒版本中,這3個任務分別在獨立的執行緒中執行。這些執行緒由作業系統來管理,在多處理器系統上可以並行處理,或者在單處理器系統上交錯執行。這使得當某個執行緒阻塞在某個資源的同時其他執行緒得以繼續執行。與完成類似功能的同步程式相比,這種方式更有效率,但程式設計師必須寫程式碼來保護共享資源,防止其被多個執行緒同時訪問。多執行緒程式更加難以推斷,因為這類程式不得不通過執行緒同步機制如鎖、可重入函式、執行緒區域性儲存或者其他機制來處理執行緒安全問題,如果實現不當就會導致出現微妙且令人痛不欲生的bug。

  在事件驅動版本的程式中,3個任務交錯執行,但仍然在一個單獨的執行緒控制中。當處理I/O或者其他昂貴的操作時,註冊一個回撥到事件迴圈中,然後當I/O操作完成時繼續執行。回撥描述了該如何處理某個事件。事件迴圈輪詢所有的事件,當事件到來時將它們分配給等待處理事件的回撥函式。這種方式讓程式儘可能的得以執行而不需要用到額外的執行緒。事件驅動型程式比多執行緒程式更容易推斷出行為,因為程式設計師不需要關心執行緒安全問題。

當我們面對如下的環境時,事件驅動模型通常是一個好的選擇:

  1. 程式中有許多工,而且…
  2. 任務之間高度獨立(因此它們不需要互相通訊,或者等待彼此)而且…
  3. 在等待事件到來時,某些任務會阻塞。

  當應用程式需要在任務間共享可變的資料時,這也是一個不錯的選擇,因為這裡不需要採用同步處理。

  網路應用程式通常都有上述這些特點,這使得它們能夠很好的契合事件驅動程式設計模型。

 

回到頂部

4 select/poll/epoll的區別及其Python示例

4.1 select/poll/epoll的區別

  首先前文已述I/O多路複用的本質就是用select/poll/epoll,去監聽多個socket物件,如果其中的socket物件有變化,只要有變化,使用者程序就知道了。

  select是不斷輪詢去監聽的socket,socket個數有限制,一般為1024個;

  poll還是採用輪詢方式監聽,只不過沒有個數限制;

  epoll並不是採用輪詢方式去監聽了,而是當socket有變化時通過回撥的方式主動告知使用者程序。

4.2 Python select示例

  Python的select()方法直接呼叫作業系統的IO介面,它監控sockets,open files, and pipes(所有帶fileno()方法的檔案控制代碼)何時變成readable 和writeable, 或者通訊錯誤,select()使得同時監控多個連線變的簡單,並且這比寫一個長迴圈來等待和監控多客戶端連線要高效,因為select直接通過作業系統提供的C的網路介面進行操作,而不是通過Python的直譯器。

  注意:Using Python’s file objects with select() works for Unix, but is not supported under Windows.

  接下來通過echo server例子要以瞭解select 是如何通過單程序實現同時處理多個非阻塞的socket連線的:

複製程式碼
 1 import select
 2 import socket
 3 import sys
 4 import Queue
 5  
 6 # Create a TCP/IP socket
 7 server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 8 server.setblocking(0)
 9  
10 # Bind the socket to the port
11 server_address = ('localhost', 10000)
12 print >>sys.stderr, 'starting up on %s port %s' % server_address
13 server.bind(server_address)
14  
15 # Listen for incoming connections
16 server.listen(5)
複製程式碼

  select()方法接收並監控3個通訊列表, 第一個是所有的輸入的data,就是指外部發過來的資料,第2個是監控和接收所有要發出去的data(outgoing data),第3個監控錯誤資訊,接下來我們需要建立2個列表來包含輸入和輸出資訊來傳給select().

1 # Sockets from which we expect to read
2 inputs = [ server ]
3 
4 # Sockets to which we expect to write
5 outputs = [ ] 

  所有客戶端的進來的連線和資料將會被server的主迴圈程式放在上面的list中處理,我們現在的server端需要等待連線可寫(writable)之後才能過來,然後接收資料並返回(因此不是在接收到資料之後就立刻返回),因為每個連線要把輸入或輸出的資料先快取到queue裡,然後再由select取出來再發出去。

  Connections are added to and removed from these lists by the server main loop. Since this version of the server is going to wait for a socket to become writable before sending any data (instead of immediately sending the reply), each output connection needs a queue to act as a buffer for the data to be sent through it.

1 # Outgoing message queues (socket:Queue)
2 message_queues = {}

  The main portion of the server program loops, calling select() to block and wait for network activity.

  下面是此程式的主迴圈,呼叫select()時會阻塞和等待直到新的連線和資料進來:

1 while inputs:
2 
3     # Wait for at least one of the sockets to be ready for processing
4     print >>sys.stderr, '\nwaiting for the next event'
5     readable, writable, exceptional = select.select(inputs, outputs, inputs)

  當你把inputs,outputs,exceptional(這裡跟inputs共用)傳給select()後,它返回3個新的list,我們上面將他們分別賦值為readable,writable,exceptional, 所有在readable list中的socket連線代表有資料可接收(recv),所有在writable list中的存放著你可以對其進行傳送(send)操作的socket連線,當連線通訊出現error時會把error寫到exceptional列表中。

  select() returns three new lists, containing subsets of the contents of the lists passed in. All of the sockets in the readable list have incoming data buffered and available to be read. All of the sockets in the writable list have free space in their buffer and can be written to. The sockets returned in exceptional have had an error (the actual definition of “exceptional condition” depends on the platform).

  Readable list 中的socket 可以有3種可能狀態,第一種是如果這個socket是main "server" socket,它負責監聽客戶端的連線,如果這個main server socket出現在readable裡,那代表這是server端已經ready來接收一個新的連線進來了,為了讓這個main server能同時處理多個連線,在下面的程式碼裡,我們把這個main server的socket設定為非阻塞模式。

  The “readable” sockets represent three possible cases. If the socket is the main “server” socket, the one being used to listen for connections, then the “readable” condition means it is ready to accept another incoming connection. In addition to adding the new connection to the list of inputs to monitor, this section sets the client socket to not block.

複製程式碼
 1 # Handle inputs
 2 for s in readable:
 3  
 4     if s is server:
 5         # A "readable" server socket is ready to accept a connection
 6         connection, client_address = s.accept()
 7         print >>sys.stderr, 'new connection from', client_address
 8         connection.setblocking(0)
 9         inputs.append(connection)
10  
11         # Give the connection a queue for data we want to send
12         message_queues[connection] = Queue.Queue()
複製程式碼

  第二種情況是這個socket是已經建立了的連線,它把資料發了過來,這個時候你就可以通過recv()來接收它發過來的資料,然後把接收到的資料放到queue裡,這樣你就可以把接收到的資料再傳回給客戶端了。

  The next case is an established connection with a client that has sent data. The data is read with recv(), then placed on the queue so it can be sent through the socket and back to the client.

複製程式碼
1 else:
2      data = s.recv(1024)
3      if data:
4          # A readable client socket has data
5          print >>sys.stderr, 'received "%s" from %s' % (data, s.getpeername())
6          message_queues[s].put(data)
7          # Add output channel for response
8          if s not in outputs:
9              outputs.append(s)
複製程式碼

  第三種情況就是這個客戶端已經斷開了,所以你再通過recv()接收到的資料就為空了,所以這個時候你就可以把這個跟客戶端的連線關閉了。

  A readable socket without data available is from a client that has disconnected, and the stream is ready to be closed.

複製程式碼
 1 else:
 2     # Interpret empty result as closed connection
 3     print >>sys.stderr, 'closing', client_address, 'after reading no data'
 4     # Stop listening for input on the connection
 5     if s in outputs:
 6         outputs.remove(s)  #既然客戶端都斷開了,我就不用再給它返回資料了,所以這時候如果這個客戶端的連線物件還在outputs列表中,就把它刪掉
 7     inputs.remove(s)    #inputs中也刪除掉
 8     s.close()           #把這個連線關閉掉
 9  
10     # Remove message queue
11     del message_queues[s]
複製程式碼

  對於writable list中的socket,也有幾種狀態,如果這個客戶端連線在跟它對應的queue裡有資料,就把這個資料取出來再發回給這個客戶端,否則就把這個連線從output list中移除,這樣下一次迴圈select()呼叫時檢測到outputs list中沒有這個連線,那就會認為這個連線還處於非活動狀態

  There are fewer cases for the writable connections. If there is data in the queue for a connection, the next message is sent. Otherwise, the connection is removed from the list of output connections so that the next time through the loop select() does not indicate that the socket is ready to send data.

複製程式碼
 1 # Handle outputs
 2 for s in writable:
 3     try:
 4         next_msg = message_queues[s].get_nowait()
 5     except Queue.Empty:
 6         # No messages waiting so stop checking for writability.
 7         print >>sys.stderr, 'output queue for', s.getpeername(), 'is empty'
 8         outputs.remove(s)
 9     else:
10         print >>sys.stderr, 'sending "%s" to %s' % (next_msg, s.getpeername())
11         s.send(next_msg)
複製程式碼

  最後,如果在跟某個socket連線通訊過程中出了錯誤,就把這個連線物件在inputs\outputs\message_queue中都刪除,再把連線關閉掉。

複製程式碼
 1 # Handle "exceptional conditions"
 2 for s in exceptional:
 3     print >>sys.stderr, 'handling exceptional condition for', s.getpeername()
 4     # Stop listening for input on the connection
 5     inputs.remove(s)
 6     if s in outputs:
 7         outputs.remove(s)
 8     s.close()
 9  
10     # Remove message queue
11     del message_queues[s]
複製程式碼

4.3 完整的server端和client端示例

  這裡實現了一個server,其功能就是可以和多個client建立連線,每個client的發過來的資料加上一個response字串返回給client端~~~

server端:

複製程式碼
 1 #! /usr/bin/env python3
 2 # -*- coding:utf-8 -*-
 3 import socket
 4 import select
 5 
 6 sk = socket.socket()
 7 sk.bind(('127.0.0.1', 9000),)
 8 sk.listen(5)
 9 
10 inputs = [sk, ]
11 outputs = []
12 message = {}  # 實現讀寫分離
13 print("start...")
14 
15 while True:
16     # 監聽的inputs中的socket物件內部如果有變化,那麼這個物件就會在rlist
17     # outputs裡有什麼物件,wlist中就有什麼物件
18     # []如果這裡的物件內部出錯,那會把這些物件加到elist中
19     # 1 是超時時間
20     rlist, wlist, elist = select.select(inputs, outputs, [], 1)
21     print(len(inputs), len(outputs))
22 
23     for r in rlist:
24         if r == sk:
25             conn, addr = sk.accept()
26             conn.sendall(b"ok")
27             # 這裡記住是吧conn新增到inputs中去監聽,千萬別寫成r了
28             inputs.append(conn)
29             message[conn] = []
30         else:
31             try:
32                 data = r.recv(1024)
33                 print(data)
34                 if not data:
35                     raise Exception('連線斷開')
36                 message[r].append(data)
37                 outputs.append(r)
38             except Exception as e:
39                 inputs.remove(r)
40                 del message[r]
41 
42     for r in wlist:
43         data = str(message[r].pop(), encoding='utf-8')
44         res = data + "response"
45         r.sendall(bytes(res, encoding='utf-8'))
46         outputs.remove(r)
47 # 實現讀寫分離
48 # IO多路複用的本質是用select、poll、epoll(系統底層提供的)來監聽socket物件內部是否有變化
49 # select 是在Win和Linux中都支援額,相當於系統內部維護了一個for迴圈,缺點是監聽個數有上限(1024),效率不高
50 # poll的監聽個數沒有限制,但仍然用迴圈,效率不高。
51 # epoll的機制是socket物件變化,主動告訴epoll。而不是輪詢,相當於有個回撥函式,效率比前兩者高
52 # Nginx就是用epoll。只要IO操作都支援,除開檔案操作
53 
54 # 列表刪除指定元素用remove
複製程式碼

client端:

複製程式碼
 1 #! /usr/bin/env python3
 2 # -*- coding:utf-8 -*-
 3 
 4 import socket
 5 
 6 
 7 sc = socket.socket()
 8 sc.connect(("127.0.0.1", 9000,))
 9 
10 
11 data = sc.recv(1024)
12 print(data)
13 while True:
14     msg = input(">>>:")
15     if msg == 'q':
16         break
17     if len(msg) == 0:
18         continue
19 
20     send_msg = bytes(msg, encoding="utf-8")
21     sc.send(send_msg)
22     res = sc.recv(1024)
23     print(str(res, encoding="utf-8"))
24 sc.close()
複製程式碼