粘包現象:html
TCP屬於長鏈接,當服務端與一個客戶端進行了鏈接之後,其餘客戶端須要(排隊)等待.若服務端想要鏈接另外一個客戶端,必須首先斷開與第一個客戶端的鏈接。node
緩衝區:它是內存空間的一部分。也就是說,在內存空間中預留了必定的存儲空間,這些存儲空間用來緩衝輸入或輸出的數據,這部分預留的空間就叫作緩衝區,顯然緩衝區是具備必定大小的。緩衝區根據其對應的是輸入設備仍是輸出設備,分爲輸入緩衝區和輸出緩衝區python
爲何引入緩衝區:高速設備與低速設備的不匹配,勢必會讓高速設備花時間等待低速設備,咱們能夠在這二者之間設立一個緩衝區,也就是一個臺階,怕低速的跟不上程序員
緩衝區(buffer)的做用:web
區別於緩存區(cache):CPU的Cache,它中文名稱是高速緩衝存儲器,讀寫速度很快,幾乎與CPU同樣。因爲CPU的運算速度太快,內存的數據存取速度沒法跟上CPU的速度,因此在cpu與內存間設置了cache爲cpu的數據快取區。當計算機執行程序時,數據與地址管理部件會預測可能要用到的數據和指令,並將這些數據和指令預先從內存中讀出送到Cache。一旦須要時,先檢查Cache,如有就從Cache中讀取,若無再訪問內存,如今的CPU還有一級cache,二級cache。算法
總結:也就是說緩衝區是內存中的對應的輸入輸出,而緩存區是cpu中的。shell
每一個socket(套接字)被建立後,都會分配兩個緩衝區: 輸入緩衝區和輸出緩衝區。json
在windows複製路徑時,從後往前複製路徑會自動添加看不見的符號,從前日後就不會有問題windows
一、write()/send()並不當即向網絡中傳輸數據,而是先將數據寫入緩衝區中,再由TCP協議將數據從緩衝區發送到目標機器. 一旦將數據寫入到緩衝區,函數就已經完成任務能夠成功返回了,而不用去考慮數據什麼時候被髮送到網絡,也不用去考慮數據是否已經到達目標機器,由於這些後續操做都是TCP協議負責的事情.
二、TCP協議獨立於 write()/send() 函數,數據有可能剛被寫入緩衝區就發送到網絡,也可能在緩衝區中不斷積壓,屢次寫入的數據被一次性發送到網絡,這取決於當時的網絡狀況,當前線程是否空閒等諸多因素,不禁程序員控制.
三、read()/recv() 函數也是如此,也從輸入緩衝區中讀取數據,而不是直接從網絡中讀取
四、這些I/O緩衝區特性可整理以下:
1). I/O緩衝區在每一個TCP套接字中單獨存在
2). I/O緩衝區在建立套接字時自動生成
3). 即便關閉套接字也會繼續傳送輸出緩衝區中遺留的數據
4). 關閉套接字將丟失輸入緩衝區中的數據
5). 輸入/輸出緩衝區的默認大小通常是8K(瞭解:能夠經過getsockopt()函數獲取)
粘包現象的緣由:(UDP不存在粘包)緩存
粘包現象的模擬:
發送方連續發送較小的數據,而且每次發送之間的時間間隔很短,此時,兩個消息在輸出緩衝區黏在一塊兒了.緣由是TCP爲了傳輸效率,作了一個優化算法(Nagle),減小連續的小包發送(由於每一個消息被包裹之後,都會有兩個過程:組包和拆包,這兩個過程是極其消耗時間的,優化算法Magle的目的就是爲了減小傳輸時間)。
#服務端
import socket
server = socket.socket()
ip_port = ("192.168.15.28", 8001)
server.bind(ip_port)
server.listen()
conn, addr = server.accept()
# 連續接收兩次消息
from_client_msg1 = conn.recv(1024).decode("utf-8")
print("第一次接收到的消息>>>", from_client_msg1)
from_client_msg2 = conn.recv(1024).decode("utf-8")
print("第二次接收到的消息>>>", from_client_msg2)
conn.close()
server.close()
#客戶端
import socket
client = socket.socket()
server_ip_port = ("192.168.15.28", 8001)
client.connect(server_ip_port)
# 連續發送兩次消息
client.send('Hello'.encode('utf-8'))
client.send('World'.encode('utf-8'))
client.close()
#結果
#第一次接收到的消息>>> HelloWorld
#第二次接收到的消息>>>
方案1、粘包問題的根源在於,接收端不知道發送端將要傳送的字節流的長度,因此解決粘包的方法就是圍繞"如何讓發送端在發送數據前,把本身將要發送的字節流總長度讓接收端知曉"
xxxxxxxxxx
#解決步驟:
#a. 發送端把"數據長度"傳輸給接收端
#b. 接收端把"確認信息"傳輸給發送端
#c. 發送端把"所有數據"傳輸給接收端
#d. 接收端使用一個死循環接收完全部數據.
#服務端代碼
import socket
import subprocess
server = socket.socket()
ip_port = ('192.168.15.28',8001)
server.bind(ip_port)
server.listen()
conn,addr = server.accept()
while 1:
from_client_cmd = conn.recv(1024).decode('utf-8') # a.接收來自客戶端的cmd指令
sub_obj = subprocess.Popen(
from_client_cmd, # 客戶端的指令
shell=True, # 使用shell,就至關於使用cmd窗口
stdout=subprocess.PIPE, # 標準錯誤輸出,凡是輸入錯誤指令,錯誤指令輸出的報錯信息就會被它拿到
stderr=subprocess.PIPE,
)
server_cmd_msg = sub_obj.stdout.read() # b.拿到cmd指令返回值 --> stdout接受到的返回值是bytes類型的,而且windows系統的默認編碼爲gbk
cmd_msg_len = str(len(server_cmd_msg)) # c.拿到返回值的長度
print("cmd返回的正確信息的長度>>>",cmd_msg_len)
conn.send(cmd_msg_len.encode('gbk')) # c.把"長度"傳輸給客戶端
from_client_ack = conn.recv(1024).decode('utf-8') # d.拿到"確認信息"
if from_client_ack == "確認":
conn.send(server_cmd_msg) # e.把"cmd指令返回值"傳輸給客戶端
else:
continue
#客戶端
import socket
client = socket.socket()
server_ip_port = ('192.168.15.28',8001)
client.connect(server_ip_port)
while 1:
cmd = input('請輸入要執行的指令>>>') # a.用戶輸入cmd指令
client.send(cmd.encode('utf-8')) # b.把"cmd指令"傳輸給服務端
from_server_msglen = int(client.recv(1024).decode('gbk')) # c.接收cmd指令返回值的"字節流長度"
print('接收到的信息長度是>>>', from_server_msglen)
client.send('確認'.encode('utf-8')) # d.把"確認信息"傳輸給服務端
from_server_stdout = client.recv(from_server_msglen).decode('gbk') # e.設置最大可接收數據量,同時接收"cmd指令返回值"
print('接收到的指令返回值是>>>', from_server_stdout)
方案2、經過struct模塊將數據實體(要傳輸的數據)的長度打包成一個"4bytes字符串",並將其傳輸給接收端.接收端取出這個"4bytes字符串",對其進行解包,解包後的內容就是"數據實體的長度",接收端再經過這個長度來繼續接收數據實體.(注意粘包的兩個send要寫一塊兒,不然緩存區會溢出,關閉鏈接)
xxxxxxxxxx
#struct模塊中最重要的兩個函數是:
#pack() -- 具備"打包"功能,struct.pack(format, values) 將value打包成bytes的4個
#unpack() -- 具備"解包"功能 struct.unpack(format, bytes) 經過bytes反解處具體的value
#解決流程
#a.拿到數據實體的長度
#b.將長度打包成"4bytes字符串"
#c.將"4bytes字符串"發送給客戶端
#d.發送數據實體
#服務端
import socket
import subprocess
import struct
server = socket.socket()
ip_port = ("127.0.0.1", 8001)
server.bind(ip_port)
server.listen()
conn, addr = server.accept()
while 1:
from_client_cmd = conn.recv(1024).decode("utf-8")
print("來自客戶端的指令是>>>")
# 經過subprocess模塊拿到指令的返回值
sub_obj = subprocess.Popen(
from_client_cmd, # 客戶端的指令
shell=True,
stdout=subprocess.PIPE, # 標準輸出:接收正確指令的執行結果
stderr=subprocess.PIPE, # 標準錯誤輸出:接收錯誤指令的執行結果
)
# 經過stdout拿到正確指令的執行結果,即須要發送的"數據實體"
server_cmd_msg = sub_obj.stdout.read()
# a.拿到數據實體的長度
cmd_msg_len = len(server_cmd_msg)
# b.將長度打包成"4bytes字符串"
msg_len_stru = struct.pack('i',cmd_msg_len)
# c.將"4bytes字符串"發送給客戶端
conn.send(msg_len_stru)
# d.發送數據實體 --> sendall() 循環發送數據,直到數據所有發送成功
conn.sendall(server_cmd_msg)
#客戶端
import socket
import struct
client = socket.socket()
server_ip_port = ("127.0.0.1", 8001)
client.connect(server_ip_port)
while 1:
cmd = input("請輸入要執行的指令>>>")
client.send(cmd.encode("utf-8"))
# a.接收打包後的"4bytes字符串"
from_server_msglen = client.recv(4)
# b.解包,拿到"數據實體的長度",即unpack_msglen
unpack_msglen = struct.unpack('i', from_server_msglen)[0]
# c.循環接收數據實體,經過"數據實體的長度"來肯定跳出循環的條件
recv_msg_len = 0 # 統計"數據長度"
all_msg = b'' # 統計"數據實體"
while recv_msg_len < unpack_msglen:
every_recv_data = client.recv(1024)
# 將每次接收到的"數據實體"進行拼接
all_msg += every_recv_data
# 將每次接收到的"數據實體的長度"進行累加
recv_msg_len += len(every_recv_data)
print(all_msg.decode("gbk"))
一、在每次文件傳送中,若是每次發送的數據長度是大於1460bytes的就會出現最後的數據總量缺乏一部分,緣由是涉及到網絡帶寬,通常是1500bytes,可是因爲系統一些報頭文件也須要一些,因此只有1460bytes能夠,解決方法是每次文件只減小len(content)
大文件的傳輸
xxxxxxxxxx
#服務端
import json
import struct
import socket
sk = socket.socket()
sk.bind(('127.0.0.1',9001))
sk.listen()
conn,addr = sk.accept()
len_bytes = conn.recv(4)
num = struct.unpack('i',len_bytes)[0]
str_dic = conn.recv(num).decode('utf-8')
dic = json.loads(str_dic)
with open(dic['filename'],'wb') as f:
while dic['filesize']:
content = conn.recv(2048)
f.write(content)
dic['filesize'] -= len(content)
#客戶端
import os
import json
import struct
import socket
sk = socket.socket()
sk.connect(('127.0.0.1',9001))
file_path = input('>>>')
filename = os.path.basename(file_path)
filesize = os.path.getsize(file_path)
dic = {'filename':filename,'filesize':filesize}
bytes_dic = json.dumps(dic).encode('utf-8')
len_bytes = struct.pack('i',len(bytes_dic))
sk.send(len_bytes)
sk.send(bytes_dic)
with open(file_path,'rb') as f:
while filesize > 2048:
content = f.read(2048)
sk.send(content)
filesize -= 2048
else:
content = f.read()
sk.send(content)
sk.close()
文件的上傳加認證
xxxxxxxxxx
#服務端
import json
import socket
import struct
import hashlib
def get_md5(usr,pwd):
md5 = hashlib.md5(usr.encode('utf-8'))
md5.update(pwd.encode('utf-8'))
return md5.hexdigest()
def login(conn):
msg = conn.recv(1024).decode('utf-8')
dic = json.loads(msg)
with open('userinfo', encoding='utf-8') as f:
for line in f:
username, password = line.strip().split('|')
if username == dic['user'] and password == get_md5(dic['user'], dic['passwd']):
res = json.dumps({'flag': True}).encode('utf-8')
conn.send(res)
return True
else:
res = json.dumps({'flag': False}).encode('utf-8')
conn.send(res)
return False
def upload(conn):
len_bytes = conn.recv(4)
num = struct.unpack('i', len_bytes)[0]
str_dic = conn.recv(num).decode('utf-8')
dic = json.loads(str_dic)
with open(dic['filename'], 'wb') as f:
while dic['filesize']:
content = conn.recv(2048)
f.write(content)
dic['filesize'] -= len(content)
sk = socket.socket()
sk.bind(('127.0.0.1',9001))
sk.listen()
while True:
try:
conn,addr = sk.accept()
ret = login(conn)
if ret:
upload(conn)
except Exception as e:
print(e)
finally:
conn.close()
sk.close()
#客戶端
import os
import json
import socket
import struct
def upload(sk):
# 上傳文件
file_path = input('>>>')
filename = os.path.basename(file_path)
filesize = os.path.getsize(file_path)
dic = {'filename': filename, 'filesize': filesize}
bytes_dic = json.dumps(dic).encode('utf-8')
len_bytes = struct.pack('i', len(bytes_dic))
sk.send(len_bytes)
sk.send(bytes_dic)
with open(file_path, 'rb') as f:
while filesize > 2048:
content = f.read(2048)
sk.send(content)
filesize -= 2048
else:
content = f.read()
sk.send(content)
usr = input('username :')
pwd = input('password :')
dic = {'operate':'login','user':usr,'passwd':pwd}
bytes_dic = json.dumps(dic).encode('utf-8')
sk = socket.socket()
sk.connect(('127.0.0.1',9001))
sk.send(bytes_dic)
res = sk.recv(1024).decode('utf-8')
dic = json.loads(res)
if dic['flag']:
print('登陸成功')
upload(sk)
else:
print('登陸失敗')
sk.close()
驗證客戶端連接合法性:當別人知道我ip,而且經過端口掃描知道個人相應的端口時,豈不是很危險,就必須進行驗證,經過驗證成功才能進來。經過 hmac模塊加鹽/也可使用hashlib,不過比較麻煩。
xxxxxxxxxx
#服務端
#其中os.urandom(n) 是一種bytes類型的隨機生成n個字節字符串的方法,並且每次生成的值都不相同。再加上md5等加密的處理,就可以成內容不一樣長度相同的字符串了。
from socket import *
import hmac,os
secret_key=b'Jedan has a big key!' #密鑰
def conn_auth(conn):
print('開始驗證新連接的合法性')
msg=os.urandom(32)#生成一個32字節的隨機字符串
conn.sendall(msg)
h=hmac.new(secret_key,msg)
digest=h.digest()
respone=conn.recv(len(digest))
return hmac.compare_digest(respone,digest)
def data_handler(conn,bufsize=1024):
if not conn_auth(conn):
print('該連接不合法,關閉')
conn.close()
return
print('連接合法,開始通訊')
while True:
data=conn.recv(bufsize)
if not data:break
conn.sendall(data.upper())
def server_handler(ip_port,bufsize,backlog=5):
tcp_socket_server=socket(AF_INET,SOCK_STREAM)
tcp_socket_server.bind(ip_port)
tcp_socket_server.listen(backlog)
while True:
conn,addr=tcp_socket_server.accept()
print('新鏈接[%s:%s]' %(addr[0],addr[1]))
data_handler(conn,bufsize)
if __name__ == '__main__':
ip_port=('127.0.0.1',9999)
bufsize=1024
server_handler(ip_port,bufsize)
#客服端
from socket import *
import hmac,os
secret_key=b'Jedan has a big key!'
def conn_auth(conn):
msg=conn.recv(32)
h=hmac.new(secret_key,msg)
digest=h.digest()
conn.sendall(digest)
def client_handler(ip_port,bufsize=1024):
tcp_socket_client=socket(AF_INET,SOCK_STREAM)
tcp_socket_client.connect(ip_port)
conn_auth(tcp_socket_client)
while True:
data=input('>>: ').strip()
if not data:continue
if data == 'quit':break
tcp_socket_client.sendall(data.encode('utf-8'))
respone=tcp_socket_client.recv(bufsize)
print(respone.decode('utf-8'))
tcp_socket_client.close()
if __name__ == '__main__':
ip_port=('127.0.0.1',9999)
bufsize=1024
client_handler(ip_port,bufsize)