所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提
高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP
段。若连续几次需要send的数据都很少,通常TCP会根据优化算
法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到
了粘包数据。
TCP(transport control protocol,传输控制协议)是面向连接
的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端
)都要有一一成对的socket,因此,发送端为了将多个发往接收端
的包,更有效的发到对方,使用了优化方法(Nagle算法),将多
次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行
封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机
制。 即面向流的通信是无消息保护边界的。
UDP(user datagram protocol,用户数据报协议)是无连接的
,面向消息的,提供高效率服务。不会使用块的合并优化算法, 由
于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区
)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中
就有了消息头(消息来源地址,端口等信息),这样,对于接收
端来说,就容易进行区分处理了。 即面向消息的通信是有消息保
护边界的。
tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户
端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基
于数据报的,即便是你输入的是空内容(直接回车),那也不是
空消息,udp协议会帮你封装上消息头。
udp的recvfrom是阻塞的,一个recvfrom(x)必须对一个一个
sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,
这意味着udp根本不会粘包,但是会丢数据,不可靠
tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续
接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的
,但是会粘包。
==============================================
FTP
client
import socket,optparse,json,os,shelve
class FtpClient:
'''Ftp 客户端'''
MSG_SIZE = 1024 # 消息最长1024
def __init__(self):
self.username=None
self.current_dir=None
self.terminal_display=None
self.shelve_obj=shelve.open('.luffy_db')
parser=optparse.OptionParser()
parser.add_option('-s','--server',dest='server',help='ftp server ip_addr')
parser.add_option('-P','--port',type='int',dest='port',help='ftp server port')
parser.add_option("-u", "--username", dest="username", help="username info")
parser.add_option("-p", "--password", dest="password", help="password info")
self.options,self.args=parser.parse_args()
print(self.options,self.args)
self.argv_verification()
self.make_connection()
def argv_verification(self):
'''检查是否提供IP和PORT'''
if not self.options.server or not self.options.port:
exit('must supply server and port parameters')
def make_connection(self):
'''建立socket链接'''
self.sock=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
self.sock.connect((self.options.server,self.options.port))
def get_response(self):
'''获得服务器返回'''
data=self.sock.recv(self.MSG_SIZE)
return json.loads(data.decode())
def auth(self):
'''登录server'''
count=0
while count<3:
username=input('username: ').strip()
if not username:continue
password=input('password: ').strip()
cmd={
'action_type':'auth',
'username':username,
'password':password
}
self.sock.send(json.dumps(cmd).encode('utf8'))
response=self.get_response()
if response.get('status_code') == 200:
self.username=username
self.terminal_display=self.username
self.current_dir='\\'
return True
else:
print(response.get('status_msg'))
count+=1
def interactive(self):
'''处理与服务器的互动'''
if self.auth():
self.unfinished_file_check()
while True:
user_input =input('[{}]>>: '.format(self.terminal_display)).strip()
if not user_input:continue
cmd_list=user_input.split()
if hasattr(self,'_{}'.format(cmd_list[0])):
func=getattr(self,'_{}'.format(cmd_list[0]))
func(cmd_list[1:])
def parameter_checke(self,args,min_args=None,max_args=None,exact_args=None):
'''参数合法性检查'''
if min_args:
if len(args) < min_args:
print('must provide at least {} parameters but {} receivede'.format(min_args,len(args)))
return False
if max_args:
if len(args) > max_args:
print("need at most %s parameters but %s received." % (max_args, len(args)))
return False
if exact_args:
if len(args) != exact_args:
print("need exactly %s parameters but %s received." % (exact_args, len(args)))
return False
return True
def send_msg(self,action_type,**kwargs):
'''打包信息并发送远程'''
msg_data={'action_type':action_type,
'fill':""}
msg_data.update(kwargs)
bytes_msg=json.dumps(msg_data).encode()
if len(bytes_msg) < self.MSG_SIZE:
msg_data['fill'].zfill(self.MSG_SIZE-len(bytes_msg))
bytes_msg = json.dumps(msg_data).encode()
self.sock.send(bytes_msg)
def get_response(self):
'''收到消息返回解码后结果'''
data=self.sock.recv(self.MSG_SIZE)
return json.loads(data.decode('utf8'))
def unfinished_file_check(self):
'''检查shelve db 展示未下载完的文件
根据用户选择下载或跳过
如要下载,向服务端 发送 文件名 总大小 已下载大小
等待回复
准备下载'''
if list(self.shelve_obj.keys()):
print("-------Unfinished file list -------------")
for index,abs_file in enumerate(self.shelve_obj.keys()):
received_file_size=os.path.getsize(self.shelve_obj[abs_file][1])
print("%s. %s %s %s %s" % (index, abs_file,
self.shelve_obj[abs_file][0],
received_file_size,
received_file_size / self.shelve_obj[abs_file][0] * 100
))
while True:
choice=input("[select file index to re-download]").strip()
if not choice:continue
if choice == 'back': break
if choice.isdigit():
choice=int(choice)
if choice >= 0 and choice <= index:
selected_file=list(self.shelve_obj.keys())[choice]
#self.shelve_obj[file_abs_path]=[file_size,'{}.dawnload'.format(filename)]
received_size = os.path.getsize(self.shelve_obj[selected_file][1])
self.send_msg('re_get',file_size=self.shelve_obj[selected_file][0],
received_size=received_size,
abs_filename=selected_file)
response=self.get_response()
if response.get('status_code') == 401:
local_file=self.shelve_obj[selected_file][1]
f=open(local_file,'ab')
total_size=self.shelve_obj[selected_file][0]
current_percent=int(received_size/total_size*100)
progess_generator=self.progrss_bar(total_size,current_percent=current_percent)
progess_generator.__next__()
while received_size > total_size:
if total_size-received_size<8192:
data=self.sock.recv(total_size-received_size)
else:
data=self.sock.recv()
f.write(data)
received_size+=len(data)
progess_generator.send(received_size)
else:
print("file re-get done")
f.close()
else:
print(response.get('status_code'))
def _get(self,cmd_args):
'''download file from server
1.拿到文件名
2.发送到服务器
3.等待服务器响应
3.1如果文件存在,拿到文件大小
3.1.1 循环接收
3.2 如果文件不在
print status_code
'''
if self.parameter_checke(cmd_args,min_args=1):
filename=cmd_args[0]
self.send_msg(filename=filename,action_type='get')
response=self.get_response()
if response.get('status_code') == 301:#文件存在
file_size=response.get('file_size')
received_size=0
progress_generator = self.progress_bar(file_size)#生成器
progress_generator.__next__()
file_abs_path=os.path.join(self.current_dir,filename)
self.shelve_obj[file_abs_path]=[file_size,'{}.dawnload'.format(filename)]
f=open('{}.dawnload'.format(filename),'wb')
while received_size < file_size:
if file_size-received_size<8192:
data=self.sock.recv(file_size-received_size)
else:
data=self.sock.recv(8192)
f.write(data)
progress_generator.send(received_size)
else:
print("---file [%s] recv done,received size [%s]----" % (filename, file_size))
del self.shelve_obj[file_abs_path]
f.close()
os.rename('{}.dawnload'.format(filename),filename)
else:
print(response.get('status_msg'))
def _ls(self,data):
'''查看当前文件夹下的文件
发送命令,接收结果'''
self.send_msg(action_type='ls')
response=self.get_response()
if response.get('status_code') == 302:#准备接收long消息
cmd_result_size=response.get('cmd_result_size')
received_size=0
cmd_result=b''
while received_size < cmd_result_size:
if cmd_result_size -received_size<8192:
data=self.sock.recv(cmd_result_size -received_size)
else:
data=self.sock.recv(8192)
received_size+=len(data)
cmd_result+=data
else:
print(cmd_result.decode('GBK'))
def _cd(self,args):
'''检测参数合法性
发送命令和参数
接收返回状态码'''
if self.parameter_checke(args,exact_args=1):
target_dir=args[0]
self.send_msg(action_type='cd',target_dir=target_dir)
response=self.get_response()
if response.get['status_code']== 350:
self.terminal_display=response.get['current_dir']
self.current_dir=response.get['current_dir']
def progrss_bar(self,total_size,current_percent=0,last_percent=0):
'''进度条'''
while True:
received_size=yield
current_percent=int(received_size/total_size*100)
if current_percent >last_percent:
print('#'*(int(current_percent)/2) +'[{}]'.format(current_percent),end='\r')
last_percent=current_percent
def _put(self,data):
'''上传文件到服务器
1.确保文件存在
2.发送文件名和文件大小
3.打开文件开始发送
:param data:
:return:
'''
if self.parameter_checke(data,exact_args=1):
local_file=data[0]
if os.path.isfile(local_file):
total_size=os.path.getsize(local_file)
self.send_msg('put',file_size=total_size,filename=local_file)
f=open(local_file,'rb')
progrss_generator=self.progrss_bar(total_size)
progrss_generator.__next__
uploaded_size=0
for line in f:
self.sock.send(line)
uploaded_size+=0
progrss_generator.send(uploaded_size)
else:
print()
print('uploaded file done...'.center(50,'-'))
f.close()
def _pwd(self,args):
'''
直接打印
:return:
'''
print('{}'.format(self.current_dir))
def _mkdir(self,args):
'''
1.发送命令
2.获得返回
:param args:
:return:
'''
if self.parameter_checke(args, exact_args=1):
new_dir = args[0]
print(new_dir)
self.send_msg(action_type='mkdir', new_dir=new_dir)
response = self.get_response()
if response.get('status_code') == 353:
cmd_result_size = response.get('cmd_result_size')
received_size = 0
cmd_result = b''
while received_size < cmd_result_size:
if cmd_result_size - received_size < 8192: # last receive
data = self.sock.recv(cmd_result_size - received_size)
else:
data = self.sock.recv(8192)
cmd_result += data
received_size += len(data)
else:
print(cmd_result.decode("gbk"))
if response.get('status_code') == 352:
print('Dir create success')
def _del(self,args):
'''
发送命令
获得反馈,
'''
if self.parameter_checke(args,min_args=1):
filename=args[0]
self.send_msg(filename=filename,action_type='del')
response=self.get_response()
if response.get('status_code')== 500:
print('delect success')
if response.get('status_code')== 501:
print("NO search file or dir")
else:
data=self.sock.recv(8192)
print(data.decode('GBK'))
if __name__ =='__main__':
client=FtpClient()
client.interactive()
server
import sys,os
BASE_DIR=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(BASE_DIR)
if __name__ == '__main__':
from core import management
argv_parser=management.ManagementTool(sys.argv)
argv_parser.execute()#执行指令
from core import main
class ManagementTool:
'''负责对用户输入的指令进行解析并调用相应的模块处理'''
def __init__(self,sys_argv):
self.sys_argv=sys_argv#接收sys.argv的信息['路径','']
print(self.sys_argv)
self.verify_argv()#验证sys_argv指令是否规范
def verify_argv(self):
'''验证指令合法性'''
if len(self.sys_argv) < 2:#如果sys.argv只有一个元素,说明没有输指令
self.help_msg()
cmd=self.sys_argv[1]
if not hasattr(self,cmd):#判断有没有这个功能
print('invalid argument')
self.help_msg()
def help_msg(self):
msg='''
start start FTP server
stop stop FTP server
restart testart FTP server
createuser username create a ftp user
'''
exit(msg)#退出并打印
def execute(self):
'''解析并执行指令'''
cmd=self.sys_argv[1]
func=getattr(self,cmd)#获取函数对象
func()
def start(self):
'''开启服务器'''
server=main.FTPServer(self)
server.run_forever()
import socket,json,configparser,hashlib,os,subprocess,time
from conf import settings
class FTPServer:
'''处理与客户端的所有交互行为'''
STATUS_CODE = {
200: "Passed authentication!",
201: "Wrong username or password!",
300: "File does not exist !",
301: "File exist , and this msg include the file size- !",
302: "This msg include the msg size!",
350: "Dir changed !",
351: "Dir doesn't exist !",
352: "Dir create success",
353:"Dir does exist",
401: "File exist ,ready to re-send !",
402: "File exist ,but file size doesn't match!",
500: 'delect success',
501: "Dir or file doesn't exist !"
}
MSG_SIZE = 1024 # 消息最长1024
def __init__(self,management_instence):
self.management_instence=management_instence
self.sock=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
self.sock.bind((settings.HOST,settings.PORT))
self.sock.listen(5)
self.accounts=self.load_accounts()
self.user_obj=None
self.user_current_dir=None
def run_forever(self):
'''启动服务器,接收连接'''
print('starting FTP server on {},{}'.center(50,'-').format(settings.HOST,settings.PORT))
while True:
self.request,self.addr=self.sock.accept()
print('got a new connection form {}....'.format(self.addr,))
self.handle()
def handle(self):
'''处理与用户的所有指令交互'''
while True:
raw_data=self.request.recv(self.MSG_SIZE)
if not raw_data:
print('connection {} is lost...'.format(self.addr))
del self.request,self.sock
break
data=json.loads(raw_data.decode('utf8'))#解码,反序列化
action_type=data.get('action_type')#从字典中取出action_type的值
if action_type:#判断是否为空
if hasattr(self,'_{}'.format(action_type)):
func=getattr(self,'_{}'.format(action_type))
func(data)
else:
print('invalid command')
def load_accounts(self):
'''加载所有帐户信息'''
config_obj=configparser.ConfigParser()
config_obj.read(settings.ACCOUNTS_FILE)#读取文件
return config_obj
def authenticate(self,username,password):
'''用户认证方法'''
if username in self.accounts:
_password = self.accounts[username]['password']
md5_obj=hashlib.md5()
md5_obj.update(password.encode())
if _password == md5_obj.hexdigest():
self.user_obj=self.accounts[username]#拿到客户在服务器上的所有信息
self.user_obj['home']=os.path.join(settings.USER_HOME_DIR,username)
self.user_current_dir=self.user_obj['home']
#增加家目录
return True
else:
return False
else:
return False
def send_response(self,status_code,*args,**kwargs):
'''打包发送信息给客户端
:param status_code:200
:param args:
:param kwargs:{'filename':filename,'size':filesize}
:return
'''
data=kwargs#变成字典,如果有值
data['status_code']=status_code
data['status_msg']=self.STATUS_CODE[status_code]
data['fill']=''
bytes_data=bytes(json.dumps(data).encode())
if len(bytes_data) < self.MSG_SIZE:
data['fill'].zfill(self.MSG_SIZE-len(bytes_data))
bytes_data = bytes(json.dumps(data).encode())
self.request.send(bytes_data)
def _auth(self,data):
'''处理用户登录认证'''
if self.authenticate(data.get('username'),data.get('password')):
print('pass auth...')
self.send_response(status_code=200)
else:
self.send_response(status_code=201)
def _get(self,data):
'''
1.收到文件名
2.判断文件是否存在
2.1 文件存在,返回文件大小和状态码
2.1.1 发送文件
2.2 文件不存在,返回状态码
:return:
'''
filename=data.get('filename')
full_path=os.path.join(self.user_obj['home'],filename)#控制用户只能在家目录下寻找文件
if os.path.isfile(full_path):
filesize=os.stat(full_path).st_size
self.send_response(status_code=301,file_size=filesize)
print('ready to send file')
f=open(filename,'rb')
for line in f:
self.request.send(line)
else:
print('file send done..',full_path)
f.close()
else:
self.send_response(status_code=300)#文件不存在
def _ls(self,data):
'''实现dir命令并返回结果给client'''
print(self.user_current_dir)
cmd_obj=subprocess.Popen('dir {}'.format(self.user_current_dir),shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
stdout=cmd_obj.stdout.read()
stderr=cmd_obj.stderr.read()
cmd_result=stdout+stderr
if not cmd_result:
cmd_result=b'current dir has not file at all'
self.send_response(status_code=302,cmd_result_size=len(cmd_result))
self.request.sendall(cmd_result)
def _cd(self,data):
'''实现让用户觉得在切换文件夹的假象
1.根据 用户发送过来target_dir 与 user_current_dir 拼接
2.检测有没有target_dir
2.1 如果有就改变user_current_dir
2.2 如果没有就返回错误值
'''
target_dir=data.get('target_dir')
full_dir=os.path.abspath(os.path.join(self.user_current_dir,target_dir))
#abspath 是为了解决 cd .. c:\a\b\c\..\..=c:\a
if os.path.isdir(full_dir):
if full_dir.startswith(self.user_obj['home']):
self.user_current_dir=full_dir
relative_current_dir=full_dir.replace(self.user_obj['home'],'')
self.send_response(status_code=350,current_dir=relative_current_dir)
else:
self.send_response(status_code=351)
else:
self.send_response(status_code=351)
def _put(self,data):
'''
1.接收文件名和文件大小
2.检测有没有同名文件
2.1如果有,给上传文件改名
2.1 准备接收
2.2 没有,直接接收
:param data:
:return:
'''
local_file=data.get('filename')
full_path=os.path.join(self.user_current_dir,local_file)
if os.path.isfile(full_path):
filename='{}.{}'.format(full_path,time.time())
else:
filename=full_path
f=open(filename,'wb')
received_size=0
total_size=data.get('tatal_size')
while received_size < total_size:
if total_size-received_size <8192:
data=self.request.recv(total_size-received_size)
else:
data=self.request.recv(8192)
received_size+=len(data)
f.write(data)
else:
print('file %s recv done'% local_file)
f.close()
def _re_get(self,args):
'''
1.拿到文件名,拼接路径
2.判断文件在不在
2.1 在,判断大小是否一样
2.1.1 大小一样,返回状态码
2.1.1.1 根据对方已经收到的大小,sock到位置继续传
2.1.2 不一样,返回状态码
2.2 不在 返回状态码
:param args:
:return:
'''
abs_filename=args.get('abs_filename')
full_path=os.path.join(self.user_obj['home'],abs_filename.strip('\\'))
if os.path.isfile(full_path):
if os.path.getsize(full_path)== args.get('file_size'):
self.send_response(status_code=401)
f=open(full_path,'rb')
f.seek(args.get('received_size'))
for line in f:
self.request.recv(line)
else:
print("-----file re-send done------")
f.close()
else:
self.send_response(status_code=402,file_size_on_server=os.path.getsize(full_path))
else:
self.send_response(status_code=300)
def _mkdir(self,argv):
'''
1.拼接路径
2.实现mkdir 命令
3.返回结果
:param argv:
:return:
'''
new_dir=argv.get('new_dir')
create_target_dir=os.path.join(self.user_current_dir,new_dir)
cmd_obj = subprocess.Popen('mkdir {}'.format(create_target_dir), shell=True, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout = cmd_obj.stdout.read()
stderr = cmd_obj.stderr.read()
cmd_result = stdout + stderr
if len(cmd_result)==0:
self.send_response(status_code=352)
else:
self.send_response(status_code=353,cmd_result_size=len(cmd_result))
self.request.sendall(cmd_result)
def _del(self,args):
'''
1.拼接路径
2.判断是文件还是文件夹
2.1 文件 使用del
2.2 DIR 使用 rd
3.返回消息
:param args:
:return:
'''
filename=args.get('filename')
file_path=os.path.join(self.user_current_dir,filename)
if os.path.isfile(file_path):
s = subprocess.Popen('del {}'.format(file_path), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
f1 = s.stdout.read()
f2 = s.stderr.read()
f = f1 + f2
if not f:
self.send_response(status_code=500)
else:
self.request.sendall(f)
elif os.path.isdir(file_path):
s = subprocess.Popen('rd {}'.format(file_path), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
f1 = s.stdout.read()
f2 = s.stderr.read()
f = f1 + f2
if not f:
self.send_response(status_code=500)
else:
self.request.sendall(f)
else:
self.send_response(status_code=501)