初學Python——Socket網絡編程
認識socket
socket本質上就是在2臺網絡互通的電腦之間,架設一個通道,兩臺電腦通過這個通道來實現數據的互相傳遞。我們知道網絡 通信 都 是基於 ip+port(端口) 方能定位到目標的具體機器上的具體服務,操作系統有0-65535個端口,每個端口都可以獨立對外提供服務,如果 把一個公司比做一臺電腦 ,那公司的總機號碼就相當於ip地址, 每個員工的分機號就相當於端口, 你想找公司某個人,必須 先打電話到總機,然後再轉分機 。
建立一個socket必須至少有2端, 一個服務端,一個客戶端, 服務端被動等待並接收請求,客戶端主動發起請求, 連接建立之後,雙方可以互發數據。
基本參數
Socket Families(地址簇)
socket.
AF_UNIX 本機進程間通信
socket.
AF_INET IPV4(默認)
socket.
AF_INET6 IPV6
Socket Types(類型)
socket.
SOCK_STREAM 流式socket,代表TCP協議(默認)
socket.
SOCK_DGRAM 數據報式socket,代表UDP協議
socket方法
sk = socket.
socket
(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)
建立socket連接對象
sk.bind(address)
s.bind(address) 將套接字綁定到地址。address地址的格式取決於地址族。在AF_INET下,以元組(host,port)的形式表示地址。
sk.listen(backlog)
開始監聽傳入連接。backlog指定在拒絕連接之前,可以掛起的最大連接數量。
backlog等於5,表示內核已經接到了連接請求,但服務器還沒有調用accept進行處理的連接個數最大為5
這個值不能無限大,因為要在內核中維護連接隊列
sk.setblocking(bool)
是否阻塞(默認True),如果設置False,那麽accept和recv時一旦無數據,則報錯。
sk.accept()
接受連接並返回(conn,address),其中conn是新的套接字對象,可以用來接收和發送數據。address是連接客戶端的地址。
接收TCP 客戶的連接(阻塞式)等待連接的到來
sk.connect(address)
連接到address處的套接字。一般,address的格式為元組(hostname,port),如果連接出錯,返回socket.error錯誤。
sk.connect_ex(address)
同上,只不過會有返回值,連接成功時返回 0 ,連接失敗時候返回編碼,例如:10061
sk.close()
關閉套接字
sk.recv(bufsize[,flag])
接受套接字的數據。數據以字符串形式返回,bufsize指定最多可以接收的數量。flag提供有關消息的其他信息,通常可以忽略。
sk.recvfrom(bufsize[.flag])
與recv()類似,但返回值是(data,address)。其中data是包含接收數據的字符串,address是發送數據的套接字地址。
sk.send(string[,flag])
將string中的數據發送到連接的套接字。返回值是要發送的字節數量,該數量可能小於string的字節大小。即:可能未將指定內容全部發送。
sk.sendall(string[,flag])
將string中的數據發送到連接的套接字,但在返回之前會嘗試發送所有數據。成功返回None,失敗則拋出異常。
內部通過遞歸調用send,將所有內容發送出去。
服務端步驟:
步驟:
1.server = socket.socket() 聲明實例,生成連接對象
2.server.bind() 綁定要監聽的端口
3.server.listen() 開始監聽
4.conn,addr = server.accept()等待客戶端發起連接,阻塞
5.接收數據(發送數據)
6.當客戶端斷開連接後,繼續監聽等待下一個客戶端建立連接
……
關閉連接對象
代碼示例:
import socket "服務器端" server = socket.socket() # 生成連接對象 server.bind(("localhost",6000)) # 綁定要監聽的端口 server.listen(5) # 開始監聽(最大允許掛起的連接) while True: print("\n服務器在等待...") conn,addr = server.accept() # 等待客戶端發起建立連接,起到阻塞作用 # conn 是客戶端鏈接過來而在服務器端為其生成的連接實例,addr是IP地址+端口 print("已成功連接") print("連接對象:{0},地址:{1}\n".format(conn,addr)) while True: try: data = conn.recv(1024) # 接收數據 # (如果客服端斷開連接,此步驟將會被無限循環操作,所以一定要有檢查機制) if data != b"000001": # 接收到的信號不是"000001"的話就正常執行 print("接收客戶端信息:", data.decode()) msg = input(">>輸入返回客戶端的數據:") conn.send(msg.encode(encoding="utf-8")) # 向客戶端發送數據 else: print("該客戶已主動斷開連接") break except ConnectionResetError as e: print("該客戶機異常!已被強迫斷開連接",e) break else: print("It‘s OK !") server.close()
客戶端步驟:
步驟:
1.client = socket.socket() 聲明實例,生成連接對象
2.client.connect() 與服務器建立連接
3.與服務器交互(發送接收數據)
4.client.close() 斷開連接
代碼示例:
import socket "通信案例客戶端消息接收與發送" client = socket.socket() # 聲明socket類型,並生成socket連接對象 try: client.connect(("localhost",6000)) # 與服務器建立連接 while True: msg = input(">>輸入要向服務器發送的信息:") client.send(msg.encode(encoding="utf-8")) # 向服務器發送信息(只能發送bytes字節類型,不能是str字符類型) data = client.recv(1024) # 接收來自服務器的1024個字節 print("接收來自服務器的數據:",data.decode()) # 打印服務器發送的數據 chioce = input("按任意鍵繼續,按0退出客戶端") if chioce == "0": client.send(b"000001") # 發送此信號表明客戶端要斷開連接 break client.close() except ConnectionRefusedError as e: print("服務器還沒開機!請靜候")
需要註意的是:
1.客戶端再發送數據時,要主要服務器接收的大小限制。如果超過了這個限制,超出的部分暫時存在系統緩沖區,第二次接收的時候再輸出剩下的部分。例如服務器端的recv(1024),而客戶端一次發了2024字節,那麽剩下的1000字節存在緩沖區,第二次接收的時候會接收緩沖區的內容,將不會接收新發來的數據,會造成數據錯誤。官方建議一次性不超過8192字節
2.雙發收發數據只能是bytes類型
3.粘包問題,下面講
socket粘包問題
什麽是粘包呢?我們知道發送數據,並且數據量比較大時,並不會一次性發送,即使能一次發送,客戶端也不一定能一次性接收,所以服務器有個緩沖區,等客戶端下次再接收數據的時候再發送給客戶端,所以,就需要將數據分成幾次發送,客戶端分成幾次接收。那又出來問題了,客戶端知道數據(文件)有多大麽?它怎麽知道要接收幾次?當然是要服務器告訴他啦!
於是,我們設計服務器首先發送數據大小(數據),再開始分批發送數據,客戶端先接收文件大小(數據),再開始分批接收。問題就有可能在這裏出現了。如果連續2次send數據,很有可能將兩次的數據黏在一起發送出去,在客戶端也無法將其分開,怎麽辦呢?
我們可以讓服務器每次發送數據後,接收來自客戶端的確認,這樣會強制清空緩沖區,就不會造成粘包。當然,基於上面講的方法,只需要在發送正式數據之前接收確認就好。
最後,如果希望100%確認雙發收發數據是否一致,可以采用MD5校驗。
服務器端步驟:
1.讀取文件名
2.檢測文件是否存在
3.打開文件
4.檢測文件大小
5.將文件大小發給客戶端
6.確認
7.開始邊讀邊發(循環發送)
8.發送MD5
代碼:
import socket,os,hashlib ser = socket.socket() ser.bind(("localhost",5000)) ser.listen() while True: try: print("正在等待客戶端連接...") conn,addr = ser.accept() print("已連接,new conn:",addr) while True: data = conn.recv(8192) filename = data.decode() print("尋找文件",filename) if os.path.isfile(filename): conn.send(b"01") conn.recv(1024) f = open(filename,"rb") m = hashlib.md5() file_size = os.stat(filename).st_size conn.send(str(file_size).encode(encoding="utf-8")) client_ack = conn.recv(1024) # 接收確認信息 if client_ack == b"1": print("開始發送數據") for line in f: m.update(line) conn.send(line) f.close() conn.send(m.hexdigest().encode(encoding="utf-8")) # 發送MD5值 else: conn.send(b"00") #表示文件不存在 print("該文件不存在!") except ConnectionResetError: print("該客戶端已斷開連接") ser.close() print("服務器已關閉")
客戶端步驟:
1.發送接收文件請求,同時將文件名發送給服務器
2.接收文件長度
3.本地新建同名文件,循環接收數據,並將其寫入文件
4.同時更新本地MD5值
5.接收數據完畢後,再接收服務器的MD5值,與本地MD5值進行比較
代碼:
import socket,hashlib def receive1(client): "真正的數據接收" while True: res = b"" res = res + client.recv(1024) return res def receive(client,filename): "接收處理" m = hashlib.md5() rece_res_size = int(client.recv(1024).decode()) # 接收的結果長度,轉成int型 client.send(b"1") rece_size = 0 res = b"" filename = filename.decode() f = open(filename + ".new","wb") while rece_size < rece_res_size: if rece_res_size - rece_size >1024: # 如果不是最後一次接收數據 size = 1024 else: # 最後一次接收數據 size = rece_res_size - rece_size a = client.recv(size) # 循環接收數據 res = res + a rece_size = len(res) m.update(a) f.write(a) print("發送數據量:{0},接收數據量:{1}".format(rece_res_size,rece_size)) else: serves_md5 = client.recv(1024).decode() print("服務器MD5:",serves_md5) print("客戶端MD5:",m.hexdigest()) if serves_md5 == m.hexdigest(): print("文件接收並校驗完畢!") res.decode() f.close() def main(): client = socket.socket() try: client.connect(("localhost", 5000)) while True: filename = input("請輸入需要的文件名").strip().encode(encoding="utf-8") if len(filename) == 0: print("輸入為空,重新輸入") continue client.send(filename) is_file = client.recv(1024) if is_file == b"01": client.send(b"OK") receive(client,filename) # 調用函數接收數據,返回結果res(bytes) else: print(" {0} 文件不存在!".format(filename.decode())) except ConnectionRefusedError: print("等待服務器開機") client.close() main()
socketserver
什麽是socketserver?為什麽需要它呢?
我們在前面的文章中普通的socket並不能同時處理多個客戶端,當一個客戶端在與服務器連接時,其它客戶端只能排隊。而sockerserver則不同,它可以並發地處理多個客戶端請求。
import socketserver ‘‘‘ 每一個客戶端請求過來,都會實例化 MyTCPHandler ‘‘‘ class MyTCPHandler(socketserver.BaseRequestHandler): def handle(self): "跟客戶端所有的交互都是在handle裏完成的" while True: try: self.data = self.request.recv(1024).strip() print("{0} wrote:".format(self.client_address[0])) print(self.data) if not self.data: print("輸入為空") self.request.send(bytes("輸入為空", "utf-8")) else: self.request.send(self.data.upper()) except ConnectionResetError : print("客戶已斷開連接") break if __name__ == "__main__": HOST, PORT = "localhost", 9999 #server = socketserver.TCPServer((HOST, PORT), MyTCPHandler) # 實例化一對一的連接對象 server = socketserver.ThreadingTCPServer((HOST, PORT), MyTCPHandler) # 實例化多並發的連接對象(多線程) server.serve_forever()
客戶端並沒有什麽區別:
import socket HOST, PORT = "localhost", 9999 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((HOST, PORT)) while True: data = input("輸入字符") try: sock.sendall(bytes(data + "\n", "utf-8")) received = str(sock.recv(1024), "utf-8") finally: print("Sent: {0}".format(data)) print("Received: {0}".format(received)) sock.close()
初學Python——Socket網絡編程