tcp协议传输方法&粘包问题

socket实现客户端和服务端

tcp协议可以用socket模块实现服务端可客户端的交互

# 服务端
import socket
#生成一个socket对象
soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#绑定地址跟端口号
soc.bind(('127.0.0.1', 8001))
#监听(半连接池的大小)
soc.listen(5)
#等着客户端来连接,conn相当于连接通道,addr是客户端的地址
conn,addr = soc.accept() #卡住,如果没有客户端连接,会一直卡在这,当有连接,才继续往下走
print(addr)
while True:
#等待接收,最大收取1024个字节
data=conn.recv(1024) #卡住,当客户端有数据过来,才会执行
print(data) #关闭通道
conn.close()
# 关闭套接字
soc.close()
# 客户端
import socket
#生成一个socket对象
soc = socket.socket()
# 连接服务器
soc.connect(('127.0.0.1',8001))
while True:
in_s = input('请输入要发送的数据:')
#发送的数据必须是b格式,in_s.encode('utf-8') 把字符串编码成b格式
#把b格式转成字符串
# ss = str(b'hello',encoding='utf-8')
# ss = b'hello'.decode('utf-8')
# #把字符串转成b格式
# by=bytes('hello',encoding='utf-8')
# by='hello'.encode('utf-8') soc.send(in_s.encode('utf-8'))

tcp协议传输方法&粘包问题

我们也可以完善一下服务端,让服务端也加上连接循环

import socket
#生成一个socket对象
soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#绑定地址跟端口号
soc.bind(('127.0.0.1', 8001))
#监听(半连接池的大小),不是连接数
soc.listen(3)
#等着客户端来连接,conn相当于连接通道,addr是客户端的地址
while True:
print('等待客户端连接')
conn,addr = soc.accept() #卡主,如果没有客户端连接,会一直卡在这,当有连接,才继续往下走
print('有个客户端连接上了',addr)
while True:
try:
#windows如果客户端断开,会报错,加了try
#linux如果客户端,断开,不会报错,会收到空,所有当data为空时,也break
#等待接收,最大收取1024个字节
data = conn.recv(1024) #会卡主,当客户端有数据过来,才会执行
if len(data) == 0: #处理linux客户端断开,如果在window下这段代码根本不会执行(即便是客服端发了空,这个地方也不会走到)
break
print(data)
except Exception: break
# 关闭通道
conn.close() # 关闭套接字
soc.close()

粘包问题

什么是粘包问题?通俗的说当客户端发送数据的时候,当一条数据还未接受的时候,下一条数据已经发送,这个时候俩条数据就会连在一起。如果这时候取的话,将俩条数据取出,就会出现问题。所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的

TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的

解决粘包问题方法

那么我们将如何解决粘包问题呢

我们是否可以先计算出每次想要传出的数据长度,将其放在本次数据的开头,随后在服务端先读取长度,随后根据长度来分割已经粘包的数据。

这个方法看上去似乎是可行的,但是我们并不知道一次数据的长度会是几位数,可能只有9 字节,又有可能有998字节,这个时候应该怎么办呢?

tcp协议传输方法&粘包问题

在这里我们还要介绍一个模块:struct

他可以将一个数字打包成固定长度的四个字节

struct模块

import struct
#把一个数字打包成固定长度的4字节
obj = struct.pack('i', 1098)
print(obj) # b'J\x04\x00\x00'
print(len(obj)) # 4 l = struct.unpack('i', obj)[0]
print(l) 1098

是不是觉得豁然开朗

这样我们就可以写一个较为完善的服务端可客户端的通信了

解决粘包问题阉割版

# 客户端
import socket
import struct
soc=socket.socket() soc.connect(('127.0.0.1',8001))
while True:
in_s=input('请输入要执行的命令:')
soc.send(in_s.encode('utf-8'))
head=soc.recv(4)
l=struct.unpack('i',head)[0]
# data=soc.recv(l)
count=0
data_total=b'' while count<l:
if l<1024: #如果接受的数据小于1024 ,直接接受数据大小
data=soc.recv(l)
else:#如果接受的数据大于1024
if l-count>=1024: #总数据长度-count(目前收到多少,count就是多少) 如果还大于1024 ,在收1024
data=soc.recv(1024)
else: #总数据长度-count(目前收到多少,count就是多少) 如果小于1024,只收剩下的部分就可
data=soc.recv(l-count) data_total+=data
count+=len(data) print(str(data_total,encoding='gbk'))
服务端
import socket
import subprocess
import struct
soc=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
soc.bind(('127.0.0.1',8001))
soc.listen(3)
while True:
print('等待客户端连接')
conn,addr=soc.accept()
print('有个客户端连接上了',addr)
while True:
try:
data=conn.recv(1024)
if len(data)==0:
break
print(data)
obj = subprocess.Popen(str(data,encoding='utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
#执行正确的结果 b 格式,gbk编码(windows平台)
msg=obj.stdout.read()
#发送的时候需要先把长度计算出来
#头必须是固定长度
#10
#100
#先取出要发送数据长度l l=len(msg)
#head 是固定四个字节
head=struct.pack('i',l)
#发了头
conn.send(head)
#发了内容
conn.send(msg)
except Exception: break
# 关闭通道
conn.close() # 关闭套接字
soc.close()

在这里使用1024字节,也就是1kb来进行循环,是怕所传输的文件过大,导致内存爆满。所以才进行循环

解决粘包问题终极版

但是碍于struct模块本身的限制,如果传输的文件实在太大,打到超过int限制时(当然,这样的文件几乎不存在),struct模块无法将其压缩成四个字节,那该怎么办?有人可能会说了,你怕不是个hape吧,赶快给爷爬,尽搞些有的没的

但是身为程序员,就要本着追求完美的原则,横眉冷对千夫指

tcp协议传输方法&粘包问题

下面就推出解决粘包问题的完美版本,其实就是带上一个字典,字典中有关于文件长度的数值,这时候只要将这个字典压成四个字节,另一端接收到这个字典后,根据字典中的值来判断数据的大小。这个时候,你还可以在字典中放入文件的其他信息,比如文件的名字或者

# 服务端

import socket
import subprocess
import struct
soc=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
soc.bind(('127.0.0.1', 8001))
soc.listen(3)
while True:
print('等待客户端连接')
conn,addr=soc.accept()
print('有个客户端连接上了',addr)
while True:
try:
data = conn.recv(1024)
if len(data) == 0:
break
print(data)
obj = subprocess.Popen(str(data, encoding='utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
#执行正确的结果 b 格式,gbk编码(windows平台)
msg = obj.stdout.read() #发送的时候需要先把长度计算出来
#头必须是固定长度
#先发4位,头的长度
import json
dic = {'size': len(msg)}
dic_bytes=(json.dumps(dic)).encode('utf-8')
#head_count是4个字节的长度
head_count = struct.pack('i', len(dic_bytes))
print(dic)
conn.send(head_count)
#发送头部内容
conn.send(dic_bytes)
#发了内容
conn.send(msg)
except Exception: break
# 关闭通道
conn.close() # 关闭套接字
soc.close()
# 客户端 import socket
import struct
import json
soc=socket.socket() soc.connect(('127.0.0.1', 8001))
while True:
in_s = input('请输入要执行的命令:')
soc.send(in_s.encode('utf-8'))
#头部字典的长度
head_dic_len = soc.recv(4)
#解出真正的长度
head_l = struct.unpack('i', head_dic_len)[0]
#byte 字典的长度
#收真正的头部字典
dic_byte = soc.recv(head_l)
head = json.loads(dic_byte)
print(head)
l = head['size']
count = 0
data_total = b''
print(l)
while count < l:
if l < 1024: #如果接受的数据小于1024 ,直接接受数据大小
data = soc.recv(l)
else:#如果接受的数据大于1024
if l-count >= 1024: #总数据长度-count(目前收到多少,count就是多少) 如果还大于1024 ,在收1024
data = soc.recv(1024)
else: #总数据长度-count(目前收到多少,count就是多少) 如果小于1024,只收剩下的部分就可
data = soc.recv(l-count) data_total += data
count += len(data) print(str(data_total, encoding='gbk'))

tcp实例

写一个ftp软件,支持上传文件,下载文件

注册,登录之后才能下载,查看所有文件

删除文件

# 客户端
import socket
import time
import struct
import json
import os user_state = {'name':None}
BASE_PATH = os.path.dirname(__file__)
DB_PATH = os.path.join(BASE_PATH,'db') def send_common_msg(conn,msg):
msg_len = len(msg)
msg_stur = struct.pack('i',msg_len)
conn.send(msg_stur)
conn.send(msg.encode('utf-8')) def recv_common_msg(conn):
recv_stur = conn.recv(4)
recv_len = struct.unpack('i',recv_stur)[0]
recv_msg = conn.recv(recv_len).decode('utf-8')
return recv_msg def download_file(conn,file_name):
name = user_state['name']
dic_stru = conn.recv(4)
dic_len = struct.unpack('i',dic_stru)[0]
dic = conn.recv(dic_len).decode('utf-8')
dic = json.loads(dic)
size = dic['size']
file = conn.recv(size)
file_path = os.path.join(DB_PATH,name)
file_path = os.path.join(file_path,file_name)
with open(file_path,'wb')as fw:
fw.write(file) def upload_file(conn,file_path):
with open(file_path,'rb')as fr:
file = fr.read()
send_upload_file(conn,file) def send_upload_file(conn,file):
len_file = len(file)
dic = {'size':len_file}
print(len(dic))
dic = json.dumps(dic).encode('utf-8')
file_dic_stur = struct.pack('i',len(dic))
print(file_dic_stur)
print(dic)
print(file)
conn.send(file_dic_stur)
conn.send(dic)
conn.send(file) soc=socket.socket() soc.connect(('127.0.0.1',8009)) while True:
msg1 = recv_common_msg(soc)
while True:
print(msg1)
choice = input('请输入你的选择').strip()
send_common_msg(soc,choice)
if choice == '1':
login_msg = recv_common_msg(soc)
print(login_msg)
name = input()
send_common_msg(soc,name)
pwd_msg = recv_common_msg(soc)
print(pwd_msg)
pwd = input()
send_common_msg(soc,pwd)
login_msg = recv_common_msg(soc)
print(login_msg)
if login_msg == 'login success':
user_state['name'] = name
msg2 = recv_common_msg(soc)
print(msg2)
choice = input('请输入你的选择').strip()
send_common_msg(soc, choice)
if choice == '1':
file_lis = recv_common_msg(soc)
file_lis = json.loads(file_lis)
print(file_lis)
continue
elif choice == '2':
file_name = input('请输入你要下载的文件名')
send_common_msg(soc,file_name)
download_file(soc,file_name)
continue
elif choice == '3':
file_path = input('请输入你要上传的文件路径')
send_common_msg(soc,file_path)
upload_file(soc,file_path)
continue
else:
break
else:
continue
elif choice == '2':
resgister_msg = recv_common_msg(soc)
print(resgister_msg)
name = input()
send_common_msg(soc, name)
pwd_msg = recv_common_msg(soc)
print(pwd_msg)
pwd = input()
send_common_msg(soc, pwd)
resgister_msg = recv_common_msg(soc)
print(resgister_msg)
continue
break
break
soc.close()
#服务端
#导入模块
'''
服务端
'''
import socket
import struct
import os, json user_state = {'name': None}
BASE_PATH = os.path.dirname(__file__)
DB_PATH = os.path.join(BASE_PATH,'db')
if not os.path.exists(DB_PATH):
os.mkdir(DB_PATH) def login(name,pwd):
user_path = os.path.join(DB_PATH, f'{name}')
if not os.path.exists(user_path):
return 'login fail' user_path = os.path.join(user_path, f'{name}.json')
if not os.path.isdir(user_path):
return '该用户未注册' with open(user_path, 'r', encoding='utf-8') as fr:
user_dic = json.load(fr)
fr.flush() if user_dic['pwd'] == pwd:
user_state['name'] = name
return '登录成功'
else:
return '登陆失败' def resgister(name, pwd):
user_path = os.path.join(DB_PATH, f'{name}') if os.path.exists(user_path):
return '该用户已被注册'
else:
os.mkdir(user_path)
user_dic = {'name': name, 'pwd': pwd}
user_path = os.path.join(user_path, f'{name}.json')
with open(user_path, 'w', encoding='utf-8') as fw:
json.dump(user_dic, fw)
fw.flush()
return '用户注册成功' def quit():
return 'quit' def check_user_all_file(name):
user_path = os.path.join(DB_PATH, f'{name}')
file_lis = os.listdir(user_path)
return file_lis def client_download_file(file_path):
name = user_state['name'] def client_del_file(file_path):
name = user_state['name'] MSG_DIC = {
'1': login,
'2': resgister,
'3': quit
} MSG = '''
1: 登录
2: 注册
3: 退出
'''
MSG1 = '''
1: 查看用户文件
2: 下载文件
3: 上传文件
4: 删除文件
5: 退出
'''
# MSG = '1:登陆\n2:注册\n3:退出'
srur_msg = struct.pack('i', len(MSG))
srur_msg1 = struct.pack('i', len(MSG1))
soc = socket.socket()
# print(len(MSG))
soc.bind(('127.0.0.1', 8085)) soc.listen(10) while True:
conn, addr = soc.accept()
while True:
conn.send(srur_msg)
conn.send(bytes(MSG.encode('gbk')))
while True:
head_choice = conn.recv(4)
head_choice_len = struct.unpack('i',head_choice)[0]
head_choice = conn.recv(head_choice_len).decode('utf-8')
if head_choice == '3':
msg = MSG_DIC[head_choice]
break
elif head_choice == '1':
msg = '请输入你的用户名'.encode('utf-8')
msg_stur = struct.pack('i', len(msg))
conn.send(msg_stur)
conn.send(bytes(msg)) msg = '请输入密码'.encode('utf-8')
msg_stur = struct.pack('i', len(msg))
conn.send(msg_stur)
conn.send(bytes(msg)) head_name_pack = conn.recv(4)
name_len = struct.unpack('i', head_name_pack)[0]
name = conn.recv(name_len).decode('utf-8') head_pwd_pack = conn.recv(4)
pwd_len = struct.unpack('i', head_pwd_pack)[0]
pwd = conn.recv(pwd_len).decode('utf-8') msg = login(name, pwd).encode('utf-8')
# print('msg', msg)
msg_stur = struct.pack('i', len(msg))
conn.send(msg_stur)
conn.send(bytes(msg))
name = user_state['name']
if name:
conn.send(srur_msg1)
conn.send(bytes(MSG1.encode('gbk')))
head_choice = conn.recv(4)
head_choice_len = struct.unpack('i', head_choice)[0]
head_choice = conn.recv(head_choice_len).decode('utf-8')
print('head_choice', head_choice)
if head_choice == '1':
msg = json.dumps(check_user_all_file(name))
srur_msg = struct.pack('i', len(msg))
conn.send(srur_msg)
print(msg.encode('utf-8'))
conn.send(bytes(msg.encode('utf-8'))) elif head_choice == '2':
head_download_file_pack = conn.recv(4)
download_file_len = struct.unpack('i', head_download_file_pack)[0]
download_file = conn.recv(download_file_len).decode('utf-8')
# if not download_file.startswith('"'):
# download_file = f'"{download_file}'
# if not download_file.endswith('"'):
# download_file = f'{download_file}"'
with open(download_file, 'rb')as fr:
file = fr.read()
file_len = len(file)
file_len_dic = {'size': file_len}
file_len_dic = json.dumps(file_len_dic)
file_len_dic_stru = struct.pack('i', len(file_len_dic))
conn.send(file_len_dic_stru)
conn.send(bytes(file_len_dic.encode('utf-8')))
conn.send(file) elif head_choice == '3':
upload_file_path_srtu = conn.recv(4)
upload_file_path_head = struct.unpack('i', upload_file_path_srtu)[0]
upload_file_path = conn.recv(upload_file_path_head).decode('utf-8')
print('upload_file_path',upload_file_path)
file_size_head = conn.recv(4)
file_head = struct.unpack('i', file_size_head)[0]
file_size_dic = conn.recv(file_head).decode('utf-8')
print(file_size_dic)
file_size_dic = eval(file_size_dic)
file_size = file_size_dic['size']
file = conn.recv(file_size)
file_path = os.path.join(DB_PATH, f'{name}')
download_file_name = upload_file_path.split('\\')[-1]
# print(download_file_name)
# print(type(download_file_name))
file_path = os.path.join(file_path, f'{download_file_name}')
with open(file_path, 'wb')as fw:
fw.write(file)
elif head_choice == '4':
del_file_path_srtu = conn.recv(4)
del_file_path_head = struct.unpack('i', del_file_path_srtu)[0]
del_file_path = conn.recv(del_file_path_head).decode('utf-8').split('\\')[-1]
print('del_file_path', del_file_path)
file_path = os.path.join(DB_PATH,f"{name}")
file_path = os.path.join(file_path, f"{del_file_path}")
os.remove(file_path)
elif head_choice == '5':
msg = quit()
break
else:
msg = 'please input true choice'.encode('utf-8')
msg_stur = struct.pack('i', len(msg))
conn.send(msg_stur)
conn.send(bytes(msg))
continue else:
print(163546) elif head_choice == '2':
msg = 'please input your name'.encode('utf-8')
print('len(msg)1', len(msg))
msg_stur = struct.pack('i', len(msg))
conn.send(msg_stur)
conn.send(bytes(msg)) msg = 'please input your password'.encode('utf-8')
print('len(msg)2', len(msg))
msg_stur = struct.pack('i', len(msg))
conn.send(msg_stur)
conn.send(bytes(msg)) head_name_pack = conn.recv(4)
name_len = struct.unpack('i', head_name_pack)[0]
name = conn.recv(name_len).decode('utf-8')
print('name', name) head_pwd_pack = conn.recv(4)
pwd_len = struct.unpack('i', head_pwd_pack)[0]
pwd = conn.recv(pwd_len).decode('utf-8')
print('pwd', pwd) msg = resgister(name,pwd).encode('utf-8')
print('msg',msg)
msg_stur = struct.pack('i', len(msg))
conn.send(msg_stur)
conn.send(bytes(msg))
print(1)
else:
msg = 'please input true choice'.encode('utf-8')
msg_stur = struct.pack('i',len(msg))
conn.send(msg_stur)
conn.send(bytes(msg))
continue
break
break conn.close()
soc.close()
上一篇:如何确保TCP协议传输稳定可靠?


下一篇:开发错误日志之FTP协议传输文件问题