單進程服務器
1. 完成一個簡單的TCP服務器
from socket import * serSocket = socket(AF_INET, SOCK_STREAM) # 重復使用綁定的信息 serSocket.setsockopt(SOL_SOCKET, SO_REUSEADDR , 1) localAddr = ('', 7788) serSocket.bind(localAddr) serSocket.listen(5) while True: print('-----主進程,,等待新客戶端的到來------') newSocket,destAddr = serSocket.accept() print('-----主進程,,接下來負責數據處理[%s]-----'%str(destAddr)) try: while True: recvData = newSocket.recv(1024) if len(recvData)>0: print('recv[%s]:%s'%(str(destAddr), recvData)) else: print('[%s]客戶端已經關閉'%str(destAddr)) break finally: newSocket.close() serSocket.close()
2. 總結
- 同一時刻只能為一個客戶進行服務,不能同時為多個客戶服務
- 類似於找一個“明星”簽字一樣,客戶需要耐心等待才可以獲取到服務
當服務器為一個客戶端服務時,而另外的客戶端發起了connect,只要服務器listen的隊列有空閑的位置,就會為這個新客戶端進行連接,並且客戶端可以發送數據,但當服務器為這個新客戶端服務時,可能一次性把所有數據接收完畢
- 當recv接收數據時,返回值為空,即沒有返回數據,那么意味着客戶端已經調用了close關閉了;因此服務器通過判斷recv接收數據是否為空 來判斷客戶端是否已經下線
多進程服務器
1. 多進程服務器
from socket import * from multiprocessing import * from time import sleep # 處理客戶端的請求並為其服務 def dealWithClient(newSocket,destAddr): while True: recvData = newSocket.recv(1024) if len(recvData)>0: print('recv[%s]:%s'%(str(destAddr), recvData)) else: print('[%s]客戶端已經關閉'%str(destAddr)) break newSocket.close() def main(): serSocket = socket(AF_INET, SOCK_STREAM) serSocket.setsockopt(SOL_SOCKET, SO_REUSEADDR , 1) localAddr = ('', 7788) serSocket.bind(localAddr) serSocket.listen(5) try: while True: print('-----主進程,,等待新客戶端的到來------') newSocket,destAddr = serSocket.accept() print('-----主進程,,接下來創建一個新的進程負責數據處理[%s]-----'%str(destAddr)) client = Process(target=dealWithClient, args=(newSocket,destAddr)) client.start() #因為已經向子進程中copy了一份(引用),並且父進程中這個套接字也沒有用處了 #所以關閉 newSocket.close() finally: #當為所有的客戶端服務完之后再進行關閉,表示不再接收新的客戶端的鏈接 serSocket.close() if __name__ == '__main__': main()
2. 總結
- 通過為每個客戶端創建一個進程的方式,能夠同時為多個客戶端進行服務
- 當客戶端不是特別多的時候,這種方式還行,如果有幾百上千個,就不可取了,因為每次創建進程等過程需要好較大的資源
多線程服務器
#coding=utf-8 from socket import * from threading import Thread from time import sleep # 處理客戶端的請求並執行事情 def dealWithClient(newSocket,destAddr): while True: recvData = newSocket.recv(1024) if len(recvData)>0: print('recv[%s]:%s'%(str(destAddr), recvData)) else: print('[%s]客戶端已經關閉'%str(destAddr)) break newSocket.close() def main(): serSocket = socket(AF_INET, SOCK_STREAM) serSocket.setsockopt(SOL_SOCKET, SO_REUSEADDR , 1) localAddr = ('', 7788) serSocket.bind(localAddr) serSocket.listen(5) try: while True: print('-----主進程,,等待新客戶端的到來------') newSocket,destAddr = serSocket.accept() print('-----主進程,,接下來創建一個新的進程負責數據處理[%s]-----'%str(destAddr)) client = Thread(target=dealWithClient, args=(newSocket,destAddr)) client.start() #因為線程中共享這個套接字,如果關閉了會導致這個套接字不可用, #但是此時在線程中這個套接字可能還在收數據,因此不能關閉 #newSocket.close() finally: serSocket.close() if __name__ == '__main__': main()
單進程服務器-非堵塞模式
服務器
#coding=utf-8 from socket import * import time # 用來存儲所有的新鏈接的socket g_socketList = [] def main(): serSocket = socket(AF_INET, SOCK_STREAM) serSocket.setsockopt(SOL_SOCKET, SO_REUSEADDR , 1) localAddr = ('', 7788) serSocket.bind(localAddr) #可以適當修改listen中的值來看看不同的現象 serSocket.listen(1000) #將套接字設置為非堵塞 #設置為非堵塞后,如果accept時,恰巧沒有客戶端connect,那么accept會 #產生一個異常,所以需要try來進行處理 serSocket.setblocking(False) while True: #用來測試 #time.sleep(0.5) try: newClientInfo = serSocket.accept() except Exception as result: pass else: print("一個新的客戶端到來:%s"%str(newClientInfo)) newClientInfo[0].setblocking(False) g_socketList.append(newClientInfo) # 用來存儲需要刪除的客戶端信息 needDelClientInfoList = [] for clientSocket,clientAddr in g_socketList: try: recvData = clientSocket.recv(1024) if len(recvData)>0: print('recv[%s]:%s'%(str(clientAddr), recvData)) else: print('[%s]客戶端已經關閉'%str(clientAddr)) clientSocket.close() g_needDelClientInfoList.append((clientSocket,clientAddr)) except Exception as result: pass for needDelClientInfo in needDelClientInfoList: g_socketList.remove(needDelClientInfo) if __name__ == '__main__': main()
#coding=utf-8 from socket import * import time # 用來存儲所有的新鏈接的socket g_socketList = [] def main(): serSocket = socket(AF_INET, SOCK_STREAM) serSocket.setsockopt(SOL_SOCKET, SO_REUSEADDR , 1) localAddr = ('', 7788) serSocket.bind(localAddr) #可以適當修改listen中的值來看看不同的現象 serSocket.listen(1000) #將套接字設置為非堵塞 #設置為非堵塞后,如果accept時,恰巧沒有客戶端connect,那么accept會 #產生一個異常,所以需要try來進行處理 serSocket.setblocking(False) while True: #用來測試 #time.sleep(0.5) try: newClientInfo = serSocket.accept() except Exception as result: pass else: print("一個新的客戶端到來:%s"%str(newClientInfo)) newClientInfo[0].setblocking(False) g_socketList.append(newClientInfo) # 用來存儲需要刪除的客戶端信息 needDelClientInfoList = [] for clientSocket,clientAddr in g_socketList: try: recvData = clientSocket.recv(1024) if len(recvData)>0: print('recv[%s]:%s'%(str(clientAddr), recvData)) else: print('[%s]客戶端已經關閉'%str(clientAddr)) clientSocket.close() g_needDelClientInfoList.append((clientSocket,clientAddr)) except Exception as result: pass for needDelClientInfo in needDelClientInfoList: g_socketList.remove(needDelClientInfo) if __name__ == '__main__': main()
客戶端
#coding=utf-8 from socket import * import random import time serverIp = raw_input("請輸入服務器的ip:") connNum = raw_input("請輸入要鏈接服務器的次數(例如1000):") g_socketList = [] for i in range(int(connNum)): s = socket(AF_INET, SOCK_STREAM) s.connect((serverIp, 7788)) g_socketList.append(s) print(i) while True: for s in g_socketList: s.send(str(random.randint(0,100))) # 用來測試用 #time.sleep(1)
select版-TCP服務器
1. select 原理
在多路復用的模型中,比較常用的有select模型和epoll模型。這兩個都是系統接口,由操作系統提供。當然,Python的select模塊進行了更高級的封裝。
網絡通信被Unix系統抽象為文件的讀寫,通常是一個設備,由設備驅動程序提供,驅動可以知道自身的數據是否可用。支持阻塞操作的設備驅動通常會實現一組自身的等待隊列,如讀/寫等待隊列用於支持上層(用戶層)所需的block或non-block操作。設備的文件的資源如果可用(可讀或者可寫)則會通知進程,反之則會讓進程睡眠,等到數據到來可用的時候,再喚醒進程。
這些設備的文件描述符被放在一個數組中,然后select調用的時候遍歷這個數組,如果對於的文件描述符可讀則會返回改文件描述符。當遍歷結束之后,如果仍然沒有一個可用設備文件描述符,select讓用戶進程則會睡眠,直到等待資源可用的時候在喚醒,遍歷之前那個監視的數組。每次遍歷都是依次進行判斷的。
2. select 回顯服務器
使用python的select模塊很容易寫出下面一個echo(回顯)服務器:
import select import socket import sys server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind(('', 7788)) server.listen(5) inputs = [server, sys.stdin] running = True while True: # 調用 select 函數,阻塞等待 readable, writeable, exceptional = select.select(inputs, [], []) # 數據抵達,循環 for sock in readable: # 監聽到有新的連接 if sock == server: conn, addr = server.accept() # select 監聽的socket inputs.append(conn) # 監聽到鍵盤有輸入 elif sock == sys.stdin: cmd = sys.stdin.readline() running = False break # 有數據到達 else: # 讀取客戶端連接發送的數據 data = sock.recv(1024) if data: sock.send(data) else: # 移除select監聽的socket inputs.remove(sock) sock.close() # 如果檢測到用戶輸入敲擊鍵盤,那么就退出 if not running: break server.close()
在windows中,使用‘網絡調試助手’,進行連接服務器即可測試
另外一個服務器(包含writeList):
#coding=utf-8 import socket import Queue from select import select SERVER_IP = ('', 9999) # 保存客戶端發送過來的消息,將消息放入隊列中 message_queue = {} input_list = [] output_list = [] if __name__ == "__main__": server = socket.socket() server.bind(SERVER_IP) server.listen(10) # 設置為非阻塞 server.setblocking(False) # 初始化將服務端加入監聽列表 input_list.append(server) while True: # 開始 select 監聽,對input_list中的服務端server進行監聽 stdinput, stdoutput, stderr = select(input_list, output_list, input_list) # 循環判斷是否有客戶端連接進來,當有客戶端連接進來時select將觸發 for obj in stdinput: # 判斷當前觸發的是不是服務端對象, 當觸發的對象是服務端對象時,說明有新客戶端連接進來了 if obj == server: # 接收客戶端的連接, 獲取客戶端對象和客戶端地址信息 conn, addr = server.accept() print("Client %s connected! "%str(addr)) # 將客戶端對象也加入到監聽的列表中, 當客戶端發送消息時 select 將觸發 input_list.append(conn) # 為連接的客戶端單獨創建一個消息隊列,用來保存客戶端發送的消息 message_queue[conn] = Queue.Queue() else: # 由於客戶端連接進來時服務端接收客戶端連接請求,將客戶端加入到了監聽列表中(input_list),客戶端發送消息將觸發 # 所以判斷是否是客戶端對象觸發 try: recv_data = obj.recv(1024) # 客戶端未斷開 if recv_data: print("received %s from client %s"%(recv_data, str(addr))) # 將收到的消息放入到各客戶端的消息隊列中 message_queue[obj].put(recv_data) # 將回復操作放到output列表中,讓select監聽 if obj not in output_list: output_list.append(obj) except ConnectionResetError: # 客戶端斷開連接了,將客戶端的監聽從input列表中移除 input_list.remove(obj) # 移除客戶端對象的消息隊列 del message_queue[obj] print("\n[input] Client %s disconnected"%str(addr)) # 如果現在沒有客戶端請求,也沒有客戶端發送消息時,開始對發送消息列表進行處理,是否需要發送消息 for sendobj in output_list: try: # 如果消息隊列中有消息,從消息隊列中獲取要發送的消息 if not message_queue[sendobj].empty(): # 從該客戶端對象的消息隊列中獲取要發送的消息 send_data = message_queue[sendobj].get() sendobj.send(send_data) else: # 將監聽移除等待下一次客戶端發送消息 output_list.remove(sendobj) except ConnectionResetError: # 客戶端連接斷開了 del message_queue[sendobj] output_list.remove(sendobj) print("\n[output] Client %s disconnected"%str(addr))
3. 總結
優點
select目前幾乎在所有的平台上支持,其良好跨平台支持也是它的一個優點。
缺點
select的一個缺點在於單個進程能夠監視的文件描述符的數量存在最大限制,在Linux上一般為1024,可以通過修改宏定義甚至重新編譯內核的方式提升這一限制,但是這樣也會造成效率的降低。
一般來說這個數目和系統內存關系很大,具體數目可以cat /proc/sys/fs/file-max察看。32位機默認是1024個。64位機默認是2048.
對socket進行掃描時是依次掃描的,即采用輪詢的方法,效率較低。
當套接字比較多的時候,每次select()都要通過遍歷FD_SETSIZE個Socket來完成調度,不管哪個Socket是活躍的,都遍歷一遍。這會浪費很多CPU時間。
epoll版-TCP服務器
1. epoll的優點:
- 沒有最大並發連接的限制,能打開的FD(指的是文件描述符,通俗的理解就是套接字對應的數字編號)的上限遠大於1024
- 效率提升,不是輪詢的方式,不會隨着FD數目的增加效率下降。只有活躍可用的FD才會調用callback函數;即epoll最大的優點就在於它只管你“活躍”的連接,而跟連接總數無關,因此在實際的網絡環境中,epoll的效率就會遠遠高於select和poll。
2. epoll使用參考代碼
import socket import select # 創建套接字 s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 設置可以重復使用綁定的信息 s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) # 綁定本機信息 s.bind(("",7788)) # 變為被動 s.listen(10) # 創建一個epoll對象 epoll=select.epoll() # 測試,用來打印套接字對應的文件描述符 # print s.fileno() # print select.EPOLLIN|select.EPOLLET # 注冊事件到epoll中 # epoll.register(fd[, eventmask]) # 注意,如果fd已經注冊過,則會發生異常 # 將創建的套接字添加到epoll的事件監聽中 epoll.register(s.fileno(),select.EPOLLIN|select.EPOLLET) connections = {} addresses = {} # 循環等待客戶端的到來或者對方發送數據 while True: # epoll 進行 fd 掃描的地方 -- 未指定超時時間則為阻塞等待 epoll_list=epoll.poll() # 對事件進行判斷 for fd,events in epoll_list: # print fd # print events # 如果是socket創建的套接字被激活 if fd == s.fileno(): conn,addr=s.accept() print('有新的客戶端到來%s'%str(addr)) # 將 conn 和 addr 信息分別保存起來 connections[conn.fileno()] = conn addresses[conn.fileno()] = addr # 向 epoll 中注冊 連接 socket 的 可讀 事件 epoll.register(conn.fileno(), select.EPOLLIN | select.EPOLLET) elif events == select.EPOLLIN: # 從激活 fd 上接收 recvData = connections[fd].recv(1024) if len(recvData)>0: print('recv:%s'%recvData) else: # 從 epoll 中移除該 連接 fd epoll.unregister(fd) # server 側主動關閉該 連接 fd connections[fd].close() print("%s---offline---"%str(addresses[fd]))
2. 說明
- EPOLLIN (可讀)
- EPOLLOUT (可寫)
- EPOLLET (ET模式)
epoll對文件描述符的操作有兩種模式:LT(level trigger)和ET(edge trigger)。LT模式是默認模式,LT模式與ET模式的區別如下:
LT模式:當epoll檢測到描述符事件發生並將此事件通知應用程序,應用程序可以不立即處理該事件。下次調用epoll時,會再次響應應用程序並通知此事件。
ET模式:當epoll檢測到描述符事件發生並將此事件通知應用程序,應用程序必須立即處理該事件。如果不處理,下次調用epoll時,不會再次響應應用程序並通知此事件。
協程
協程,又稱微線程,纖程。英文名Coroutine。
協程是啥
首先我們得知道協程是啥?協程其實可以認為是比線程更小的執行單元。 為啥說他是一個執行單元,因為他自帶CPU上下文。這樣只要在合適的時機, 我們可以把一個協程 切換到另一個協程。 只要這個過程中保存或恢復 CPU上下文那么程序還是可以運行的。
通俗的理解:在一個線程中的某個函數,可以在任何地方保存當前函數的一些臨時變量等信息,然后切換到另外一個函數中執行,注意不是通過調用函數的方式做到的,並且切換的次數以及什么時候再切換到原來的函數都由開發者自己確定
協程和線程差異
那么這個過程看起來比線程差不多。其實不然, 線程切換從系統層面遠不止保存和恢復 CPU上下文這么簡單。 操作系統為了程序運行的高效性每個線程都有自己緩存Cache等等數據,操作系統還會幫你做這些數據的恢復操作。 所以線程的切換非常耗性能。但是協程的切換只是單純的操作CPU的上下文,所以一秒鍾切換個上百萬次系統都抗的住。
協程的問題
但是協程有一個問題,就是系統並不感知,所以操作系統不會幫你做切換。 那么誰來幫你做切換?讓需要執行的協程更多的獲得CPU時間才是問題的關鍵。
例子
目前的協程框架一般都是設計成 1:N 模式。所謂 1:N 就是一個線程作為一個容器里面放置多個協程。 那么誰來適時的切換這些協程?答案是有協程自己主動讓出CPU,也就是每個協程池里面有一個調度器, 這個調度器是被動調度的。意思就是他不會主動調度。而且當一個協程發現自己執行不下去了(比如異步等待網絡的數據回來,但是當前還沒有數據到), 這個時候就可以由這個協程通知調度器,這個時候執行到調度器的代碼,調度器根據事先設計好的調度算法找到當前最需要CPU的協程。 切換這個協程的CPU上下文把CPU的運行權交個這個協程,直到這個協程出現執行不下去需要等等的情況,或者它調用主動讓出CPU的API之類,觸發下一次調度。
那么這個實現有沒有問題?
其實是有問題的,假設這個線程中有一個協程是CPU密集型的他沒有IO操作, 也就是自己不會主動觸發調度器調度的過程,那么就會出現其他協程得不到執行的情況, 所以這種情況下需要程序員自己避免。這是一個問題,假設業務開發的人員並不懂這個原理的話就可能會出現問題。
協程的好處
在IO密集型的程序中由於IO操作遠遠慢於CPU的操作,所以往往需要CPU去等IO操作。 同步IO下系統需要切換線程,讓操作系統可以在IO過程中執行其他的東西。 這樣雖然代碼是符合人類的思維習慣但是由於大量的線程切換帶來了大量的性能的浪費,尤其是IO密集型的程序。
所以人們發明了異步IO。就是當數據到達的時候觸發我的回調。來減少線程切換帶來性能損失。 但是這樣的壞處也是很大的,主要的壞處就是操作被 “分片” 了,代碼寫的不是 “一氣呵成” 這種。 而是每次來段數據就要判斷 數據夠不夠處理哇,夠處理就處理吧,不夠處理就在等等吧。這樣代碼的可讀性很低,其實也不符合人類的習慣。
但是協程可以很好解決這個問題。比如 把一個IO操作 寫成一個協程。當觸發IO操作的時候就自動讓出CPU給其他協程。要知道協程的切換很輕的。 協程通過這種對異步IO的封裝 既保留了性能也保證了代碼的容易編寫和可讀性。在高IO密集型的程序下很好。但是高CPU密集型的程序下沒啥好處。
協程一個簡單實現
import time def A(): while True: print("----A---") yield time.sleep(0.5) def B(c): while True: print("----B---") c.next() time.sleep(0.5) if __name__=='__main__': a = A() B(a) 運行結果: --B-- --A-- --B-- --A-- --B-- --A-- --B-- --A-- --B-- --A-- --B-- --A-- ...省略...
協程-greenlet版
為了更好使用協程來完成多任務,python中的greenlet模塊對其封裝,從而使得切換任務變的更加簡單
安裝方式
使用如下命令安裝greenlet模塊:
sudo pip install greenlet
#coding=utf-8 from greenlet import greenlet import time def test1(): while True: print "---A--" gr2.switch() time.sleep(0.5) def test2(): while True: print "---B--" gr1.switch() time.sleep(0.5) gr1 = greenlet(test1) gr2 = greenlet(test2) #切換到gr1中運行 gr1.switch()
運行效果
---A--
---B--
---A--
---B--
---A--
---B--
---A--
---B--
...省略...
gevent
greenlet已經實現了協程,但是這個還的人工切換,是不是覺得太麻煩了,不要捉急,python還有一個比greenlet更強大的並且能夠自動切換任務的模塊gevent
其原理是當一個greenlet遇到IO(指的是input output 輸入輸出,比如網絡、文件操作等)操作時,比如訪問網絡,就自動切換到其他的greenlet,等到IO操作完成,再在適當的時候切換回來繼續執行。
由於IO操作非常耗時,經常使程序處於等待狀態,有了gevent為我們自動切換協程,就保證總有greenlet在運行,而不是等待IO
1. gevent的使用
#coding=utf-8 #請使用python 2 來執行此程序 import gevent def f(n): for i in range(n): print gevent.getcurrent(), i g1 = gevent.spawn(f, 5) g2 = gevent.spawn(f, 5) g3 = gevent.spawn(f, 5) g1.join() g2.join() g3.join()
運行結果
<Greenlet at 0x10e49f550: f(5)> 0 <Greenlet at 0x10e49f550: f(5)> 1 <Greenlet at 0x10e49f550: f(5)> 2 <Greenlet at 0x10e49f550: f(5)> 3 <Greenlet at 0x10e49f550: f(5)> 4 <Greenlet at 0x10e49f910: f(5)> 0 <Greenlet at 0x10e49f910: f(5)> 1 <Greenlet at 0x10e49f910: f(5)> 2 <Greenlet at 0x10e49f910: f(5)> 3 <Greenlet at 0x10e49f910: f(5)> 4 <Greenlet at 0x10e49f4b0: f(5)> 0 <Greenlet at 0x10e49f4b0: f(5)> 1 <Greenlet at 0x10e49f4b0: f(5)> 2 <Greenlet at 0x10e49f4b0: f(5)> 3 <Greenlet at 0x10e49f4b0: f(5)> 4
可以看到,3個greenlet是依次運行而不是交替運行
2. gevent切換執行
import gevent def f(n): for i in range(n): print gevent.getcurrent(), i #用來模擬一個耗時操作,注意不是time模塊中的sleep gevent.sleep(1) g1 = gevent.spawn(f, 5) g2 = gevent.spawn(f, 5) g3 = gevent.spawn(f, 5) g1.join() g2.join() g3.join()
運行結果
<Greenlet at 0x7fa70ffa1c30: f(5)> 0 <Greenlet at 0x7fa70ffa1870: f(5)> 0 <Greenlet at 0x7fa70ffa1eb0: f(5)> 0 <Greenlet at 0x7fa70ffa1c30: f(5)> 1 <Greenlet at 0x7fa70ffa1870: f(5)> 1 <Greenlet at 0x7fa70ffa1eb0: f(5)> 1 <Greenlet at 0x7fa70ffa1c30: f(5)> 2 <Greenlet at 0x7fa70ffa1870: f(5)> 2 <Greenlet at 0x7fa70ffa1eb0: f(5)> 2 <Greenlet at 0x7fa70ffa1c30: f(5)> 3 <Greenlet at 0x7fa70ffa1870: f(5)> 3 <Greenlet at 0x7fa70ffa1eb0: f(5)> 3 <Greenlet at 0x7fa70ffa1c30: f(5)> 4 <Greenlet at 0x7fa70ffa1870: f(5)> 4 <Greenlet at 0x7fa70ffa1eb0: f(5)> 4
3個greenlet交替運行
3. gevent並發下載器
當然,實際代碼里,我們不會用gevent.sleep()去切換協程,而是在執行到IO操作時,gevent自動切換,代碼如下
#coding=utf-8 from gevent import monkey; import gevent import urllib2 #有IO才做時需要這一句 monkey.patch_all() def myDownLoad(url): print('GET: %s' % url) resp = urllib2.urlopen(url) data = resp.read() print('%d bytes received from %s.' % (len(data), url)) gevent.joinall([ gevent.spawn(myDownLoad, 'http://www.baidu.com/'), gevent.spawn(myDownLoad, 'http://www.itcast.cn/'), gevent.spawn(myDownLoad, 'http://www.itheima.com/'), ])
運行結果
GET: http://www.baidu.com/ GET: http://www.itcast.cn/ GET: http://www.itheima.com/ 102247 bytes received from http://www.baidu.com/. 166903 bytes received from http://www.itheima.com/. 162294 bytes received from http://www.itcast.cn/.
從上能夠看到是先發送的獲取baidu的相關信息,然后依次是itcast、itheima,但是收到數據的先后順序不一定與發送順序相同,這也就體現出了異步,即不確定什么時候會收到數據,順序不一定
gevent版-TCP服務器
import sys import time import gevent from gevent import socket,monkey monkey.patch_all() def handle_request(conn): while True: data = conn.recv(1024) if not data: conn.close() break print("recv:", data) conn.send(data) def server(port): s = socket.socket() s.bind(('', port)) s.listen(5) while True: cli, addr = s.accept() gevent.spawn(handle_request, cli) if __name__ == '__main__': server(7788)