粘包問題的解決,上傳與下載,多用戶聊天
- 2019 年 12 月 16 日
- 筆記
subprocess模塊補充
1.可以幫你通過代碼執行操作系統終端的命令
2.並可以返回終端的執行結果 subprocess.Popen(1,2,3,4) 1:cmd命令 2:shell = True 3:返回正確結果參數 stdout = subprocess.PIPE 4:返回錯誤的參數 stderr = subprocess.PIPE 返回的數據類型是二進制(bytes) 當res = stdout.read()+stderr.read()時 則正確的和錯誤的結果都可以返回
#服務端 import subprocess import socket server = socket.socket() server.bind(('127.0.0.1',6666)) while True: try: conn,addr = server.accept() print(addr) cmd = conn.recv(1024).decode('utf-8') res = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE) result = res.stdout.read() + res.stderr.read()#這裡有人說stdout要放在stderr前面否則可能會出錯,不過本人沒有測試 conn.send(result) except Exception as e : print(e) break conn.close()
粘包問題
1.什麼是粘包問題?
服務端第一次發送數據,客戶端無法第一次精確接收完畢,或者每次發送的數據太少而且發送的次數比較的頻繁時,下一次發送的數據與上一次數據黏在了一起。
1.無法預測對方需要接收的數據大小和長度。 2.多次連續發送時,發送數據量小其發送時間間隔短的數據會打包並在一起發送。
模擬問題的發生
#服務端 import socket server = socket.socket() server.bind(('127.0.0.1',6666)) server.listen(5) conn,addr = server.accept() print(addr) res1 = conn.recv(1024).decode("utf-8") print(res1) res2 = conn.recv(1024).decode('utf-8') print(res2) res3 = conn.recv(1024).decode('utf-8') #執行結果 ('127.0.0.1', 3066) 123123123
#客戶端 import socket client = socket.socket() client.connect(('127.0.0.1',6666)) client.send(b'123') print(b'123') client.send(b'123') print(b'123') client.send(b'123') print(b'123') #執行結果 b'123' b'123' b'123'
可以看到,發送的三個數據報文被當做一個進行接收了。
關於數據報文、數據包、數據報的區別:
數據發送時,由上層向下層封裝。 四層,協議層傳輸的是數據報文,主要是協議格式; 三層,網絡層傳輸的是數據包,包含數據報文,並且增加傳輸使用的IP地址等三層信息; 二層,數據鏈路層傳輸的是數據幀,包含數據包,並且增加相應MAC地址與二層信息。 數據接收的時候,下層向上層解封裝。 具體區別就是所工作的層不同,可根據ISO七層模型或者TCP/IP四層模型理解。
2.如何解決粘包問題?
解決這個問題的方法有兩種:
1.先讀出文件的長度,將文件的長度放在數據頭部,傳輸的過程中連同數據的長度信息一同傳過去,這樣就可以實現動態的修改接收端的接收數據的長度。但是缺陷是如果數據文件太大就會造成內存爆滿。
2.每次按照固定的長度去讀取文件然後判斷文件的長度,如果文件太大就固定的長度一點一點發送。如果發送的不是文件則按照第一種方式去發送,如果是文件就按照第二種方法去發送。
struct模塊 是一個可以將很大的數據壓縮成一個固定長度的二進制數據,可以作為報文的報頭 這裡的作用是獲取數據包的數據長度。 struct.pack('i',數據) i模式:會將數據長度壓縮成四個byte的一個數據
第一種情況解決方法
#客戶端 import socket import struct client = socket.socket() client.connect(('127.0.0.1',6666)) while True: send_data = input('請輸入你的指令:').strip() client.send(send_data.encode('utf-8')) header1 = client.recv(4) header = struct.unpack('i',header1) msg = client.recv(header[0]).decode('gbk')#header是一個元組,第一個元素是數據 print(msg)
可以通過執行cmd指令:tasklist查看Windows所有任務進程的方法進行驗證
#服務端 import struct import socket import subprocess server = socket.socket() server.bind(('127.0.0.1',6666)) server.listen(5) while True: conn,addr = server.accept() print(addr) while True: cmd = conn.recv(1024).decode('utf-8') res = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE) result = res.stdout.read() + res.stderr.read() header = len(result) print(result.decode('gbk')) header1 = struct.pack('i',header) conn.send(header1) conn.send(result)
第二種方法(用於傳輸較大的文件,本人用700M的電影進行了測試還是會出現內存佔用較大的問題可能比直接讀入700m要好一些):
#客戶端 import socket import struct import json client = socket.socket() client.connect( ('127.0.0.1', 6666) ) while True: file_name = input('請輸入你的文件名:').strip() client.send(file_name.encode('utf-8')) init_data = 0 num = 1 tag = 1 with open(r'G:360Downloads怦然心動BD中英雙字.rmvb', 'rb') as f: while tag: send_data = f.read(1024) n = len(send_data) if n < 1024:#當讀出的數據長度小於1024時就跳出循環 tag = 0 print(send_data, num) # 每次發送1024數據 client.send(send_data)
#接收端 import socket while True: conn, addr = server.accept() try: file_name = conn.recv(1000).decode('utf-8') init_data = 0 # 1.以文件名打開文件,準備寫入 with open(f'{file_name}', 'wb') as f: tag = 1 # 一點一點接收文件,並寫入 while tag: data = conn.recv(1024) # 2.開始寫入視頻文件 f.write(data) if len(data) < 1024: tag = 0 init_data += 1 print(init_data) print('文件接收完畢!') except Exception as e: print(e) break conn.close()
UDP協議
UDP(UDP,User Datagram Protoco)是一種數據傳輸協議。UDP 為應用程序提供了一種無需建立連接就可以發送封裝的 IP 數據報的方法。
UDP協議的特點:
- 不需要建立雙向管道
- 不會粘包
- 發數據不需要接收回復
- 不可靠,會丟包
基於UDP的套接字
udp是無鏈接的,先啟動哪一端都不會報錯
UDP協議是數據報協議,發空的時候也會自帶報頭,因此客戶端輸入空,服務端也能收到
#服務端 import socket #SOCK_DGRAM:代表UDP server = socket.socket(type=socket.SOCK_DGRAM) #服務端綁定計算機的IP和軟件的端口號 server.bind(('127.0.0.1',6666)) msg,addr = server.recvfrom(1024)#接收的方法與TCP傳輸方式下不同 msg1,addr1 = server.recvfrom(1024) msg2,addr2 = server.recvfrom(1024) print(msg,msg1,msg2)
#客戶端 import socket client = socket.socket(type=socket.SOCK_DGRAM) server_ip_port = ('127.0.0.1',6666)#此處與TCP傳輸套接字的方法不同 client.sendto(b'hello',server_ip_port) client.sendto(b'hello',server_ip_port) client.sendto(b'hello',server_ip_port) client.sendto(b'hello',server_ip_port) client.sendto(b'hello',server_ip_port)
基於UDP套接字實現多對一聊天
下面的服務端程序有一個bug,就是當服務端與多人聊天的時候必須先回復前一個人發來的消息才能收到後一個人的消息。
#服務端 import socket #SOCK_DGRAM:代表UDP server = socket.socket(type=socket.SOCK_DGRAM) #服務端綁定計算機的IP和軟件的端口號 server.bind(('127.0.0.1',6666)) while True: msg,addr = server.recvfrom(1024) print(addr) print(msg.decode('utf-8')) send_msg = input('服務端發送消息:').encode('utf-8') server.sendto(send_msg, addr) msg1, addr1 = server.recvfrom(1024) print(addr1) print(msg1.decode('utf-8')) send_msg = input('服務端發送消息:').encode('utf-8') server.sendto(send_msg, addr1) msg2, addr2 = server.recvfrom(1024) print(addr2) print(msg2.decode('utf-8')) send_msg = input('服務端發送消息:').encode('utf-8') server.sendto(send_msg, addr2)
#客戶端1 import socket client = socket.socket(type=socket.SOCK_DGRAM) server_ip_port = ('127.0.0.1',6666)#此處與TCP傳輸套接字的方法不同 while True: send_msg = input('客戶端1:').encode('utf-8') #發送消息時需加上對方的地址 client.sendto(send_msg,server_ip_port) #可以接收任何人的消息 msg = client.recv(1204) print(msg.decode('utf-8')) #客戶端2 import socket client = socket.socket(type=socket.SOCK_DGRAM) server_ip_port = ('127.0.0.1', 6666) # 此處與TCP傳輸套接字的方法不同 while True: send_msg = input('客戶端2:').encode('utf-8') # 發送消息時需加上對方的地址 client.sendto(send_msg, server_ip_port) # 可以接收任何人的消息 msg = client.recv(1204) print(msg.decode('utf-8')) #客戶端3 import socket client = socket.socket(type=socket.SOCK_DGRAM) server_ip_port = ('127.0.0.1', 6666) # 此處與TCP傳輸套接字的方法不同 while True: send_msg = input('客戶端3:').encode('utf-8') # 發送消息時需加上對方的地址 client.sendto(send_msg, server_ip_port) # 可以接收任何人的消息 msg = client.recv(1204) print(msg.decode('utf-8'))

socketserver模塊
SocketServer內部使用 IO多路復用 以及 「多線程」 和 「多進程」 ,從而實現並發處理多個客戶端請求的Socket服務端。即:每個客戶端請求連接到服務器時,Socket服務端都會在服務器是創建一個「線程」或者「進程」 專門負責處理當前客戶端的所有請求。

使用這個模塊就可以解決多人聊天時不能同時收到多人消息的bug。
ThreadingTCPServer
ThreadingTCPServer實現的Soket服務器內部會為每個client創建一個 「線程」,該線程用來和客戶端進行交互。
1、ThreadingTCPServer基礎
使用ThreadingTCPServer:
- 創建一個繼承自 SocketServer.BaseRequestHandler 的類
- 類中必須定義一個名稱為 handle 的方法
- 啟動ThreadingTCPServer
這個程序只實現了群發消息,至於怎樣實現多人聊天而且消息單發,還有待探索。
#服務端 import socketserver class MyServer(socketserver.BaseRequestHandler): def handle(self): print(self.request,self.client_address,self.server) conn = self.request conn.sendall('歡迎致電XXXX公司,退出請輸『exit』,和我聊天請輸『0』.'.encode('utf-8')) Flag = True while Flag: data = conn.recv(1024).decode('utf-8') if data == 'exit': Flag = False elif data == '0': conn.sendall('通話可能會被錄音.balabala一大推'.encode('utf-8')) else: conn.sendall('請重新輸入.'.encode('utf-8')) if __name__ == '__main__': server = socketserver.ThreadingTCPServer(('127.0.0.1',6666),MyServer) server.serve_forever()
注意這裡使用的是TCP協議,別搞錯了!
#客戶端 import socket client = socket.socket() client.connect(('127.0.0.1',6666))#此處與TCP傳輸套接字的方法不同 while True: send_msg = input('客戶端1:').strip().encode('utf-8') #發送消息時需加上對方的地址 client.send(send_msg) #可以接收任何人的消息 msg = client.recv(1204) print(msg.decode('utf-8'))


