IO模式和IO多路復用


  大綱:

  (1)基礎概念回顧

  (2)Linux的I/O模式

  (3)事件驅動編程模型

  (4)select/poll/epoll的區別和Python示例

  網絡編程里常聽到阻塞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()

  終於寫完了~~~

 


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
  © 2014-2022 ITdaan.com