Python 提供了兩個級別訪問的網絡服務。:html
查看socket類的幫助以下python
import socket # 導入socket模塊 >>> help(socket.socket)
重點關注初始化函數:程序員
__init__(self, family=<AddressFamily.AF_INET: 2>, type=<SocketKind.SOCK_STREAM: 1>, proto=0, fileno=None)
SOCK_STREAM
或SOCK_DGRAM
下面實現一個TCP聊天室和一個UDP聊天室segmentfault
<!--more-->服務器
獲取多個鏈接的處理網絡
開啓accept線程,執行accept操做開始阻塞,有客戶端鏈接時,再開啓一個線程recv進行數據接收的處理。而後accept線程繼續阻塞,等待後續客戶端的鏈接。socket
阻塞的處理tcp
服務端處理客戶端的鏈接時,有兩處存在阻塞,分別是:ide
所以這兩處都須要開啓線程單獨處理,不然會阻塞主線程。函數
客戶端主動斷開的處理
客戶端主動斷開時,若是不通知服務端,那麼服務端上保存的客戶端鏈接不會被清理,這是不合理的。所以客戶端主動斷開時,咱們在應用層約定,客戶端推出前須要發送/quit
指令到服務端上,而後有服務端關閉socket。
聊天室的server端主要是監聽端口,處理來自client端的鏈接,而且分發數據到全部的client端
代碼
import socket import threading class TcpChatServer: def __init__(self, ip='192.168.110.13', port=9001): self.ip = ip self.port = port self.clients = {} self.sock = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) self.event = threading.Event() def recv(self, so, ip ,port): while not self.event.is_set(): data = so.recv(1024).decode() # 將接受到的字節數據bytes轉化爲utf-8格式的字符串 if data.strip() == '/quit': # 客戶端主動斷開時的處理 so.close() self.clients.pop((ip, port)) return for s in self.clients.values(): # 廣播發送 s.send('{}:{}\n{}'.format(ip, port, data).encode()) def accept(self): while not self.event.is_set(): so, (ip, port) = self.sock.accept() self.clients[(ip, port)] = so # 由於so.recv會產生阻塞,所以單獨開一個線程處理數據的接受部分。這樣accept能夠繼續接受來自其餘客戶端的連接 threading.Thread(target=self.recv, args=(so, ip, port), name='client-{}:{}'.format(ip, port)).start() def start(self): self.sock.bind((self.ip, self.port)) self.sock.listen() t = threading.Thread(target=self.accept, daemon=True) # 爲了避免阻塞主線程,單獨開啓一個線程處理accept(accept會阻塞線程) try: t.start() t.join() # 阻塞直到獲取到KeyboardInterrupt except KeyboardInterrupt: self.stop() def stop(self): for s in self.clients.values(): s.close() self.sock.close() self.event.set() # 中止全部的循環 if __name__ == '__main__': tcp_chat_server = TcpChatServer() tcp_chat_server.start()
聊天室的client端主要是發起鏈接,鏈接到server端,而且要接受來自服務端廣播分發的消息。
代碼
import socket import threading class TcpChatClient: def __init__(self): self.sock = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) self.event = threading.Event() def recv(self): # 客戶端須要一直接收服務端廣播分發的消息 while not self.event.is_set(): data = self.sock.recv(1024).decode() data = data.strip() print(data) def send(self): # 輸入消息就發送 while not self.event.is_set(): data = input() self.sock.send(data.encode()) if data.strip() == '/quit': # 發送/quit的時候自身關閉 self.stop() def start(self, ip, port): self.sock.connect((ip, port)) s = threading.Thread(target=self.send, daemon=False) r = threading.Thread(target=self.recv, daemon=False) s.start() r.start() def stop(self): self.sock.close() self.event.set() if __name__ == '__main__': tcp_chat_client = TcpChatClient() tcp_chat_client.start('192.168.110.13', 9001)
阻塞的處理
在UDP服務端接收客戶端的消息時,採用socket.recvfrom(1024)
這個方法以便保存客戶端的地址信息,這個方法會阻塞當前線程,所以須要開啓線程單獨處理。
客戶端主動斷開的處理
UDP客戶端主動關閉以後,服務端是沒法檢測到客戶端已經關閉的。咱們能夠採用如下兩種方法:
UDP服務端程序開啓線程等待接收客戶端的數據,而後廣播給其餘的客戶端,而且檢查全部鏈接的心跳是否超時。
代碼
import socket import datetime import threading class UdpChatServer: def __init__(self, ip='192.168.110.13', port=9001): self.addr = (ip, port) self.sock = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM) self.clients = {} self.event = threading.Event() def recv(self): while not self.event.is_set(): data, addr = self.sock.recvfrom(1024) data = data.decode().strip() now = datetime.datetime.now() if data == '#ping#': # 判斷是否收到心跳 self.clients[addr] = now # 收到心跳則保存客戶端地址,而且更新時間戳 continue disconnected = set() # 沒收到一次數據就判斷全部的失效連接 for addr, timestamp in self.clients.items(): if (now - timestamp).total_seconds() > 10: # 失效條件:2次(即10s)沒收到心跳就判斷客戶端關閉 disconnected.add(addr) else: self.sock.sendto('{}:{}\n{}'.format(addr[0], addr[1], data).encode(), addr) for addr in disconnected: self.clients.pop(addr) def start(self): self.sock.bind(self.addr) # 綁定端口以後就開啓線程一直接受客戶端的數據 t = threading.Thread(target=self.recv(), daemon=True) try: t.start() t.join() except KeyboardInterrupt: self.stop() def stop(self): self.event.set() self.sock.close() if __name__ == '__main__': udp_chat_server = UdpChatServer() udp_chat_server.start()
UDP的客戶端的主線程一直在等待用戶輸入數據而後將數據發送到服務端,同時開啓了一個心跳進程和一個接受服務端廣播數據的線程。
代碼
import socket import threading import time class UdpChatClient: def __init__(self, ip, port): self.addr = (ip, port) self.sock = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM) self.event = threading.Event() def heartbeat(self): # 心跳線程函數:每5s發一次心跳 while not self.event.wait(5): self.sock.sendto(b'#ping#', self.addr) def recv(self): # 一直等待接受udp服務器廣播的數據 while not self.event.is_set(): data = self.sock.recv(1024) print(data.decode()) def start(self): threading.Thread(target=self.heartbeat, name='heartbeat', daemon=True).start() threading.Thread(target=self.recv, name='recv', daemon=True).start() print('請在5s後發言') time.sleep(5) # 由於服務端必須收到一個心跳以後纔會保存次客戶端,所以須要等待5s print('請開始發言') while not self.event.is_set(): data = input('') data = data.strip() if data == '/quit': self.event.set() self.sock.close() return self.sock.sendto(data.encode(), self.addr) if __name__ == '__main__': udp_chat_client = UdpChatClient('192.168.110.13', 9001) udp_chat_client.start()
TODO(Flowsnow):改寫聊天室程序的TcpChatServer和UdpChatServer
附一:TCP和UDP的本質區別
附二:參考資料
記得幫我點贊哦!
精心整理了計算機各個方向的從入門、進階、實戰的視頻課程和電子書,按照目錄合理分類,總能找到你須要的學習資料,還在等什麼?快去關注下載吧!!!
念念不忘,必有迴響,小夥伴們幫我點個贊吧,很是感謝。
我是職場亮哥,YY高級軟件工程師、四年工做經驗,拒絕鹹魚爭當龍頭的斜槓程序員。聽我說,進步多,程序人生一把梭
若是有幸能幫到你,請幫我點個【贊】,給個關注,若是能順帶評論給個鼓勵,將不勝感激。
職場亮哥文章列表:更多文章
本人全部文章、回答都與版權保護平臺有合做,著做權歸職場亮哥全部,未經受權,轉載必究!