一、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. 听大佬学长RQY报告有感

    听了RQY大佬的报告,我深有感触…… 数学基础很重要.首先我们要学好数学,众所周知信息学奥赛的实质是做数学题.如果你的编程能力再高,绞尽脑汁就是不会解数学题那有什么用呢?如果你会解数学题,那么你可以根 ...

  2. 醉汉随机行走/随机漫步问题(Random Walk Randomized Algorithm Python)

    世界上有些问题看似是随机的(stochastic),没有规律可循,但很可能是人类还未发现和掌握这类事件的规律,所以说它们是随机发生的. 随机漫步(Random  Walk)是一种解决随机问题的方法,它 ...

  3. 【XSY2667】摧毁图状树 贪心 堆 DFS序 线段树

    题目大意 给你一棵有根树,有\(n\)个点.还有一个参数\(k\).你每次要删除一条长度为\(k\)(\(k\)个点)的祖先-后代链,问你最少几次删完.现在有\(q\)个询问,每次给你一个\(k\), ...

  4. ElasticSearch5.4.1 搜索引擎搭建文档

    安装配置JDK环境JDK安装(不能安装JRE)JDK下载地址:http://www.oracle.com/technetwork/java/javase/downloads/jdk8-download ...

  5. Codeforces Round #471 (Div. 2) F. Heaps(dp)

    题意 给定一棵以 \(1\) 号点为根的树.若满足以下条件,则认为节点 \(p\) 处有一个 \(k\) 叉高度为 \(m\) 的堆: 若 \(m = 1\) ,则 \(p\) 本身就是一个 \(k\ ...

  6. FlatList

    FlatList 之前使用的组件是ListView,当时要添加一个下拉刷新,上拉加载的功能,所以对ListView做了一些封装,但是后来看官方文档,不建议再使用ListView,因为效率问题,做过An ...

  7. 【BZOJ1299】巧克力棒(博弈论,线性基)

    [BZOJ1299]巧克力棒(博弈论,线性基) 题面 BZOJ 题解 \(Nim\)博弈的变形形式. 显然,如果我们不考虑拿巧克力棒出来的话,这就是一个裸的\(Nim\)博弈. 但是现在可以加入巧克力 ...

  8. Crash 的文明世界

    题目描述 给一棵树,求以每个点为根时下列式子的值. 题解 当k=1时这就是一个经典的换根dp问题. 所以这道题还是要用换根dp解决. 部分分做法: 考虑转移时是这样的一个形式(图是抄的). 用二项式定 ...

  9. 【mysql】mysql尾部空格

    mysql 字段为varchar类型的在查询时候胡忽略尾部空格. 先看表结构 插入一条数据包含空格 在查询是可以查到的 所有在插入数据的时候要对插入字段的数据处理下,php可以用函数trim()去掉两 ...

  10. sublime text3 插件的安装

    ctrl + shift +p 打开搜索框界面 安装: 输入install package并回车,再在列表中选中所需要安装的插件,你会发现左下角正在显示安装.安装成功后,一般会打开插件说明. 卸载: ...