一、TCP协议 粘包现象 和解决方案

黏包现象
让我们基于tcp先制作一个远程执行命令的程序(命令ls -l ; lllllll ; pwd)
执行远程命令的模块

需要用到模块subprocess

subprocess通过子进程来执行外部指令,并通过input/output/error管道,获取子进程的执行的返回信息。

import subprocess
sub_obj = subprocess.Popen(
'ls', #系统指令
shell=True, #固定
stdout=subprocess.PIPE, #标准输出 PIPE 管道,保存着指令的执行结果
stderr=subprocess.PIPE #标准错误输出
)
print('正确输出',sub_obj.stdout.read().decode('gbk'))
print('错误输出',sub_obj.stderr.read().decode('gbk'))

基于tcp协议实现的黏包

###server
while 1:
from_client_cmd=conn.recv(1024)
print(from_client_cmd.decode('utf-8'))#接收客户端数据解码 sub_obj=subprocess.Popen(
from_client_cmd.decode('utf-8'),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
###client
import socket
client=socket.socket()
client.connect(('127.0.0.1',8001)) while 1:
cmd=input('请输入指令:')
client.send(cmd.encode('utf-8'))
server_cmd_result=client.recv(1024)
print(server_cmd_result.decode('gbk'))

这就是黏包现象

因为每次执行,固定为1024字节。它只能接收到1024字节,那么超出部分怎么办?
等待下一次执行命令dir时,优先执行上一次,还没有传完的信息。传完之后,再执行dir命令

 总结:

发送过来的一整条信息
由于server端没有及时接受
后来发送的数据和之前没有接收完的数据黏在了一起
这就是著名的黏包现象

TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。
收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。
这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
对于空消息:tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),也可以被发送,udp协议会帮你封装上消息头发送过去。
可靠黏包的tcp协议:tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。
UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。
不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
对于空消息:tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),也可以被发送,udp协议会帮你封装上消息头发送过去。
不可靠不黏包的udp协议:udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y;x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠。

解决方案一

问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据。

原理:

黏包现象的成因

  你不知道在哪儿断句

解决问题

  在发送数据的时候,先告诉对方要发送的大小就可以了

自定义协议

先和服务端商量好,发送多少字节,再传输数据。

####server
# 原理
# 黏包现象的成因
# 你不知道在哪儿断句
# 解决问题
# 在发送数据的时候,先告诉对方要发送的大小就可以了
# 在发送的时候 先发送数据的大小 在发送内容
# 在接受的时候 先接受大小 再根据大小接受内容
# 自定义协议 #_*_coding:utf-8_*_
from socket import *
ip_port=('127.0.0.1',8080) tcp_socket_server=socket()
tcp_socket_server.bind(ip_port)
tcp_socket_server.listen(5) conn,addr=tcp_socket_server.accept()
lenth = conn.recv(1) # 接收1个字节,返回 b'5'
#print(lenth)
lenth = int(lenth.decode('utf-8')) # 转化字符串,返回5 data1=conn.recv(lenth) # 接收5字节,返回 b'hello'
lenth2 = conn.recv(1) # 接收1个字节
lenth2 = int(lenth2.decode('utf-8')) # 转化字符串,返回3
data2=conn.recv(lenth2) # 接收3个字节,返回b'egg' print('----->',data1.decode('utf-8'))
print('----->',data2.decode('utf-8')) conn.close()
tcp_socket_server.close()
####client
import socket
BUFSIZE=1024
ip_port=('127.0.0.1',8080) s=socket.socket()
res=s.connect_ex(ip_port) # 功能与connect(address)相同,但是成功返回0,失败返回errno的值
lenth = str(len('hello')).encode('utf-8') # 获取hello的字符的长度,并转化为str,最后编码
s.send(lenth) # 发送数字5
s.send('hello'.encode('utf-8')) # 发送hello
lenth = str(len('egg')).encode('utf-8') # 获取长度,结果为3
s.send(lenth) # 发送3
s.send('egg'.encode('utf-8')) # 发送egg s.close()
先执行服务端,再执行客户端,执行输出:

-----> hello
-----> egg
存在的问题:
程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用send去发送该字节流长度,这种方式会放大网络延迟带来的性能损耗

解决方案进阶

刚刚的方法,问题在于我们我们在发送

通过struck模块将需要发送的内容的长度进行打包,打包成一个4字节长度的数据发送到对端,对端只要取出前4个字节,然后对这四个字节的数据进行解包,拿到你要发送的内容的长度,然后通过这个长度来继续接收我们实际要发送的内容。

这个模块可以把要发送的数据长度转换成固定长度的字节。这样客户端每次接收消息之前只要先接受这个固定长度字节的内容看一看接下来要接收的信息大小,那么最终接受的数据只要达到这个值就停止,就能刚好不多不少的接收完整的数据了。

struct模块

该模块可以把一个类型,如数字,转成固定长度的bytes

import struct
ret = struct.pack('i',1000000) # i表示int类型
print(ret)
print(len(ret)) # 返回4 ret1 = struct.unpack('i',ret) # 按照给定的格式(fmt)解析字节流string,返回解析出来的tuple
print(ret1) # 返回一个元组 执行输出:
b'@B\x0f\x00'
4
(1000000,)

借助struct模块,我们知道长度数字可以被转换成一个标准大小的4字节数字。因此可以利用这个特点来预先发送数据长度。

发送时 接收时

先发报头长度

先收报头长度,用struct取出来
再编码报头内容然后发送 根据取出的长度收取报头内容,然后解码,反序列化
最后发真实内容 从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容
####server
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)#接收的大小 print(from_client_cmd.decode('utf-8'))#答应查看一下
#接收到客户端发送来的系统指令,我服务端通过subprocess模块到服务端自己的系统里面执行这条指令
sub_obj = subprocess.Popen(
from_client_cmd.decode('utf-8'),#解析客户端发来的命令
shell=True,
stdout=subprocess.PIPE, #正确结果的存放位置
stderr=subprocess.PIPE #错误结果的存放位置
)
#从管道里面拿出结果,通过subprocess.Popen的实例化对象.stdout.read()方法来获取管道中的结果
std_msg = sub_obj.stdout.read()#管道里面拿出结果 #为了解决黏包现象,我们统计了一下消息的长度,先将消息的长度发送给客户端,客户端通过这个长度来接收后面我们要发送的真实数据
std_msg_len = len(std_msg)
print('指令的执行结果长度>>>>',len(std_msg)) msg_lenint_struct = struct.pack('i',std_msg_len)#把长度byes加上int标识 4个长度 conn.send(msg_lenint_struct+std_msg)#发送拼接给客户端
####client
import socket
import struct
client = socket.socket()
client.connect(('127.0.0.1',8001))
while 1:
cmd = input('请输入指令:')
#发送指令
client.send(cmd.encode('utf-8'))#发送给server你想要执行的命令
#接收数据长度,首先接收4个字节长度的数据,因为这个4个字节是长度
server_res_len = client.recv(4)
msg_len = struct.unpack('i',server_res_len)[0] print('来自服务端的消息长度',msg_len)
#通过解包出来的长度,来接收后面的真实数据
server_cmd_result = client.recv(msg_len) print(server_cmd_result.decode('gbk'))

简单的文件传送

文件的上传和下载

需要文件的名字,文件的大小,文件的内容

自定义一个文件传输协议:

{'filesize':000,'filename':'XXXX'}

###server
import os
import json
import struct
import socket
sk=socket.socket()
sk.bind(('127.0.0.1',9090))
sk.listen() conn,addr=sk.accept() #接收来自客户端的连接
dic={'filename':'python18期 2组员资料.rar',
'filesize': os.path.getsize(r'E:\python18期 2组员资料.rar')
}
str_dic=json.dumps(dic).encode('utf-8')#把字典转换成json然后转成byes
dic_len=struct.pack('i',len(str_dic))#把长度byes加上int标识 4个长度
conn.send(dic_len +str_dic) # conn.send(str_dic)
with open(r'E:\python18期 2组员资料.rar','rb')as f:
content=f.read()
conn.send(content)
conn.close()
sk.close()
import struct
import socket
sk=socket.socket()
sk.connect(('127.0.0.1',9090))
dic_len=sk.recv(4)
dic_len=struct.unpack('i',dic_len)[0]#接受到长度
str_dic = sk.recv(dic_len).decode('utf-8')#接收server的字节
dic = json.loads(str_dic)
# print(dic)#{'filename': 'python18期 2组员资料.rar', 'filesize': 4786326} #接收到的文件名 包大小
with open(dic['filename'],'wb') as f:
content=sk.recv(dic['filesize'])
f.write(content)
sk.close()

注意:

大文件的传输,不能一次性读到内存里

上传一个视频,几台电脑之间能互相传,视频要3个G左右。

进阶需求,加一个登陆功能

1. 引入模块
import hashlib
2. 创建md5对象(实例化)
obj = hashlib.md5(b"盐")
3. 把加密的内容交给md5
obj.update(bytes)
4. 获取密文
obj.hexdigest()

import  hashlib
obj=hashlib.md5(b'121212')#加盐
obj.update('2131231'.encode('utf-8'))
print(obj.hexdigest())#拿到密文

server.py

import os
import json
import struct
import socket
import hashlib sk = socket.socket()
sk.bind(('127.0.0.1',9999))
sk.listen() conn,addr = sk.accept()
print(addr) filename = '[电影天堂www.dy2018.com]移动迷宫3:死亡解药BD国英双语中英双字.mp4' # 文件名
absolute_path = os.path.join('E:\BaiduYunDownload',filename) # 文件绝对路径
buffer_size = 1024*1024 # 缓冲大小,这里表示1MB md5obj = hashlib.md5()
with open(absolute_path, 'rb') as f:
while True:
content = f.read(buffer_size) # 每次读取指定字节
if content:
md5obj.update(content)
else:
break # 当内容为空时,终止循环 md5 = md5obj.hexdigest()
print(md5) # 打印md5值 dic = {'filename':filename, 'filename_md5':str(md5),'buffer_size':buffer_size,
'filesize':os.path.getsize(absolute_path)}
str_dic = json.dumps(dic).encode('utf-8') # 将字典转换为json
dic_len = struct.pack('i', len(str_dic)) # 获取字典长度,转换为struct
conn.send(dic_len) # 发送字典长度
conn.send(str_dic) # 发送字典 with open(absolute_path, 'rb') as f: # 打开文件
while True:
content = f.read(buffer_size) # 每次读取指定大小的字节
if content: # 判断内容不为空
conn.send(content) # 每次读取指定大小的字节
else:
break conn.close() # 关闭连接
sk.close() # 关闭套接字

client.py

import json
import struct
import socket
import hashlib
import time start_time = time.time()
sk = socket.socket()
sk.connect(('127.0.0.1',9999)) dic_len = sk.recv(4) # 接收4字节,因为struct的int为4字节
dic_len = struct.unpack('i',dic_len)[0] # 反解struct得到元组,获取元组第一个元素
#print(dic_len) # 返回一个数字
str_dic = sk.recv(dic_len).decode('utf-8') # 接收指定长度,获取完整的字典,并解码
#print(str_dic) # json类型的字典
dic = json.loads(str_dic) # 反序列化得到真正的字典
#print(dic) # 返回字典 md5 = hashlib.md5()
with open(dic['filename'],'wb') as f:
while True:
content = sk.recv(dic['buffer_size'])
if not content:
break
md5.update(content)
md5 = md5.hexdigest()
print(md5) # 打印md5值 if dic['filename_md5'] == str(md5):
f.write(content)
print('md5校验正确--下载成功')
else:
print('文件验证失败') sk.close() end_time = time.time()
print('本次下载花费了{}秒'.format(end_time-start_time))

先执行server.py,再执行client.py

server输出:

('127.0.0.1', 54230)
30e63a254cf081e8e93c036b21057347

client输出:

30e63a254cf081e8e93c036b21057347
md5校验正确--下载成功
本次下载花费了25.687340021133423秒

python tcp黏包和struct模块解决方法,大文件传输方法及MD5校验的更多相关文章

  1. Python之黏包的解决

    黏包的解决方案 发生黏包主要是因为接收者不知道发送者发送内容的长度,因为tcp协议是根据数据流的,计算机操作系统有缓存机制, 所以当出现连续发送或连续接收的时候,发送的长度和接收的长度不匹配的情况下就 ...

  2. Python之黏包

    黏包现象 让我们基于tcp先制作一个远程执行命令的程序(命令ls -l ; lllllll ; pwd) res=subprocess.Popen(cmd.decode('utf-8'), shell ...

  3. tcp粘包问题原因及解决办法

    1.粘包概念及产生原因 1.1粘包概念: TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾. 粘包可能由发送方造成,也可能由接收方造成. ...

  4. Python网络编程基础 struct模块 解决黏包问题 FTP

    struct模块 解决黏包问题 FTP

  5. 黏包-黏包的成因、解决方式及struct模块初识、文件的上传和下载

    黏包: 同时执行多条命令之后,得到的结果很可能只有一部分,在执行其他命令的时候又接收到之前执行的另外一部分结果,这种显现就是黏包. 只有TCP协议中才会产生黏包,UDP协议中不会有黏包(udp协议中数 ...

  6. Python网络编程基础 ❷ 基于upd的socket服务 TCP黏包现象

    TCP的长连接 基于upd的socket服务 TCP黏包现象

  7. (day27)subprocess模块+粘包问题+struct模块+ UDP协议+socketserver

    目录 昨日回顾 软件开发架构 C/S架构 B/S架构 网络编程 互联网协议 socket套接字 今日内容 一.subprocess模块 二.粘包问题 三.struct模块 四.UDP 五.QQ聊天室 ...

  8. socketserver tcp黏包

    socket (套接字) tcp(黏包现象原因) 传输中由于内核区缓冲机制(等待时间,文件大小),会在 发送端 缓冲区合并连续send的数据,也会出现在 接收端 缓冲区合并recv的数据给指定port ...

  9. netty]--最通用TCP黏包解决方案

    netty]--最通用TCP黏包解决方案:LengthFieldBasedFrameDecoder和LengthFieldPrepender 2017年02月19日 15:02:11 惜暮 阅读数:1 ...

随机推荐

  1. BZOJ4946[Noi2017]蔬菜——线段树+堆+模拟费用流

    题目链接: [Noi2017]蔬菜 题目大意:有$n$种蔬菜,每种蔬菜有$c_{i}$个,每种蔬菜每天有$x_{i}$个单位会坏掉(准确来说每天每种蔬菜坏掉的量是$x_{i}-$当天这种蔬菜卖出量), ...

  2. 洛谷P1360 [USACO07MAR]黄金阵容均衡题解

    题目 不得不说这个题非常毒瘤. 简化题意 这个题的暴力还是非常好想的,完全可以过\(50\%\)的数据.但是\(100\%\)就很难想了. 因为数据很大,所以我们需要用\(O(\sqrt n)\)的时 ...

  3. Ikki's Story IV - Panda's Trick POJ - 3207(水2 - sat 在圈内 还是 在圈外)

    题意: 就是一个圈上有n个点,给出m对个点,这m对个点,每一对都有一条边,合理安排这些边在圈内或圈外,能否不相交 解析: 我手残 我手残 我手残 写一下情况 只能是一个在圈外 一个在圈内 即一个1一个 ...

  4. prufer序列

    介绍 其实是\(pr\ddot{u}fer\)序列 什么是prufer序列? 我们认为度数为\(1\)的点是叶子节点 有一颗无根树,每次选出编号最小的叶子节点,加到当前prufer序列的后面,然后删掉 ...

  5. 【XSY2484】mex 离散化 线段树

    题目大意 给你一个无限长的数组,初始的时候都为\(0\),有3种操作: 操作\(1\)是把给定区间\([l,r]\)设为\(1\): 操作\(2\)是把给定区间\([l,r]\)设为\(0\): 操作 ...

  6. fullcalendar 日历插件3.9.0 -- 基本插件使用

    以下主要结构,直接执行即可以使用 ,仅用参考: html: <!DOCTYPE html> <html> <head> <title>test</ ...

  7. 【CF1009F】Dominant Indices(长链剖分)

    [CF1009F]Dominant Indices(长链剖分) 题面 洛谷 CF 翻译: 给定一棵\(n\)个点,以\(1\)号点为根的有根树. 对于每个点,回答在它子树中, 假设距离它为\(d\)的 ...

  8. AXURE 8弄一个轮播图的步骤

    这个图是网上找到,7.0可以使用. 如果是8.0.没有找到"动态面板"这个地方,如下图所示

  9. LVM-COW写实备份

    [root@localhost ~]# fdisk -l /dev/sdb /dev/sdc | grep "LVM"/dev/sdb1 1 9660 77593918+ 8e L ...

  10. bzoj2560串珠子(子集dp)

    铭铭有n个十分漂亮的珠子和若干根颜色不同的绳子.现在铭铭想用绳子把所有的珠子连接成一个整体. 现在已知所有珠子互不相同,用整数1到n编号.对于第i个珠子和第j个珠子,可以选择不用绳子连接,或者在ci, ...